feat: unify signature channel reporting status

This commit is contained in:
hectorzhao
2026-07-12 20:38:20 +08:00
parent 3f3ff8a793
commit eab059583f
16 changed files with 439 additions and 110 deletions
+67 -6
View File
@@ -101,6 +101,12 @@ export interface CreateReportTaskDto {
createdById?: string;
}
export interface ChangeReportTaskStatusesDto {
items: Array<{ signatureId: string; channelId: string; status: string }>;
reason?: string;
operatorId?: string;
}
export interface CreateReportExportDto {
fileObjectId?: string;
fileName: string;
@@ -958,9 +964,9 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
});
}
listReportTasks(tenantId?: string, status?: string) {
listReportTasks(tenantId?: string, status?: string, channelId?: string) {
return this.prisma.channelSignatureReportTask.findMany({
where: { tenantId, status },
where: { tenantId, status, channelId },
include: { signature: true, channel: true },
orderBy: { createdAt: 'desc' },
});
@@ -980,6 +986,53 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
return task;
}
async changeReportTaskStatuses(data: ChangeReportTaskStatusesDto) {
if (!data.items.length) throw new BadRequestException('items is required');
const allowed = new Set(['pending', 'waiting_material', 'reporting', 'approved', 'failed', 'rejected', 'abandoned']);
for (const item of data.items) {
if (!allowed.has(item.status)) throw new BadRequestException('unsupported report task status');
}
return this.prisma.$transaction(async (tx) => {
const signatureIds = [...new Set(data.items.map((item) => item.signatureId))];
for (const item of data.items) {
const signature = await tx.smsSignature.findUnique({ where: { id: item.signatureId } });
const channel = await tx.smsChannel.findUnique({ where: { id: item.channelId } });
if (!signature || !channel) throw new NotFoundException('Signature or channel not found');
const existing = await tx.channelSignatureReportTask.findFirst({ where: { signatureId: item.signatureId, channelId: item.channelId } });
const task = existing
? await tx.channelSignatureReportTask.update({ where: { id: existing.id }, data: { status: item.status, reason: data.reason } })
: await tx.channelSignatureReportTask.create({ data: { tenantId: signature.tenantId, signatureId: item.signatureId, channelId: item.channelId, status: item.status, reason: data.reason, createdById: data.operatorId } });
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: item.channelId, action: 'manual_status_change', statusBefore: existing?.status, statusAfter: item.status, reason: data.reason, operatorId: data.operatorId } });
}
const summaries = [];
for (const signatureId of signatureIds) summaries.push(await this.recomputeSignatureReportSummary(tx, signatureId));
return summaries;
});
}
private async recomputeSignatureReportSummary(tx: Prisma.TransactionClient, signatureId: string) {
const signature = await tx.smsSignature.findUnique({ where: { id: signatureId } });
if (!signature) throw new NotFoundException('Signature not found');
const routes = signature.applicationId ? await tx.channelRouteRule.findMany({
where: { applicationId: signature.applicationId, status: 'active' },
include: { group: { include: { items: { include: { channel: true } } } } },
}) : [];
const configuredChannels = routes.flatMap((route) => route.group.items.map((item) => item.channel)).filter((channel) => channel.status !== 'deleted');
const tasks = await tx.channelSignatureReportTask.findMany({ where: { signatureId }, include: { channel: true } });
const channels = configuredChannels.length ? configuredChannels : tasks.map((task) => task.channel);
const uniqueChannels = [...new Map(channels.map((channel) => [channel.id, channel])).values()];
const taskByChannel = new Map(tasks.map((task) => [task.channelId, task]));
const carrierReportSummary = Object.fromEntries(['mobile', 'unicom', 'telecom'].map((carrier) => {
const targets = uniqueChannels.filter((channel) => channel.carrier === carrier || channel.carrier === 'all');
const statuses = targets.map((channel) => taskByChannel.get(channel.id)?.status ?? 'pending');
return [carrier, summarizeReportStatuses(statuses)];
}));
const allStatuses = uniqueChannels.map((channel) => taskByChannel.get(channel.id)?.status ?? 'pending');
const reportStatus = summarizeReportStatuses(allStatuses).status;
await tx.smsSignature.update({ where: { id: signatureId }, data: { reportStatus } });
return { signatureId, reportStatus, carrierReportSummary };
}
async createReportExport(taskId: string, data: CreateReportExportDto) {
const task = await this.getReportTaskOrThrow(taskId);
const exported = await this.prisma.reportExportFile.create({
@@ -1014,10 +1067,7 @@ export class ChannelsService implements OnModuleInit, OnModuleDestroy {
},
});
await this.updateReportTaskStatus(taskId, task.channelId, task.status, statusAfter, 'receipt_import', data.reason);
await this.prisma.smsSignature.update({
where: { id: task.signatureId },
data: { reportStatus: statusAfter },
});
await this.recomputeSignatureReportSummary(this.prisma as unknown as Prisma.TransactionClient, task.signatureId);
return imported;
}
@@ -1644,6 +1694,17 @@ function normalizeReportType(value?: string) {
throw new BadRequestException('reportType must be signature, drainage or both');
}
function summarizeReportStatuses(statuses: string[]) {
if (!statuses.length) return { status: 'not_applicable', approved: 0, total: 0 };
const approved = statuses.filter((status) => status === 'approved').length;
let status = 'pending';
if (approved === statuses.length) status = 'approved';
else if (statuses.some((item) => ['failed', 'rejected'].includes(item))) status = 'failed';
else if (statuses.some((item) => ['reporting', 'exporting', 'partial', 'partial_success'].includes(item)) || approved > 0) status = 'reporting';
else if (statuses.some((item) => item === 'waiting_material')) status = 'waiting_material';
return { status, approved, total: statuses.length };
}
function normalizeLinkEvent(action: string) {
if (action.includes('connect_requested')) {
return '连接请求';