feat: add HTTP API and complete client workflows

This commit is contained in:
hectorzhao
2026-07-16 11:34:06 +08:00
parent 4f07b331e5
commit dcb6162dcf
40 changed files with 2548 additions and 365 deletions
+82 -1
View File
@@ -65,6 +65,7 @@ function createPrismaMock() {
count: jest.fn().mockResolvedValue(0),
},
smsSignature: {
groupBy: jest.fn().mockResolvedValue([]),
findMany: jest.fn().mockResolvedValue([{
id: 'sig-1',
tenantId: 'tenant-1',
@@ -224,7 +225,7 @@ describe('SmsConfigService', () => {
]);
expect(prisma.smsApplication.findMany).toHaveBeenCalledWith(expect.objectContaining({
where: expect.objectContaining({ status: 'active', tenant: { name: { contains: '租户' } }, name: { contains: '应用' } }),
include: { tenant: true, ipAllowlist: true },
include: { tenant: true, ipAllowlist: true, httpConfig: true },
}));
expect(prisma.smsApplication.findMany.mock.calls[0][0]).not.toHaveProperty('take');
expect(prisma.smsMessageRecord.groupBy).toHaveBeenCalledWith(expect.objectContaining({
@@ -661,6 +662,86 @@ describe('SmsConfigService', () => {
]);
});
it('removes channel sources from client report-field responses', async () => {
const prisma = createPrismaMock();
prisma.channelRouteRule.findMany.mockResolvedValue([{
id: 'route-1', priority: 10,
group: {
id: 'group-1', name: '内部通道组',
items: [{ channel: { id: 'channel-secret', code: 'SECRET-CH', name: '内部通道', reportFields: [{ status: 'active', required: true, reportType: 'signature', drainageField: { id: 'field-1', code: 'license', name: '营业执照', fieldType: 'file', description: null, status: 'active' } }] } }],
},
}] as never);
const service = new SmsConfigService(prisma as never);
const fields = await service.getClientApplicationReportFields('app-1', 'signature');
expect(fields).toEqual([expect.objectContaining({ id: 'field-1', code: 'license', required: true })]);
expect(JSON.stringify(fields)).not.toContain('channel-secret');
expect(JSON.stringify(fields)).not.toContain('内部通道');
expect(fields[0]).not.toHaveProperty('channels');
});
it('returns client signatures without report tasks, channels, or internal requirement snapshots', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findMany.mockResolvedValue([{
id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '签名A', purpose: '通知',
auditStatus: 'rejected', rejectReason: '请补充资料',
drainageInfo: { signatureReportValues: { license: 'file-1' }, reportRequirements: [{ channelId: 'channel-secret', channelName: '内部通道' }] },
createdAt: new Date('2026-07-16T01:00:00Z'), updatedAt: new Date('2026-07-16T02:00:00Z'),
application: { id: 'app-1', name: '应用A', status: 'active' },
materials: [{ id: 'material-1', fileObjectId: 'file-1', materialType: 'license', title: '营业执照', description: null, createdAt: new Date() }],
drainageItems: [{ id: 'drainage-1', siteName: '官网', url: 'https://example.com', remark: null, reportValues: { owner: '企业A' }, auditStatus: 'pending', rejectReason: null, submittedAt: new Date(), reviewedAt: null, createdAt: new Date(), updatedAt: new Date() }],
_count: { reportMaterials: 2 },
reportTasks: [{ channelId: 'channel-secret' }],
}] as never);
const service = new SmsConfigService(prisma as never);
const result = await service.listClientSignatures('tenant-1');
const serialized = JSON.stringify(result);
expect(result[0]).toEqual(expect.objectContaining({
id: 'sig-1',
submittedMaterialCount: 3,
reportValues: { license: 'file-1' },
drainageInfo: { links: [expect.objectContaining({ id: 'drainage-1', siteName: '官网' })] },
}));
expect(serialized).not.toContain('channel-secret');
expect(serialized).not.toContain('内部通道');
expect(result[0]).not.toHaveProperty('reportTasks');
expect(result[0]).not.toHaveProperty('reportStatus');
});
it('returns real client signature workspace counts from database grouping', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findMany.mockResolvedValue([]);
prisma.smsSignature.groupBy.mockResolvedValue([
{ auditStatus: 'pending', _count: { _all: 2 } },
{ auditStatus: 'approved', _count: { _all: 5 } },
{ auditStatus: 'rejected', _count: { _all: 1 } },
] as never);
const service = new SmsConfigService(prisma as never);
await expect(service.getClientSignatureWorkspace('tenant-1')).resolves.toEqual({
items: [],
summary: { total: 8, pending: 2, approved: 5, rejected: 1, draft: 0 },
});
expect(prisma.smsSignature.groupBy).toHaveBeenCalledWith(expect.objectContaining({
where: { tenantId: 'tenant-1', auditStatus: { notIn: ['deleted', 'disabled'] } },
}));
});
it('selects client drainage information without internal tasks or channels', async () => {
const prisma = createPrismaMock();
prisma.smsDrainageInfo.findMany.mockResolvedValue([{ id: 'drainage-1', siteName: '官网' }] as never);
const service = new SmsConfigService(prisma as never);
await expect(service.listClientDrainageInfos('tenant-1')).resolves.toEqual([{ id: 'drainage-1', siteName: '官网' }]);
const query = prisma.smsDrainageInfo.findMany.mock.calls[0][0];
expect(query.where).toEqual({ id: undefined, tenantId: 'tenant-1', auditStatus: { not: 'deleted' } });
expect(query.select).not.toHaveProperty('reportTasks');
expect(JSON.stringify(query.select)).not.toContain('channel');
});
it('requires common signature fields even when a signature is not bound to an application', async () => {
const prisma = createPrismaMock();
prisma.commonReportField.findMany.mockResolvedValue([{