Files
photo-live/packages/db-cn/prisma/schema.prisma
T
浅念 955bea49b5 feat(sync): stat snapshot return path with version replacement
P2 Task 9 统计回传(ov→cn):
- cn 迁移:albums 增 viewCountReplica/statsVersion/statsSyncedAt、
  photos 增 statsVersion(like_count 已有副本位;无 VARCHAR 新列不涉校对手决)
- cn 接收端 POST /v1/internal/sync/stat-snapshots(InternalSyncGuard):
  整批单事务,逐快照 updateMany WHERE statsVersion < statVersion —— 仅更高
  statVersion 以权威 total 整体替换副本(绝不相加),重复/乱序幂等忽略;
  accepted 只数实际应用条数;views 应用同时推进 statsSyncedAt(A27 展示)
- ov 回传发送器 services/sync/stat-return.ts:聚合 outbox_stat_events 未发送
  行按(kind, photoId|albumId)取 MAX(statVersion) 折叠突发 → HMAC 推送 →
  200 后按计数器标记截至该版本全部源行 sentAt;失败留待下轮重发;main.ts
  第三循环 STAT_RETURN_INTERVAL_MS(默认 60s,env 可调)
- 契约 AlbumSummary + statsSyncedAt(string|null readOnly,+gen);listAlbums/
  getAlbum 经 toSummaryResponse 暴露(管理 UI 列留 P4a)
- e2e(services/sync stat-return.e2e-spec,7 例):逐次点赞回传替换式收敛
  (2→5→10 非累加)、浏览窗口去重回传 + statsSyncedAt、重复回传 accepted=0
  值不动(A27 不重复累计)、乱序高版本替换(total 可下调)+ 低版本忽略、
  发送端折叠只发最高版本且被折叠行随批确认、管理列表 likeCount/statsSyncedAt
  展示、新端点 401/400 守卫

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-18 18:45:43 +08:00

219 lines
9.3 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)
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)
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
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])
}