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:
浅念
2026-09-18 08:40:32 +08:00
parent c2a9cc51f3
commit 361a72f300
10 changed files with 974 additions and 24 deletions
@@ -0,0 +1,11 @@
-- AlterTable
-- P1b Task 3:album_assets 行内一次性上传会话——state(pending→effective)+
-- 申明值 expectedBytes/contentType(complete head 核验依据)。
-- VARCHAR 列显式 utf8mb4_0900_bin(沿用库级约定:--create-only 后手工定校对)。
ALTER TABLE `album_assets` ADD COLUMN `contentType` VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NOT NULL DEFAULT '',
ADD COLUMN `expectedBytes` INTEGER NOT NULL DEFAULT 0,
ADD COLUMN `state` VARCHAR(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin NOT NULL DEFAULT 'pending';
-- 存量行(状态概念引入前已直种的对象)视为已生效:公开查询自此过滤
-- state='effective',不回填会把迁移前的资产整体藏掉。
UPDATE `album_assets` SET `state` = 'effective';
+9 -6
View File
@@ -117,12 +117,15 @@ model category_translations {
}
model album_assets {
id String @id @default(cuid())
albumId String @db.VarChar(64)
kind String @db.VarChar(16) // banner|cover|logo
locale String? @db.VarChar(16)
objectKey String @db.VarChar(255)
createdAt DateTime @default(now()) @db.DateTime(6)
id String @id @default(cuid())
albumId String @db.VarChar(64)
kind String @db.VarChar(16) // banner|cover|logo
locale String? @db.VarChar(16)
objectKey String @db.VarChar(255)
state String @default("pending") @db.VarChar(16) // pending|effective(行内一次性上传会话,P1b Task 3)
expectedBytes Int @default(0) // init 申明字节数(complete head 核验依据)
contentType String @default("") @db.VarChar(64) // init 申明 MIME(complete head 核验依据)
createdAt DateTime @default(now()) @db.DateTime(6)
@@index([albumId, kind])
}