110 lines
5.1 KiB
TypeScript
110 lines
5.1 KiB
TypeScript
import { BadRequestException, HttpException, HttpStatus, Logger, NotFoundException } from '@nestjs/common';
|
|
import { Prisma } from '@prisma/client';
|
|
import { Queue, Worker } from 'bullmq';
|
|
import IORedis from 'ioredis';
|
|
import { createHash, randomUUID } from 'node:crypto';
|
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
import { BillingService } from '../billing/billing.service';
|
|
import { isIpAllowed } from '../common/ip-allowlist';
|
|
import { moneyToNumber } from '../common/money';
|
|
import { PhoneRoutingLookupService } from '../dictionaries/phone-routing-lookup.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { RiskReviewService } from '../risk-review/risk-review.service';
|
|
import { PhoneFrequencyService } from '../risk-review/phone-frequency.service';
|
|
import type { CreateBatchTaskDto, CreateHttpBatchTaskDto, GatewayInboundAuthDto, GatewayInboundSubmitDto, GatewayInboundSingleSubmitResult, ImportPreviewDto, ConfirmImportDto, SendJob, QueuePriority, RoutedChannel } from './send-chain.contracts';
|
|
import { SEND_QUEUE, GATEWAY_SUBMIT_QUEUE, GATEWAY_SUBMIT_STREAM, DEFAULT_SCHEDULED_DISPATCH_STALE_MS, DEFAULT_INBOUND_LONG_MESSAGE_PROCESSING_STALE_SECONDS, GATEWAY_SUBMIT_REQUEUE_IDEMPOTENCY_TTL_SECONDS, BULLMQ_PRIORITY, drainageRejectionReason, statusFromRisk, parseSchedule, parseImportRows, splitImportLine, cellByHeader, normalizeCarrier, normalizeQueuePriority, getPositiveConfigInteger, getNonNegativeConfigInteger, isCarrierCompatible, matchTemplateContent, isNationalChannel, validateInboundApplicationSrcId, composeUpstreamSrcId, positiveInteger, parseOptionalSequenceId, shanghaiDateKey, bullmqConnection, matchesApplicationSecret, octetString, selectChannelCandidate } from './send-chain.helpers';
|
|
import type { SendSubmissionCallbacks, SendSubmissionService } from './send-submission.service';
|
|
|
|
/**
|
|
* R9 reviewContinuation implementation. Cross-method calls return through the stable SendChainService seam.
|
|
*/
|
|
export class SendReviewContinuationService {
|
|
private readonly logger = new Logger('SendChainService');
|
|
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly billing: BillingService,
|
|
private readonly riskReview: RiskReviewService,
|
|
private readonly phoneFrequency: PhoneFrequencyService,
|
|
private readonly phoneRouting: PhoneRoutingLookupService,
|
|
private readonly facade: SendSubmissionService,
|
|
private readonly callbacks: SendSubmissionCallbacks,
|
|
) {}
|
|
|
|
private releaseMessageReservation(
|
|
message: { tenantId: string; batchTaskId: string; messageId: string; amountCents: number | bigint; billingUnits: number },
|
|
remark: string,
|
|
) {
|
|
return this.callbacks.releaseMessageReservation(message, remark);
|
|
}
|
|
|
|
private recordCmppFailureReceipt(
|
|
message: {
|
|
id: string;
|
|
tenantId?: string | null;
|
|
batchTaskId?: string | null;
|
|
applicationId?: string | null;
|
|
messageId: string;
|
|
phoneNumber: string;
|
|
cmppSubmitSequenceId?: string | null;
|
|
cmppSubmitGroupMessageId?: string | null;
|
|
},
|
|
errorCode: string,
|
|
reason: string,
|
|
) {
|
|
return this.callbacks.recordCmppFailureReceipt(message, errorCode, reason);
|
|
}
|
|
|
|
|
|
async handleReviewDecision(reviewTaskId: string, decision: 'approved' | 'rejected', reason: string) {
|
|
const reviewTask = await this.prisma.smsSendTask.findUnique({
|
|
where: { id: reviewTaskId },
|
|
});
|
|
if (!reviewTask) {
|
|
return { reviewTaskId, decision, affected: 0 };
|
|
}
|
|
const messageRecords = await this.prisma.smsMessageRecord.findMany({
|
|
where: {
|
|
status: 'pending_review',
|
|
OR: [
|
|
{ reviewTaskId },
|
|
{ batchTask: { riskTaskId: reviewTaskId } },
|
|
],
|
|
},
|
|
include: { batchTask: true },
|
|
});
|
|
if (messageRecords.length === 0) {
|
|
return { reviewTaskId, decision, affected: 0 };
|
|
}
|
|
const batchTaskIds = new Set<string>();
|
|
for (const message of messageRecords) {
|
|
if (!message.tenantId || !message.applicationId || !message.batchTaskId) continue;
|
|
if (decision === 'approved') {
|
|
await this.prisma.smsMessageRecord.update({
|
|
where: { id: message.id },
|
|
data: { status: 'queued', errorCode: null, errorMessage: null },
|
|
});
|
|
await this.prisma.smsBatchTask.update({
|
|
where: { id: message.batchTaskId },
|
|
data: { status: 'ready', auditStatus: 'approved', reviewReason: reason, rejectReason: null },
|
|
});
|
|
batchTaskIds.add(message.batchTaskId);
|
|
} else {
|
|
await this.releaseMessageReservation(
|
|
message as typeof message & { tenantId: string; batchTaskId: string },
|
|
'模板不匹配人工审核驳回释放冻结',
|
|
);
|
|
await this.prisma.smsBatchTask.update({
|
|
where: { id: message.batchTaskId },
|
|
data: { status: 'rejected', auditStatus: 'rejected', rejectReason: reason },
|
|
});
|
|
await this.recordCmppFailureReceipt(message, 'REVIEW_REJECTED', reason);
|
|
}
|
|
}
|
|
for (const batchTaskId of batchTaskIds) {
|
|
await this.facade.enqueueBatchTask(batchTaskId);
|
|
}
|
|
return { reviewTaskId, decision, affected: messageRecords.length };
|
|
}
|
|
}
|