|
|
|
@@ -5,11 +5,10 @@ import { decryptSecret, encryptSecret } from '../open-api/open-api.crypto';
|
|
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
|
import { shanghaiDateRange } from '../common/shanghai-date-range';
|
|
|
|
|
import { normalizeChannelCarriers } from '../channels/channels.helpers';
|
|
|
|
|
import type { CancelRetirementSuppressionDto, ConfirmLegacyReportDto, CreateRetirementWebhookDto, RetirementMessageQuery, RetirementRuleType, SuppressRetirementMessageDto, UnreportedSignatureQuery, UpsertRetirementRuleDto } from './signature-retirement.contracts';
|
|
|
|
|
import type { CancelRetirementSuppressionDto, CreateRetirementWebhookDto, RetirementMessageQuery, RetirementRuleType, SuppressRetirementMessageDto, UnreportedSignatureQuery, UpsertRetirementRuleDto } from './signature-retirement.contracts';
|
|
|
|
|
|
|
|
|
|
const DAY_MS = 86_400_000;
|
|
|
|
|
const CARRIERS = ['mobile', 'unicom', 'telecom'] as const;
|
|
|
|
|
const REPORT_STATUSES = new Set(['pending', 'waiting_material', 'reporting', 'approved', 'failed', 'rejected', 'abandoned']);
|
|
|
|
|
const DEFAULT_DELIVERY_INTERVAL_MS = 60_000;
|
|
|
|
|
const carrierLabels: Record<string, string> = { mobile: '移动', unicom: '联通', telecom: '电信' };
|
|
|
|
|
|
|
|
|
@@ -396,48 +395,6 @@ export class SignatureRetirementService implements OnModuleInit, OnModuleDestroy
|
|
|
|
|
return { notificationDate: notificationKey, created };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listLegacyReportTasks() {
|
|
|
|
|
return this.prisma.channelSignatureReportTask.findMany({
|
|
|
|
|
where: { reportType: 'signature', carrier: null, approvalScope: 'legacy_channel', signature: { auditStatus: { not: 'deleted' } }, channel: { status: { not: 'deleted' } } },
|
|
|
|
|
include: { signature: { include: { tenant: true, application: true } }, channel: true, records: { orderBy: { createdAt: 'desc' }, take: 5 } },
|
|
|
|
|
orderBy: { createdAt: 'desc' },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async confirmLegacyReport(id: string, data: ConfirmLegacyReportDto, operatorId?: string) {
|
|
|
|
|
if (!data.results?.length) throw new BadRequestException('至少确认一个运营商结果');
|
|
|
|
|
const carriers = new Set<string>(data.results.map((item) => item.carrier));
|
|
|
|
|
if (carriers.size !== data.results.length) throw new BadRequestException('运营商结果不能重复');
|
|
|
|
|
for (const result of data.results) {
|
|
|
|
|
if (!REPORT_STATUSES.has(result.status)) throw new BadRequestException('报备状态无效');
|
|
|
|
|
// 历史通道级通过时间不能代替运营商通过时间,否则仍是在自动伪造运营商事实。
|
|
|
|
|
if (result.status === 'approved' && !parseApprovedAt(result.approvedAt)) throw new BadRequestException(`${carrierLabels[result.carrier]}通过时间必填且必须有效`);
|
|
|
|
|
}
|
|
|
|
|
return this.prisma.$transaction(async (tx) => {
|
|
|
|
|
const legacy = await tx.channelSignatureReportTask.findUnique({ where: { id }, include: { channel: true } });
|
|
|
|
|
if (!legacy || legacy.reportType !== 'signature' || legacy.carrier !== null || legacy.approvalScope !== 'legacy_channel') throw new NotFoundException('历史通道级任务不存在');
|
|
|
|
|
const supported = normalizeChannelCarriers(legacy.channel.carriers, legacy.channel.carrier);
|
|
|
|
|
const resultTasks = [];
|
|
|
|
|
for (const result of data.results) {
|
|
|
|
|
if (!supported.includes(result.carrier)) throw new BadRequestException('确认运营商不在通道支持范围内');
|
|
|
|
|
const approvedAt = result.status === 'approved' ? parseApprovedAt(result.approvedAt) : null;
|
|
|
|
|
const existing = await tx.channelSignatureReportTask.findFirst({ where: { signatureId: legacy.signatureId, channelId: legacy.channelId, carrier: result.carrier, reportType: 'signature', drainageItemId: null } });
|
|
|
|
|
const task = existing
|
|
|
|
|
? await tx.channelSignatureReportTask.update({ where: { id: existing.id }, data: { status: result.status, approvedAt, reason: data.reason, approvalScope: 'carrier_specific' } })
|
|
|
|
|
: await tx.channelSignatureReportTask.create({ data: { tenantId: legacy.tenantId, signatureId: legacy.signatureId, channelId: legacy.channelId, carrier: result.carrier, reportType: 'signature', status: result.status, approvedAt, reason: data.reason, approvalScope: 'carrier_specific', createdById: operatorId } });
|
|
|
|
|
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'legacy_carrier_confirmed', statusBefore: existing?.status, statusAfter: result.status, reason: data.reason, operatorId, sourceEntry: 'report_task' } });
|
|
|
|
|
resultTasks.push(task);
|
|
|
|
|
}
|
|
|
|
|
await tx.channelSignatureReportRecord.create({ data: { taskId: legacy.id, channelId: legacy.channelId, action: 'legacy_scope_split', statusBefore: legacy.status, statusAfter: legacy.status, reason: data.reason, operatorId, sourceEntry: 'report_task' } });
|
|
|
|
|
if (supported.every((carrier) => carriers.has(carrier))) {
|
|
|
|
|
// 全部适用运营商均已人工确认后,旧通道级事实退出发送链兼容读取,避免长期双口径。
|
|
|
|
|
await tx.channelSignatureReportTask.update({ where: { id: legacy.id }, data: { approvalScope: 'legacy_split' } });
|
|
|
|
|
}
|
|
|
|
|
await tx.operationLog.create({ data: { userId: operatorId, action: 'signature_report.legacy_carriers_confirmed', resource: 'channel_signature_report_task', resourceId: legacy.id, detail: { results: data.results, reason: data.reason } as Prisma.InputJsonValue } });
|
|
|
|
|
return { legacyTaskId: legacy.id, tasks: resultTasks };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async runStartupCompensation() {
|
|
|
|
|
const now = new Date();
|
|
|
|
|
const hour = shanghaiHour(now);
|
|
|
|
@@ -687,13 +644,6 @@ function databaseDate(value: string) {
|
|
|
|
|
return new Date(`${assertDateKey(value)}T00:00:00.000Z`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function parseApprovedAt(value?: string) {
|
|
|
|
|
if (!value) return null;
|
|
|
|
|
const parsed = new Date(value);
|
|
|
|
|
if (Number.isNaN(parsed.getTime())) throw new BadRequestException('报备通过时间无效');
|
|
|
|
|
return parsed;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function assertRuleType(value: string): asserts value is RetirementRuleType {
|
|
|
|
|
if (!['enterprise_global', 'enterprise_application', 'channel_global', 'channel'].includes(value)) throw new BadRequestException('不支持的规则类型');
|
|
|
|
|
}
|
|
|
|
|