feat(api): identity and audit schema with mysql semantics tests

packages/db-cn:@photo/db-cn(prisma 5.22,api/worker 共享 client)+
schema(users/sessions/audit_logs,DateTime(6)、cuid、账号唯一)。
迁移 SQL 手工改写表级 collation 为 utf8mb4_0900_bin——Prisma 生成的 MySQL
DDL 固定 utf8mb4_unicode_ci 会覆盖库级 bin collation,账号/token 大小写
敏感语义依赖它(后续迁移需同样复核)。services/api:@Global PrismaModule
(PrismaService 封装 $connect/$disconnect,池上限仍由
DATABASE_URL_CN ?connection_limit=10 承担);reset.ts 落地 FK 包裹式
TRUNCATE;e2e 语义复核 4 例(UTC 往返/大小写敏感/audit Json+actor/
session FK Restrict P2003)。pnpm 10 未拦截 prisma engines(平台包内置),
无需 onlyBuiltDependencies。

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-17 19:51:56 +08:00
parent 7d250ec342
commit 2801e4f158
14 changed files with 417 additions and 14 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"name": "@photo/db-cn",
"version": "0.1.0",
"private": true,
"description": "CN Prisma schema + shared client (api 与 worker 共用;MySQL 8.4 utf8mb4_0900_bin,迁移经根 db:* 脚本注入 .env)",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"scripts": {
"generate": "prisma generate",
"migrate": "prisma migrate dev",
"build": "tsc -p tsconfig.json",
"typecheck": "tsc --noEmit",
"lint": "eslint ."
},
"dependencies": {
"@prisma/client": "^5.22.0"
},
"devDependencies": {
"prisma": "^5.22.0",
"typescript": "^5.6.0"
},
"prisma": {
"schema": "prisma/schema.prisma"
}
}
@@ -0,0 +1,50 @@
-- 注意:Prisma 生成的 MySQL DDL 固定写 utf8mb4_unicode_ci,会覆盖库级
-- utf8mb4_0900_bin;账号/token 的大小写敏感语义依赖 bin collation,故手工
-- 改写为 utf8mb4_0900_bin(后续迁移同样需要复核 CREATE TABLE 的 collation)。
-- CreateTable
CREATE TABLE `users` (
`id` VARCHAR(191) NOT NULL,
`account` VARCHAR(64) NOT NULL,
`passwordHash` VARCHAR(255) NOT NULL,
`displayName` VARCHAR(128) NOT NULL,
`role` VARCHAR(191) NOT NULL DEFAULT 'staff',
`sourceStatus` VARCHAR(191) NOT NULL DEFAULT 'local',
`localLocked` BOOLEAN NOT NULL DEFAULT false,
`credentialStatus` VARCHAR(191) NOT NULL DEFAULT 'active',
`locale` VARCHAR(16) NULL,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
UNIQUE INDEX `users_account_key`(`account`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `sessions` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(191) NOT NULL,
`tokenHash` VARCHAR(64) NOT NULL,
`expiresAt` DATETIME(6) NOT NULL,
`revokedAt` DATETIME(6) NULL,
UNIQUE INDEX `sessions_tokenHash_key`(`tokenHash`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `audit_logs` (
`id` VARCHAR(191) NOT NULL,
`actorId` VARCHAR(191) NULL,
`action` VARCHAR(64) NOT NULL,
`resourceId` VARCHAR(64) NULL,
`metadata` JSON NULL,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- AddForeignKey
ALTER TABLE `sessions` ADD CONSTRAINT `sessions_userId_fkey` FOREIGN KEY (`userId`) REFERENCES `users`(`id`) ON DELETE RESTRICT ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE `audit_logs` ADD CONSTRAINT `audit_logs_actorId_fkey` FOREIGN KEY (`actorId`) REFERENCES `users`(`id`) ON DELETE SET NULL ON UPDATE CASCADE;
@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (i.e. Git)
provider = "mysql"
+48
View File
@@ -0,0 +1,48 @@
// 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])
}
+5
View File
@@ -0,0 +1,5 @@
// packages/db-cn/src/index.ts —— CN 侧唯一 Prisma client 出口。
// api(PrismaService)与 worker(Task 20)都从本包导入,schema/迁移只此一份。
// commonjs 包:消费方(api/worker)均为 CJS,tsc 直出无需打包。
export { PrismaClient } from '@prisma/client';
export type { Prisma } from '@prisma/client';
+15
View File
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"lib": ["ES2022"],
"module": "commonjs",
"moduleResolution": "node10",
"declaration": true,
"outDir": "dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src"]
}