Files
photo-live/packages/db-cn/prisma/migrations/20260920114149_user_sync_domain/migration.sql
T
浅念 3681999667 feat(api): scheduled user sync with mock source (T30 semantics)
P4a Task 7 —— cn 侧用户定期同步(mock 源先行,G4 真实接口到位后仅替换
UserSourcePort provider,引擎/契约/表不变)。

数据模型(db-cn 迁移 20260920114149,utf8mb4_0900_bin 逐表校对):
- user_sync_runs:批次账本(trigger cron|manual、state running|success|
  partial|failed、cursor 水位、counts JSON、起止时间与错误摘要);
- user_sync_errors:单批次坏行明细(runId 索引);
- external_user_mappings:本地↔外部 ID 映射(@@unique([sourceSystem,
  externalUserId]),sourceVersion 逐行高水位);
- user_sync_state:单行状态(lastRunAt/lastSuccessAt/cursor/runCount)。

同步引擎(UserSyncEngine):
- Redis SETNX 分布式互斥(TTL 10min,锁值=runId,Lua CAS 释放);
  定时 tick 与手动触发共用,运行中批次复用 runId(202);
- 游标增量 + 周期全量对账(RECONCILE_EVERY_N_RUNS 默认 10);
- 逐页单事务提交(run counts+cursor / state cursor),分页中断→failed
  且水位不前进、会话不动;
- 红线遵守:sync 不写 password_hash/role/album_members/localLocked;
  空/部分页不误伤停用;上游恢复不解除本地锁定;
- 新用户按 pending 建(随机不可用哨兵哈希,激活+重置后方可登录);
- 停用即撤销该用户全部会话;本地预建同名账号走关联而非重建。

Admin API(/admin/user-sync):GET status|runs|errors?runId=、POST runs
(202 {runId});runs 为 startedAt+ID 倒序游标分页。

auth.service:登录对哨兵/损坏哈希加固(等代价 dummy verify,
401 而非 500,时序平坦)。

测试:user-sync.e2e-spec 12 用例(首轮全量/改名幂等/停用撤会话/
恢复不解锁/新增/空页/分页中断/部分无效/手动互斥/增量+对账/API 形状
与权限/pending 登录门);api e2e 22 套件 239 用例全绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-20 20:01:17 +08:00

71 lines
3.1 KiB
SQL
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.
-- P4a Task 7:用户同步域(A29/T30 语义,mock 源先行)。
-- 校对手决:Prisma 生成 DDL 固定 utf8mb4_unicode_ci,逐表改写为库级约定的
-- utf8mb4_0900_bin(sourceSystem/externalUserId/code 等字符串列的大小写
-- 敏感唯一/比较语义依赖它)。
-- · user_sync_runs.cursor 为 VarChar(128):G4 真实源可能出现非数值游标;
-- mock 源恒为数值串(源版本高水位),API 层投影为契约 int64。
-- · external_user_mappings.userId 为扁平字符串外键(无 FK,同相册域约定:
-- 用户注销等跨域删除语义由应用层决策,且减轻 TRUNCATE 隔离负担)。
-- · user_sync_state 单行状态表:迁移内种子行(固定 id 见
-- USER_SYNC_STATE_SINGLETON_ID;reset.ts TRUNCATE 后重种同值)。
-- `trigger` 为 MySQL 保留字:Prisma 查询恒反引号标识符,本迁移同样反引号。
-- CreateTable
CREATE TABLE `user_sync_runs` (
`id` VARCHAR(191) NOT NULL,
`trigger` VARCHAR(16) NOT NULL,
`state` VARCHAR(16) NOT NULL DEFAULT 'running',
`cursor` VARCHAR(128) NULL,
`counts` JSON NOT NULL,
`startedAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`finishedAt` DATETIME(6) NULL,
`error` VARCHAR(1024) NULL,
INDEX `user_sync_runs_startedAt_idx`(`startedAt`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `user_sync_errors` (
`id` VARCHAR(191) NOT NULL,
`runId` VARCHAR(64) NOT NULL,
`externalUserId` VARCHAR(64) NULL,
`code` VARCHAR(64) NOT NULL,
`detail` VARCHAR(1024) NOT NULL,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
INDEX `user_sync_errors_runId_idx`(`runId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `external_user_mappings` (
`id` VARCHAR(191) NOT NULL,
`userId` VARCHAR(64) NOT NULL,
`sourceSystem` VARCHAR(32) NOT NULL DEFAULT 'mock',
`externalUserId` VARCHAR(64) NOT NULL,
`sourceVersion` INTEGER NOT NULL DEFAULT 0,
`createdAt` DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
INDEX `external_user_mappings_userId_idx`(`userId`),
UNIQUE INDEX `external_user_mappings_sourceSystem_externalUserId_key`(`sourceSystem`, `externalUserId`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- CreateTable
CREATE TABLE `user_sync_state` (
`id` VARCHAR(191) NOT NULL,
`lastRunAt` DATETIME(6) NULL,
`lastSuccessAt` DATETIME(6) NULL,
`cursor` VARCHAR(128) NULL,
`runCount` INTEGER NOT NULL DEFAULT 0,
`updatedAt` DATETIME(6) NOT NULL,
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_bin;
-- 单行种子(引擎 upsert 兜底,但迁移种子保证常规路径下不变量成立——
-- status 端点直接读本行,缺行视作「从未运行」)。
INSERT INTO `user_sync_state` (`id`, `lastRunAt`, `lastSuccessAt`, `cursor`, `runCount`, `updatedAt`)
VALUES ('user-sync-state-single-row', NULL, NULL, NULL, 0, CURRENT_TIMESTAMP(6));