feat(api): album asset upload chain
P1b Task 3: initAlbumAssetUpload / completeAlbumAssetUpload (tags
[assets]) over album_assets with in-row one-shot sessions.
- migration asset_state: album_assets + state (pending|effective),
expectedBytes, contentType (utf8mb4_0900_bin); pre-existing rows
backfilled to effective. Complete head-verifies bytes AND
contentType vs the init declaration (P1a Task 11 server-side
fallback), then conditionally flips pending->effective with the
winner writing the album.asset.add audit (resourceId=albumId,
metadata assetId/kind/locale). Effective rows re-complete
idempotently (re-signed URL, no re-verify, survives object
cleanup).
- object key is the final key (albums/{albumId}/assets/{assetId}.{ext});
the pending row is the logical staging - no copy step, keys embed
assetId so re-init can never overwrite another row's object.
- permissions: owner/admin only (collaborator 403, outsider 404);
uploadEnabled does NOT gate assets (switch governs photo uploads
per 4.9 - both endpoints carry no ALBUM_UPLOAD_DISABLED).
- public banner now locale-aware (req 4.7): effective rows resolved
as display-locale row -> global (locale NULL) row -> null, latest
wins per tier (same-kind re-upload replaces, old rows retained).
- e2e: test/assets.e2e-spec.ts (14 cases, real jpegs via sharp,
MinIO round-trips); public spec seed marked effective.
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
// services/api/src/albums/dto.ts —— 相册端点入参 DTO(Task 15;Task 16 增补更新版;
|
||||
// Task 17 增补成员/分类)。校验约束对齐契约
|
||||
// Task 17 增补成员/分类;P1b Task 3 增补资产)。校验约束对齐契约
|
||||
// AlbumCreateRequest / AlbumUpdateRequest / MemberAddRequest /
|
||||
// CategoryCreateRequest / CategoryUpdateRequest / AlbumTranslation:
|
||||
// CategoryCreateRequest / CategoryUpdateRequest / AlbumTranslation /
|
||||
// AssetInitRequest:
|
||||
// · translations 至少 1 条(minItems: 1),locale 限 P1a 双语枚举;
|
||||
// · defaultLocale 必须有对应翻译条目(跨字段校验,契约 required 语义);
|
||||
// · endAt 可空(契约 [string,'null']),给值时须晚于 startAt(跨字段校验);
|
||||
@@ -29,6 +30,10 @@ import {
|
||||
type ValidatorConstraintInterface,
|
||||
type ValidationArguments,
|
||||
} from 'class-validator';
|
||||
import {
|
||||
UPLOAD_CONTENT_TYPES,
|
||||
type UploadContentType,
|
||||
} from '../uploads/support';
|
||||
|
||||
/** 契约 Locale 枚举(P1a 双语)。 */
|
||||
export const ALBUM_LOCALES = ['zh-CN', 'en'] as const;
|
||||
@@ -294,3 +299,37 @@ export class CategoryUpdateRequestDto {
|
||||
@Type(() => CategoryTranslationDto)
|
||||
translations?: CategoryTranslationDto[];
|
||||
}
|
||||
|
||||
// ── 资产(P1b Task 3)──
|
||||
|
||||
/** 契约 AssetKind:banner(观众页宣传图,PublicAlbum.banner 消费方);cover/logo 建链就绪,展示消费随后续阶段。 */
|
||||
export const ASSET_KINDS = ['banner', 'cover', 'logo'] as const;
|
||||
export type AssetKind = (typeof ASSET_KINDS)[number];
|
||||
|
||||
/**
|
||||
* 契约 AssetInitRequest。大小限额 UPLOAD_MAX_BYTES 与 filename 扩展名白名单是
|
||||
* 环境相关/派生校验,在服务层做(同 UploadInitRequestDto 约定);contentType
|
||||
* 与照片直传共用白名单(image/heic 暂定,G2 真机验证)。
|
||||
*/
|
||||
export class AssetInitRequestDto {
|
||||
@IsIn(ASSET_KINDS)
|
||||
kind!: AssetKind;
|
||||
|
||||
/** 可选语言维度:同 (kind, locale) 再上传即替换(新行生效);语言无关资产(如 logo)不传。 */
|
||||
@IsOptional()
|
||||
@IsIn(ALBUM_LOCALES)
|
||||
locale?: AlbumLocale;
|
||||
|
||||
/** 原始文件名,仅元数据;扩展名白名单校验在服务层(对象键后缀派生)。 */
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(255)
|
||||
filename!: string;
|
||||
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
bytes!: number;
|
||||
|
||||
@IsIn(UPLOAD_CONTENT_TYPES)
|
||||
contentType!: UploadContentType;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user