feat: strengthen risk controls and review workflows

This commit is contained in:
hectorzhao
2026-07-26 13:08:12 +08:00
parent b461532075
commit 2ce682c3fc
34 changed files with 2167 additions and 390 deletions
+15
View File
@@ -17,6 +17,9 @@ function createPrismaMock() {
create: jest.fn().mockResolvedValue(tenant),
update: jest.fn().mockResolvedValue(tenant),
},
smsApplication: {
count: jest.fn().mockResolvedValue(0),
},
enterpriseCertification: {
findFirst: jest.fn().mockResolvedValue(null),
create: jest.fn().mockResolvedValue({ id: 'cert-1' }),
@@ -81,6 +84,18 @@ describe('TenantsService', () => {
expect(prisma.tenant.update).not.toHaveBeenCalled();
});
it('blocks enterprise deletion while applications are active or disabling', async () => {
const prisma = createPrismaMock();
prisma.smsApplication.count.mockResolvedValue(2);
const service = new TenantsService(prisma as never);
await expect(service.delete('tenant-1')).rejects.toThrow('还有 2 个启用或停用中的企业应用');
expect(prisma.tenant.update).not.toHaveBeenCalled();
expect(prisma.smsApplication.count).toHaveBeenCalledWith({
where: { tenantId: 'tenant-1', status: { in: ['active', 'disabling'] } },
});
});
it('rejects enterprise credit codes containing non-alphanumeric characters', async () => {
const prisma = createPrismaMock();
const service = new TenantsService(prisma as never);
+15 -1
View File
@@ -123,7 +123,21 @@ export class TenantsService {
}
delete(id: string) {
return this.changeStatus(id, 'deleted');
return this.deleteAfterApplicationCheck(id);
}
private async deleteAfterApplicationCheck(id: string) {
await this.ensureTenant(id);
const blockingApplications = await this.prisma.smsApplication.count({
where: { tenantId: id, status: { in: ['active', 'disabling'] } },
});
if (blockingApplications > 0) {
throw new BadRequestException(`该企业还有 ${blockingApplications} 个启用或停用中的企业应用,请先完成应用停用`);
}
return this.prisma.tenant.update({
where: { id },
data: { status: 'deleted' },
});
}
private async ensureTenant(id: string) {