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'