fix(correctness): 并发/事务/实体/时区/状态一致性修复

由 OCR(open-codereview.ai,deepseek-v4-flash)审查驱动修复:
- wallets 原子扣款防 double-spend;refund 条件更新幂等;findTransactions 分页
- financial/imports/occupancies/attendance 事务与 advisory lock;重复生成/提交幂等
- 矛盾校验器、日期区间、实体双映射/DECIMAL/nullable、时区统一(china-time)
- rbac-seed 防重激活、exam 权限恢复、状态一致性、路由顺序、N+1/IN 分块等性能项

Reviewed-by: OCR (open-codereview.ai)
This commit is contained in:
2026-08-09 21:29:54 +08:00
parent 99ea931409
commit f50301148d
54 changed files with 3843 additions and 722 deletions
+71
View File
@@ -28,6 +28,7 @@ describe('RbacService seedData', () => {
remove: jest.fn(async (value: any) => value),
};
const roleRepo = {
count: jest.fn(async () => 0),
findOne: jest.fn(async ({ where }: any) =>
where.code === 'system_admin' || where.name === '系统管理员' ? systemAdminRole : null,
),
@@ -99,6 +100,7 @@ describe('RbacService seedData', () => {
remove: jest.fn(async (value) => value),
};
const roleRepo = {
count: jest.fn(async () => 0),
findOne: jest.fn(async ({ where }: any) =>
where.code === 'teacher' || where.name === '老师' ? teacherRole : null,
),
@@ -187,6 +189,7 @@ describe('RbacService legacy role consolidation', () => {
remove: jest.fn(async (value) => value),
};
const roleRepo = {
count: jest.fn(async () => 0),
findOne: jest.fn(async () => targetRole),
create: jest.fn((value) => ({ ...value, permissions: [] })),
save: jest.fn(async (value) => value),
@@ -217,3 +220,71 @@ describe('RbacService legacy role consolidation', () => {
expect(roleRepo.remove).toHaveBeenCalledWith(legacyRole);
});
});
describe('RbacSeedService startup gating', () => {
const makeService = (roleCount: number) => {
const permRepo = {
findOne: jest.fn(async () => null),
create: jest.fn(),
save: jest.fn(),
find: jest.fn(async () => []),
remove: jest.fn(),
};
const roleRepo = {
count: jest.fn(async () => roleCount),
findOne: jest.fn(async () => null),
create: jest.fn(),
save: jest.fn(),
find: jest.fn(async () => []),
remove: jest.fn(),
};
const userRepo = {
update: jest.fn(async () => ({ affected: 0 })),
count: jest.fn(),
create: jest.fn(),
save: jest.fn(),
findOne: jest.fn(),
};
const service = new RbacService(
permRepo as never,
roleRepo as never,
userRepo as never,
{} as never,
{} as never,
{} as never,
{} as never,
{} as never,
);
return { service, permRepo, roleRepo, userRepo };
};
it('skips re-seeding (no reactivation) when roles exist and SEED_ROLES is not set', async () => {
const original = process.env.SEED_ROLES;
delete process.env.SEED_ROLES;
try {
const { service, permRepo, roleRepo, userRepo } = makeService(3);
await service.seedData();
expect(roleRepo.count).toHaveBeenCalled();
expect(permRepo.findOne).not.toHaveBeenCalled();
expect(roleRepo.save).not.toHaveBeenCalled();
expect(userRepo.update).not.toHaveBeenCalled();
} finally {
if (original === undefined) delete process.env.SEED_ROLES;
else process.env.SEED_ROLES = original;
}
});
it('seeds when SEED_ROLES=true even if roles already exist', async () => {
const original = process.env.SEED_ROLES;
process.env.SEED_ROLES = 'true';
try {
const { service, permRepo, roleRepo } = makeService(3);
await service.seedData();
expect(permRepo.findOne).toHaveBeenCalled();
expect(roleRepo.save).toHaveBeenCalled();
} finally {
if (original === undefined) delete process.env.SEED_ROLES;
else process.env.SEED_ROLES = original;
}
});
});