Files
photo-live/packages/contracts/openapi.yaml
T
浅念 31ba03de11 feat(api): password change endpoint and source device id on uploads
P3 Task 3 契约增补批次(单一事实来源 openapi.yaml + cn 实现 + e2e):

- POST /v1/auth/password-change(operationId changePassword,需求 4.1 的
  P1 契约缺口补齐):Bearer + {oldPassword 1-128, newPassword 8-128} → 204;
  旧密码错误 401 AUTH_INVALID_CREDENTIALS;成功后事务内更新 argon2 哈希、
  撤销该用户除当前会话外的全部其他会话(当前会话保留,本人不下线)、写
  审计 auth.password_change。
- sourceDeviceId(A19 设备来源语义):UploadInitRequest 可选 8-64 位设备
  标识;upload_sessions 落列(VARCHAR(64) utf8mb4_0900_bin,可空,存量
  行 NULL 兼容);GET /uploads 条目回显;幂等重放不比对(first-writer,
  首次落库值不随重放漂移)。
- e2e:auth 增 4 例(未认证 401 / 新密码过短 400 / 旧密码错误 401 且会话
  无副作用 / 成功后他端会话撤销当前保留 + 新旧密码登录裁决 + 审计);
  uploads 增 1 例(携带落库回显、换设备重放保留首值、缺省 null、长度
  越界 400)。api e2e 179 → 184;contracts gen/redocly/单测、worker、
  sync 门全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-19 00:59:39 +08:00

