3681999667
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>
29 lines
1.3 KiB
TypeScript
29 lines
1.3 KiB
TypeScript
// services/api/src/user-sync/user-sync.module.ts —— 用户同步域模块(P4a Task 7)。
|
||
// 仅 cn 装配(AppCnModule import;ov 入口不碰——用户身份是 cn 单域事实)。
|
||
// 装配:SyncLock(cn Redis 互斥)、MockUserSource(USER_SOURCE 端口实现,
|
||
// G4 后替换 provider 即切真实源)、UserSyncEngine(批次引擎)、
|
||
// UserSyncScheduler(setInterval 定时触发,仅 cn bootstrap 启动)、
|
||
// UserSyncAdminService + UserSyncController(/admin/user-sync 端点)。
|
||
// useExisting 装配端口:引擎只依赖 UserSourcePort 符号,e2e 经
|
||
// app.get(MockUserSource) 注入场景(mock-user-source.ts 文件头)。
|
||
import { Module } from '@nestjs/common';
|
||
import { MockUserSource } from './mock-user-source';
|
||
import { SyncLock } from './sync-lock';
|
||
import { UserSyncAdminService } from './user-sync-admin.service';
|
||
import { UserSyncController } from './user-sync.controller';
|
||
import { UserSyncEngine, UserSyncScheduler } from './user-sync.service';
|
||
import { USER_SOURCE } from './user-source.port';
|
||
|
||
@Module({
|
||
controllers: [UserSyncController],
|
||
providers: [
|
||
SyncLock,
|
||
MockUserSource,
|
||
{ provide: USER_SOURCE, useExisting: MockUserSource },
|
||
UserSyncEngine,
|
||
UserSyncScheduler,
|
||
UserSyncAdminService,
|
||
],
|
||
})
|
||
export class UserSyncModule {}
|