feat(api): photo, upload, outbox schema

Task 10: photos/photo_variants/upload_sessions/photo_jobs/outbox_events
on the CN schema (flat string FKs, no Prisma relations); migration
20260917120827_photo_upload_domain rewritten to utf8mb4_0900_bin per the
固化 collation recipe; reset.ts table order extended; one semantics e2e
case for photo_jobs @@unique([photoId, type]) and the upload_sessions
A03 idempotency anchor @@unique([userId, clientTaskId]).

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 20:11:26 +08:00
parent a99b8f200b
commit 59464bd73d
4 changed files with 230 additions and 0 deletions
@@ -0,0 +1,95 @@
-- 同 20260917114313_init_identity:Prisma 生成的 MySQL DDL 固定写
-- utf8mb4_unicode_ci,会覆盖库级 utf8mb4_0900_bin;照片与上传域
-- (photos.objectKey、upload_sessions.objectKey、outbox_events.type 等)
-- 同样依赖 bin 校对的大小写敏感语义,故逐表改写为 utf8mb4_0900_bin。
-- CreateTable
CREATE TABLE `photos` (
`id` VARCHAR(191) NOT NULL,
`albumId` VARCHAR(64) NOT NULL,
`uploaderId` VARCHAR(64) NOT NULL,
`categoryId` VARCHAR(64) NULL,
`objectKey` VARCHAR(255) NOT NULL,
`bytes` INTEGER NOT NULL DEFAULT 0,
`width` INTEGER NULL,
`height` INTEGER NULL,
`shotAt` DATETIME(6) NULL,
`uploadedAt` DATETIME(6) NULL,
`publishedAt` DATETIME(6) NULL,
`processingState` VARCHAR(191) NOT NULL DEFAULT 'pending_upload',
`errorCode` VARCHAR(64) NULL,
`visibility` VARCHAR(191) NOT NULL DEFAULT 'unpublished',
`likeCount` INTEGER NOT NULL DEFAULT 0,
`entityVersion` INTEGER NOT NULL DEFAULT 1,
UNIQUE INDEX `photos_objectKey_key`(`objectKey`),
INDEX `photos_albumId_visibility_publishedAt_id_idx`(`albumId`, `visibility`, `publishedAt`, `id`),
INDEX `photos_albumId_uploaderId_uploadedAt_idx`(`albumId`, `uploaderId`, `uploadedAt`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `photo_variants` (
`photoId` VARCHAR(64) NOT NULL,
`variant` VARCHAR(32) NOT NULL,
`objectKey` VARCHAR(255) NOT NULL,
`bytes` INTEGER NOT NULL,
`version` INTEGER NOT NULL DEFAULT 1,
`width` INTEGER NULL,
`height` INTEGER NULL,
UNIQUE INDEX `photo_variants_objectKey_key`(`objectKey`),
UNIQUE INDEX `photo_variants_photoId_variant_key`(`photoId`, `variant`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `upload_sessions` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(64) NOT NULL,
`albumId` VARCHAR(64) NOT NULL,
`clientTaskId` VARCHAR(128) NOT NULL,
`photoId` VARCHAR(64) NOT NULL,
`objectKey` VARCHAR(255) NOT NULL,
`expectedBytes` INTEGER NOT NULL,
`contentType` VARCHAR(128) NOT NULL,
`multipartId` VARCHAR(255) NULL,
`state` VARCHAR(191) NOT NULL DEFAULT 'pending',
`expiresAt` DATETIME(6) NOT NULL,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
UNIQUE INDEX `upload_sessions_photoId_key`(`photoId`),
UNIQUE INDEX `upload_sessions_objectKey_key`(`objectKey`),
UNIQUE INDEX `upload_sessions_userId_clientTaskId_key`(`userId`, `clientTaskId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `photo_jobs` (
`id` VARCHAR(191) NOT NULL,
`photoId` VARCHAR(64) NOT NULL,
`type` VARCHAR(32) NOT NULL DEFAULT 'process',
`state` VARCHAR(191) NOT NULL DEFAULT 'pending',
`attempt` INTEGER NOT NULL DEFAULT 0,
`errorCode` VARCHAR(64) NULL,
`nextRetryAt` DATETIME(6) NULL,
`updatedAt` DATETIME(6) NOT NULL,
INDEX `photo_jobs_state_nextRetryAt_idx`(`state`, `nextRetryAt`),
UNIQUE INDEX `photo_jobs_photoId_type_key`(`photoId`, `type`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `outbox_events` (
`id` VARCHAR(191) NOT NULL,
`type` VARCHAR(64) NOT NULL,
`albumId` VARCHAR(64) NOT NULL,
`photoId` VARCHAR(64) NULL,
`entityVersion` INTEGER NOT NULL,
`payload` JSON NOT NULL,
`occurredAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`deliveredAt` DATETIME(6) NULL,
INDEX `outbox_events_deliveredAt_occurredAt_idx`(`deliveredAt`, `occurredAt`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
+74
View File
@@ -126,3 +126,77 @@ model album_assets {
@@index([albumId, kind])
}
// —— 照片与上传域 + outbox(Task 10)——
// 同相册域约定:扁平字符串外键、无 Prisma relation/FK 约束。
// photos.objectKey 是 originals 正式键(Task 18 上传确认时写入);
// upload_sessions.objectKey 是 staging 键;outbox_events 承载领域事件
// (契约见 packages/contracts),由 worker 轮询投递。
model photos {
id String @id @default(cuid())
albumId String @db.VarChar(64)
uploaderId String @db.VarChar(64)
categoryId String? @db.VarChar(64)
objectKey String @unique @db.VarChar(255) // originals 正式键
bytes Int @default(0)
width Int?
height Int?
shotAt DateTime? @db.DateTime(6)
uploadedAt DateTime? @db.DateTime(6)
publishedAt DateTime? @db.DateTime(6)
processingState String @default("pending_upload") // pending_upload|uploaded|processing|ready|processing_failed
errorCode String? @db.VarChar(64)
visibility String @default("unpublished") // unpublished|published|hidden|deleted
likeCount Int @default(0) // P2 起为统计副本
entityVersion Int @default(1)
@@index([albumId, visibility, publishedAt, id])
@@index([albumId, uploaderId, uploadedAt])
}
model photo_variants {
photoId String @db.VarChar(64)
variant String @db.VarChar(32) // thumb|browse
objectKey String @unique @db.VarChar(255)
bytes Int
version Int @default(1)
width Int?
height Int?
@@unique([photoId, variant])
}
model upload_sessions {
id String @id @default(cuid())
userId String @db.VarChar(64)
albumId String @db.VarChar(64)
clientTaskId String @db.VarChar(128)
photoId String @unique @db.VarChar(64)
objectKey String @unique @db.VarChar(255) // staging 键
expectedBytes Int
contentType String @db.VarChar(128)
multipartId String? @db.VarChar(255)
state String @default("pending") // pending|confirmed|cancelled|expired(对齐契约 UploadSessionState;核验失败是请求级错误不改会话态,expired 由 expiresAt 判定或 sweep 置位)
expiresAt DateTime @db.DateTime(6)
createdAt DateTime @default(now()) @db.DateTime(6)
@@unique([userId, clientTaskId]) // 幂等锚点(A03)
}
model photo_jobs {
id String @id @default(cuid())
photoId String @db.VarChar(64)
type String @default("process") @db.VarChar(32)
state String @default("pending") // pending|active|done|failed
attempt Int @default(0)
errorCode String? @db.VarChar(64)
nextRetryAt DateTime? @db.DateTime(6)
updatedAt DateTime @updatedAt @db.DateTime(6)
@@unique([photoId, type])
@@index([state, nextRetryAt])
}
model outbox_events {
id String @id @default(cuid())
type String @db.VarChar(64)
albumId String @db.VarChar(64)
photoId String? @db.VarChar(64)
entityVersion Int
payload Json
occurredAt DateTime @default(now()) @db.DateTime(6)
deliveredAt DateTime? @db.DateTime(6)
@@index([deliveredAt, occurredAt])
}