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: embed-slots description: | 展示位管理(P4b 悬浮轮播组件挂载点):创建、设置与相册绑定/解绑。 权限简化决策:仅公司 admin 可管理展示位(成员授权面 P5 按需补); 无删除端点——enabled=false 即下线(软停用,公开端随即 404)。 - 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' /albums/{albumId}/watermark-jobs: parameters: - $ref: '#/components/parameters/AlbumId' post: operationId: createWatermarkJob tags: [albums] summary: 保存水印配置并发起历史重生成 description: | 保存新的水印配置版本(configVersion 递增)并创建历史照片重生成任务 (A18/A20)。仅创建者或管理员可操作。语义约定: 新照片发布默认套用最新版本;历史照片重生成须显式经本端点发起; 任务为原子切换——全部照片处理完成前旧浏览图保持可用(失败照片保留 旧图,任务可重试);原图(original)永不覆盖。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WatermarkConfig' responses: '202': description: 配置已保存、重生成任务已受理(异步执行,进度经 GET 查询)。 content: application/json: schema: $ref: '#/components/schemas/WatermarkJobCreated' '400': description: 参数校验失败(VALIDATION_FAILED;opacity/scalePercent/fontSize 越界等)。 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' get: operationId: getWatermarkJobs tags: [albums] summary: 水印配置版本与重生成任务状态 description: | 当前生效配置(current,从未配置为 null)与历史任务列表(创建时间倒序)。 任务进度(progress/total/failed)供前端轮询;state=failed 的任务可经 POST 重新发起(按照片粒度幂等续跑)。 security: - bearerAuth: [] responses: '200': description: 当前配置与任务列表。 content: application/json: schema: $ref: '#/components/schemas/WatermarkJobsStatus' '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 稳定倒序 游标分页。 scope(UI-01 可选扩展,缺省 all=既有行为不变):协作者显式查询自己 上传的照片——服务端过滤 uploaderId=当前用户,成员资格逐请求校验; 仅 owner/admin 可用 all(协作者 all → 403)。条目 preview 按请求者 权限逐张裁决(见 PhotoPreview)。 security: - bearerAuth: [] parameters: - name: visibility in: query description: 可见性过滤;缺省为全部状态。 schema: $ref: '#/components/schemas/PhotoVisibility' - name: categoryId in: query description: 分类 ID 过滤。 schema: type: string - name: scope in: query description: | 数据范围(UI-01):all=本相册全部照片(缺省,仅 owner/admin); own=仅当前用户上传(任何有效成员,服务端过滤)。 schema: type: string enum: - all - own default: all - $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: | 仅创建者或管理员可访问全量管理列表(scope=all 或缺省时协作者 AUTH_FORBIDDEN);scope=own 需相册有效成员。 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}/photos/hide-batch: parameters: - $ref: '#/components/parameters/AlbumId' post: operationId: hidePhotosBatch tags: [photos] summary: 批量隐藏照片 description: | 批量将 published 照片置为 hidden(A16)。逐项返回结果:不存在、已删除 或状态不可切换单项 ok=false 并携带 code,不影响其余项(部分成功语义)。 单批上限 200(minItems/maxItems)。权限与 403/404 语义同管理照片列表 (仅创建者或管理员;ALBUM_NOT_FOUND 防枚举)。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PhotoHideBatchRequest' responses: '200': description: 批量处理结果(逐项;单项失败不影响其余项)。 content: application/json: schema: $ref: '#/components/schemas/BatchHideResult' '400': description: 参数校验失败(VALIDATION_FAILED;photoIds 空/超 200/含空串)。 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/albums/{albumId}/transfer: parameters: - $ref: '#/components/parameters/AlbumId' post: operationId: transferAlbum tags: [admin] summary: 相册负责人交接 description: | 将相册 owner 移交给目标用户(A17)。仅 admin 可操作(负责人交接为 组织级治理动作,区别于相册级配置修改)。keepAsCollaborator=true 时 原 owner 转为 collaborator 成员,否则原 owner 移出成员表。历史上传人 不变(photos.uploaderId 不动);审计 album.transfer。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AlbumTransferRequest' responses: '200': description: 交接后的相册详情(ownerId 已变更)。 content: application/json: schema: $ref: '#/components/schemas/Album' '400': description: | 参数校验失败(VALIDATION_FAILED;targetUserId 非法,或目标账号 不可被指定为 owner——不存在/停用/待激活/锁定)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可交接相册负责人(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' /admin/users: get: operationId: listAdminUsers tags: [admin] summary: 用户查询与管理列表 description: | 双模式统一端点(P4a 决策:与 P1b 账号前缀查询合一,响应统一 AdminUserList——条目为 AdminUser 字段超集,管理端 UI 按场景裁剪展示): · 前缀模式(提供 accountPrefix):邀请协作者前置查询,沿用 P1b 语义 ——大小写敏感前缀匹配(服务端按二进制校对执行 LIKE prefix%)、 仅「激活资格」用户(credentialStatus=active 且 sourceStatus≠disabled 且 !localLocked,与成员添加的目标校验一致)、account 升序; 访问策略沿用 P1b 决策:任何已认证工作人员可用(邀请发起方不限于 创建者/管理员,泄露面为已认证内部人员,风险可接受)。 · 管理列表模式(未提供 accountPrefix):全量用户治理列表(游标分页, 按 createdAt 与 ID 稳定倒序),可选 query/status 过滤(不做全文 搜索);仅 admin 可用(非 admin 403)。 两模式响应形状一致(AdminUserList);前缀模式小上限即取即用, hasMore/nextCursor 同样按游标语义填充。 security: - bearerAuth: [] parameters: - name: accountPrefix in: query description: 账号前缀(≥2 字符以防过宽匹配;提供即进入前缀模式)。 schema: type: string minLength: 2 maxLength: 64 - name: query in: query description: | 管理列表过滤:账号或展示名前缀(大小写敏感二进制前缀匹配,同 accountPrefix 规则;不做全文搜索)。 schema: type: string minLength: 1 maxLength: 64 - name: status in: query description: 管理列表过滤:治理状态单值归类(归类优先级见 AdminUserStatusFilter)。 schema: $ref: '#/components/schemas/AdminUserStatusFilter' - $ref: '#/components/parameters/CursorQuery' - name: limit in: query description: 每页条数(游标模式)。 schema: type: integer minimum: 1 maximum: 100 default: 20 - name: page in: query description: | 页码(偏移分页模式):提供 page 且不带 cursor 时启用,响应返回 total 且 nextCursor=null / hasMore=false(翻页由 total 驱动)。 未提供 → 游标模式(keyset,兼容旧客户端);提供 cursor 时忽略 page。仅管理列表模式有意义。 schema: type: integer minimum: 1 - name: pageSize in: query description: 偏移分页每页条数(仅 page 模式生效,缺省 20)。 schema: type: integer minimum: 1 maximum: 100 default: 20 responses: '200': description: 用户列表页(前缀模式无匹配为空数组)。 content: application/json: schema: $ref: '#/components/schemas/AdminUserList' '400': description: | 查询参数校验失败(VALIDATION_FAILED;accountPrefix/query 长度非法、 status 取值非法、cursor/limit 非法)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 管理列表模式仅 admin 可用(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/users/{userId}: parameters: - $ref: '#/components/parameters/UserId' patch: operationId: updateAdminUser tags: [admin] summary: 更新用户治理属性 description: | 部分更新 role/localLocked/displayName;仅 admin。sourceStatus 只读 (上游状态仅由同步任务维护,本地不越权改写——技术方案 3.1A 红线, 需求侧只读展示)。localLocked 置 true 等价停用(撤销该用户全部会话); 置 false 仅解除本地锁定,不改变上游停用状态。审计全程。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AdminUserUpdateRequest' responses: '200': description: 更新后的用户条目。 content: application/json: schema: $ref: '#/components/schemas/AdminUser' '400': description: 参数校验失败(VALIDATION_FAILED;role 取值非法、displayName 长度非法、空请求体)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可管理用户(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: 目标用户不存在(NOT_FOUND)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/users/{userId}/reset-password: parameters: - $ref: '#/components/parameters/UserId' post: operationId: resetAdminUserPassword tags: [admin] summary: 管理员重置用户密码 description: | 仅 admin。body 可选 newPassword(8-128):缺省由服务端生成随机密码 (e2e 两种路径均覆盖)。重置成功后撤销该目标用户全部既有会话(须 重新登录);新密码仅在响应中返回一次(不入库明文、不留日志),管理 端一次性展示转交。审计全程。 security: - bearerAuth: [] requestBody: content: application/json: schema: $ref: '#/components/schemas/AdminPasswordResetRequest' responses: '200': description: 重置成功(newPassword 为请求指定或服务端生成)。 content: application/json: schema: $ref: '#/components/schemas/AdminPasswordResetResponse' '400': description: 参数校验失败(VALIDATION_FAILED;newPassword 长度非法)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可重置密码(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: 目标用户不存在(NOT_FOUND)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/users/{userId}/activate: parameters: - $ref: '#/components/parameters/UserId' post: operationId: activateAdminUser tags: [admin] summary: 激活待激活用户 description: | 仅 admin。credentialStatus pending→active(闭环用户同步创建的待激活 账号,§10 语义)。激活本身不出凭据:初始密码经 reset-password 出具 (推荐流程 activate → reset-password 生成一次性初始密码 → 用户登录)。 幂等:对已是 active 的用户重复激活返回当前条目。 security: - bearerAuth: [] responses: '200': description: 激活后的用户条目(credentialStatus=active)。 content: application/json: schema: $ref: '#/components/schemas/AdminUser' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可激活用户(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: 目标用户不存在(NOT_FOUND)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/users/{userId}/deactivate: parameters: - $ref: '#/components/parameters/UserId' post: operationId: deactivateAdminUser tags: [admin] summary: 停用用户 description: | 仅 admin。localLocked=true 并撤销该用户全部会话(既有令牌即刻 401)。 上游状态不动(sourceStatus 保持;上游恢复 active 不解除本地锁定—— 本地锁定独立,技术方案 3.1A)。重新启用经 PATCH localLocked=false。 审计全程。 security: - bearerAuth: [] responses: '200': description: 停用后的用户条目(localLocked=true)。 content: application/json: schema: $ref: '#/components/schemas/AdminUser' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可停用用户(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: 目标用户不存在(NOT_FOUND)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/user-sync/status: get: operationId: getUserSyncStatus tags: [admin] summary: 用户同步状态 description: | 仅 admin。同步引擎当前状态、最近批次计数与源水位(A29)。当前为 mock 源(G4:真实接口到位后仅替换 provider,契约不变)。 security: - bearerAuth: [] responses: '200': description: 同步状态。 content: application/json: schema: $ref: '#/components/schemas/UserSyncStatus' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可查看同步状态(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/user-sync/runs: parameters: - $ref: '#/components/parameters/CursorQuery' - name: limit in: query description: 每页条数。 schema: type: integer minimum: 1 maximum: 100 default: 20 get: operationId: listUserSyncRuns tags: [admin] summary: 同步运行历史 description: 仅 admin。历史批次列表(startedAt 与 ID 稳定倒序游标分页)。 security: - bearerAuth: [] responses: '200': description: 运行历史页。 content: application/json: schema: $ref: '#/components/schemas/UserSyncRunList' '400': description: 查询参数校验失败(VALIDATION_FAILED;cursor/limit 非法)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可查看运行历史(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' post: operationId: triggerUserSyncRun tags: [admin] summary: 手动触发同步补拉 description: | 仅 admin。手动发起一次同步(与定时调度共用互斥锁;已有批次运行中时 不重复执行,返回进行中批次的 runId)。202 表示已受理,异步执行, 结果经 status/runs 端点查询。 security: - bearerAuth: [] responses: '202': description: 已受理(或复用进行中批次)。 content: application/json: schema: $ref: '#/components/schemas/UserSyncRunCreated' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可手动触发(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/user-sync/errors: get: operationId: listUserSyncErrors tags: [admin] summary: 同步失败明细 description: | 仅 admin。单批次失败行明细(state=partial/failed 批次的坏行记录, 其余行照常应用的证据链);runId 必填。 security: - bearerAuth: [] parameters: - name: runId in: query required: true description: 批次 ID。 schema: type: string minLength: 1 responses: '200': description: 失败明细(createdAt 倒序;无失败为空数组)。 content: application/json: schema: $ref: '#/components/schemas/UserSyncErrorList' '400': description: 查询参数校验失败(VALIDATION_FAILED;runId 缺失)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可查看失败明细(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/usage/summary: get: operationId: getUsageSummary tags: [admin] summary: 用量汇总与阈值告警 description: | 仅 admin。按相册聚合存储用量(原图 + 变体,实时 SUM——P1 规模够用; P5 接 OSS 用量 API/物化视图,登记偏差)、当前阈值与告警列表 (A23 后台侧;告警接收人通知渠道 P5)。 security: - bearerAuth: [] responses: '200': description: 用量汇总。 content: application/json: schema: $ref: '#/components/schemas/UsageSummary' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可查看用量(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/usage/thresholds: put: operationId: updateUsageThresholds tags: [admin] summary: 更新用量阈值 description: | 仅 admin。部分更新:缺省字段保持不变,显式 null 清除该阈值(清除后 对应告警维度停用)。修改后即刻重新评估(超阈产生新告警、阈值上调将 已回落旧告警标记 resolvedAt)。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UsageThresholdsUpdate' responses: '200': description: 更新后的阈值。 content: application/json: schema: $ref: '#/components/schemas/UsageThresholds' '400': description: 参数校验失败(VALIDATION_FAILED;负值或非整数)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可更新阈值(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/embed-slots: get: operationId: listEmbedSlots tags: [embed-slots] summary: 展示位列表 description: | 展示位(悬浮轮播组件挂载点)治理列表,游标分页(按 createdAt 与 ID 稳定倒序);仅公司 admin 可用(P4b 权限简化:成员授权面 P5 按需补)。 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/EmbedSlotList' '400': description: 查询参数校验失败(VALIDATION_FAILED;cursor/limit 非法)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' post: operationId: createEmbedSlot tags: [embed-slots] summary: 创建展示位 description: | 创建展示位(仅 admin):服务端生成不可枚举 publicToken(嵌入代码 data-slot 引用);初始 enabled=true、albumId=null(未绑定,公开端 404)、configVersion=1。settings 整体缺省时按各字段缺省值落库。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmbedSlotCreateRequest' responses: '201': description: 创建后的展示位条目。 content: application/json: schema: $ref: '#/components/schemas/EmbedSlot' '400': description: 请求体校验失败(VALIDATION_FAILED;name 长度非法、settings 字段越界或取值非法)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '500': $ref: '#/components/responses/InternalError' /admin/embed-slots/{id}: parameters: - $ref: '#/components/parameters/EmbedSlotId' patch: operationId: updateEmbedSlot tags: [embed-slots] summary: 更新展示位 description: | 展示位部分更新(仅 admin):name/enabled/settings/albumId 任一管理面 变更均递增 configVersion 并触发 slot-state 同步事件(嵌入端配置版本 变化即整体重置渲染)。albumId 语义:非 null 为绑定/换绑目标相册 (相册须存在,不存在 404 ALBUM_NOT_FOUND),null 为解绑(公开端随即 404);albumId 值本身变化(含换绑与解绑)即视为配置变更。settings 为整体替换(提供时字段齐全)。无删除端点:enabled=false 即下线。 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/EmbedSlotUpdateRequest' responses: '200': description: 更新后的展示位条目。 content: application/json: schema: $ref: '#/components/schemas/EmbedSlot' '400': description: 参数校验失败(VALIDATION_FAILED;name 长度非法、settings 字段越界或取值非法、空请求体)。 content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': description: 仅 admin 可管理展示位(AUTH_FORBIDDEN)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: 展示位不存在(NOT_FOUND),或绑定目标相册不存在(ALBUM_NOT_FOUND)。 content: application/json: schema: $ref: '#/components/schemas/Error' '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' /public/embed-slots/{slotToken}: parameters: - $ref: '#/components/parameters/EmbedSlotToken' get: operationId: getPublicEmbedSlot tags: [public] summary: 展示位公开数据(嵌入组件数据源) description: | 悬浮轮播组件的数据源(ov 域部署,匿名只读;嵌入 iframe 轮询调用): 返回展示位绑定相册的近期已发布照片(≤20,按 publishedAt 与 ID 稳定 倒序,小尺寸缩略图省流量)与相册摘要。lang 语义:auto(缺省)或取值 不支持时按相册 defaultLocale 回退(不报 400,与公开相册一致)。 settings(UI-01)返回后台配置的归一化全集(嵌入端应用后台配置, data-* 覆盖优先)。 - 防探测:slotToken 不存在、展示位停用(enabled=false)或未绑定相册 一律 404 NOT_FOUND(同码同文案,不区分原因,避免枚举探测)。 - 条件请求(A23 省流量):200/304 响应携带强 ETag,取值为四元组 `"{configVersion}-{photoCount}-{maxPublishedAtMs}-{maxPhotoId}"` (含照片条数与最新发布时间——新照片发布必变,避免仅比对实体版本 时新照片小版本导致 304 陈旧);请求携带 If-None-Match 且匹配时 返回 304(无响应体,仍携带 ETag)。 security: [] parameters: - name: lang in: query description: 内容语言;auto(缺省)按相册 defaultLocale,取值不支持时静默回退(不报 400)。 schema: $ref: '#/components/schemas/EmbedSlotLang' - name: If-None-Match in: header description: 条件请求:先前响应返回的 ETag 值;匹配则返回 304。 schema: type: string responses: '200': description: 展示位公开数据。 headers: ETag: description: 强校验器(组成与失效语义见端点 description)。 schema: type: string content: application/json: schema: $ref: '#/components/schemas/PublicEmbedSlot' '304': description: 内容未变(If-None-Match 匹配);无响应体。 headers: ETag: description: 当前 ETag(与匹配值一致)。 schema: type: string '400': description: 查询参数校验失败(VALIDATION_FAILED;lang 取值不支持时不报错,按相册 defaultLocale 回退)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: 展示位不存在、已停用或未绑定相册(NOT_FOUND;不区分以避免枚举)。 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' /public/albums/{token}/photos/{photoId}/download: parameters: - $ref: '#/components/parameters/AlbumPublicToken' - $ref: '#/components/parameters/PhotoId' get: operationId: downloadPublicPhoto tags: [interactions] summary: 公开照片下载 description: | 匿名下载(ov 域部署,观众动作端点故归 interactions)。按相册两级下载 开关校验(A18):variant=browse 需 downloadEnabled;variant=original 需 originalDownloadEnabled(在 downloadEnabled 基础上的更高级开关, 两者均开才可下载原图)。对应级别开关关闭 → 403 DOWNLOAD_DISABLED; 相册/照片不可见 404(ALBUM_NOT_PUBLIC/PHOTO_NOT_PUBLIC,不区分枚举)。 URL 为短期签名凭证,禁止持久保存/写入分享链接,过期经本端点刷新; 服务端日志不记完整签名 URL。 security: [] parameters: - name: variant in: query required: true description: 下载版本。 schema: $ref: '#/components/schemas/DownloadVariant' responses: '200': description: 下载凭证(短期签名 URL)。 content: application/json: schema: $ref: '#/components/schemas/PublicPhotoDownload' '400': description: 查询参数校验失败(VALIDATION_FAILED;variant 缺失或取值非法)。 content: application/json: schema: $ref: '#/components/schemas/Error' '403': description: 相册下载开关关闭(DOWNLOAD_DISABLED;对应 variant 所需级别的开关未开)。 content: application/json: schema: $ref: '#/components/schemas/Error' '404': description: | 相册不存在或未公开(ALBUM_NOT_PUBLIC),或照片不存在、未发布、 不属于该相册(PHOTO_NOT_PUBLIC)。 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 EmbedSlotId: name: id in: path required: true description: 展示位 ID。 schema: type: string EmbedSlotToken: name: slotToken in: path required: true description: 展示位公开令牌(不可枚举,持有即可访问;格式见 EmbedSlot.publicToken)。 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: # ── 通用 ── ErrorCode: type: string description: | 稳定错误码(服务器生成,与 src/errors.ts 的 ErrorCodes 一一对应, 1:1 类型守护见 __tests__/events.test.ts);P4a 起供批量逐项结果等 内嵌失败原因复用。 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 - DOWNLOAD_DISABLED - VALIDATION_FAILED - SYNC_AUTH_FAILED - INTERNAL_ERROR Error: type: object required: [code, message, requestId] properties: code: $ref: '#/components/schemas/ErrorCode' 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)的同步时间不经相册行表达。 cover: description: | 相册封面摘要(UI-01,readOnly 可选扩展):当前生效封面资产 (album_assets kind=cover、state=effective 的最新一行)的短期 签名 URL;未配置封面为 null。列表端点按页批量组装(非逐相册 查询);旧客户端忽略本字段不受影响。 readOnly: true anyOf: - $ref: '#/components/schemas/SignedUrl' - type: 'null' 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' # ── 水印(P4a) ── WatermarkConfig: type: object description: | 水印配置(P4a A18,创建请求与当前配置响应共用)。enabled=false 表示 关闭水印(新照片不加水印),其余字段此时被忽略;可选字段缺省用服务端 默认值。 required: [enabled] properties: enabled: type: boolean description: 是否开启浏览图水印。 position: type: string description: 水印位置;缺省服务端默认。 enum: - topLeft - topRight - bottomLeft - bottomRight - center opacity: type: integer minimum: 0 maximum: 100 description: 不透明度百分比。 scalePercent: type: integer minimum: 5 maximum: 50 description: 水印尺寸相对浏览图宽度的百分比。 text: description: 文字水印参数;缺省不绘制文字。 allOf: - $ref: '#/components/schemas/WatermarkTextConfig' WatermarkTextConfig: type: object description: 文字水印参数(SVG 渲染实现,Task 3 登记)。 properties: content: type: string minLength: 1 maxLength: 64 description: 文字内容。 fontSize: type: integer minimum: 8 maximum: 72 description: 字号(像素)。 WatermarkJobCreated: type: object description: 重生成任务受理结果;configVersion 为本次保存的配置版本。 required: [jobId, configVersion] properties: jobId: type: string readOnly: true description: 重生成任务 ID。 configVersion: type: integer readOnly: true minimum: 1 description: 本次保存的水印配置版本(新照片即刻套用)。 WatermarkJobState: type: string description: 重生成任务状态;failed 可重新发起(按照片粒度幂等续跑)。 enum: - pending - running - done - failed WatermarkJob: type: object description: 历史重生成任务条目(进度供前端轮询)。 required: - jobId - configVersion - state - progress - total - failed - createdAt properties: jobId: type: string readOnly: true description: 任务 ID。 configVersion: type: integer readOnly: true minimum: 1 description: 目标配置版本。 state: $ref: '#/components/schemas/WatermarkJobState' progress: type: integer readOnly: true minimum: 0 description: 已处理张数(含失败)。 total: type: integer readOnly: true minimum: 0 description: 待处理总张数。 failed: type: integer readOnly: true minimum: 0 description: 失败张数。 errorCode: description: 任务失败原因(state=failed 时必有)。 allOf: - $ref: '#/components/schemas/ErrorCode' createdAt: type: string format: date-time readOnly: true description: 任务创建时间。 WatermarkCurrent: type: object description: 当前生效的水印配置。 required: [configVersion, config] properties: configVersion: type: integer readOnly: true minimum: 1 description: 当前配置版本(同 AlbumSummary.watermarkVersion)。 config: $ref: '#/components/schemas/WatermarkConfig' WatermarkJobsStatus: type: object description: 水印配置与任务状态;current 从未配置为 null(watermarkVersion=0)。 required: [current, jobs] properties: current: description: 当前生效配置;从未配置为 null。 anyOf: - $ref: '#/components/schemas/WatermarkCurrent' - type: 'null' jobs: type: array description: 历史任务(创建时间倒序)。 items: $ref: '#/components/schemas/WatermarkJob' # ── 资产 ── 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 PhotoHideBatchRequest: type: object description: 批量隐藏请求(P4a A16);逐项处理,部分成功。 required: [photoIds] properties: photoIds: type: array minItems: 1 maxItems: 200 description: 目标照片 ID 列表(单批上限 200,执行注意约定)。 items: type: string minLength: 1 BatchHideItem: type: object description: 单项隐藏结果;ok=false 时 code 说明原因(如 PHOTO_NOT_PUBLIC/PHOTO_NOT_FOUND)。 required: [photoId, ok] properties: photoId: type: string readOnly: true description: 照片 ID。 ok: type: boolean readOnly: true description: 本项是否隐藏成功。 code: description: 失败原因错误码(ok=false 时必有)。 allOf: - $ref: '#/components/schemas/ErrorCode' BatchHideResult: type: object description: 批量隐藏结果;results 与请求 photoIds 同序逐项对应。 required: [results] properties: results: type: array description: 逐项结果(与请求同序)。 items: $ref: '#/components/schemas/BatchHideItem' 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: 展示姓名(来自用户资料)。 PhotoPreview: type: object description: | 管理端照片预览(UI-01,readOnly):thumb/browse 短期签名 URL 与过期 时间。签发条件(服务端逐张裁决):请求者有该照片管理权(admin 全部 / 相册 owner 本相册全部 / 协作者仅自己上传)且照片有有效变体、未删除; 隐藏照片仅 admin/owner 签发。不满足条件时 AdminPhoto.preview 为 null (PhotoRecord 本身仍如实返回)。URL 过期后经列表重取刷新。 required: [thumb, browse] properties: thumb: $ref: '#/components/schemas/SignedUrl' browse: $ref: '#/components/schemas/SignedUrl' AdminPhoto: allOf: - $ref: '#/components/schemas/PhotoRecord' - type: object required: [uploader] properties: uploader: $ref: '#/components/schemas/AdminPhotoUploader' preview: description: | 受权限管理的照片预览(UI-01,readOnly 可选扩展;无变体/已删除/ 无权签发时为 null)。旧客户端忽略本字段不受影响。 readOnly: true anyOf: - $ref: '#/components/schemas/PhotoPreview' - type: 'null' AdminPhotoList: allOf: - $ref: '#/components/schemas/CursorPageMeta' - type: object required: [items] properties: items: type: array items: $ref: '#/components/schemas/AdminPhoto' # ── 管理辅助与账号治理(P4a) ── AlbumTransferRequest: type: object description: 相册负责人交接请求(A17)。 required: [targetUserId, keepAsCollaborator] properties: targetUserId: type: string minLength: 1 maxLength: 64 description: 新 owner 用户 ID(须为可被指定为 owner 的账号,见端点 400 说明)。 keepAsCollaborator: type: boolean description: 原 owner 是否保留为 collaborator;false 时移出成员表。 AdminUser: type: object description: | 用户治理条目(P4a;不含邮箱/手机/密码等凭据字段)。sourceStatus 只读 展示(上游状态仅同步任务维护)。id 用于邀请协作者 (POST /albums/{albumId}/members 的 userId)。 required: - id - account - displayName - role - sourceStatus - localLocked - credentialStatus - createdAt properties: id: type: string readOnly: true description: 用户 ID。 account: type: string readOnly: true description: 登录账号。 displayName: type: string readOnly: true description: 展示姓名。 role: type: string readOnly: true description: 公司角色(staff/admin;相册角色另表)。 enum: - staff - admin sourceStatus: type: string readOnly: true description: 来源状态:local=本地创建;active/disabled=同步源状态(P4a 同步)。 enum: - local - active - disabled localLocked: type: boolean readOnly: true description: 本地锁定(停用/deactivate 置 true;与上游状态独立)。 credentialStatus: type: string readOnly: true description: 凭据状态:active=可登录;pending=同步创建待激活(不可登录,待管理员激活并出初始凭据)。 enum: - active - pending locale: type: [string, 'null'] readOnly: true description: 界面语言偏好;未设置为 null。 createdAt: type: string format: date-time readOnly: true description: 创建时间。 synced: type: [object, 'null'] readOnly: true description: | 同步源档案快照(G4:工号/邮箱/公司/头像,来自用户同步、落 external_user_mappings.profile 列);本地创建/未同步用户为 null。 纯展示维度,不参与任何治理判定。 properties: sn: type: string description: 工号。 email: type: string description: 企业邮箱。 companyName: type: string description: 公司全称。 companyAbbr: type: string description: 公司简称。 avatar: type: string description: 头像 URL。 AdminUserList: allOf: - $ref: '#/components/schemas/CursorPageMeta' - type: object required: [items] properties: items: type: array description: 用户条目(前缀模式按 account 升序且仅激活资格用户;管理列表模式按 createdAt 与 ID 稳定倒序)。 items: $ref: '#/components/schemas/AdminUser' total: type: [integer, 'null'] description: 过滤命中总数——仅管理列表 page 模式返回;游标/前缀模式为 null。 AdminUserStatusFilter: type: string description: | 管理列表治理状态过滤(单值归类):pending=待激活;disabled=上游停用; locked=本地锁定;active=其余可正常登录。同时命中多状态时按 pending > disabled > locked > active 优先级归类后匹配。 enum: - active - pending - disabled - locked AdminUserUpdateRequest: type: object description: 用户治理属性部分更新(仅提交的字段被修改;sourceStatus 不在可更新集)。 properties: role: type: string description: 公司角色。 enum: - staff - admin localLocked: type: boolean description: 本地锁定;置 true 撤销该用户全部会话。 displayName: type: string minLength: 1 maxLength: 128 description: 展示姓名。 AdminPasswordResetRequest: type: object description: 管理员重置密码请求;newPassword 缺省由服务端生成随机密码。 properties: newPassword: type: string minLength: 8 maxLength: 128 description: 指定新密码(至少 8 位);缺省服务端生成。 AdminPasswordResetResponse: type: object description: 重置结果;newPassword 仅此一次返回(请求指定或服务端生成)。 required: [newPassword] properties: newPassword: type: string readOnly: true description: 重置后的密码(一次性展示,转交用户后不再可见)。 UserSyncState: type: string description: 同步引擎状态:idle=空闲/从未运行;其余为最近批次结局。 enum: - idle - running - success - partial - failed UserSyncRunState: type: string description: 批次状态(partial=部分行失败,其余行照常应用)。 enum: - running - success - partial - failed UserSyncCounts: type: object description: 批次计数。 required: [added, updated, disabled, failed] properties: added: type: integer minimum: 0 description: 新增用户数(建为待激活)。 updated: type: integer minimum: 0 description: 幂等更新数(改名等)。 disabled: type: integer minimum: 0 description: 上游停用数(撤会话)。 failed: type: integer minimum: 0 description: 失败行数(明细见 /admin/user-sync/errors)。 UserSyncStatus: type: object description: 用户同步引擎状态(A29;mock 源先行,G4 后仅替换 provider)。 required: [state] properties: lastRunAt: type: [string, 'null'] format: date-time readOnly: true description: 最近批次开始时间;从未运行为 null。 lastSuccessAt: type: [string, 'null'] format: date-time readOnly: true description: 最近完全成功批次开始时间;从未成功为 null。 state: $ref: '#/components/schemas/UserSyncState' sourceVersion: type: [integer, 'null'] format: int64 readOnly: true description: 已提交的源水位(版本游标);从未同步为 null。 counts: description: 最近批次计数;从未运行为 null。 anyOf: - $ref: '#/components/schemas/UserSyncCounts' - type: 'null' UserSyncRun: type: object description: 同步批次条目。 required: [id, trigger, state, counts, startedAt] properties: id: type: string readOnly: true description: 批次 ID。 trigger: type: string readOnly: true description: 触发方式(定时/手动补拉)。 enum: - cron - manual state: $ref: '#/components/schemas/UserSyncRunState' cursor: type: [integer, 'null'] format: int64 readOnly: true description: 本批次已提交的源水位(同步游标,非分页游标);未提交为 null。 counts: $ref: '#/components/schemas/UserSyncCounts' startedAt: type: string format: date-time readOnly: true description: 批次开始时间。 finishedAt: type: [string, 'null'] format: date-time readOnly: true description: 批次结束时间;运行中为 null。 error: type: [string, 'null'] readOnly: true description: 批次级失败摘要;无为 null。 UserSyncRunList: allOf: - $ref: '#/components/schemas/CursorPageMeta' - type: object required: [items] properties: items: type: array description: 批次列表(startedAt 与 ID 稳定倒序)。 items: $ref: '#/components/schemas/UserSyncRun' UserSyncRunCreated: type: object description: 手动触发受理结果。 required: [runId] properties: runId: type: string readOnly: true description: 批次 ID(已有批次运行中时为该进行中批次)。 UserSyncError: type: object description: 同步失败行明细(坏行记录,其余行照常应用的证据链)。 required: [runId, externalUserId, code, detail, createdAt] properties: runId: type: string readOnly: true description: 归属批次 ID。 externalUserId: type: string readOnly: true description: 上游用户标识。 code: type: string readOnly: true description: 失败分类码(provider 级约定,如 upstream_timeout/invalid_record)。 detail: type: string readOnly: true description: 失败详情(诊断用)。 createdAt: type: string format: date-time readOnly: true description: 记录时间。 UserSyncErrorList: type: object description: 失败明细列表(createdAt 倒序;无游标即取即用)。 required: [items] properties: items: type: array items: $ref: '#/components/schemas/UserSyncError' UsageAlbum: type: object description: 单相册用量聚合(实时 SUM;P5 换物化/OSS 用量,登记偏差)。 required: - albumId - title - photoCount - originalBytes - variantBytes - totalBytes properties: albumId: type: string readOnly: true description: 相册 ID。 title: type: string readOnly: true description: 相册标题(defaultLocale 解析)。 photoCount: type: integer readOnly: true minimum: 0 description: 照片总数(全部状态)。 originalBytes: type: integer format: int64 readOnly: true minimum: 0 description: 原图存储字节数。 variantBytes: type: integer format: int64 readOnly: true minimum: 0 description: 变体(缩略/浏览/水印等)存储字节数。 totalBytes: type: integer format: int64 readOnly: true minimum: 0 description: 合计字节数(originalBytes + variantBytes)。 UsageTotals: type: object description: 全库用量合计。 required: [photoCount, totalBytes] properties: photoCount: type: integer readOnly: true minimum: 0 description: 照片总数。 totalBytes: type: integer format: int64 readOnly: true minimum: 0 description: 存储总字节数。 UsageThresholds: type: object description: 用量阈值;null 表示该维度未设置(告警停用)。 required: [storageBytes, perAlbumBytes] properties: storageBytes: type: [integer, 'null'] format: int64 readOnly: true minimum: 0 description: 全库存储阈值。 perAlbumBytes: type: [integer, 'null'] format: int64 readOnly: true minimum: 0 description: 单相册存储阈值。 UsageThresholdsUpdate: type: object description: 阈值更新请求;字段缺省=保持不变,显式 null=清除该阈值。 properties: storageBytes: type: [integer, 'null'] format: int64 minimum: 0 description: 全库存储阈值;null 清除。 perAlbumBytes: type: [integer, 'null'] format: int64 minimum: 0 description: 单相册存储阈值;null 清除。 UsageAlert: type: object description: | 阈值告警(A23 后台侧;接收人通知渠道 P5)。超阈写入一次直到回落 (同 kind+albumId 活跃告警唯一——重复告警去重);resolvedAt 非空为 已解除。 required: [id, kind, message, createdAt] properties: id: type: string readOnly: true description: 告警 ID。 kind: type: string readOnly: true description: 告警维度。 enum: - storage - perAlbum albumId: type: [string, 'null'] readOnly: true description: 相册 ID;kind=perAlbum 必有,storage 为 null。 message: type: string readOnly: true description: 告警描述。 createdAt: type: string format: date-time readOnly: true description: 产生时间。 resolvedAt: type: [string, 'null'] format: date-time readOnly: true description: 解除时间(阈值上调/用量回落时标记);活跃告警为 null。 UsageSummary: type: object description: 用量汇总(A23 后台侧)。 required: [albums, totals, thresholds, alerts] properties: albums: type: array description: 相册用量(按 totalBytes 降序)。 items: $ref: '#/components/schemas/UsageAlbum' totals: $ref: '#/components/schemas/UsageTotals' thresholds: $ref: '#/components/schemas/UsageThresholds' alerts: type: array description: 告警列表(createdAt 倒序,含已解除)。 items: $ref: '#/components/schemas/UsageAlert' # ── 展示位(管理端,P4b) ── EmbedSlotLang: type: string description: | 展示位内容语言(设置与公开查询共用):auto=按相册 defaultLocale, 其余为指定语言(取值不支持时服务端静默回退相册 defaultLocale)。 enum: - auto - zh-CN - en EmbedSlotSettings: type: object description: | 展示位展示设置。整体提交语义:创建时 settings 可整体缺省(各字段按 下列缺省值落库),提供时字段齐全;更新为整体替换(未单独提交 settings 则保持不变)。响应中字段恒齐全。UI-01 新增字段 (imageFit/offsetX/offsetBottom/zIndex)为可选扩展:提交时可缺省 (按默认值归一化落库),旧 JSON/旧客户端零影响;响应恒为归一化全集。 required: [position, size, intervalMs, lang, collapsedDefault] properties: position: type: string description: 悬浮位置(宿主页面左/右侧)。 enum: - left - right default: right size: type: string description: 组件尺寸(sm=窄屏降级档,宿主 viewport<480px 时自动降为 sm)。 enum: - sm - md default: md intervalMs: type: integer minimum: 3000 maximum: 30000 description: 轮播切换间隔毫秒。 default: 5000 lang: $ref: '#/components/schemas/EmbedSlotLang' collapsedDefault: type: boolean description: 初始是否收起(收起后仅展示小把手,点击展开)。 default: false imageFit: type: string description: | 轮播图片适配方式(UI-01):contain=完整展示于 4:3 舞台(中性底); cover=裁切填满舞台。可缺省(默认 contain)。 enum: - contain - cover default: contain offsetX: type: integer minimum: 0 maximum: 120 description: 组件距宿主页面边缘的水平偏移(像素,UI-01)。可缺省。 default: 16 offsetBottom: type: integer minimum: 0 maximum: 240 description: 组件距宿主页面底部的垂直偏移(像素,UI-01)。可缺省。 default: 24 zIndex: type: integer minimum: 1 maximum: 2147483000 description: 组件在宿主页面的层级(UI-01)。可缺省。 default: 1000 displayMode: type: string description: | 展示模式(UI-04):carousel=旧版轮播(v1 loader,footer/舞台结构); vertical-stream=新版垂直照片流(v2 loader,固定 195×280 连续上滚照片窗, 忽略 size/intervalMs/imageFit 的布局作用)。可缺省(默认 carousel—— UI-04 前旧记录读取归一为旧模式;新建走 v2 由管理端显式提交)。 enum: - carousel - vertical-stream default: carousel scrollSpeed: type: integer minimum: 8 maximum: 32 description: | vertical-stream 自动上滚速度(CSS px/秒,UI-04)。可缺省(默认 16); 仅 v2 生效,carousel 模式忽略。 default: 16 reactionsEnabled: type: boolean description: | vertical-stream 点赞心形气泡开关(UI-04)。可缺省(默认 true); false 仅隐藏气泡(不产生 WS 订阅),照片滚动不受影响。仅 v2 生效。 default: true EmbedSlot: type: object description: | 展示位条目(管理视角)。albumId/albumTitle 为当前绑定相册(null=未 绑定,公开端 404);albumTitle 按相册 defaultLocale 解析(管理列表 展示用)。任一管理面变更递增 configVersion。 required: - id - publicToken - name - enabled - albumId - albumTitle - configVersion - settings - updatedAt properties: id: type: string readOnly: true description: 展示位 ID。 publicToken: type: string readOnly: true pattern: '^[0-9a-z]{20,32}$' description: 展示位公开令牌(嵌入代码 data-slot 引用,不可枚举)。 name: type: string minLength: 1 maxLength: 64 description: 展示位名称(管理标识)。 enabled: type: boolean description: 是否启用;false 即下线(公开端 404,等同删除语义)。 albumId: type: [string, 'null'] readOnly: true description: 绑定相册 ID;未绑定为 null。 albumTitle: type: [string, 'null'] readOnly: true description: 绑定相册标题(按相册 defaultLocale 解析);未绑定为 null。 configVersion: type: integer minimum: 1 readOnly: true description: 配置版本(任一管理面变更递增;嵌入端据此整体重置渲染,亦为公开端 ETag 组成部分)。 settings: $ref: '#/components/schemas/EmbedSlotSettings' updatedAt: type: string format: date-time readOnly: true description: 最近更新时间。 EmbedSlotList: allOf: - $ref: '#/components/schemas/CursorPageMeta' - type: object required: [items] properties: items: type: array description: 展示位条目(按 createdAt 与 ID 稳定倒序)。 items: $ref: '#/components/schemas/EmbedSlot' EmbedSlotCreateRequest: type: object description: 创建展示位请求;settings 可整体缺省(各字段按缺省值落库),提供时字段齐全。 required: [name] properties: name: type: string minLength: 1 maxLength: 64 description: 展示位名称。 settings: $ref: '#/components/schemas/EmbedSlotSettings' EmbedSlotUpdateRequest: type: object description: | 展示位部分更新请求(仅提交的字段被修改,空请求体 400)。settings 为 整体替换(提供时字段齐全);albumId:非 null 绑定/换绑目标相册(相册 须存在),null 解绑(公开端随即 404)——albumId 值变化即 configVersion+1。 properties: name: type: string minLength: 1 maxLength: 64 description: 展示位名称。 enabled: type: boolean description: 启停;false 即下线(软停用,无删除端点)。 settings: $ref: '#/components/schemas/EmbedSlotSettings' albumId: type: [string, 'null'] description: 绑定相册 ID;null 解绑。 # ── 公开(观众端) ── 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 - downloadEnabled - originalDownloadEnabled 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)。 downloadEnabled: type: boolean readOnly: true description: | 浏览图下载开关(P4a Task 4 / A18):false 时观众端不出现下载入口, download 端点 browse/original 均 403 DOWNLOAD_DISABLED。 originalDownloadEnabled: type: boolean readOnly: true description: | 原图下载开关(更高级:需与 downloadEnabled 同时开启才允许 variant=original;单独开启不生效——两级级联语义)。 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 域,P4b) ── PublicEmbedAlbum: type: object description: 展示位绑定相册摘要(title 已按 lang→相册 defaultLocale 回退解析)。 required: [publicToken, title, defaultLocale] properties: publicToken: type: string readOnly: true pattern: '^[0-9a-z]{20,32}$' description: 相册公开令牌(点击照片跳转 viewer 相册页用)。 title: type: string readOnly: true description: 相册标题(实际返回语言)。 defaultLocale: $ref: '#/components/schemas/Locale' PublicEmbedPhoto: type: object description: | 展示位照片条目(小尺寸缩略图省流量,与公开列表的 browse 大图区分; 签名 URL 为短期凭证,过期后经本端点刷新)。 required: [photoId, thumb, width, height] properties: photoId: type: string readOnly: true description: 照片 ID(跳转 viewer 单图定位用)。 thumb: $ref: '#/components/schemas/SignedUrl' width: type: integer readOnly: true description: 缩略图宽度(像素);用于占位,减少加载跳动。 height: type: integer readOnly: true description: 缩略图高度(像素)。 PublicEmbedSlot: type: object description: | 展示位公开数据(嵌入组件数据源;ov 只读副本)。photos 为近期已发布 照片,按 publishedAt 与 ID 稳定倒序,上限 20(空数组=相册暂无已发布 照片,嵌入端展示空态卡)。album 当前恒非 null(未绑定/停用的展示位 公开端一律 404 防探测);null 为契约前瞻保留,嵌入端按空态处理。 settings(UI-01 可选扩展)为后台配置的归一化全集(缺字段按默认值 补齐)——嵌入端据此应用后台配置,data-* 覆盖优先于它。 required: [slotToken, configVersion, album, photos] properties: slotToken: type: string readOnly: true pattern: '^[0-9a-z]{20,32}$' description: 展示位公开令牌(回显请求路径)。 configVersion: type: integer minimum: 1 readOnly: true description: 配置版本(嵌入端变更检测:版本变化即整体重置渲染)。 settings: description: | 展示设置(UI-01 可选扩展;ov 副本 JSON 按契约默认值归一化后返回, 恒为全集)。旧客户端忽略本字段不受影响。 readOnly: true allOf: - $ref: '#/components/schemas/EmbedSlotSettings' album: description: 绑定相册摘要;未绑定为 null(当前部署不会出现,见 schema 描述)。 anyOf: - $ref: '#/components/schemas/PublicEmbedAlbum' - type: 'null' photos: type: array maxItems: 20 description: 近期照片(publishedAt 与 ID 稳定倒序,小尺寸缩略图)。 items: $ref: '#/components/schemas/PublicEmbedPhoto' # ── 互动(观众端,ov 域) ── DownloadVariant: type: string description: | 下载版本(P4a A18):browse=当前生效浏览图(水印版); original=原图(需 originalDownloadEnabled 更高级开关)。 enum: - browse - original PublicPhotoDownload: allOf: - $ref: '#/components/schemas/SignedUrl' - type: object required: [variant] properties: variant: $ref: '#/components/schemas/DownloadVariant' 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。