6.9 KiB
API 统一约定
1. 鉴权与工作空间隔离
除 POST /api/auth/register、POST /api/auth/login、POST /api/auth/refresh 外,所有接口都要求:
Authorization: Bearer <short-lived-access-token>
访问令牌只携带用户会话标识,不以客户端传入的 workspace_id 判定数据归属。全局认证守卫先验证 session,全局工作空间守卫再解析首期唯一的有效工作空间并验证成员关系;受保护处理器从 request.auth.workspace 或 @CurrentWorkspace() 取得已验证的 workspaceId 与角色。业务 SQL 必须将该 workspaceId 传给短生命周期的 workspaceTransaction,由其在每一个数据库事务中执行 SET LOCAL app.workspace_id。RLS 仅是第二道防线,业务层仍须验证资源归属。
/health、/ready、注册、登录与 refresh 是显式标记的公开端点。新接口默认受上述两个全局守卫保护;不得因便利而自行跳过,除非先在契约中说明公开理由。
Web 登录后的 refresh token 放在 HttpOnly、Secure(生产)、SameSite=Lax 的 cookie 中,永不出现在 JSON、日志或前端持久化存储。access token 由前端仅在内存中短暂持有;插件使用独立授权码流程,留待采集插件接口文档定义。
2. 页头平台与默认店铺
平台范围接口使用:
X-Atlas-Platform-Id: 57ad…-uuid
- 无此 header 的接口不依赖平台,例如
GET /api/auth/me、水印和通用素材接口。 - 需要平台却没有 header 时返回
400 PLATFORM_CONTEXT_REQUIRED。 - header 指向停用或不存在的平台时返回
422 PLATFORM_UNAVAILABLE。 - 默认店铺是服务端按工作空间和该 header 查出的状态,响应中以
default_store明确返回;客户端不应缓存为事实。
列表筛选中的 platform_id 只用于平台目录或跨平台管理页,不能绕开数据授权;同一请求不得同时带 X-Atlas-Platform-Id 和语义相同的 platform_id。
3. 响应、分页和字段格式
3.1 强制响应信封
本项目不采用完整 JSON:API 规范,但所有业务 HTTP 接口必须使用以下响应信封;这是 前端、服务端、Worker 和扩展共同遵守的强制契约:
| 场景 | HTTP 响应体 |
|---|---|
| 成功的单资源或同步动作 | { "data": T } |
| 成功的列表 | { "data": T[], "meta": PaginationMeta } |
| 失败 | { "error": ApiError } |
204 No Content |
无响应体 |
data 只能位于顶层且只能出现一次;不得返回 { "data": { "data": ... } },也不得以
{ "data": null } 表示失败。HTTP 状态码仍表达请求结果,错误必须使用本文件第 4 节的
error 信封,不能混入成功响应。meta 只承载分页、游标或与整个响应相关的元信息,不能
替代资源字段。
3.2 成功与列表响应
成功响应统一为:
{ "data": { "id": "uuid" } }
列表响应:
{
"data": [{ "id": "uuid" }],
"meta": { "next_cursor": "opaque-string-or-null", "has_more": false }
}
- 游标是服务端不透明值,客户端只能原样传回
cursor;默认按文档指定稳定排序。 limit默认20,最大100;超出时返回422 VALIDATION_FAILED。- 金额与费率以 JSON 字符串返回和提交,例如
"199.90"、"0.125",避免 JavaScript 浮点误差;币种用 ISO 4217 大写代码。 - 所有时间用 UTC,例如
"2026-09-02T08:00:00Z";日期用YYYY-MM-DD。 - 不返回
password_hash、API Key、Cookie、密文、对象存储永久密钥或长期访问 URL。
4. 错误格式和 HTTP 状态
{
"error": {
"code": "VALIDATION_FAILED",
"message": "请求参数不符合要求",
"request_id": "req_…",
"details": [
{ "field": "email", "code": "EMAIL_ALREADY_REGISTERED", "message": "该邮箱已注册" }
]
}
}
| 状态 | 使用场景 |
|---|---|
400 |
缺少平台上下文、请求语义矛盾、无法解析的 JSON。 |
401 |
缺少、失效或撤销的 access token。 |
403 |
已登录但无工作空间成员资格或无操作权限。 |
404 |
当前工作空间范围内不存在该资源;不暴露其他工作空间资源。 |
409 |
重复唯一资源、幂等键与既有请求不一致、乐观锁冲突。 |
422 |
字段格式、业务状态或平台能力不满足。 |
429 |
登录、刷新、上传签发或重试操作触发限流。 |
500 |
未预期服务错误;不返回秘密或平台原始敏感响应。 |
5. 写操作的幂等、并发和异步
- 创建店铺、上传意图、删除水印模板等可重试写操作应携带
Idempotency-Key: <UUID>。服务端按“当前用户 + 工作空间 + HTTP 方法 + 规范化实际路径 + key”保存至少 24 小时(首期默认 48 小时);请求摘要保存为服务端 HMAC,而非明文请求体。相同键、相同请求返回已保存响应;相同键、不同请求返回409 IDEMPOTENCY_KEY_REUSED;原请求仍在执行返回409 IDEMPOTENCY_REQUEST_IN_PROGRESS。 PATCH /api/auth/me同样要求幂等键。POST /api/auth/change-password是例外:其成功响应包含新 access token,不能将该 token 持久化到幂等回放表;客户端不得自动重试此操作。- 更新可编辑资源时,读取响应携带
revision;客户端在PATCH/PUT中提交If-Match: "<revision>"。版本已变化返回409 VERSION_CONFLICT和最新current_revision摘要。revision是并发控制版本;不要把水印的渲染current_version当作它。 - 耗时动作(店铺凭据验证、仓库同步、媒体处理)先返回
202 Accepted,并在资源中返回operation。客户端轮询GET /api/operations/{operation_id};后续可追加 SSE,不改变资源接口。 - 所有启动异步工作的业务写入,必须在同一 PostgreSQL 事务中写入业务资源/
operation和outbox_events;独立 Dispatcher 再投递 BullMQ。BullMQ 和 Redis 不得作为任务存在或最终业务结果的唯一事实。
operation 统一形状:
{
"id": "uuid",
"kind": "store_credential_verification",
"status": "queued",
"created_at": "2026-09-02T08:00:00Z",
"completed_at": null,
"error": null
}
操作接口只返回当前工作空间可见的任务,且任务状态不能作为唯一事实;业务资源中的状态字段才是页面最终展示依据。
GET /api/operations/{operation_id} 返回上述 operation 形状。任务不属于当前工作空间时返回 404;error / error_message 只能是可展示的脱敏摘要,不能包含 API Key、Cookie、对象存储签名或平台原始敏感响应。
6. 删除与软删除
首期店铺和水印模板使用软删除。成功删除返回 204 No Content;之后以普通 GET 读取返回 404,列表默认不包含删除记录。历史发布、素材派生和审计记录保留其快照,不因软删除被改写。