feat: refine report material workflow states

This commit is contained in:
hectorzhao
2026-09-03 12:50:33 +08:00
parent 55915ed826
commit 4ff6ed0786
12 changed files with 237 additions and 31 deletions
@@ -103,8 +103,15 @@ export class AdminSmsConfigController {
}
@Put('enterprise-signatures/:id')
updateSignature(@Param('id') signatureId: string, @Body() body: UpdateSmsSignatureDto) {
return this.smsConfig.updateSignature(signatureId, body);
updateSignature(
@Param('id') signatureId: string,
@Body() body: UpdateSmsSignatureDto,
@CurrentSessionUserId() reviewerId?: string,
) {
return this.smsConfig.updateSignature(signatureId, body, undefined, {
initialAuditStatus: 'approved',
reviewerId,
});
}
@Get('drainage-infos')
+43 -10
View File
@@ -440,15 +440,22 @@ export class SmsSignatureService {
]
: undefined,
};
const [items, total, pendingReportDetailTotal] = await Promise.all([
const [items, total, pendingReportSummary] = await Promise.all([
this.listSignatures({ ...query, page, pageSize }, true),
this.prisma.smsSignature.count({ where }),
this.countPendingReportDetails(where),
this.countPendingReportSummary(where),
]);
return { items, total, page, pageSize, pendingReportDetailTotal };
return {
items,
total,
page,
pageSize,
pendingReportMaterialTotal: pendingReportSummary.materialTotal,
pendingReportDetailTotal: pendingReportSummary.detailTotal,
};
}
private async countPendingReportDetails(where: Prisma.SmsSignatureWhereInput) {
private async countPendingReportSummary(where: Prisma.SmsSignatureWhereInput) {
const signatures = await this.prisma.smsSignature.findMany({
where: { AND: [where, { auditStatus: 'approved', pendingReport: true }] },
select: {
@@ -487,9 +494,11 @@ export class SmsSignatureService {
},
})
: [];
let total = 0;
let materialTotal = 0;
let detailTotal = 0;
for (const signature of signatures) {
if (!signature.applicationId || signature.application?.status !== 'active') continue;
let hasPendingTarget = false;
const generatedTargets = new Set<string>();
for (const item of signature.reportBatchItems.filter(
(entry) => entry.materialVersion === signature.materialVersion,
@@ -526,11 +535,13 @@ export class SmsSignatureService {
);
if (task?.status === 'abandoned') continue;
if (generatedTargets.has(`${channel.id}:${carrier}`) || generatedTargets.has(`${channel.id}:legacy`)) continue;
total += 1;
detailTotal += 1;
hasPendingTarget = true;
}
}
if (hasPendingTarget) materialTotal += 1;
}
return total;
return { materialTotal, detailTotal };
}
async getSignature(id: string) {
@@ -817,7 +828,12 @@ export class SmsSignatureService {
};
}
async updateSignature(signatureId: string, data: UpdateSmsSignatureDto, tenantId?: string) {
async updateSignature(
signatureId: string,
data: UpdateSmsSignatureDto,
tenantId?: string,
options: CreateSmsSignatureOptions = {},
) {
const signature = await this.prisma.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature || (tenantId && signature.tenantId !== tenantId)) {
throw new NotFoundException('Signature not found');
@@ -837,7 +853,9 @@ export class SmsSignatureService {
(data.purpose !== undefined && data.purpose !== signature.purpose) ||
(data.drainageInfo !== undefined &&
JSON.stringify(drainageInfo ?? null) !== JSON.stringify(signature.drainageInfo ?? null));
const auditStatus = materialChanged && signature.auditStatus === 'approved' ? 'pending' : data.auditStatus;
const auditStatus =
options.initialAuditStatus ??
(materialChanged && signature.auditStatus === 'approved' ? 'pending' : data.auditStatus);
const updated = await this.prisma.smsSignature.update({
where: { id: signatureId },
data: {
@@ -845,7 +863,7 @@ export class SmsSignatureService {
name,
purpose: data.purpose,
auditStatus,
rejectReason: auditStatus === 'pending' ? null : undefined,
rejectReason: auditStatus === 'pending' || auditStatus === 'approved' ? null : undefined,
drainageInfo: drainageInfo as Prisma.InputJsonValue | undefined,
materialVersion: materialChanged ? { increment: 1 } : undefined,
pendingReport: materialChanged ? true : undefined,
@@ -858,6 +876,21 @@ export class SmsSignatureService {
updated.applicationId ?? undefined,
drainageInfo,
);
if (
options.initialAuditStatus === 'approved' &&
(materialChanged || signature.auditStatus !== 'approved')
) {
await this.audit.createAuditRecord({
tenantId: signature.tenantId,
targetType: 'sms_signature',
targetId: signatureId,
action: 'admin_update_approved',
statusBefore: signature.auditStatus,
statusAfter: 'approved',
reason: '运营端修改签名并自动审核通过',
reviewerId: options.reviewerId,
});
}
return {
...updated,
reportMaterialChanged: materialChanged,
@@ -47,6 +47,7 @@ export interface CreateSmsSignatureDto {
export interface CreateSmsSignatureOptions {
initialAuditStatus?: string;
reviewerId?: string;
}
export type UpdateSmsSignatureDto = Partial<Omit<CreateSmsSignatureDto, 'tenantId'>> & {
@@ -1106,6 +1106,44 @@ describe('SmsConfigService', () => {
}));
});
it('keeps an operator-edited signature approved and records the operator audit', async () => {
const prisma = createPrismaMock();
prisma.smsSignature.findUnique.mockResolvedValue({
id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1', name: '【旧签名】',
purpose: '通知', drainageInfo: {}, auditStatus: 'approved',
});
const service = new SmsConfigService(prisma as never);
const result = await service.updateSignature(
'sig-1',
{ name: '【新签名】' },
undefined,
{ initialAuditStatus: 'approved', reviewerId: 'operator-1' },
);
expect(prisma.smsSignature.update).toHaveBeenCalledWith(expect.objectContaining({
data: expect.objectContaining({
auditStatus: 'approved',
rejectReason: null,
materialVersion: { increment: 1 },
pendingReport: true,
}),
}));
expect(prisma.auditRecord.create).toHaveBeenCalledWith({
data: expect.objectContaining({
action: 'admin_update_approved',
reviewerId: 'operator-1',
statusBefore: 'approved',
statusAfter: 'approved',
}),
});
expect(result).toEqual(expect.objectContaining({
auditStatus: 'approved',
reportMaterialChanged: true,
reportPoolAvailableAfter: 'immediate',
}));
});
it('resets an approved template to pending when key content is changed', async () => {
const prisma = createPrismaMock();
prisma.smsTemplate.findUnique.mockResolvedValue({
@@ -1223,6 +1261,7 @@ describe('SmsConfigService', () => {
const result = await service.listSignaturesPage({ page: 1, pageSize: 10 });
expect(result.total).toBe(1);
expect(result.pendingReportMaterialTotal).toBe(0);
expect(result.pendingReportDetailTotal).toBe(0);
expect(result.items[0]).toEqual(expect.objectContaining({ id: 'sig-1', name: '【签名A】' }));
expect(result.items[0]).not.toHaveProperty('materials');
@@ -1280,6 +1319,7 @@ describe('SmsConfigService', () => {
const result = await service.listSignaturesPage({ signatureKeyword: '测试', page: 1, pageSize: 10 });
expect(result.pendingReportMaterialTotal).toBe(1);
expect(result.pendingReportDetailTotal).toBe(expectedTotal);
expect(prisma.smsSignature.findMany).toHaveBeenCalledWith(
expect.objectContaining({
+7 -2
View File
@@ -163,8 +163,13 @@ export class SmsConfigService implements OnModuleInit, OnModuleDestroy {
return this.signatures.createSignature(data, options);
}
async updateSignature(signatureId: string, data: UpdateSmsSignatureDto, tenantId?: string) {
return this.signatures.updateSignature(signatureId, data, tenantId);
async updateSignature(
signatureId: string,
data: UpdateSmsSignatureDto,
tenantId?: string,
options: CreateSmsSignatureOptions = {},
) {
return this.signatures.updateSignature(signatureId, data, tenantId, options);
}
async updateClientSignature(signatureId: string, data: UpdateSmsSignatureDto, tenantId?: string) {