feat(contracts): p1b asset upload, management photo list, account lookup

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-18 08:23:59 +08:00
parent 47fb81ad31
commit c2a9cc51f3
2 changed files with 744 additions and 0 deletions
+359
View File
@@ -28,6 +28,8 @@ tags:
description: 密码登录会话(签发、滑动续期、退出)
- name: albums
description: 相册创建、列表、详情与配置(工作人员)
- name: assets
description: 相册宣传图/封面/Logo 资产直传(创建者或管理员)
- name: members
description: 相册成员管理(创建者或管理员)
- name: categories
@@ -36,6 +38,8 @@ tags:
description: 直传任务初始化、授权续签、完成核验与会话记录
- name: photos
description: 照片可见性调整与软删除(工作人员)
- name: admin
description: 管理辅助查询
- name: public
description: 观众匿名只读(相册信息与公开照片)
@@ -278,6 +282,105 @@ paths:
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── assets ───────────────────────────
/albums/{albumId}/assets:
parameters:
- $ref: '#/components/parameters/AlbumId'
post:
operationId: initAlbumAssetUpload
tags: [assets]
summary: 初始化资产上传
description: |
为相册宣传图/封面/Logo 创建上传会话,返回 staging 对象的临时直传凭证
(凭证结构与照片直传同形)。无 clientTaskId 幂等键:assetId 即一次性会话,
失败重试重新初始化即可。替换语义:同一 (kind, locale) 再次上传→新行生效,
旧行保留作历史(P1b 无删除端点);公开相册响应始终取该 kind 最新生效行
(P1a 公开服务已按 latest 实现)。
security:
- bearerAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AssetInitRequest'
responses:
'201':
description: 资产上传会话已创建。
content:
application/json:
schema:
$ref: '#/components/schemas/AssetInitResponse'
'400':
description: 参数校验失败(VALIDATION_FAILED):文件超过大小限额或类型不接受。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可管理资产(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/albums/{albumId}/assets/{assetId}/complete:
parameters:
- $ref: '#/components/parameters/AlbumId'
- $ref: '#/components/parameters/AssetId'
post:
operationId: completeAlbumAssetUpload
tags: [assets]
summary: 完成资产上传核验
description: |
核验 staging 对象(存在性、字节数与可用完整性信息)后将资产挂到相册生效,
并返回短期签名访问 URL。幂等:已核验通过的资产重复调用返回 200
(URL 重新现签,本端点即签名刷新入口)。
security:
- bearerAuth: []
responses:
'200':
description: 核验通过,资产已生效。
content:
application/json:
schema:
$ref: '#/components/schemas/AssetCompleteResponse'
'400':
description: |
核验失败(UPLOAD_VERIFY_FAILED):对象缺失、字节数与申明不符
或完整性信息不可用。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 资产不存在或无权操作(AUTH_FORBIDDEN,不区分以避免枚举)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── members ───────────────────────────
/albums/{albumId}/members:
@@ -695,6 +798,69 @@ paths:
# ─────────────────────────── photos ───────────────────────────
/albums/{albumId}/photos:
parameters:
- $ref: '#/components/parameters/AlbumId'
get:
operationId: listAlbumPhotos
tags: [photos]
summary: 管理端照片列表
description: |
相册照片管理列表(区别于公开列表):全部状态可见(unpublished/published/
hidden/deleted),条目含 uploader 脱敏信息。按 uploadedAt 与 ID 稳定倒序
游标分页。
security:
- bearerAuth: []
parameters:
- name: visibility
in: query
description: 可见性过滤;缺省为全部状态。
schema:
$ref: '#/components/schemas/PhotoVisibility'
- name: categoryId
in: query
description: 分类 ID 过滤。
schema:
type: string
- $ref: '#/components/parameters/CursorQuery'
- name: limit
in: query
description: 每页条数。
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: 管理照片列表页。
content:
application/json:
schema:
$ref: '#/components/schemas/AdminPhotoList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
description: 仅创建者或管理员可访问管理列表(AUTH_FORBIDDEN)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'404':
description: 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'500':
$ref: '#/components/responses/InternalError'
/photos/{photoId}:
parameters:
- $ref: '#/components/parameters/PhotoId'
@@ -768,6 +934,58 @@ paths:
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── admin ───────────────────────────
/admin/users:
get:
operationId: lookupUsersByAccount
tags: [admin]
summary: 账号前缀查询
description: |
邀请协作者的前置查询:按账号前缀搜索激活用户,返回脱敏列表
(id/account/displayName,不含邮箱、手机与状态字段)。
访问策略(P1b 决策):任何已认证工作人员可用——邀请的发起方不限于
创建者/管理员,而泄露面仅为激活用户的最小展示字段、接收方为已认证
内部人员,风险可接受;未认证一律 401。前缀匹配为大小写敏感
(服务端按二进制校对执行 LIKE prefix%);按账号升序返回,无游标
(前缀搜索 + 小上限即取即用)。
security:
- bearerAuth: []
parameters:
- name: accountPrefix
in: query
required: true
description: 账号前缀(≥2 字符以防过宽匹配)。
schema:
type: string
minLength: 2
maxLength: 64
- name: limit
in: query
description: 返回条数上限。
schema:
type: integer
minimum: 1
maximum: 20
default: 10
responses:
'200':
description: 匹配的激活用户列表(无匹配为空数组)。
content:
application/json:
schema:
$ref: '#/components/schemas/AdminUserLookupList'
'400':
description: 查询参数校验失败(VALIDATION_FAILED;accountPrefix 缺失或长度非法)。
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'401':
$ref: '#/components/responses/Unauthorized'
'500':
$ref: '#/components/responses/InternalError'
# ─────────────────────────── public ───────────────────────────
/public/albums/{token}:
@@ -912,6 +1130,13 @@ components:
description: 分类 ID。
schema:
type: string
AssetId:
name: assetId
in: path
required: true
description: 相册资产 ID。
schema:
type: string
UploadId:
name: uploadId
in: path
@@ -1260,6 +1485,71 @@ components:
items:
$ref: '#/components/schemas/Category'
# ── 资产 ──
AssetKind:
type: string
description: |
相册资产类型(技术实施方案 §4 album_assets.kind)。banner 为观众页宣传图
(PublicAlbum.banner,P1b 消费方);cover/logo 建表与上传链路就绪,展示
消费随后续阶段。
enum:
- banner
- cover
- logo
AssetInitRequest:
type: object
description: |
资产上传初始化请求;大小与类型白名单与照片直传一致(UPLOAD_MAX_BYTES
环境策略,逐请求惰性读取)。locale 为可选语言维度:同一 (kind, locale)
的再次上传即替换(新行生效);语言无关的资产(如 logo)可不传。
required: [kind, filename, bytes, contentType]
properties:
kind:
$ref: '#/components/schemas/AssetKind'
locale:
$ref: '#/components/schemas/Locale'
filename:
type: string
maxLength: 255
description: 原始文件名(仅元数据,不作对象唯一标识)。
bytes:
type: integer
format: int64
minimum: 1
description: 申明文件字节数;完成核验时逐一比对。
contentType:
type: string
enum: [image/jpeg, image/png, image/heic]
description: MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。
AssetInitResponse:
type: object
required: [assetId, objectKey, credential]
properties:
assetId:
type: string
readOnly: true
description: 资产 ID(完成核验的寻址凭据,一次性会话)。
objectKey:
type: string
readOnly: true
description: staging 对象键(photo-live/{env}/albums/{albumId}/assets/{assetId}.{ext})。
credential:
$ref: '#/components/schemas/UploadCredential'
AssetCompleteResponse:
allOf:
- $ref: '#/components/schemas/SignedUrl'
- type: object
required: [assetId]
properties:
assetId:
type: string
readOnly: true
description: 资产 ID。
# ── 成员 ──
MemberAddRequest:
@@ -1660,6 +1950,75 @@ components:
minimum: 0
description: 累计点赞数。
AdminPhotoUploader:
type: object
description: 管理列表条目内嵌的上传人脱敏信息(不含邮箱、手机等凭据字段)。
required: [userId, account, displayName]
properties:
userId:
type: string
readOnly: true
description: 上传人用户 ID(与 PhotoRecord.uploaderId 一致)。
account:
type: string
readOnly: true
description: 登录账号。
displayName:
type: string
readOnly: true
description: 展示姓名(来自用户资料)。
AdminPhoto:
allOf:
- $ref: '#/components/schemas/PhotoRecord'
- type: object
required: [uploader]
properties:
uploader:
$ref: '#/components/schemas/AdminPhotoUploader'
AdminPhotoList:
allOf:
- $ref: '#/components/schemas/CursorPageMeta'
- type: object
required: [items]
properties:
items:
type: array
items:
$ref: '#/components/schemas/AdminPhoto'
# ── 管理辅助 ──
AdminUserLookupItem:
type: object
description: 账号查询条目(脱敏:仅邀请协作者所需的最小字段,不含邮箱/手机/状态)。
required: [id, account, displayName]
properties:
id:
type: string
readOnly: true
description: 用户 ID(用于 POST /albums/{albumId}/members 的 userId)。
account:
type: string
readOnly: true
description: 登录账号。
displayName:
type: string
readOnly: true
description: 展示姓名。
AdminUserLookupList:
type: object
description: 账号前缀查询结果;无游标(前缀搜索 + 小上限即取即用)。
required: [items]
properties:
items:
type: array
description: 匹配的激活用户(按账号升序)。
items:
$ref: '#/components/schemas/AdminUserLookupItem'
# ── 公开(观众端) ──
SignedUrl:
+385
View File
@@ -118,6 +118,60 @@ export interface paths {
patch: operations["updateAlbum"];
trace?: never;
};
"/albums/{albumId}/assets": {
parameters: {
query?: never;
header?: never;
path: {
/** @description 相册 ID。 */
albumId: components["parameters"]["AlbumId"];
};
cookie?: never;
};
get?: never;
put?: never;
/**
* 初始化资产上传
* @description 为相册宣传图/封面/Logo 创建上传会话,返回 staging 对象的临时直传凭证
* (凭证结构与照片直传同形)。无 clientTaskId 幂等键:assetId 即一次性会话,
* 失败重试重新初始化即可。替换语义:同一 (kind, locale) 再次上传→新行生效,
* 旧行保留作历史(P1b 无删除端点);公开相册响应始终取该 kind 最新生效行
* (P1a 公开服务已按 latest 实现)。
*/
post: operations["initAlbumAssetUpload"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/albums/{albumId}/assets/{assetId}/complete": {
parameters: {
query?: never;
header?: never;
path: {
/** @description 相册 ID。 */
albumId: components["parameters"]["AlbumId"];
/** @description 相册资产 ID。 */
assetId: components["parameters"]["AssetId"];
};
cookie?: never;
};
get?: never;
put?: never;
/**
* 完成资产上传核验
* @description 核验 staging 对象(存在性、字节数与可用完整性信息)后将资产挂到相册生效,
* 并返回短期签名访问 URL。幂等:已核验通过的资产重复调用返回 200
* (URL 重新现签,本端点即签名刷新入口)。
*/
post: operations["completeAlbumAssetUpload"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/albums/{albumId}/members": {
parameters: {
query?: never;
@@ -294,6 +348,31 @@ export interface paths {
patch?: never;
trace?: never;
};
"/albums/{albumId}/photos": {
parameters: {
query?: never;
header?: never;
path: {
/** @description 相册 ID。 */
albumId: components["parameters"]["AlbumId"];
};
cookie?: never;
};
/**
* 管理端照片列表
* @description 相册照片管理列表(区别于公开列表):全部状态可见(unpublished/published/
* hidden/deleted),条目含 uploader 脱敏信息。按 uploadedAt 与 ID 稳定倒序
* 游标分页。
*/
get: operations["listAlbumPhotos"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/photos/{photoId}": {
parameters: {
query?: never;
@@ -321,6 +400,32 @@ export interface paths {
patch: operations["updatePhoto"];
trace?: never;
};
"/admin/users": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
/**
* 账号前缀查询
* @description 邀请协作者的前置查询:按账号前缀搜索激活用户,返回脱敏列表
* (id/account/displayName,不含邮箱、手机与状态字段)。
* 访问策略(P1b 决策):任何已认证工作人员可用——邀请的发起方不限于
* 创建者/管理员,而泄露面仅为激活用户的最小展示字段、接收方为已认证
* 内部人员,风险可接受;未认证一律 401。前缀匹配为大小写敏感
* (服务端按二进制校对执行 LIKE prefix%);按账号升序返回,无游标
* (前缀搜索 + 小上限即取即用)。
*/
get: operations["lookupUsersByAccount"];
put?: never;
post?: never;
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/public/albums/{token}": {
parameters: {
query?: never;
@@ -573,6 +678,45 @@ export interface components {
/** @description 相册分类(按 sortOrder 升序)。 */
categories: components["schemas"]["Category"][];
};
/**
* @description 相册资产类型(技术实施方案 §4 album_assets.kind)。banner 为观众页宣传图
* (PublicAlbum.banner,P1b 消费方);cover/logo 建表与上传链路就绪,展示
* 消费随后续阶段。
* @enum {string}
*/
AssetKind: "banner" | "cover" | "logo";
/**
* @description 资产上传初始化请求;大小与类型白名单与照片直传一致(UPLOAD_MAX_BYTES
* 环境策略,逐请求惰性读取)。locale 为可选语言维度:同一 (kind, locale)
* 的再次上传即替换(新行生效);语言无关的资产(如 logo)可不传。
*/
AssetInitRequest: {
kind: components["schemas"]["AssetKind"];
locale?: components["schemas"]["Locale"];
/** @description 原始文件名(仅元数据,不作对象唯一标识)。 */
filename: string;
/**
* Format: int64
* @description 申明文件字节数;完成核验时逐一比对。
*/
bytes: number;
/**
* @description MIME 类型白名单;image/heic 为暂定项,解码能力待 G2 真机验证后确认。
* @enum {string}
*/
contentType: "image/jpeg" | "image/png" | "image/heic";
};
AssetInitResponse: {
/** @description 资产 ID(完成核验的寻址凭据,一次性会话)。 */
readonly assetId: string;
/** @description staging 对象键(photo-live/{env}/albums/{albumId}/assets/{assetId}.{ext})。 */
readonly objectKey: string;
credential: components["schemas"]["UploadCredential"];
};
AssetCompleteResponse: components["schemas"]["SignedUrl"] & {
/** @description 资产 ID。 */
readonly assetId: string;
};
MemberAddRequest: {
/** @description 被添加用户的 ID(须为已开通账号)。 */
userId: string;
@@ -790,6 +934,35 @@ export interface components {
/** @description 累计点赞数。 */
readonly likeCount: number;
};
/** @description 管理列表条目内嵌的上传人脱敏信息(不含邮箱、手机等凭据字段)。 */
AdminPhotoUploader: {
/** @description 上传人用户 ID(与 PhotoRecord.uploaderId 一致)。 */
readonly userId: string;
/** @description 登录账号。 */
readonly account: string;
/** @description 展示姓名(来自用户资料)。 */
readonly displayName: string;
};
AdminPhoto: components["schemas"]["PhotoRecord"] & {
uploader: components["schemas"]["AdminPhotoUploader"];
};
AdminPhotoList: components["schemas"]["CursorPageMeta"] & {
items: components["schemas"]["AdminPhoto"][];
};
/** @description 账号查询条目(脱敏:仅邀请协作者所需的最小字段,不含邮箱/手机/状态)。 */
AdminUserLookupItem: {
/** @description 用户 ID(用于 POST /albums/{albumId}/members 的 userId)。 */
readonly id: string;
/** @description 登录账号。 */
readonly account: string;
/** @description 展示姓名。 */
readonly displayName: string;
};
/** @description 账号前缀查询结果;无游标(前缀搜索 + 小上限即取即用)。 */
AdminUserLookupList: {
/** @description 匹配的激活用户(按账号升序)。 */
items: components["schemas"]["AdminUserLookupItem"][];
};
/** @description 私有对象的短期签名 URL;不得写入持久分享链接,过期后经原入口刷新。 */
SignedUrl: {
/** @description 签名访问 URL。 */
@@ -900,6 +1073,8 @@ export interface components {
UserId: string;
/** @description 分类 ID。 */
CategoryId: string;
/** @description 相册资产 ID。 */
AssetId: string;
/** @description 上传会话 ID。 */
UploadId: string;
/** @description 照片 ID。 */
@@ -1193,6 +1368,119 @@ export interface operations {
500: components["responses"]["InternalError"];
};
};
initAlbumAssetUpload: {
parameters: {
query?: never;
header?: never;
path: {
/** @description 相册 ID。 */
albumId: components["parameters"]["AlbumId"];
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["AssetInitRequest"];
};
};
responses: {
/** @description 资产上传会话已创建。 */
201: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AssetInitResponse"];
};
};
/** @description 参数校验失败(VALIDATION_FAILED):文件超过大小限额或类型不接受。 */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
401: components["responses"]["Unauthorized"];
/** @description 仅创建者或管理员可管理资产(AUTH_FORBIDDEN)。 */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。 */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
500: components["responses"]["InternalError"];
};
};
completeAlbumAssetUpload: {
parameters: {
query?: never;
header?: never;
path: {
/** @description 相册 ID。 */
albumId: components["parameters"]["AlbumId"];
/** @description 相册资产 ID。 */
assetId: components["parameters"]["AssetId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 核验通过,资产已生效。 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AssetCompleteResponse"];
};
};
/**
* @description 核验失败(UPLOAD_VERIFY_FAILED):对象缺失、字节数与申明不符
* 或完整性信息不可用。
*/
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
401: components["responses"]["Unauthorized"];
/** @description 资产不存在或无权操作(AUTH_FORBIDDEN,不区分以避免枚举)。 */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。 */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
500: components["responses"]["InternalError"];
};
};
listAlbumMembers: {
parameters: {
query?: {
@@ -1695,6 +1983,67 @@ export interface operations {
500: components["responses"]["InternalError"];
};
};
listAlbumPhotos: {
parameters: {
query?: {
/** @description 可见性过滤;缺省为全部状态。 */
visibility?: components["schemas"]["PhotoVisibility"];
/** @description 分类 ID 过滤。 */
categoryId?: string;
/** @description 分页游标(上一页响应的 nextCursor)。 */
cursor?: components["parameters"]["CursorQuery"];
/** @description 每页条数。 */
limit?: number;
};
header?: never;
path: {
/** @description 相册 ID。 */
albumId: components["parameters"]["AlbumId"];
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 管理照片列表页。 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AdminPhotoList"];
};
};
/** @description 查询参数校验失败(VALIDATION_FAILED)。 */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
401: components["responses"]["Unauthorized"];
/** @description 仅创建者或管理员可访问管理列表(AUTH_FORBIDDEN)。 */
403: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
/** @description 相册不存在,或当前用户不可见(ALBUM_NOT_FOUND)。 */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
500: components["responses"]["InternalError"];
};
};
deletePhoto: {
parameters: {
query?: never;
@@ -1792,6 +2141,42 @@ export interface operations {
500: components["responses"]["InternalError"];
};
};
lookupUsersByAccount: {
parameters: {
query: {
/** @description 账号前缀(≥2 字符以防过宽匹配)。 */
accountPrefix: string;
/** @description 返回条数上限。 */
limit?: number;
};
header?: never;
path?: never;
cookie?: never;
};
requestBody?: never;
responses: {
/** @description 匹配的激活用户列表(无匹配为空数组)。 */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["AdminUserLookupList"];
};
};
/** @description 查询参数校验失败(VALIDATION_FAILED;accountPrefix 缺失或长度非法)。 */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["Error"];
};
};
401: components["responses"]["Unauthorized"];
500: components["responses"]["InternalError"];
};
};
getPublicAlbum: {
parameters: {
query?: {