3057 lines
102 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.1.0
info:
title: photo-live
version: 0.1.0
license:
name: UNLICENSED
identifier: UNLICENSED
description: |
photo-live 直播相册系统 API 契约(单一事实来源)。
- 类型经 `openapi-typescript` 生成并提交入库(`src/generated/openapi.d.ts`),
三端(services/api、apps/admin、apps/viewer)直接消费生成类型,不自行跑 gen。
- 错误响应使用稳定 `code`(见 `components.schemas.Error` 的枚举,与
`src/errors.ts` 的 `ErrorCodes` 一一对应),客户端按 code 映射多语言文案,
`message` 仅供诊断。
- 列表接口统一游标分页,元信息见 `components.schemas.CursorPageMeta`。
- 业务端点一律 Bearer 认证;`/public/**` 匿名访问(`security: []`)。
- `/internal/**` 为服务间同步接口(P2):HMAC 签名鉴权
(`X-Sync-Signature`/`X-Sync-Timestamp`,见 [internal] tag 描述),
OpenAPI 无对应标准 scheme 故同样标注 `security: []`,但匿名不可访问。
- WebSocket 实时消息契约不经 OpenAPI 生成,单独维护于
`src/realtime.ts`(判别联合 + 心跳常量),与生成类型同属单一事实来源。
- 状态码通用映射:401 → AUTH_TOKEN_EXPIRED、500 → INTERNAL_ERROR;
其余状态码对应的 code 在各响应的 description 中标注。
- 多语言内容按「请求 locale → 相册 defaultLocale」回退解析为单语言对象,
响应携带 `resolvedLocale` 供界面判断实际语言。
servers:
- url: /v1
tags:
- name: auth
description: 密码登录会话(签发、滑动续期、退出)
- name: albums
description: 相册创建、列表、详情与配置(工作人员)
- name: assets
description: 相册宣传图/封面/Logo 资产直传(创建者或管理员)
- name: members
description: 相册成员管理(创建者或管理员)
- name: categories
description: 相册分类管理(创建者或管理员)
- name: uploads
description: 直传任务初始化、授权续签、完成核验与会话记录
- name: photos
description: 照片可见性调整与软删除(工作人员)
- name: admin
description: 管理辅助查询
- name: public
description: 观众匿名只读(相册信息与公开照片)
- name: interactions
description: 观众匿名互动(点赞批次与浏览上报;ov 域部署,路径前缀不变、域名区分)
- name: internal
description: |
服务间内部接口(cn↔ov 同步 + ov 紧急停用通道),HMAC 鉴权,不对匿名
开放。共用签名规则:
`X-Sync-Signature` = hex(HMAC-SHA256(原始请求体, SYNC_INTERNAL_KEY)),
`X-Sync-Timestamp` = Unix 秒(与服务端时差 ±300s 内有效,防重放);
签名缺失/不符或时间戳超窗一律拒绝。OpenAPI 无 HMAC 标准 scheme,
故下列操作 `security` 标注为 `[]`(无标准认证要求),鉴权由服务端强制执行。
paths:
# ─────────────────────────── auth ───────────────────────────
/auth/password:
post:
operationId: loginWithPassword
tags: [auth]
summary: 密码登录
description: 校验账号密码,签发会话令牌。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: 登录成功,返回会话令牌与过期时间。
content:
application/json:
schema:
$ref: '#/components/schemas/LoginResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED,经全局校验管道)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 账号或密码错误(AUTH_INVALID_CREDENTIALS)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: |
登录失败次数过多(AUTH_RATE_LIMITED,Task 13 契约修订)。
同账号+来源 IP 在窗口期内连续失败达到阈值后锁定一段时间。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/auth/refresh:
post:
operationId: refreshSession
tags: [auth]
summary: 会话滑动续期
description: 携带有效 Bearer 令牌滑动续期,返回新的过期时间。
security:
- bearerAuth: []
responses:
'200':
description: 续期成功。
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED,经全局校验管道)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
/auth/logout:
post:
operationId: logout
tags: [auth]
summary: 退出登录
description: 使当前 Bearer 令牌失效。
security:
- bearerAuth: []
responses:
'204':
description: 已退出,无响应体。
'400':
description: 请求体不可解析(VALIDATION_FAILED,经全局校验管道)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
/auth/password-change:
post:
operationId: changePassword
tags: [auth]
summary: 修改密码
description: |
已认证用户修改自身密码:校验旧密码,成功后更新密码哈希并撤销该用户
除当前会话外的全部其他会话(当前会话保持有效,无需重新登录)。
P3 Task 3 契约增补(需求 4.1「支持修改密码」,P1 契约缺口补齐)。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PasswordChangeRequest'
responses:
'204':
description: 修改成功,无响应体;当前会话保留,其他会话已撤销。
'400':
description: 参数校验失败(VALIDATION_FAILED,经全局校验管道)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
description: 旧密码错误(AUTH_INVALID_CREDENTIALS)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── albums ───────────────────────────
/albums:
post:
operationId: createAlbum
tags: [albums]
summary: 创建相册
description: |
创建相册并写入多语言内容;创建者自动成为 owner 成员。
初始默认值:uploadEnabled/publicEnabled/downloadEnabled=true、
originalDownloadEnabled=false、status=active、watermarkVersion=0。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AlbumCreateRequest'
responses:
'201':
description: 相册已创建(新相册 categories 为空数组)。
content:
application/json:
schema:
$ref: '#/components/schemas/Album'
'400':
description: 参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
get:
operationId: listAlbums
tags: [albums]
summary: 相册列表
description: 当前用户可见(参与)的相册列表;管理员可见全部。按创建时间倒序。
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
description: 每页条数。
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 相册列表页。
content:
application/json:
schema:
$ref: '#/components/schemas/AlbumList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
/albums/{albumId}:
parameters:
- $ref: '#/components/parameters/AlbumId'
get:
operationId: getAlbum
tags: [albums]
summary: 相册详情
description: 相册完整配置、多语言内容与分类列表。
security:
- bearerAuth: []
responses:
'200':
description: 相册详情。
content:
application/json:
schema:
$ref: '#/components/schemas/Album'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: 相册不存在,或当前用户非成员不可见(ALBUM_NOT_FOUND,避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
patch:
operationId: updateAlbum
tags: [albums]
summary: 更新相册配置
description: |
部分更新:仅提交的字段被修改,各开关互相独立。
translations 为全量替换(须包含 defaultLocale 对应条目)。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AlbumUpdateRequest'
responses:
'200':
description: 更新后的相册详情。
content:
application/json:
schema:
$ref: '#/components/schemas/Album'
'400':
description: 参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可修改(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── assets ───────────────────────────
/albums/{albumId}/assets:
parameters:
- $ref: '#/components/parameters/AlbumId'
post:
operationId: initAlbumAssetUpload
tags: [assets]
summary: 初始化资产上传
description: |
为相册宣传图/封面/Logo 创建上传会话,返回 staging 对象的临时直传凭证
(凭证结构与照片直传同形)。无 clientTaskId 幂等键:assetId 即一次性会话,
失败重试重新初始化即可。替换语义:同一 (kind, locale) 再次上传→新行生效,
旧行保留作历史(P1b 无删除端点);公开相册响应始终取该 kind 最新生效行
(P1a 公开服务已按 latest 实现)。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AssetInitRequest'
responses:
'201':
description: 资产上传会话已创建。
content:
application/json:
schema:
$ref: '#/components/schemas/AssetInitResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED):文件超过大小限额或类型不接受。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可管理资产(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/albums/{albumId}/assets/{assetId}/complete:
parameters:
- $ref: '#/components/parameters/AlbumId'
- $ref: '#/components/parameters/AssetId'
post:
operationId: completeAlbumAssetUpload
tags: [assets]
summary: 完成资产上传核验
description: |
核验 staging 对象(存在性、字节数与可用完整性信息)后将资产挂到相册生效,
并返回短期签名访问 URL。幂等:已核验通过的资产重复调用返回 200
(URL 重新现签,本端点即签名刷新入口)。
security:
- bearerAuth: []
responses:
'200':
description: 核验通过,资产已生效。
content:
application/json:
schema:
$ref: '#/components/schemas/AssetCompleteResponse'
'400':
description: |
核验失败(UPLOAD_VERIFY_FAILED):对象缺失、字节数与申明不符
或完整性信息不可用。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 资产不存在或无权操作(AUTH_FORBIDDEN,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── members ───────────────────────────
/albums/{albumId}/members:
parameters:
- $ref: '#/components/parameters/AlbumId'
post:
operationId: addAlbumMember
tags: [members]
summary: 添加相册成员
description: 将已开通的同事添加为协作者;仅创建者或管理员可操作。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/MemberAddRequest'
responses:
'201':
description: 成员已添加。
content:
application/json:
schema:
$ref: '#/components/schemas/Member'
'400':
description: 参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可管理成员(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 该用户已是相册成员(MEMBER_CONFLICT)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
get:
operationId: listAlbumMembers
tags: [members]
summary: 相册成员列表
description: 相册全部成员(含 owner)。
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
description: 每页条数。
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 成员列表页。
content:
application/json:
schema:
$ref: '#/components/schemas/MemberList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/albums/{albumId}/members/{userId}:
parameters:
- $ref: '#/components/parameters/AlbumId'
- $ref: '#/components/parameters/UserId'
delete:
operationId: removeAlbumMember
tags: [members]
summary: 移除相册成员
description: |
移除协作者;仅创建者或管理员可操作。不能移除创建者(403)。
目标成员不存在时幂等返回 204。
security:
- bearerAuth: []
responses:
'204':
description: 已移除,无响应体。
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无管理权限,或试图移除创建者(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── categories ───────────────────────────
/albums/{albumId}/categories:
parameters:
- $ref: '#/components/parameters/AlbumId'
post:
operationId: createCategory
tags: [categories]
summary: 创建分类
description: 在相册下创建分类;仅创建者或管理员可操作。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CategoryCreateRequest'
responses:
'201':
description: 分类已创建。
content:
application/json:
schema:
$ref: '#/components/schemas/Category'
'400':
description: 参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可管理分类(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/categories/{categoryId}:
parameters:
- $ref: '#/components/parameters/CategoryId'
patch:
operationId: updateCategory
tags: [categories]
summary: 更新分类
description: 部分更新;translations 为全量替换。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CategoryUpdateRequest'
responses:
'200':
description: 更新后的分类。
content:
application/json:
schema:
$ref: '#/components/schemas/Category'
'400':
description: 参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无所属相册的管理权限(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 分类不存在,或所属相册不存在/不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
delete:
operationId: deleteCategory
tags: [categories]
summary: 删除分类
description: 删除分类(分类下仍有照片时是否允许删除由服务端策略决定,400 拒绝)。
security:
- bearerAuth: []
responses:
'204':
description: 已删除,无响应体。
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无所属相册的管理权限(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 分类不存在,或所属相册不存在/不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── uploads ───────────────────────────
/uploads:
post:
operationId: initUpload
tags: [uploads]
summary: 初始化上传任务
description: |
幂等初始化:预创建上传会话与照片 ID,返回 staging 对象的临时直传凭证。
相同 clientTaskId 且参数一致时返回原会话,不重复创建照片;
参数不一致时返回 409(UPLOAD_SESSION_CONFLICT)。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UploadInitRequest'
responses:
'201':
description: 上传会话已创建(幂等重复调用返回原会话)。
content:
application/json:
schema:
$ref: '#/components/schemas/UploadInitResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED):文件超过大小限额或类型不接受。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无该相册的上传权限(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: |
相册已关闭上传(ALBUM_UPLOAD_DISABLED),或同 clientTaskId 已存在
且参数不一致(UPLOAD_SESSION_CONFLICT)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
get:
operationId: listUploads
tags: [uploads]
summary: 上传会话记录
description: 当前用户的上传会话历史(新在前),用于任务队列恢复与状态展示。
security:
- bearerAuth: []
parameters:
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
description: 每页条数。
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 会话记录列表页。
content:
application/json:
schema:
$ref: '#/components/schemas/UploadSessionList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
/uploads/{uploadId}/renew:
parameters:
- $ref: '#/components/parameters/UploadId'
post:
operationId: renewUploadCredential
tags: [uploads]
summary: 续签上传凭证
description: 为仍处于 pending 状态的会话重新签发直传凭证(结构同初始化响应)。
security:
- bearerAuth: []
responses:
'200':
description: 新凭证已签发(credential 为新值,其余字段回显原会话)。
content:
application/json:
schema:
$ref: '#/components/schemas/UploadInitResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无权操作该会话或会话不存在(AUTH_FORBIDDEN,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: |
相册已关闭上传(ALBUM_UPLOAD_DISABLED),或会话状态不允许续签
(已完成/取消/过期,UPLOAD_SESSION_CONFLICT)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/uploads/{uploadId}/complete:
parameters:
- $ref: '#/components/parameters/UploadId'
post:
operationId: completeUpload
tags: [uploads]
summary: 完成核验
description: |
核验 staging 对象(存在性、字节数与可用完整性信息)后进入处理流水线。
幂等:核验通过的会话重复调用返回相同结果。
security:
- bearerAuth: []
responses:
'200':
description: 核验通过,照片进入处理流水线。
content:
application/json:
schema:
$ref: '#/components/schemas/UploadCompleteResponse'
'400':
description: |
核验失败(UPLOAD_VERIFY_FAILED):对象缺失、字节数与申明不符
或完整性信息不可用。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无权操作该会话或会话不存在(AUTH_FORBIDDEN,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 会话状态不允许完成(已取消或已过期,UPLOAD_SESSION_CONFLICT)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── photos ───────────────────────────
/albums/{albumId}/photos:
parameters:
- $ref: '#/components/parameters/AlbumId'
get:
operationId: listAlbumPhotos
tags: [photos]
summary: 管理端照片列表
description: |
相册照片管理列表(区别于公开列表):全部状态可见(unpublished/published/
hidden/deleted),条目含 uploader 脱敏信息。按 uploadedAt 与 ID 稳定倒序
游标分页。
security:
- bearerAuth: []
parameters:
- name: visibility
in: query
description: 可见性过滤;缺省为全部状态。
schema:
$ref: '#/components/schemas/PhotoVisibility'
- name: categoryId
in: query
description: 分类 ID 过滤。
schema:
type: string
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
description: 每页条数。
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 管理照片列表页。
content:
application/json:
schema:
$ref: '#/components/schemas/AdminPhotoList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可访问管理列表(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/photos/{photoId}:
parameters:
- $ref: '#/components/parameters/PhotoId'
patch:
operationId: updatePhoto
tags: [photos]
summary: 调整照片可见性
description: 在 hidden 与 published 间切换(隐藏/恢复发布);按相册角色与上传人共同判定权限。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PhotoVisibilityUpdateRequest'
responses:
'200':
description: 更新后的照片记录。
content:
application/json:
schema:
$ref: '#/components/schemas/PhotoRecord'
'400':
description: 参数校验失败(VALIDATION_FAILED,visibility 取值非法)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无该照片的管理权限(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 照片不存在(PHOTO_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
delete:
operationId: deletePhoto
tags: [photos]
summary: 软删除照片
description: 标记 deleted(软删除),公开端点即刻不可见;物理删除走独立运维流程。
security:
- bearerAuth: []
responses:
'204':
description: 已软删除,无响应体。
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 无该照片的管理权限(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 照片不存在(PHOTO_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── admin ───────────────────────────
/admin/users:
get:
operationId: lookupUsersByAccount
tags: [admin]
summary: 账号前缀查询
description: |
邀请协作者的前置查询:按账号前缀搜索激活用户,返回脱敏列表
(id/account/displayName,不含邮箱、手机与状态字段)。
访问策略(P1b 决策):任何已认证工作人员可用——邀请的发起方不限于
创建者/管理员,而泄露面仅为激活用户的最小展示字段、接收方为已认证
内部人员,风险可接受;未认证一律 401。前缀匹配为大小写敏感
(服务端按二进制校对执行 LIKE prefix%);按账号升序返回,无游标
(前缀搜索 + 小上限即取即用)。
security:
- bearerAuth: []
parameters:
- name: accountPrefix
in: query
required: true
description: 账号前缀(≥2 字符以防过宽匹配)。
schema:
type: string
minLength: 2
maxLength: 64
- name: limit
in: query
description: 返回条数上限。
schema:
type: integer
minimum: 1
maximum: 20
default: 10
responses:
'200':
description: 匹配的激活用户列表(无匹配为空数组)。
content:
application/json:
schema:
$ref: '#/components/schemas/AdminUserLookupList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED;accountPrefix 缺失或长度非法)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── public ───────────────────────────
/public/albums/{token}:
parameters:
- $ref: '#/components/parameters/AlbumPublicToken'
get:
operationId: getPublicAlbum
tags: [public]
summary: 公开相册信息
description: |
观众页入口:按「请求 locale → defaultLocale」回退解析为单语言对象。
publicToken 不可枚举,但持有链接者均可访问。
security: []
parameters:
- $ref: '#/components/parameters/LocaleQuery'
responses:
'200':
description: 相册公开信息。
content:
application/json:
schema:
$ref: '#/components/schemas/PublicAlbum'
'400':
description: 查询参数校验失败(VALIDATION_FAILED;locale 取值不支持时不报错,按 defaultLocale 回退)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在或未公开(ALBUM_NOT_PUBLIC,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/public/albums/{token}/photos:
parameters:
- $ref: '#/components/parameters/AlbumPublicToken'
get:
operationId: listPublicPhotos
tags: [public]
summary: 公开照片列表
description: |
仅返回公开相册中 published 的照片,按 publishedAt 与 ID 稳定倒序游标分页。
签名 URL 为短期凭证,过期后凭同一入口刷新。
security: []
parameters:
- name: category
in: query
description: 分类 ID 过滤(来自 PublicAlbum.categories 的 id)。
schema:
type: string
- name: sort
in: query
description: |
排序模式:latest=发布时间倒序(缺省);hot=点赞数倒序(photoId 倒序
决胜,短期快照语义——服务端约 30s 缓冲,窗口内顺序不随点赞变化,
技术实施方案 §7)。hot 仅 ov 部署实现;cn 部署不感知该参数(固定
latest 语义)。ov 部署对非法取值返回 400 VALIDATION_FAILED。
schema:
type: string
enum: [latest, hot]
default: latest
- $ref: '#/components/parameters/LocaleQuery'
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
description: 每页张数(技术实施方案 §7 建议 30-60)。
schema:
type: integer
minimum: 1
maximum: 60
default: 30
responses:
'200':
description: 公开照片列表页。
content:
application/json:
schema:
$ref: '#/components/schemas/PublicPhotoList'
'400':
description: 查询参数校验失败(category/cursor/limit;locale 取值不支持时不报错,按 defaultLocale 回退)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在或未公开(ALBUM_NOT_PUBLIC,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/public/albums/{token}/photos/{photoId}:
parameters:
- $ref: '#/components/parameters/AlbumPublicToken'
- $ref: '#/components/parameters/PhotoId'
get:
operationId: getPublicPhoto
tags: [public]
summary: 公开单图
description: |
深度链接定位单图,同时作为签名 URL 过期后的刷新入口。
服务端校验相册与照片归属;隐藏/删除后的旧链接返回 404。
security: []
responses:
'200':
description: 单图信息(含新签名的 thumb/browse URL)。
content:
application/json:
schema:
$ref: '#/components/schemas/PublicPhotoDetail'
'404':
description: |
相册不存在或未公开(ALBUM_NOT_PUBLIC),或照片不存在、未发布、
不属于该相册(PHOTO_NOT_PUBLIC)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── interactions ───────────────────────────
/public/albums/{token}/photos/{photoId}/likes:
parameters:
- $ref: '#/components/parameters/AlbumPublicToken'
- $ref: '#/components/parameters/PhotoId'
post:
operationId: likePhotoBatch
tags: [interactions]
summary: 点赞批次上报
description: |
匿名点赞批次(ov 域部署;路径前缀与 cn 相同,按部署域名区分)。
客户端将短时间内的多次点击合并为批次提交,delta 为本批新增点赞数。
- 幂等:同一 (photoId, sessionId, batchId) 重复提交返回已确认结果
(batchAccepted=true、total 为权威累计值),不重复加计;失败重试
必须复用同一 batchId。
- 窗口外过期批次(LIKE_BATCH_WINDOW_HOURS,默认 24h,清理任务保留窗口)
拒绝重放:不加计,返回当前权威 total(batchAccepted=false),
客户端据此校准本地乐观计数。
- 基础限速:sessionId+来源 IP 滑窗,超限 429(RATE_LIMITED)。
- 相册非公开/紧急停用 404(ALBUM_NOT_PUBLIC);照片不存在、未发布、
不属于该相册或相册非公开 404(PHOTO_NOT_PUBLIC)。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LikeBatchRequest'
responses:
'200':
description: |
本批处理结果(含被拒绝的过期批次——拒绝也返回 200 与权威 total,
语义由 batchAccepted 表达,避免客户端把重放当失败重试)。
content:
application/json:
schema:
$ref: '#/components/schemas/LikeBatchResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED;delta 越界 1..20 或字段长度非法)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: |
相册不存在或未公开(ALBUM_NOT_PUBLIC),或照片不存在、未发布、
不属于该相册(PHOTO_NOT_PUBLIC)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 基础限速触发(RATE_LIMITED;sessionId+IP 滑窗)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/public/albums/{token}/visits:
parameters:
- $ref: '#/components/parameters/AlbumPublicToken'
post:
operationId: reportAlbumVisit
tags: [interactions]
summary: 浏览上报
description: |
匿名浏览上报(ov 域部署)。仅由客户端显式调用:页面可见且相册信息
加载成功后上报一次(列表/图片/WS 等接口调用一律不触发计数)。
- requestId 为上报幂等键(建议 UUID),counted 确认前跨刷新/重试持久复用;
重复 requestId 幂等重放:counted 回显该 requestId 首次处理的结局
(确认响应丢失后重试仍见 true),viewCount 恒为当前权威值,不重复计数。
- 去重窗口(VISIT_WINDOW_MINUTES,默认 30 分钟,D07)内同一 visitorId
不重复计数;窗口过后再上报按新访次计数。
- 过期 requestId(VISIT_REQUEST_TTL_MINUTES,默认 35 分钟)拒绝重放
(counted=false,无论原结局),不得重新加计。
- 基础限速:visitorId+来源 IP 固定窗(VISIT_WINDOW_REQUESTS,默认
60/窗),超限 429(RATE_LIMITED);去重本身为主缓解。
- 相册非公开/紧急停用 404(ALBUM_NOT_PUBLIC)。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/VisitReportRequest'
responses:
'200':
description: 上报结果(窗口内重复/重复 requestId/过期重放均 200 + counted=false)。
content:
application/json:
schema:
$ref: '#/components/schemas/VisitReportResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED;visitorId/requestId 长度非法)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在或未公开(ALBUM_NOT_PUBLIC)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: 基础限速触发(RATE_LIMITED;visitorId 滑窗)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── internal ───────────────────────────
/internal/sync/content-batches:
post:
operationId: syncContentBatches
tags: [internal]
summary: 内容事件批量推送
description: |
cn→ov 方向:cn outbox 未投递事件批量推送(默认每 2s 一批、单批
SYNC_BATCH_SIZE 条;priority=high 的下线/隐藏/删除类事件即时单发)。
ov 侧语义:eventId 幂等(重复推送忽略)、entityVersion 单调
(旧版本忽略)、tombstone 墓碑优先(副本删除且不被旧事件复活)、
依赖缺失暂存待补。acceptedEventIds 为本批受理(含幂等重复)的事件 ID,
applied 为实际应用(新版本生效)条数。鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ContentBatchRequest'
responses:
'200':
description: 本批受理结果。
content:
application/json:
schema:
$ref: '#/components/schemas/ContentBatchResponse'
'400':
description: 请求体校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/sync/stat-snapshots:
post:
operationId: syncStatSnapshots
tags: [internal]
summary: 统计快照回传
description: |
ov→cn 方向:ov 侧聚合的统计快照(likes/views 权威累计值,默认每 60s
增量回传)。cn 按 statVersion 单调替换副本列(仅更高版本替换、
绝不相加),重复回传幂等。kind=likes 时 photoId 必填(照片点赞数);
kind=views 为相册级浏览数(无 photoId)。鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/StatSnapshotRequest'
responses:
'200':
description: 回传受理结果。
content:
application/json:
schema:
$ref: '#/components/schemas/StatSnapshotResponse'
'400':
description: 请求体校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/sync/reconcile:
post:
operationId: syncReconcile
tags: [internal]
summary: 缺失事件对账
description: |
ov 侧按 sync_state 水位向 cn 请求指定实体类型在 sinceVersion 之后
的事件清单(albumId 可选过滤;中断恢复=水位重放)。cn 返回按版本
升序的事件数组,ov 重放补漏;全量快照(bootstrap)的事件本体也经
本通道分批拉取。鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReconcileRequest'
responses:
'200':
description: 缺失事件清单(按 entityVersion 升序)。
content:
application/json:
schema:
$ref: '#/components/schemas/ReconcileResponse'
'400':
description: 请求体校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/sync/bootstrap:
post:
operationId: syncBootstrap
tags: [internal]
summary: 全量快照引导
description: |
ov 冷启动或新建副本时请求 cn 生成当前全量快照事件序列
(album → categories → assets → photos,含各实体当前 entityVersion)。
本端点仅返回事件计数 eventCount 与快照水位 watermark;事件本体经
对账通道(/internal/sync/reconcile)以 watermark 为起点分批拉取。
albumId 缺省表示全部相册。鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BootstrapRequest'
responses:
'200':
description: 快照规模与水位。
content:
application/json:
schema:
$ref: '#/components/schemas/BootstrapResponse'
'400':
description: 请求体校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/sync/digest:
post:
operationId: syncDigest
tags: [internal]
summary: 每相册实体计数摘要
description: |
cn 侧权威计数(P2 Task 4):每相册 {categoryCount, publishedPhotoCount,
photoCount, assetCount}。ov 对账调度定期拉取并与副本计数比对——不一致
的相册触发该相册 bootstrap(当前状态快照重发,含分类全量清单整体替换),
收敛错过的墓碑与漏掉的事件。albumId 缺省表示全部相册。鉴权见 [internal]
tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/DigestRequest'
responses:
'200':
description: 每相册计数摘要(albumId 升序)。
content:
application/json:
schema:
$ref: '#/components/schemas/DigestResponse'
'400':
description: 请求体校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/emergency/disable:
post:
operationId: emergencyDisable
tags: [internal]
summary: 海外紧急停用相册(ov 域)
description: |
ov 侧独立于同步链路的紧急停用入口:直接置 albums_ov.emergency_disabled
=true(ov 本地写,不经同步版本链——后续任何同步事件不得解除,恢复仅经
/internal/emergency/enable 显式操作)。停用相册在 ov 公开 API/WS 侧按
404 ALBUM_NOT_PUBLIC 语义处理;cn 侧不感知(同步照常)。albumToken 与
albumId 二选一(都缺省 400)。审计:ov 侧结构化日志以 audit=emergency
字段标记(P2 决策:不做独立文件通道,P5 运维按字段路由聚合)。
鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyDisableRequest'
responses:
'200':
description: 已停用(幂等:重复停用返回同一结果)。
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyStatus'
'400':
description: 请求体校验失败(VALIDATION_FAILED;含 albumToken/albumId 均缺省)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册副本不存在(ALBUM_NOT_FOUND——ov 尚未同步该相册)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/internal/emergency/enable:
post:
operationId: emergencyEnable
tags: [internal]
summary: 解除海外紧急停用(ov 域,显式唯一恢复通道)
description: |
唯一能清除 albums_ov.emergency_disabled 的入口(显式操作,任何同步
事件都不会代为解除)。albumToken 与 albumId 二选一(都缺省 400)。
审计同 disable(audit=emergency 字段)。鉴权见 [internal] tag 描述。
security: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyEnableRequest'
responses:
'200':
description: 已恢复(幂等:重复启用返回同一结果)。
content:
application/json:
schema:
$ref: '#/components/schemas/EmergencyStatus'
'400':
description: 请求体校验失败(VALIDATION_FAILED;含 albumToken/albumId 均缺省)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册副本不存在(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
parameters:
AlbumId:
name: albumId
in: path
required: true
description: 相册 ID。
schema:
type: string
UserId:
name: userId
in: path
required: true
description: 用户 ID。
schema:
type: string
CategoryId:
name: categoryId
in: path
required: true
description: 分类 ID。
schema:
type: string
AssetId:
name: assetId
in: path
required: true
description: 相册资产 ID。
schema:
type: string
UploadId:
name: uploadId
in: path
required: true
description: 上传会话 ID。
schema:
type: string
PhotoId:
name: photoId
in: path
required: true
description: 照片 ID。
schema:
type: string
AlbumPublicToken:
name: token
in: path
required: true
description: 相册公开令牌(不可枚举,持有即可访问)。
schema:
type: string
CursorQuery:
name: cursor
in: query
description: 分页游标(上一页响应的 nextCursor)。
schema:
type: string
maxLength: 512
LocaleQuery:
name: locale
in: query
description: 请求语言;缺省或取值不支持时按相册 defaultLocale 回退(不报 400,响应 resolvedLocale 指示实际返回语言)。
schema:
$ref: '#/components/schemas/Locale'
responses:
Unauthorized:
description: 未认证:令牌缺失、无效或已过期(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
InternalError:
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
schemas:
# ── 通用 ──
Error:
type: object
required: [code, message, requestId]
properties:
code:
type: string
readOnly: true
description: 稳定错误码,与 src/errors.ts 的 ErrorCodes 一一对应。
enum:
- AUTH_INVALID_CREDENTIALS
- AUTH_TOKEN_EXPIRED
- AUTH_RATE_LIMITED
- RATE_LIMITED
- AUTH_FORBIDDEN
- NOT_FOUND
- ALBUM_NOT_FOUND
- ALBUM_NOT_PUBLIC
- ALBUM_UPLOAD_DISABLED
- MEMBER_CONFLICT
- UPLOAD_SESSION_CONFLICT
- UPLOAD_VERIFY_FAILED
- PHOTO_NOT_FOUND
- PHOTO_NOT_PUBLIC
- VALIDATION_FAILED
- SYNC_AUTH_FAILED
- INTERNAL_ERROR
message:
type: string
readOnly: true
description: 服务器生成的可读消息(诊断用,不作为客户端展示文案依据)。
requestId:
type: string
readOnly: true
description: 请求追踪 ID。
CursorPageMeta:
type: object
required: [nextCursor, hasMore]
description: 游标分页元信息;各列表响应以 allOf 内嵌本结构(条目 schema 在业务端点中定义)。不变式:hasMore=true 当且仅当 nextCursor≠null,两者必须成对填充。
properties:
nextCursor:
type: [string, 'null']
readOnly: true
description: 下一页游标;null 表示没有更多数据。
hasMore:
type: boolean
readOnly: true
description: 是否还有下一页。
Locale:
type: string
description: P1a 支持的语言。
enum:
- zh-CN
- en
# ── 认证 ──
LoginRequest:
type: object
required: [account, password]
properties:
account:
type: string
description: 账号(用户名或邮箱)。
password:
type: string
description: 密码。
LoginResponse:
type: object
required: [sessionToken, expiresAt]
properties:
sessionToken:
type: string
readOnly: true
description: 会话令牌(Bearer)。
expiresAt:
type: string
format: date-time
readOnly: true
description: 会话过期时间。
RefreshResponse:
type: object
required: [expiresAt]
properties:
expiresAt:
type: string
format: date-time
readOnly: true
description: 续期后的过期时间。
PasswordChangeRequest:
type: object
description: 修改密码请求(P3 Task 3 契约增补)。
required: [oldPassword, newPassword]
properties:
oldPassword:
type: string
minLength: 1
maxLength: 128
description: 当前密码。
newPassword:
type: string
minLength: 8
maxLength: 128
description: 新密码(至少 8 位)。
# ── 相册 ──
AlbumTranslation:
type: object
required: [locale, title]
description: 相册多语言条目(创建/更新与响应共用;未填的可选字段存储为 null)。
properties:
locale:
$ref: '#/components/schemas/Locale'
title:
type: string
minLength: 1
description: 相册标题。
description:
type: [string, 'null']
description: 相册描述。
venue:
type: [string, 'null']
description: 场地名称。
address:
type: [string, 'null']
description: 场地地址。
AlbumCreateRequest:
type: object
required: [translations, defaultLocale, startAt]
properties:
translations:
type: array
minItems: 1
description: 多语言内容;必须包含 defaultLocale 对应条目。
items:
$ref: '#/components/schemas/AlbumTranslation'
defaultLocale:
$ref: '#/components/schemas/Locale'
startAt:
type: string
format: date-time
description: 活动开始时间(UTC 存储,展示按 timezone)。
endAt:
type: [string, 'null']
format: date-time
description: 活动结束时间;null 或缺省表示未定。
timezone:
type: string
default: Asia/Shanghai
description: 活动 IANA 时区。
examples:
- Asia/Shanghai
downloadEnabled:
type: boolean
default: true
description: 允许下载浏览版本。
originalDownloadEnabled:
type: boolean
default: false
description: 允许下载原图。
AlbumUpdateRequest:
type: object
minProperties: 1
description: 部分更新:仅提交的字段被修改,各开关互相独立;endAt 传 null 清除。
properties:
translations:
type: array
minItems: 1
description: 全量替换多语言内容;须包含 defaultLocale 对应条目。
items:
$ref: '#/components/schemas/AlbumTranslation'
defaultLocale:
$ref: '#/components/schemas/Locale'
startAt:
type: string
format: date-time
description: 活动开始时间。
endAt:
type: [string, 'null']
format: date-time
description: 活动结束时间;null 清除。
timezone:
type: string
description: 活动 IANA 时区。
uploadEnabled:
type: boolean
description: 是否允许新上传任务。
publicEnabled:
type: boolean
description: 是否公开可见。
downloadEnabled:
type: boolean
description: 允许下载浏览版本。
originalDownloadEnabled:
type: boolean
description: 允许下载原图。
AlbumSummary:
type: object
description: 相册列表条目(完整开关与多语言标题,管理列表无需二次请求详情)。
required:
- id
- publicToken
- ownerId
- status
- defaultLocale
- timezone
- startAt
- endAt
- uploadEnabled
- publicEnabled
- downloadEnabled
- originalDownloadEnabled
- watermarkVersion
- translations
- createdAt
- updatedAt
- statsSyncedAt
properties:
id:
type: string
readOnly: true
description: 相册 ID。
publicToken:
type: string
readOnly: true
pattern: '^[0-9a-z]{20,32}$'
description: 公开访问令牌(不可枚举)。
ownerId:
type: string
readOnly: true
description: 创建者用户 ID。
status:
type: string
readOnly: true
default: active
description: 相册状态;offline 表示下线(停止公开访问与新授权签发)。
enum:
- active
- offline
defaultLocale:
$ref: '#/components/schemas/Locale'
timezone:
type: string
description: 活动 IANA 时区。
startAt:
type: string
format: date-time
description: 活动开始时间。
endAt:
type: [string, 'null']
format: date-time
description: 活动结束时间;null 表示未定。
uploadEnabled:
type: boolean
description: 是否允许新上传任务。
publicEnabled:
type: boolean
description: 是否公开可见。
downloadEnabled:
type: boolean
description: 允许下载浏览版本。
originalDownloadEnabled:
type: boolean
description: 允许下载原图。
watermarkVersion:
type: integer
readOnly: true
minimum: 0
default: 0
description: 当前水印配置版本。
translations:
type: array
description: 各语言内容。
items:
$ref: '#/components/schemas/AlbumTranslation'
createdAt:
type: string
format: date-time
readOnly: true
description: 创建时间。
updatedAt:
type: string
format: date-time
readOnly: true
description: 最近更新时间。
statsSyncedAt:
type: [string, 'null']
format: date-time
readOnly: true
description: |
统计副本最近同步时间(P2 A27:ov→cn 统计快照在本相册的应用时刻);
null 表示尚无回传应用(viewCountReplica 副本位仍为初始 0)。照片级
点赞副本(PhotoRecord.likeCount)的同步时间不经相册行表达。
AlbumList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/AlbumSummary'
Album:
allOf:
- $ref: '#/components/schemas/AlbumSummary'
- type: object
required: [categories]
properties:
categories:
type: array
description: 相册分类(按 sortOrder 升序)。
items:
$ref: '#/components/schemas/Category'
# ── 资产 ──
AssetKind:
type: string
description: |
相册资产类型(技术实施方案 §4 album_assets.kind)。banner 为观众页宣传图
(PublicAlbum.banner,P1b 消费方);cover/logo 建表与上传链路就绪,展示
消费随后续阶段。
enum:
- banner
- cover
- logo
AssetInitRequest:
type: object
description: |
资产上传初始化请求;大小与类型白名单与照片直传一致(UPLOAD_MAX_BYTES
环境策略,逐请求惰性读取)。locale 为可选语言维度:同一 (kind, locale)
的再次上传即替换(新行生效);语言无关的资产(如 logo)可不传。
required: [kind, filename, bytes, contentType]
properties:
kind:
$ref: '#/components/schemas/AssetKind'
locale:
$ref: '#/components/schemas/Locale'
filename:
type: string
maxLength: 255
description: 原始文件名(仅元数据,不作对象唯一标识)。
bytes:
type: integer
format: int64
minimum: 1
description: 申明文件字节数;完成核验时逐一比对。
contentType:
type: string
enum: [image/jpeg, image/png, image/heic]
description: MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。
AssetInitResponse:
type: object
required: [assetId, objectKey, credential]
properties:
assetId:
type: string
readOnly: true
description: 资产 ID(完成核验的寻址凭据,一次性会话)。
objectKey:
type: string
readOnly: true
description: staging 对象键(photo-live/{env}/albums/{albumId}/assets/{assetId}.{ext})。
credential:
$ref: '#/components/schemas/UploadCredential'
AssetCompleteResponse:
allOf:
- $ref: '#/components/schemas/SignedUrl'
- type: object
required: [assetId]
properties:
assetId:
type: string
readOnly: true
description: 资产 ID。
# ── 成员 ──
MemberAddRequest:
type: object
required: [userId]
properties:
userId:
type: string
description: 被添加用户的 ID(须为已开通账号)。
role:
type: string
default: collaborator
description: 成员角色;P1a 仅协作者(owner 由创建流程产生)。
enum:
- collaborator
Member:
type: object
required: [userId, account, displayName, role, status]
properties:
userId:
type: string
readOnly: true
description: 用户 ID。
account:
type: string
readOnly: true
description: 登录账号。
displayName:
type: string
readOnly: true
description: 展示姓名(来自用户资料同步)。
role:
type: string
readOnly: true
description: 相册角色。
enum:
- owner
- collaborator
status:
type: string
readOnly: true
description: 成员状态;P1a 仅 active,预留扩展。
enum:
- active
MemberList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/Member'
# ── 分类 ──
CategoryTranslation:
type: object
required: [locale, name]
properties:
locale:
$ref: '#/components/schemas/Locale'
name:
type: string
minLength: 1
description: 分类名称。
CategoryCreateRequest:
type: object
required: [translations]
properties:
sortOrder:
type: integer
default: 0
description: 排序序号(升序)。
translations:
type: array
minItems: 1
description: 分类多语言名称。
items:
$ref: '#/components/schemas/CategoryTranslation'
CategoryUpdateRequest:
type: object
minProperties: 1
description: 部分更新;translations 为全量替换。
properties:
sortOrder:
type: integer
description: 排序序号(升序)。
translations:
type: array
minItems: 1
description: 全量替换多语言名称。
items:
$ref: '#/components/schemas/CategoryTranslation'
Category:
type: object
required: [id, albumId, sortOrder, translations]
properties:
id:
type: string
readOnly: true
description: 分类 ID。
albumId:
type: string
readOnly: true
description: 所属相册 ID。
sortOrder:
type: integer
description: 排序序号(升序)。
translations:
type: array
description: 各语言名称。
items:
$ref: '#/components/schemas/CategoryTranslation'
# ── 上传 ──
UploadSessionState:
type: string
description: |
上传会话状态(服务端视角;本地任务状态见技术实施方案 §5.5)。
会话没有 failed 状态:完成核验失败属于单次请求错误(UPLOAD_VERIFY_FAILED),
会话保持 pending 可重试;cancelled 为用户主动取消;
expired 为超过 expiresAt 未完成(核验时推导或由补偿扫描落定)。
enum:
- pending
- confirmed
- cancelled
- expired
PhotoProcessingState:
type: string
description: 照片服务端处理状态(技术实施方案 §5.5「服务端处理」链路)。
enum:
- pending_upload
- uploaded
- processing
- ready
- processing_failed
PhotoVisibility:
type: string
description: 照片对外可见性(技术实施方案 §5.5「对外可见性」;deleted 为软删除)。
enum:
- unpublished
- published
- hidden
- deleted
UploadInitRequest:
type: object
required: [albumId, clientTaskId, filename, bytes, contentType]
properties:
albumId:
type: string
description: 目标相册 ID。
categoryId:
type: string
description: 目标分类 ID;缺省为相册默认分类。
clientTaskId:
type: string
maxLength: 128
description: 客户端任务幂等键;同一任务的重试必须复用(会话与照片 ID 均复用)。
filename:
type: string
maxLength: 255
description: 原始文件名(仅元数据,不作对象唯一标识)。
bytes:
type: integer
format: int64
minimum: 1
description: 申明文件字节数;完成核验时逐一比对。
contentType:
type: string
enum: [image/jpeg, image/png, image/heic]
description: MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。
sourceDeviceId:
type: string
minLength: 8
maxLength: 64
description: |
客户端首次生成并持久化的设备标识;服务端历史返回用于区分上传来源
(A19)。可选;同一 clientTaskId 幂等重放时保留首次落库的值。
UploadCredential:
type: object
description: staging 对象的临时直传凭证;仅可写入指定 objectKey,不可访问 originals/variants。
required: [mode, url, method, headers, expiresAt]
properties:
mode:
type: string
readOnly: true
default: presigned
description: 凭证模式;P1a 仅预签名 URL,预留 STS 扩展。
enum:
- presigned
url:
type: string
readOnly: true
description: 预签名上传 URL。
method:
type: string
readOnly: true
description: 上传 HTTP 方法。
enum:
- PUT
headers:
type: object
readOnly: true
description: 上传时必须携带的请求头(如 Content-Type)。
additionalProperties:
type: string
expiresAt:
type: string
format: date-time
readOnly: true
description: 凭证过期时间;过期后续签(renew)再重试。
UploadInitResponse:
type: object
required: [uploadId, photoId, objectKey, credential]
properties:
uploadId:
type: string
readOnly: true
description: 上传会话 ID。
photoId:
type: string
readOnly: true
description: 预创建的照片 ID(重试复用,不重复建照片)。
objectKey:
type: string
readOnly: true
description: staging 对象键(photo-live/{env}/staging/{uploadId}/{photoId}.{ext})。
credential:
$ref: '#/components/schemas/UploadCredential'
UploadCompleteResponse:
type: object
required: [photoId, processingState]
properties:
photoId:
type: string
readOnly: true
description: 照片 ID。
processingState:
$ref: '#/components/schemas/PhotoProcessingState'
UploadSessionPhoto:
type: object
required: [photoId, processingState, visibility]
description: 会话关联照片的当前状态(会话记录内嵌,客户端据此展示服务端进度)。
properties:
photoId:
type: string
readOnly: true
description: 照片 ID。
processingState:
$ref: '#/components/schemas/PhotoProcessingState'
visibility:
$ref: '#/components/schemas/PhotoVisibility'
UploadSessionRecord:
type: object
required:
- uploadId
- albumId
- clientTaskId
- state
- expectedBytes
- contentType
- createdAt
- photo
properties:
uploadId:
type: string
readOnly: true
description: 上传会话 ID。
albumId:
type: string
readOnly: true
description: 目标相册 ID。
clientTaskId:
type: string
readOnly: true
description: 客户端任务幂等键。
state:
$ref: '#/components/schemas/UploadSessionState'
expectedBytes:
type: integer
format: int64
readOnly: true
description: 申明文件字节数。
contentType:
type: string
readOnly: true
description: 申明 MIME 类型。
createdAt:
type: string
format: date-time
readOnly: true
description: 会话创建时间。
sourceDeviceId:
type: [string, 'null']
readOnly: true
description: |
发起该上传的设备标识(客户端首次生成并持久化;init 时未携带为 null)。
P3 Task 3 契约增补(A19 设备来源语义)。
photo:
$ref: '#/components/schemas/UploadSessionPhoto'
UploadSessionList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/UploadSessionRecord'
# ── 照片(管理端) ──
PhotoVisibilityUpdateRequest:
type: object
required: [visibility]
properties:
visibility:
type: string
description: 目标可见性;仅允许 hidden 与 published 间切换。
enum:
- hidden
- published
PhotoRecord:
type: object
description: 管理端照片记录(技术实施方案 §4 photos 表字段)。
required:
- photoId
- albumId
- categoryId
- uploaderId
- visibility
- processingState
- bytes
- width
- height
- shotAt
- uploadedAt
- publishedAt
- likeCount
properties:
photoId:
type: string
readOnly: true
description: 照片 ID。
albumId:
type: string
readOnly: true
description: 所属相册 ID。
categoryId:
type: [string, 'null']
readOnly: true
description: 分类 ID;null 表示未分类。
uploaderId:
type: string
readOnly: true
description: 上传人用户 ID。
visibility:
$ref: '#/components/schemas/PhotoVisibility'
processingState:
$ref: '#/components/schemas/PhotoProcessingState'
bytes:
type: integer
format: int64
readOnly: true
description: 原图字节数。
width:
type: [integer, 'null']
readOnly: true
description: 原图宽度(像素);处理前为 null。
height:
type: [integer, 'null']
readOnly: true
description: 原图高度(像素);处理前为 null。
shotAt:
type: [string, 'null']
format: date-time
readOnly: true
description: 拍摄时间(EXIF);无有效值时为 null。
uploadedAt:
type: string
format: date-time
readOnly: true
description: 上传完成时间。
publishedAt:
type: [string, 'null']
format: date-time
readOnly: true
description: 发布时间;未发布为 null。
likeCount:
type: integer
readOnly: true
minimum: 0
description: 累计点赞数。
AdminPhotoUploader:
type: object
description: 管理列表条目内嵌的上传人脱敏信息(不含邮箱、手机等凭据字段)。
required: [userId, account, displayName]
properties:
userId:
type: string
readOnly: true
description: 上传人用户 ID(与 PhotoRecord.uploaderId 一致)。
account:
type: string
readOnly: true
description: 登录账号。
displayName:
type: string
readOnly: true
description: 展示姓名(来自用户资料)。
AdminPhoto:
allOf:
- $ref: '#/components/schemas/PhotoRecord'
- type: object
required: [uploader]
properties:
uploader:
$ref: '#/components/schemas/AdminPhotoUploader'
AdminPhotoList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/AdminPhoto'
# ── 管理辅助 ──
AdminUserLookupItem:
type: object
description: 账号查询条目(脱敏:仅邀请协作者所需的最小字段,不含邮箱/手机/状态)。
required: [id, account, displayName]
properties:
id:
type: string
readOnly: true
description: 用户 ID(用于 POST /albums/{albumId}/members 的 userId)。
account:
type: string
readOnly: true
description: 登录账号。
displayName:
type: string
readOnly: true
description: 展示姓名。
AdminUserLookupList:
type: object
description: 账号前缀查询结果;无游标(前缀搜索 + 小上限即取即用)。
required: [items]
properties:
items:
type: array
description: 匹配的激活用户(按账号升序)。
items:
$ref: '#/components/schemas/AdminUserLookupItem'
# ── 公开(观众端) ──
SignedUrl:
type: object
description: 私有对象的短期签名 URL;不得写入持久分享链接,过期后经原入口刷新。
required: [url, expiresAt]
properties:
url:
type: string
readOnly: true
description: 签名访问 URL。
expiresAt:
type: string
format: date-time
readOnly: true
description: URL 失效时间。
PublicAlbumTranslation:
type: object
description: 按请求 locale 回退解析后的单语言相册内容(locale 为实际返回语言)。
required: [locale, title, description, venue, address]
properties:
locale:
$ref: '#/components/schemas/Locale'
title:
type: string
description: 相册标题。
description:
type: [string, 'null']
description: 相册描述;无内容为 null。
venue:
type: [string, 'null']
description: 场地名称;无内容为 null。
address:
type: [string, 'null']
description: 场地地址;无内容为 null。
PublicCategory:
type: object
description: 公开分类(名称已按请求 locale 回退解析)。
required: [id, name, sortOrder]
properties:
id:
type: string
readOnly: true
description: 分类 ID。
name:
type: string
readOnly: true
description: 分类名称。
sortOrder:
type: integer
readOnly: true
description: 排序序号(升序)。
PublicAlbum:
type: object
description: 观众端相册信息(不泄露工作人员信息与内部存储键)。
required:
- publicToken
- resolvedLocale
- defaultLocale
- timezone
- startAt
- endAt
- translation
- categories
- banner
- viewCount
properties:
publicToken:
type: string
readOnly: true
pattern: '^[0-9a-z]{20,32}$'
description: 相册公开令牌。
resolvedLocale:
$ref: '#/components/schemas/Locale'
defaultLocale:
$ref: '#/components/schemas/Locale'
timezone:
type: string
readOnly: true
description: 活动 IANA 时区(展示用,不随请求语言变化)。
startAt:
type: string
format: date-time
readOnly: true
description: 活动开始时间(UTC,客户端按时区格式化)。
endAt:
type: [string, 'null']
format: date-time
readOnly: true
description: 活动结束时间;未定为 null。
translation:
$ref: '#/components/schemas/PublicAlbumTranslation'
categories:
type: array
description: 分类列表(按 sortOrder 升序)。
items:
$ref: '#/components/schemas/PublicCategory'
banner:
description: 封面图签名 URL;未设置为 null。
anyOf:
- $ref: '#/components/schemas/SignedUrl'
- type: 'null'
viewCount:
type: [integer, 'null']
format: int64
minimum: 0
readOnly: true
description: |
累计浏览数。cn 部署返回 null(P1a 占位,Task 9 起由统计副本回填);
ov 部署返回 album_stats 权威累计值(无行默认 0;计数由 /visits
端点维护,技术实施方案 §7.1)。
PublicPhoto:
type: object
description: 公开照片条目(仅 published 照片,处理完成即有宽高;URL 为短期签名,需在过期前刷新)。
required: [photoId, width, height, thumb, browse, likeCount, publishedAt]
properties:
photoId:
type: string
readOnly: true
description: 照片 ID。
width:
type: integer
readOnly: true
description: 浏览图宽度(像素);用于列表占位,减少加载跳动。
height:
type: integer
readOnly: true
description: 浏览图高度(像素)。
thumb:
$ref: '#/components/schemas/SignedUrl'
browse:
$ref: '#/components/schemas/SignedUrl'
likeCount:
type: integer
readOnly: true
minimum: 0
description: 累计点赞数。
publishedAt:
type: string
format: date-time
readOnly: true
description: 发布时间(列表排序依据)。
PublicPhotoDetail:
allOf:
- $ref: '#/components/schemas/PublicPhoto'
- type: object
required: [categoryId]
properties:
categoryId:
type: [string, 'null']
readOnly: true
description: 所属分类 ID;未分类为 null。
PublicPhotoList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/PublicPhoto'
# ── 互动(观众端,ov 域) ──
LikeBatchRequest:
type: object
description: |
点赞批次上报请求。sessionId 为客户端生成的会话随机标识(会话期间复用);
batchId 为批次幂等键,同一批的失败重试必须复用,跨批次不得复用。
required: [sessionId, batchId, delta]
properties:
sessionId:
type: string
minLength: 1
maxLength: 64
description: 会话标识(客户端随机生成,会话期间复用)。
batchId:
type: string
minLength: 1
maxLength: 64
description: 批次幂等键;同批重试复用,重复提交不加计。
delta:
type: integer
minimum: 1
maximum: 20
description: 本批新增点赞数(客户端短窗合并后的累计点击数)。
LikeBatchResponse:
type: object
description: |
点赞批次处理结果。total 恒为服务端权威累计值;batchAccepted 区分
本批是否被计入(幂等重复=true;窗口外过期批次=false 且不加计)。
required: [total, countVersion, batchAccepted]
properties:
total:
type: integer
format: int64
readOnly: true
minimum: 0
description: 照片权威累计点赞数(客户端据此校准乐观计数)。
countVersion:
type: integer
readOnly: true
minimum: 0
description: 计数版本(单调递增,实时消息与校准的乱序判别依据)。
batchAccepted:
type: boolean
readOnly: true
description: |
本批是否被计入:true=新批次或幂等重放已确认批次;
false=窗口外过期批次(拒绝重放,返回权威 total)。
VisitReportRequest:
type: object
description: |
浏览上报请求。visitorId 跨标签页共享(同一访客);requestId 为本次
访次的上报幂等键(建议 UUID),counted 确认前持久复用。
required: [visitorId, requestId]
properties:
visitorId:
type: string
minLength: 8
maxLength: 128
description: 访客标识(客户端持久化并在标签页间共享)。
requestId:
type: string
minLength: 8
maxLength: 128
description: 上报幂等键;重复提交不重复计数。
VisitReportResponse:
type: object
description: 浏览上报结果。viewCount 恒为相册权威累计浏览数。
required: [viewCount, counted]
properties:
viewCount:
type: integer
format: int64
readOnly: true
minimum: 0
description: 相册权威累计浏览数。
counted:
type: boolean
readOnly: true
description: |
本次是否计入:true=新访次,或幂等重放已确认计入的 requestId
(回显原结局);false=去重窗口内重复或过期 requestId 重放
(均不重复计数)。
# ── 内部同步 ──
SyncEntityType:
type: string
description: 同步实体类型(slot-state 为预留给后续阶段)。
enum:
- album
- category
- photo
- asset
- slot-state
SyncPriority:
type: string
description: |
推送优先级:high 为相册下线、照片隐藏/删除等需即时生效的事件
(发送器即发,不等批间隔);normal 走常规批量推送。
enum:
- normal
- high
SyncEvent:
type: object
description: |
cn→ov 内容同步事件(线格式)。与 cn outbox 事件(src/events.ts)
一一对应:eventId 幂等、entityVersion 单调、tombstone 墓碑优先。
payload 为实体类型相关载荷(album.updated 携带 translations/开关/
banner 对象键,photo.* 携带对象键与宽高等展示字段)。
required:
- eventId
- schemaVersion
- entityType
- entityId
- albumId
- entityVersion
- priority
- payload
properties:
eventId:
type: string
description: 事件 ID(全局唯一,接收方幂等键)。
schemaVersion:
type: integer
minimum: 1
description: 载荷 schema 版本;当前为 1。
entityType:
$ref: '#/components/schemas/SyncEntityType'
entityId:
type: string
description: 实体 ID(albumId/categoryId/photoId/assetId 等)。
albumId:
type: string
description: 归属相册 ID(album 实体时与 entityId 相同)。
entityVersion:
type: integer
format: int64
minimum: 1
description: 实体版本(单调递增;接收方仅应用更高版本)。
tombstone:
type: boolean
default: false
description: 墓碑标记:true 表示实体删除,副本删除且不被旧事件复活。
priority:
$ref: '#/components/schemas/SyncPriority'
payload:
type: object
additionalProperties: true
description: 实体类型相关载荷;结构由发送方按 schemaVersion 约定。
ContentBatchRequest:
type: object
description: 内容事件批量推送请求体。
required: [batch]
properties:
batch:
type: array
minItems: 1
maxItems: 100
description: 事件批次(单批上限对齐 SYNC_BATCH_SIZE,默认 100)。
items:
$ref: '#/components/schemas/SyncEvent'
ContentBatchResponse:
type: object
description: 批量推送受理结果。
required: [acceptedEventIds, applied]
properties:
acceptedEventIds:
type: array
description: 本批受理的事件 ID(含此前已应用的同 eventId 幂等重复)。
items:
type: string
applied:
type: integer
description: 本批实际应用(新版本生效)的条数。
SyncStatKind:
type: string
description: 统计快照种类。
enum:
- likes
- views
StatSnapshot:
type: object
description: |
ov→cn 统计快照:权威累计值的版本化替换单元(cn 侧仅更高 statVersion
替换副本列,绝不相加)。
required: [albumId, kind, total, statVersion, source, confirmedAt]
properties:
photoId:
type: string
description: 照片 ID;kind=likes 时必填,kind=views 无此字段。
albumId:
type: string
description: 归属相册 ID。
kind:
$ref: '#/components/schemas/SyncStatKind'
total:
type: integer
format: int64
minimum: 0
description: 权威累计值(点赞数或浏览数)。
statVersion:
type: integer
format: int64
minimum: 1
description: 统计版本(单调递增;接收方幂等与替换依据)。
source:
type: string
description: 快照来源标识(如 ov)。
confirmedAt:
type: string
format: date-time
description: 快照确认时间。
StatSnapshotRequest:
type: object
description: 统计快照回传请求体。
required: [snapshots]
properties:
snapshots:
type: array
minItems: 1
description: 增量快照批次。
items:
$ref: '#/components/schemas/StatSnapshot'
StatSnapshotResponse:
type: object
description: 快照回传受理结果。
required: [accepted]
properties:
accepted:
type: integer
description: 接受(应用或幂等确认)的快照条数;旧版本被忽略的不计。
ReconcileRequest:
type: object
description: 对账拉取请求:某实体类型在 sinceVersion 之后的事件清单。
required: [entityType, sinceVersion, limit]
properties:
entityType:
$ref: '#/components/schemas/SyncEntityType'
albumId:
type: string
description: 可选:限定单相册;缺省为该实体类型全部。
sinceVersion:
type: integer
format: int64
minimum: 0
description: 起始版本(exclusive;0 表示从头拉取,配合 bootstrap)。
limit:
type: integer
minimum: 1
maximum: 1000
default: 100
description: 本批拉取上限(分批重放,对齐 SYNC_BATCH_SIZE)。
ReconcileResponse:
type: object
description: |
对账结果:按 entityVersion 升序的**当前状态事件**(可直接重放的完整
SyncEvent,非仅版本清单——P2 Task 4 设计决策:重放走既有
content-batches 管道,无需第二条拉取通道;sinceVersion 之后无实体时为
空数组)。
required: [events]
properties:
events:
type: array
description: 当前状态事件(升序);请求方按序经 content-batches 重放。
items:
$ref: '#/components/schemas/SyncEvent'
BootstrapRequest:
type: object
description: 全量快照引导请求。
properties:
albumId:
type: string
description: 指定相册;缺省为全部相册。
BootstrapResponse:
type: object
description: 全量快照规模与水位;事件本体经 reconcile 通道分批拉取。
required: [eventCount, watermark]
properties:
eventCount:
type: integer
minimum: 0
description: 快照事件总数(供拉取分批与完成校验)。
watermark:
type: integer
format: int64
minimum: 0
description: 快照水位:后续增量对账的 sinceVersion 起点。
DigestRequest:
type: object
description: 对账摘要请求(P2 Task 4)。
properties:
albumId:
type: string
description: 指定相册;缺省为全部相册。
AlbumDigest:
type: object
description: |
单相册权威计数摘要。ov 侧比对口径:albums_ov.categories 元素数应等于
categoryCount;photos_ov 行数落在 [publishedPhotoCount, photoCount]
区间内即视为一致(hidden/deleted 照片若从未以 published 态同步过,ov
合法地无行——缺席即撤下语义);相册行缺失即不一致。assetCount 供观测
(ov 无 asset 副本行,不参与比对)。
required:
- albumId
- categoryCount
- publishedPhotoCount
- photoCount
- assetCount
properties:
albumId:
type: string
description: 相册 ID。
categoryCount:
type: integer
minimum: 0
description: 当前分类数(cn 权威行数)。
publishedPhotoCount:
type: integer
minimum: 0
description: 可见(published)照片数——ov 必须持有的副本行数下界。
photoCount:
type: integer
minimum: 0
description: published+hidden+deleted 照片总数——ov 副本行数上界。
assetCount:
type: integer
minimum: 0
description: 生效(effective)资产数(观测用;ov 无 asset 副本行)。
DigestResponse:
type: object
description: 每相册计数摘要(albumId 升序)。
required: [albums]
properties:
albums:
type: array
description: 相册摘要清单;空库为空数组。
items:
$ref: '#/components/schemas/AlbumDigest'
EmergencyDisableRequest:
type: object
description: 紧急停用请求(albumToken 与 albumId 恰好提供其一)。
required: [reason]
properties:
albumToken:
type: string
description: 公开令牌定位相册(与 albumId 二选一)。
albumId:
type: string
description: 相册 ID 定位(与 albumToken 二选一)。
reason:
type: string
minLength: 1
maxLength: 500
description: 停用原因(审计留痕)。
EmergencyEnableRequest:
type: object
description: 解除停用请求(albumToken 与 albumId 恰好提供其一)。
properties:
albumToken:
type: string
description: 公开令牌定位相册(与 albumId 二选一)。
albumId:
type: string
description: 相册 ID 定位(与 albumToken 二选一)。
EmergencyStatus:
type: object
description: 紧急停用通道操作结果。
required: [disabled, albumId]
properties:
disabled:
type: boolean
description: 操作后的停用状态(disable 恒 true / enable 恒 false)。
albumId:
type: string
description: 相册 ID。