Files
photo-live/packages/db-cn/prisma/schema.prisma
T
浅念 59464bd73d 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>
2026-09-17 20:11:26 +08:00

203 lines
7.8 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// packages/db-cn/prisma/schema.prisma —— CN 侧唯一 schema(Task 8 起步:身份与审计域)。
// 约定:MySQL 8.4、库级 collation utf8mb4_0900_bin(大小写敏感,账号/token 语义依赖它);
// 时间一律 @db.DateTime(6);ID cuid;连接池上限由 DATABASE_URL_CN 的 ?connection_limit=10
// 承担,不在应用侧另行放大。
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL_CN")
}
model users {
id String @id @default(cuid())
account String @unique @db.VarChar(64)
passwordHash String @db.VarChar(255)
displayName String @db.VarChar(128)
role String @default("staff") // staff|admin(公司角色;相册角色另表)
sourceStatus String @default("local") // local|active|disabled(P4a 同步用)
localLocked Boolean @default(false)
credentialStatus String @default("active") // active|pending
locale String? @db.VarChar(16)
createdAt DateTime @default(now()) @db.DateTime(6)
sessions sessions[]
auditLogs audit_logs[]
}
model sessions {
id String @id @default(cuid())
userId String
tokenHash String @unique @db.VarChar(64)
// 长效会话期限(默认 30 天,AUTH_SESSION_TTL_SECONDS 可调;不做短时效
// access/refresh 双层——用户决策 2026-09-17)
expiresAt DateTime @db.DateTime(6)
revokedAt DateTime? @db.DateTime(6) // 注销/停用撤销
user users @relation(fields: [userId], references: [id])
}
model audit_logs {
id String @id @default(cuid())
actorId String?
action String @db.VarChar(64)
resourceId String? @db.VarChar(64)
metadata Json?
createdAt DateTime @default(now()) @db.DateTime(6)
actor users? @relation(fields: [actorId], references: [id])
}
// —— 相册域(Task 9)——
// 约定:与身份域不同,相册域一律使用扁平字符串外键(albumId/ownerId/userId/
// categoryId 为纯字符串,不建 Prisma relation/FK 约束)——Task 10 的照片域同此。
// 原因:跨域删除语义(用户注销→相册归属、相册下线→照片保留)由应用层显式
// 决策,不做数据库级联;也避免 TRUNCATE 隔离工具为绕 FK 检查承担更多顺序负担。
model albums {
id String @id @default(cuid())
publicToken String @unique @db.VarChar(32)
ownerId String @db.VarChar(64)
startAt DateTime @db.DateTime(6)
endAt DateTime? @db.DateTime(6)
timezone String @default("Asia/Shanghai") @db.VarChar(64)
status String @default("active") // active|offline
defaultLocale String @db.VarChar(16)
uploadEnabled Boolean @default(true)
publicEnabled Boolean @default(true)
downloadEnabled Boolean @default(true)
originalDownloadEnabled Boolean @default(false)
watermarkVersion Int @default(0)
createdAt DateTime @default(now()) @db.DateTime(6)
updatedAt DateTime @updatedAt @db.DateTime(6)
@@index([ownerId])
@@index([status, publicEnabled])
}
model album_translations {
id String @id @default(cuid())
albumId String @db.VarChar(64)
locale String @db.VarChar(16)
title String @db.VarChar(255)
description String? @db.Text
venue String? @db.VarChar(255)
address String? @db.VarChar(255)
@@unique([albumId, locale])
@@index([albumId])
}
model album_members {
id String @id @default(cuid())
albumId String @db.VarChar(64)
userId String @db.VarChar(64)
role String @default("collaborator") // owner|collaborator
status String @default("active")
addedAt DateTime @default(now()) @db.DateTime(6)
@@unique([albumId, userId])
@@index([userId])
}
model categories {
id String @id @default(cuid())
albumId String @db.VarChar(64)
sortOrder Int @default(0)
createdAt DateTime @default(now()) @db.DateTime(6)
@@index([albumId, sortOrder])
}
model category_translations {
id String @id @default(cuid())
categoryId String @db.VarChar(64)
locale String @db.VarChar(16)
name String @db.VarChar(128)
@@unique([categoryId, locale])
}
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)
@@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])
}