Files
lislgosms/api/src/sms-config/report-validation.service.ts
T

123 lines
8.8 KiB
TypeScript

import { BadRequestException, ForbiddenException, Injectable, Logger, NotFoundException, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
import { Prisma } from '@prisma/client';
import { randomInt, randomUUID } from 'node:crypto';
import { isIpAllowed } from '../common/ip-allowlist';
import { assertMoneyUnits } from '../common/money';
import { PrismaService } from '../prisma/prisma.service';
import { automaticDeliveryMode } from '../open-api/delivery-mode';
import type { ApplicationListQuery, CreateSignatureMaterialDto, CreateSmsApplicationDto, CreateSmsDrainageInfoDto, CreateSmsSignatureDto, CreateSmsSignatureOptions, CreateSmsTemplateDto, CreateSmsTemplateOptions, DrainageInfoListQuery, GatewayDownstreamConnectionEventDto, ReplaceApplicationRouteRulesDto, ReviewDto, SignatureListQuery, StatusChangeDto, TemplateListQuery, UpdateSmsApplicationDto, UpdateSmsDrainageInfoDto, UpdateSmsSignatureDto, UpdateSmsTemplateDto } from './sms-config.contracts';
import { APPLICATION_DISABLE_GRACE_MS, DEFAULT_APPLICATION_DISABLE_SCAN_INTERVAL_MS, DEFAULT_DOWNSTREAM_HEARTBEAT_TIMEOUT_MS, UNRESOLVED_DOWNSTREAM_STATUSES, type TemplateVariableInput, estimateBillingUnits, generateApplicationPassword, getPositiveInteger, getPositiveIntegerEnv, hasReportValue, inferTemplateVariables, isRecord, normalizeApplicationCmppStatus, normalizeApplicationInterfaceType, normalizeApplicationPassword, normalizeApplicationQueuePriority, normalizeCmppAccessNumberConfig, normalizeSmsSignature, parseGatewayDate, reportValueParts, startOfToday, validateAndNormalizeTemplateVariables, validateCompleteSmsSignature } from './sms-config.helpers';
import { SmsApplicationConfigService } from './application-config.service';
/** R3 domain service. Kept framework-agnostic and composed behind SmsConfigService. */
export class SmsReportValidationService {
constructor(private readonly prisma: PrismaService, private readonly applications: SmsApplicationConfigService) {}
async withReportRequirementSnapshot(applicationId?: string, drainageInfo?: Record<string, unknown>) {
if (!drainageInfo) return drainageInfo;
const fields = await this.applications.getApplicationReportFields(applicationId);
return {
...drainageInfo,
reportRequirementSnapshot: {
capturedAt: new Date().toISOString(),
applicationId,
fields: fields.map((field) => ({
id: field.id,
code: field.code,
name: field.name,
fieldType: field.fieldType,
required: field.required,
reportTypes: field.reportTypes,
commonReportTypes: field.commonReportTypes,
channels: field.channels,
})),
},
};
}
async syncSignatureReportValues(signatureId: string, applicationId?: string, drainageInfo?: Record<string, unknown>) {
if (!drainageInfo) return;
const fields = await this.applications.getApplicationReportFields(applicationId);
const signatureValues = isRecord(drainageInfo.signatureReportValues) ? drainageInfo.signatureReportValues : {};
for (const field of fields.filter((item) => item.reportTypes.some((type) => type === 'signature' || type === 'both'))) {
const value = reportValueParts(signatureValues[field.code]);
for (const channel of field.channels) {
await this.prisma.signatureReportMaterial.upsert({
where: { signatureId_channelId_fieldCode: { signatureId, channelId: channel.id, fieldCode: field.code } },
update: value,
create: { signatureId, channelId: channel.id, fieldCode: field.code, ...value },
});
}
}
}
async validateSignatureReportValues(applicationId?: string, drainageInfo?: Record<string, unknown>) {
const fields = await this.applications.getApplicationReportFields(applicationId, 'signature');
const signatureValues = isRecord(drainageInfo?.signatureReportValues) ? drainageInfo.signatureReportValues : {};
const missingSignature = fields
.filter((field) => field.required && field.reportTypes.some((type) => type === 'signature' || type === 'both'))
.filter((field) => !hasReportValue(signatureValues[field.code]));
if (missingSignature.length > 0) {
throw new BadRequestException(`缺少必填签名报备资料:${missingSignature.map((field) => field.name).join('、')}`);
}
}
async validateDrainageReportValues(applicationId?: string, reportValues: Record<string, unknown> = {}) {
const fields = await this.applications.getApplicationReportFields(applicationId, 'drainage');
const missing = fields.filter((field) => field.required && !hasReportValue(reportValues[field.code]));
if (missing.length > 0) {
throw new BadRequestException(`引流信息缺少必填报备资料:${missing.map((field) => field.name).join('、')}`);
}
}
async activateDrainageReporting(itemId: string) {
const item = await this.prisma.smsDrainageInfo.findUnique({ where: { id: itemId }, include: { signature: true } });
if (!item) throw new NotFoundException('Drainage info not found');
if (item.auditStatus !== 'approved') throw new BadRequestException('引流信息审核通过后才能进入通道报备');
const applicationId = item.signature.applicationId ?? item.applicationId ?? undefined;
if (!applicationId) return;
const fields = (await this.applications.getApplicationReportFields(applicationId, 'drainage'))
.filter((field) => field.reportTypes.some((type) => type === 'drainage' || type === 'both'));
const channels = new Map(fields.flatMap((field) => field.channels).map((channel) => [channel.id, channel]));
const values = isRecord(item.reportValues) ? item.reportValues : {};
await this.prisma.$transaction(async (tx) => {
await tx.drainageReportMaterial.deleteMany({ where: { signatureId: item.signatureId, drainageItemId: item.id } });
for (const field of fields) {
const value = reportValueParts(values[field.code]);
for (const channel of field.channels) {
await tx.drainageReportMaterial.create({
data: { signatureId: item.signatureId, drainageItemId: item.id, channelId: channel.id, fieldCode: field.code, ...value },
});
}
}
const existingTasks = await tx.channelSignatureReportTask.findMany({ where: { drainageItemId: item.id, reportType: 'drainage' } });
const existingByChannel = new Map(existingTasks.map((task) => [task.channelId, task]));
for (const channel of channels.values()) {
const existing = existingByChannel.get(channel.id);
const task = existing
? await tx.channelSignatureReportTask.update({ where: { id: existing.id }, data: { status: 'pending', reason: null } })
: await tx.channelSignatureReportTask.create({ data: { tenantId: item.tenantId, signatureId: item.signatureId, channelId: channel.id, reportType: 'drainage', drainageItemId: item.id, status: 'pending' } });
await tx.channelSignatureReportRecord.create({
data: { taskId: task.id, channelId: channel.id, action: existing ? 'audit_approved_reset' : 'audit_approved_create', statusBefore: existing?.status, statusAfter: 'pending', reason: '引流信息运营审核通过' },
});
}
for (const task of existingTasks.filter((current) => !channels.has(current.channelId) && current.status !== 'abandoned')) {
await tx.channelSignatureReportTask.update({ where: { id: task.id }, data: { status: 'abandoned', reason: '应用当前路由已不包含此通道' } });
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'route_removed', statusBefore: task.status, statusAfter: 'abandoned', reason: '应用当前路由已不包含此通道' } });
}
});
}
async suspendDrainageReporting(itemId: string, reason: string, statusAfter = 'waiting_review') {
await this.prisma.$transaction(async (tx) => {
const item = await tx.smsDrainageInfo.findUnique({ where: { id: itemId } });
if (!item) throw new NotFoundException('Drainage info not found');
await tx.drainageReportMaterial.deleteMany({ where: { signatureId: item.signatureId, drainageItemId: item.id } });
const tasks = await tx.channelSignatureReportTask.findMany({ where: { drainageItemId: item.id, reportType: 'drainage' } });
for (const task of tasks.filter((current) => current.status !== statusAfter)) {
await tx.channelSignatureReportTask.update({ where: { id: task.id }, data: { status: statusAfter, reason } });
await tx.channelSignatureReportRecord.create({ data: { taskId: task.id, channelId: task.channelId, action: 'audit_suspended', statusBefore: task.status, statusAfter, reason } });
}
});
}
}