fix: enforce drainage uniqueness and carrier-specific reporting

This commit is contained in:
hectorzhao
2026-09-14 18:12:38 +08:00
parent ac6449028c
commit 7f9abe3da0
26 changed files with 2637 additions and 965 deletions
+41 -15
View File
@@ -1,3 +1,4 @@
import { selectDrainageReportTask } from '../common/drainage-report-task';
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { Prisma } from '@prisma/client';
@@ -450,17 +451,25 @@ export class ChannelReportingService {
}),
);
const drainageDetails = signature.drainageItems.flatMap((drainageInfo) =>
channels
.map((channel) => {
const existing = signature.reportTasks.find(
(task) =>
task.reportType === 'drainage' &&
task.channelId === channel.id &&
task.drainageItemId === drainageInfo.id,
channels.flatMap((channel) =>
normalizeChannelCarriers(channel.carriers, channel.carrier).flatMap((carrier) => {
const tasks = signature.reportTasks.filter(
(task) => task.reportType === 'drainage' && task.drainageItemId === drainageInfo.id,
);
return existing ? { ...existing, signature } : undefined;
})
.filter(Boolean),
const existing = selectDrainageReportTask(tasks, channel.id, carrier);
return existing
? [
{
...existing,
id: existing.carrier ? existing.id : `virtual:${drainageInfo.id}:${channel.id}:${carrier}`,
carrier,
virtual: !existing.carrier,
signature,
},
]
: [];
}),
),
);
return [...signatureDetails, ...drainageDetails];
})
@@ -551,6 +560,9 @@ export class ChannelReportingService {
throw new BadRequestException('unsupported report task source entry');
}
return this.prisma.$transaction(async (tx) => {
for (const signatureId of [...new Set(data.items.map((item) => item.signatureId))].sort()) {
await tx.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${signatureId}, 910))`;
}
const signatureIds = [
...new Set(
data.items.filter((item) => (item.reportType ?? 'signature') === 'signature').map((item) => item.signatureId),
@@ -561,6 +573,7 @@ export class ChannelReportingService {
reportType: 'drainage';
drainageItemId: string;
channelId: string;
carrier: string | null;
status: string;
}> = [];
for (const item of data.items) {
@@ -577,7 +590,7 @@ export class ChannelReportingService {
if (drainageInfo.auditStatus !== 'approved')
throw new BadRequestException('引流信息审核通过后才能修改通道报备状态');
}
const carrier = reportType === 'signature' && item.carrier ? normalizeBusinessCarrier(item.carrier) : null;
const carrier = item.carrier ? normalizeBusinessCarrier(item.carrier) : null;
if (carrier && !normalizeChannelCarriers(channel.carriers, channel.carrier).includes(carrier)) {
throw new BadRequestException('报备运营商不在通道支持范围内');
}
@@ -587,11 +600,23 @@ export class ChannelReportingService {
channelId: item.channelId,
reportType,
drainageItemId: reportType === 'drainage' ? item.drainageItemId : null,
carrier: reportType === 'signature' ? carrier : null,
carrier,
},
});
if (reportType === 'drainage' && !existing)
throw new BadRequestException('引流信息通道报备任务不存在,请先完成运营审核');
if (reportType === 'drainage' && !existing) {
const legacy = carrier
? await tx.channelSignatureReportTask.findFirst({
where: {
signatureId: item.signatureId,
channelId: item.channelId,
reportType,
drainageItemId: item.drainageItemId,
carrier: null,
},
})
: null;
if (!legacy) throw new BadRequestException('引流信息通道报备任务不存在,请先完成运营审核');
}
if (reportType === 'signature' && !carrier && !existing)
throw new BadRequestException('签名报备状态必须指定运营商');
const approvedAt =
@@ -603,7 +628,7 @@ export class ChannelReportingService {
const task = existing
? await tx.channelSignatureReportTask.update({
where: { id: existing.id },
data: { status: item.status, reason: data.reason, ...(reportType === 'signature' ? { approvedAt } : {}) },
data: { status: item.status, reason: data.reason, approvedAt },
})
: await tx.channelSignatureReportTask.create({
data: {
@@ -638,6 +663,7 @@ export class ChannelReportingService {
reportType,
drainageItemId: item.drainageItemId!,
channelId: item.channelId,
carrier,
status: item.status,
});
}
@@ -370,6 +370,7 @@ describe('ChannelsService', () => {
updatedAt: new Date(),
};
const tx = {
$executeRaw: jest.fn().mockResolvedValue(1),
channelReportField: {
findMany: jest
.fn()
@@ -519,6 +520,7 @@ describe('ChannelsService', () => {
async (sourceEntry) => {
const prisma = createPrismaMock();
const tx = {
$executeRaw: jest.fn().mockResolvedValue(1),
smsSignature: {
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1' }),
update: jest.fn().mockResolvedValue({ id: 'sig-1', reportStatus: 'approved' }),
@@ -601,6 +603,7 @@ describe('ChannelsService', () => {
it('uses the enterprise-signature save time when creating an approved carrier task', async () => {
const prisma = createPrismaMock();
const tx = {
$executeRaw: jest.fn().mockResolvedValue(1),
smsSignature: {
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1' }),
update: jest.fn().mockResolvedValue({ id: 'sig-1', reportStatus: 'approved' }),
@@ -655,6 +658,7 @@ describe('ChannelsService', () => {
it('changes a drainage report task without overwriting the signature report summary', async () => {
const prisma = createPrismaMock();
const tx = {
$executeRaw: jest.fn().mockResolvedValue(1),
smsSignature: {
findUnique: jest.fn().mockResolvedValue({ id: 'sig-1', tenantId: 'tenant-1', applicationId: 'app-1' }),
update: jest.fn(),
@@ -699,6 +703,7 @@ describe('ChannelsService', () => {
reportType: 'drainage',
drainageItemId: 'drain-1',
channelId: 'channel-1',
carrier: null,
status: 'approved',
},
]);
@@ -1259,6 +1264,7 @@ describe('ChannelsService', () => {
const transactionCallback = prisma.$transaction.mock.calls[0][0];
const tx = {
$executeRaw: jest.fn().mockResolvedValue(1),
smsChannelGroupItem: { deleteMany: jest.fn(), createMany: jest.fn() },
smsChannelGroup: {
update: jest.fn(),