Files
photo-live/packages/contracts/openapi.yaml
T

1825 lines
57 KiB
YAML
Raw Normal View History

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`。
2026-09-17 17:31:11 +08:00
- 业务端点一律 Bearer 认证;`/public/**` 匿名访问(`security: []`)。
- 状态码通用映射:401 → AUTH_TOKEN_EXPIRED、500 → INTERNAL_ERROR;
其余状态码对应的 code 在各响应的 description 中标注。
- 多语言内容按「请求 locale → 相册 defaultLocale」回退解析为单语言对象,
响应携带 `resolvedLocale` 供界面判断实际语言。
servers:
- url: /v1
tags:
- name: auth
description: 密码登录会话(签发、滑动续期、退出)
2026-09-17 17:31:11 +08:00
- name: albums
description: 相册创建、列表、详情与配置(工作人员)
- name: members
description: 相册成员管理(创建者或管理员)
- name: categories
description: 相册分类管理(创建者或管理员)
- name: uploads
description: 直传任务初始化、授权续签、完成核验与会话记录
- name: photos
description: 照片可见性调整与软删除(工作人员)
- name: public
description: 观众匿名只读(相册信息与公开照片)
paths:
2026-09-17 17:31:11 +08:00
# ─────────────────────────── 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'
2026-09-17 17:31:11 +08:00
'400':
description: 参数校验失败(VALIDATION_FAILED,经全局校验管道)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
'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: 已退出,无响应体。
2026-09-17 17:31:11 +08:00
'400':
description: 请求体不可解析(VALIDATION_FAILED,经全局校验管道)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
2026-09-17 17:31:11 +08:00
'500':
$ref: '#/components/responses/InternalError'
2026-09-17 17:31:11 +08:00
# ─────────────────────────── 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'
2026-09-17 17:31:11 +08:00
'500':
$ref: '#/components/responses/InternalError'
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
'500':
$ref: '#/components/responses/InternalError'
2026-09-17 17:31:11 +08:00
/albums/{albumId}:
parameters:
- $ref: '#/components/parameters/AlbumId'
get:
operationId: getAlbum
tags: [albums]
summary: 相册详情
description: 相册完整配置、多语言内容与分类列表。
security:
- bearerAuth: []
responses:
'200':
description: 相册详情。
content:
application/json:
schema:
$ref: '#/components/schemas/Album'
'401':
$ref: '#/components/responses/Unauthorized'
2026-09-17 17:31:11 +08:00
'404':
description: 相册不存在,或当前用户非成员不可见(ALBUM_NOT_FOUND,避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
# ─────────────────────────── 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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
2026-09-17 17:31:11 +08:00
/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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
# ─────────────────────────── 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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
/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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
delete:
operationId: deleteCategory
tags: [categories]
summary: 删除分类
description: 删除分类(分类下仍有照片时是否允许删除由服务端策略决定,400 拒绝)。
security:
- bearerAuth: []
responses:
'204':
description: 已删除,无响应体。
'401':
$ref: '#/components/responses/Unauthorized'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
# ─────────────────────────── 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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
'500':
$ref: '#/components/responses/InternalError'
2026-09-17 17:31:11 +08:00
/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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
/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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
# ─────────────────────────── photos ───────────────────────────
/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'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
delete:
operationId: deletePhoto
tags: [photos]
summary: 软删除照片
description: 标记 deleted(软删除),公开端点即刻不可见;物理删除走独立运维流程。
security:
- bearerAuth: []
responses:
'204':
description: 已软删除,无响应体。
'401':
$ref: '#/components/responses/Unauthorized'
2026-09-17 17:31:11 +08:00
'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'
2026-09-17 17:31:11 +08:00
# ─────────────────────────── 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 回退)。
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
/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
- $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 回退)。
2026-09-17 17:31:11 +08:00
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'
2026-09-17 17:31:11 +08:00
/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'
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
2026-09-17 17:31:11 +08:00
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
UploadId:
name: uploadId
in: path
required: true
description: 上传会话 ID。
schema:
type: string
PhotoId:
name: photoId
in: path
required: true
description: 照片 ID。
schema:
type: string
AlbumPublicToken:
name: token
in: path
required: true
description: 相册公开令牌(不可枚举,持有即可访问)。
schema:
type: string
CursorQuery:
name: cursor
in: query
description: 分页游标(上一页响应的 nextCursor)。
schema:
type: string
maxLength: 512
2026-09-17 17:31:11 +08:00
LocaleQuery:
name: locale
in: query
description: 请求语言;缺省或取值不支持时按相册 defaultLocale 回退(不报 400,响应 resolvedLocale 指示实际返回语言)。
2026-09-17 17:31:11 +08:00
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:
2026-09-17 17:31:11 +08:00
# ── 通用 ──
Error:
type: object
required: [code, message, requestId]
properties:
code:
type: string
readOnly: true
description: 稳定错误码,与 src/errors.ts 的 ErrorCodes 一一对应。
enum:
- AUTH_INVALID_CREDENTIALS
- AUTH_TOKEN_EXPIRED
- AUTH_RATE_LIMITED
- AUTH_FORBIDDEN
- ALBUM_NOT_FOUND
- ALBUM_NOT_PUBLIC
- ALBUM_UPLOAD_DISABLED
- MEMBER_CONFLICT
- UPLOAD_SESSION_CONFLICT
- UPLOAD_VERIFY_FAILED
- PHOTO_NOT_FOUND
- PHOTO_NOT_PUBLIC
- VALIDATION_FAILED
- INTERNAL_ERROR
message:
type: string
readOnly: true
description: 服务器生成的可读消息(诊断用,不作为客户端展示文案依据)。
requestId:
type: string
readOnly: true
description: 请求追踪 ID。
CursorPageMeta:
type: object
required: [nextCursor, hasMore]
description: 游标分页元信息;各列表响应以 allOf 内嵌本结构(条目 schema 在业务端点中定义)。不变式:hasMore=true 当且仅当 nextCursor≠null,两者必须成对填充。
properties:
nextCursor:
type: [string, 'null']
readOnly: true
description: 下一页游标;null 表示没有更多数据。
hasMore:
type: boolean
readOnly: true
description: 是否还有下一页。
2026-09-17 17:31:11 +08:00
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: 续期后的过期时间。
2026-09-17 17:31:11 +08:00
# ── 相册 ──
AlbumTranslation:
type: object
required: [locale, title]
description: 相册多语言条目(创建/更新与响应共用;未填的可选字段存储为 null)。
properties:
locale:
$ref: '#/components/schemas/Locale'
title:
type: string
minLength: 1
2026-09-17 17:31:11 +08:00
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
properties:
id:
type: string
readOnly: true
description: 相册 ID。
publicToken:
type: string
readOnly: true
pattern: '^[0-9a-z]{20,32}$'
2026-09-17 17:31:11 +08:00
description: 公开访问令牌(不可枚举)。
ownerId:
type: string
readOnly: true
description: 创建者用户 ID。
status:
type: string
readOnly: true
default: active
description: 相册状态;offline 表示下线(停止公开访问与新授权签发)。
2026-09-17 17:31:11 +08:00
enum:
- active
- offline
2026-09-17 17:31:11 +08:00
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: 最近更新时间。
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'
# ── 成员 ──
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
2026-09-17 17:31:11 +08:00
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 未完成(核验时推导或由补偿扫描落定)。
2026-09-17 17:31:11 +08:00
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
2026-09-17 17:31:11 +08:00
description: 客户端任务幂等键;同一任务的重试必须复用(会话与照片 ID 均复用)。
filename:
type: string
maxLength: 255
2026-09-17 17:31:11 +08:00
description: 原始文件名(仅元数据,不作对象唯一标识)。
bytes:
type: integer
format: int64
minimum: 1
description: 申明文件字节数;完成核验时逐一比对。
contentType:
type: string
enum: [image/jpeg, image/png, image/heic]
description: MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。
2026-09-17 17:31:11 +08:00
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: 会话创建时间。
photo:
$ref: '#/components/schemas/UploadSessionPhoto'
UploadSessionList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/UploadSessionRecord'
# ── 照片(管理端) ──
PhotoVisibilityUpdateRequest:
type: object
required: [visibility]
properties:
visibility:
type: string
description: 目标可见性;仅允许 hidden 与 published 间切换。
enum:
- hidden
- published
PhotoRecord:
type: object
description: 管理端照片记录(技术实施方案 §4 photos 表字段)。
required:
- photoId
- albumId
- categoryId
- uploaderId
- visibility
- processingState
- bytes
- width
- height
- shotAt
- uploadedAt
- publishedAt
- likeCount
properties:
photoId:
type: string
readOnly: true
description: 照片 ID。
albumId:
type: string
readOnly: true
description: 所属相册 ID。
categoryId:
type: [string, 'null']
readOnly: true
description: 分类 ID;null 表示未分类。
uploaderId:
type: string
readOnly: true
description: 上传人用户 ID。
visibility:
$ref: '#/components/schemas/PhotoVisibility'
processingState:
$ref: '#/components/schemas/PhotoProcessingState'
bytes:
type: integer
format: int64
readOnly: true
description: 原图字节数。
width:
type: [integer, 'null']
readOnly: true
description: 原图宽度(像素);处理前为 null。
height:
type: [integer, 'null']
readOnly: true
description: 原图高度(像素);处理前为 null。
shotAt:
type: [string, 'null']
format: date-time
readOnly: true
description: 拍摄时间(EXIF);无有效值时为 null。
uploadedAt:
type: string
format: date-time
readOnly: true
description: 上传完成时间。
publishedAt:
type: [string, 'null']
format: date-time
readOnly: true
description: 发布时间;未发布为 null。
likeCount:
type: integer
readOnly: true
minimum: 0
description: 累计点赞数。
# ── 公开(观众端) ──
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
2026-09-17 17:31:11 +08:00
- categories
- banner
- viewCount
properties:
publicToken:
type: string
readOnly: true
pattern: '^[0-9a-z]{20,32}$'
2026-09-17 17:31:11 +08:00
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:
2026-09-17 17:31:11 +08:00
$ref: '#/components/schemas/PublicAlbumTranslation'
categories:
type: array
description: 分类列表(按 sortOrder 升序)。
items:
$ref: '#/components/schemas/PublicCategory'
banner:
description: 封面图签名 URL;未设置为 null。
anyOf:
- $ref: '#/components/schemas/SignedUrl'
- type: 'null'
viewCount:
type: 'null'
readOnly: true
description: P1a 占位 null;浏览计数由后续 /visits 端点提供(技术实施方案 §7.1)。
PublicPhoto:
type: object
description: 公开照片条目(仅 published 照片,处理完成即有宽高;URL 为短期签名,需在过期前刷新)。
required: [photoId, width, height, thumb, browse, likeCount, publishedAt]
2026-09-17 17:31:11 +08:00
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'