Files
photo-live/packages/contracts/openapi.yaml
T
2026-09-17 17:31:11 +08:00

1950 lines
60 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: []`)。
- 状态码通用映射:401 → AUTH_TOKEN_EXPIRED、500 → INTERNAL_ERROR;
其余状态码对应的 code 在各响应的 description 中标注。
- 多语言内容按「请求 locale → 相册 defaultLocale」回退解析为单语言对象,
响应携带 `resolvedLocale` 供界面判断实际语言。
servers:
- url: /v1
tags:
- name: auth
description: 密码登录会话(签发、滑动续期、退出)
- name: albums
description: 相册创建、列表、详情与配置(工作人员)
- name: members
description: 相册成员管理(创建者或管理员)
- name: categories
description: 相册分类管理(创建者或管理员)
- name: uploads
description: 直传任务初始化、授权续签、完成核验与会话记录
- name: photos
description: 照片可见性调整与软删除(工作人员)
- name: public
description: 观众匿名只读(相册信息与公开照片)
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'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 令牌缺失、无效或已过期(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 令牌缺失、无效或已过期(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ─────────────────────────── 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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户非成员不可见(ALBUM_NOT_FOUND,避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ─────────────────────────── 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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ─────────────────────────── 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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
operationId: deleteCategory
tags: [categories]
summary: 删除分类
description: 删除分类(分类下仍有照片时是否允许删除由服务端策略决定,400 拒绝)。
security:
- bearerAuth: []
responses:
'204':
description: 已删除,无响应体。
'401':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ─────────────────────────── 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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ─────────────────────────── 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':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
delete:
operationId: deletePhoto
tags: [photos]
summary: 软删除照片
description: 标记 deleted(软删除),公开端点即刻不可见;物理删除走独立运维流程。
security:
- bearerAuth: []
responses:
'204':
description: 已软删除,无响应体。
'401':
description: 未认证(AUTH_TOKEN_EXPIRED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
# ─────────────────────────── 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)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在或未公开(ALBUM_NOT_PUBLIC,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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: 查询参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在或未公开(ALBUM_NOT_PUBLIC,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
/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':
description: 服务器内部错误(INTERNAL_ERROR)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
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
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
LocaleQuery:
name: locale
in: query
description: 请求语言;缺省时按相册 defaultLocale 回退。
schema:
$ref: '#/components/schemas/Locale'
schemas:
# ── 通用 ──
Error:
type: object
required: [code, message, requestId]
properties:
code:
type: string
readOnly: true
description: 稳定错误码,与 src/errors.ts 的 ErrorCodes 一一对应。
enum:
- AUTH_INVALID_CREDENTIALS
- AUTH_TOKEN_EXPIRED
- AUTH_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 在业务端点中定义)。
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: 续期后的过期时间。
# ── 相册 ──
AlbumTranslation:
type: object
required: [locale, title]
description: 相册多语言条目(创建/更新与响应共用;未填的可选字段存储为 null)。
properties:
locale:
$ref: '#/components/schemas/Locale'
title:
type: string
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
description: 公开访问令牌(不可枚举)。
ownerId:
type: string
readOnly: true
description: 创建者用户 ID。
status:
type: string
readOnly: true
default: active
description: 相册状态。
enum:
- active
- archived
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
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)。
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
description: 客户端任务幂等键;同一任务的重试必须复用(会话与照片 ID 均复用)。
filename:
type: string
description: 原始文件名(仅元数据,不作对象唯一标识)。
bytes:
type: integer
format: int64
minimum: 1
description: 申明文件字节数;完成核验时逐一比对。
contentType:
type: string
description: MIME 类型;仅接受图片类型(JPEG/PNG/HEIC)。
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
- translations
- categories
- banner
- viewCount
properties:
publicToken:
type: string
readOnly: true
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。
translations:
$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, 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'