Files
photo-live/packages/db-cn/prisma/schema.prisma
T
2026-09-20 17:53:44 +08:00

259 lines
12 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)
// ── 水印版本化(P4a Task 3)── 双版本位语义(权威表述见 contracts watermark.ts):
// watermarkVersion = 最新「已创建」配置版本(POST 即推进,新照片发布即刻套用,
// 也就是契约 AlbumSummary.watermarkVersion / WatermarkJobCreated.configVersion);
// watermarkEnabled + watermarkEffectiveVersion = 「有效」版本位——仅当该版本的
// 历史重生成任务 done 才推进(worker 完成事务维护,禁用版同样推进以翻转
// enabled=false)。ov 副本与公开侧变体选择以有效版本为准(对象存在性保证)。
watermarkVersion Int @default(0)
watermarkEnabled Boolean @default(false)
watermarkEffectiveVersion Int @default(0)
entityVersion Int @default(1) // P2 Task 3:album.updated 事件版本(同 photos 模式)
// P2 Task 9 统计回传副本位(ov→cn):viewCountReplica 为 ov album_stats
// 权威值的替换式副本;statsVersion 是回传替换版本(仅更高 statVersion 替换,
// 绝不相加);statsSyncedAt 记最近一次**应用**时刻(A27 同步时间展示)。
viewCountReplica Int @default(0)
statsVersion Int @default(0)
statsSyncedAt DateTime? @db.DateTime(6)
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)
entityVersion Int @default(1) // P2 Task 3:category.* 事件版本(同 photos 模式)
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)
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 核验依据)
entityVersion Int @default(1) // P2 Task 3:asset.updated 事件版本(仅 effective 迁移自增)
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 起为统计副本(ov likeTotal 替换式回传)
// P2 Task 9:回传替换版本(ov photos_ov.countVersion 的 cn 侧镜像比较位,
// 仅更高 statVersion 替换 likeCount,重复/乱序回传幂等)。
statsVersion Int @default(0)
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)
sourceDeviceId String? @db.VarChar(64) // 客户端设备标识(A19 来源语义;幂等重放保留首值)
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])
}
// —— 水印版本化(P4a Task 3)——
// watermark_configs:配置版本账本(append-only,每 POST 一行,config 为归一化
// StoredWatermarkConfig——contracts watermark.ts 的形状,缺省已填齐);
// watermark_jobs:历史重生成任务(id 兼作对外 jobId 与 BullMQ jobId——cuid 无
// ':',满足 BullMQ 保留分隔符约束)。state 语义:pending=已入队待消费;
// running=消费中(progress/failed 随照片逐张推进);done=全部处理完成(允许
// failed>0 的部分成功——失败张数记录在案,重试=重新 POST 新任务,按照片粒度
// 幂等);failed=整任务中止(配置缺失等任务级错误,errorCode 必有)。
model watermark_configs {
id String @id @default(cuid())
albumId String @db.VarChar(64)
configVersion Int
config Json
createdAt DateTime @default(now()) @db.DateTime(6)
@@unique([albumId, configVersion])
}
model watermark_jobs {
id String @id @default(cuid())
albumId String @db.VarChar(64)
configVersion Int
state String @default("pending") @db.VarChar(16) // pending|running|done|failed
progress Int @default(0)
total Int @default(0)
failed Int @default(0)
errorCode String? @db.VarChar(64)
createdAt DateTime @default(now()) @db.DateTime(6)
updatedAt DateTime @updatedAt @db.DateTime(6)
@@index([albumId, createdAt])
}
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
priority String @default("normal") @db.VarChar(16) // normal|high(P2 Task 3:下线/隐藏/删除类 high,发送器批次内优先)
payload Json
occurredAt DateTime @default(now()) @db.DateTime(6)
deliveredAt DateTime? @db.DateTime(6)
@@index([deliveredAt, occurredAt])
}