Files
photo-live/packages/contracts/openapi.yaml
T
浅念 2618ad3163 feat(assets): album asset status — show current banner/cover/logo in admin branding tab
- 契约 listAlbumAssetStatus(GET /albums/{albumId}/assets):每 (kind,locale)
  最新 effective 行 + 现签 URL;权限同详情(可见即读);deleted → 404
- 服务端 AssetsService.status(createdAt,id DESC 组内取首;presignGet 现签)
- Admin 品牌资产页签改造:进入即加载现状——三种 kind 卡片(预览图/未设置
  空态/设置时间),banner 分全局/zh/en 槽位;上传成功自动刷新(替换语义)
- API 56/56、admin 87/87 + build 全绿

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-23 10:51:53 +08:00

5065 lines
172 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
openapi: 3.1.0
info:
title: photo-live
version: 0.1.0
license:
name: UNLICENSED
identifier: UNLICENSED
description: |
photo-live 直播相册系统 API 契约(单一事实来源)。
- 类型经 `openapi-typescript` 生成并提交入库(`src/generated/openapi.d.ts`),
三端(services/api、apps/admin、apps/viewer)直接消费生成类型,不自行跑 gen。
- 错误响应使用稳定 `code`(见 `components.schemas.Error` 的枚举,与
`src/errors.ts` 的 `ErrorCodes` 一一对应),客户端按 code 映射多语言文案,
`message` 仅供诊断。
- 列表接口统一游标分页,元信息见 `components.schemas.CursorPageMeta`。
- 业务端点一律 Bearer 认证;`/public/**` 匿名访问(`security: []`)。
- `/internal/**` 为服务间同步接口(P2):HMAC 签名鉴权
(`X-Sync-Signature`/`X-Sync-Timestamp`,见 [internal] tag 描述),
OpenAPI 无对应标准 scheme 故同样标注 `security: []`,但匿名不可访问。
- WebSocket 实时消息契约不经 OpenAPI 生成,单独维护于
`src/realtime.ts`(判别联合 + 心跳常量),与生成类型同属单一事实来源。
- 状态码通用映射:401 → AUTH_TOKEN_EXPIRED、500 → INTERNAL_ERROR;
其余状态码对应的 code 在各响应的 description 中标注。
- 多语言内容按「请求 locale → 相册 defaultLocale」回退解析为单语言对象,
响应携带 `resolvedLocale` 供界面判断实际语言。
servers:
- url: /v1
tags:
- name: auth
description: 密码登录会话(签发、滑动续期、退出)
- name: albums
description: 相册创建、列表、详情、配置与水印版本管理(工作人员)
- name: assets
description: 相册宣传图/封面/Logo 资产直传(创建者或管理员)
- name: members
description: 相册成员管理(创建者或管理员)
- name: categories
description: 相册分类管理(创建者或管理员)
- name: uploads
description: 直传任务初始化、授权续签、完成核验与会话记录
- name: photos
description: 照片可见性调整(单项与批量)与软删除(工作人员)
- name: admin
description: 管理辅助查询与账号治理(用户管理、相册交接、用户同步、用量统计)
- name: 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'
delete:
operationId: deleteAlbum
tags: [albums]
summary: 软删除相册
description: >-
标记 status=deleted(软删除):管理端列表与公开端即刻不可见,成员/上传/
资产等全部子资源随之 404;数据保留(物理删除走独立运维流程),不提供
恢复端点。权限同「配置相册」(公司管理员或相册创建者)。事件走
album.updated(priority=high)同步 ov 副本下线。
security:
- bearerAuth: []
responses:
'204':
description: 已删除(幂等:重复删除同样 204)。
'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'
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'
get:
operationId: listAlbumAssetStatus
tags: [assets]
summary: 各类资产当前生效项(管理端现状展示)
description: >-
返回每个 (kind, locale) 组合的最新 effective 资产行 + 现签短期 URL
(同「替换语义」——同组再上传即整体替换)。未设置的组合不出现;
签名过期重取本端点刷新。权限同相册详情(可见即可读)。
security:
- bearerAuth: []
responses:
'200':
description: 生效资产列表(可能为空数组)。
content:
application/json:
schema:
$ref: '#/components/schemas/AssetStatusResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
description: 相册不存在(ALBUM_NOT_FOUND,含已删除)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
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
- deleted
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。宣传图/封面共用(2026-09-23 决策):
未配置封面时回退最新生效宣传图(传一张即可两用)。列表端点按页
批量组装(非逐相册查询);旧客户端忽略本字段不受影响。
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。
# ── 成员 ──
AssetStatusItem:
type: object
description: 单个生效资产(管理端现状展示用)。
required: [kind, locale, url, expiresAt, contentType, createdAt]
properties:
kind:
type: string
enum: [banner, cover, logo]
locale:
type: [string, "null"]
description: 语言标签(banner 可带;null=全局)。
url:
type: string
description: 现签短期 GET URL。
expiresAt:
type: string
format: date-time
contentType:
type: string
createdAt:
type: string
format: date-time
AssetStatusResponse:
type: object
description: 各 (kind, locale) 组合最新生效资产列表。
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/AssetStatusItem'
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。宣传图/封面共用(2026-09-23 决策):
未配置宣传图时回退最新生效封面(传一张即可两用)。
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。