fix: 修复上行归属并实现签名质量日报优化
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { completionContext } from './completion-context';
|
||||
import { resolveUplinkMatch } from './uplink-matching';
|
||||
import { BadRequestException, Logger, NotFoundException } from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
@@ -32,7 +33,19 @@ export class SendDownstreamDeliveryService {
|
||||
) {}
|
||||
|
||||
async handleUplink(data: GatewayUplinkEventDto) {
|
||||
if (!completionContext.getStore()) {
|
||||
return this.prisma.$transaction(
|
||||
(tx) => completionContext.run({ tx, messageRecordId: '' }, () => this.persistUplink(data)),
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
}
|
||||
return this.persistUplink(data);
|
||||
}
|
||||
|
||||
private async persistUplink(data: GatewayUplinkEventDto) {
|
||||
if (data.eventId) {
|
||||
await this.prisma
|
||||
.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`uplink-event:${data.eventId}`},0))`;
|
||||
const existing = await this.prisma.smsUplinkMessage.findUnique({ where: { eventId: data.eventId } });
|
||||
if (existing) return existing;
|
||||
}
|
||||
@@ -48,7 +61,7 @@ export class SendDownstreamDeliveryService {
|
||||
applicationId: match.applicationId,
|
||||
messageRecordId: match.messageRecordId,
|
||||
channelId: data.channelId,
|
||||
messageId: data.messageId,
|
||||
messageId: match.messageId,
|
||||
gatewayMessageId: data.gatewayMessageId,
|
||||
sequenceId: data.sequenceId,
|
||||
phoneNumber: data.phoneNumber,
|
||||
@@ -78,10 +91,10 @@ export class SendDownstreamDeliveryService {
|
||||
tenantId: match.tenantId,
|
||||
applicationId: match.applicationId,
|
||||
messageRecordId: match.messageRecordId,
|
||||
messageId: data.messageId,
|
||||
messageId: match.messageId,
|
||||
deliveryType: 'uplink',
|
||||
payload: {
|
||||
messageId: data.messageId,
|
||||
messageId: match.messageId,
|
||||
applicationId: match.applicationId,
|
||||
phoneNumber: data.phoneNumber,
|
||||
destId: data.destId,
|
||||
@@ -95,6 +108,21 @@ export class SendDownstreamDeliveryService {
|
||||
}
|
||||
|
||||
async claimUplinkMatchCandidate(uplinkMessageId: string, candidateId: string, operatorId?: string) {
|
||||
if (!completionContext.getStore()) {
|
||||
return this.prisma.$transaction(
|
||||
(tx) =>
|
||||
completionContext.run({ tx, messageRecordId: '' }, () =>
|
||||
this.persistUplinkClaim(uplinkMessageId, candidateId, operatorId),
|
||||
),
|
||||
{ timeout: 15_000 },
|
||||
);
|
||||
}
|
||||
return this.persistUplinkClaim(uplinkMessageId, candidateId, operatorId);
|
||||
}
|
||||
|
||||
private async persistUplinkClaim(uplinkMessageId: string, candidateId: string, operatorId?: string) {
|
||||
await this.prisma
|
||||
.$executeRaw`SELECT pg_advisory_xact_lock(hashtextextended(${`uplink-claim:${uplinkMessageId}`},0))`;
|
||||
const candidate = await this.prisma.smsUplinkMatchCandidate.findFirst({
|
||||
where: { id: candidateId, uplinkMessageId },
|
||||
include: {
|
||||
@@ -112,56 +140,55 @@ export class SendDownstreamDeliveryService {
|
||||
if (candidate.uplinkMessage.matchStatus === 'matched' && candidate.status !== 'claimed') {
|
||||
throw new BadRequestException('该上行记录已完成匹配,不能重复认领');
|
||||
}
|
||||
if (candidate.status === 'claimed') return candidate.uplinkMessage;
|
||||
const claimedAt = new Date();
|
||||
const messageId = candidate.uplinkMessage.messageId ?? candidate.messageRecord?.messageId ?? null;
|
||||
const [updatedUplink] = await this.prisma.$transaction([
|
||||
this.prisma.smsUplinkMessage.update({
|
||||
where: { id: uplinkMessageId },
|
||||
data: {
|
||||
tenantId: candidate.tenantId,
|
||||
const messageId = candidate.messageRecord?.messageId ?? null;
|
||||
const updatedUplink = await this.prisma.smsUplinkMessage.update({
|
||||
where: { id: uplinkMessageId },
|
||||
data: {
|
||||
tenantId: candidate.tenantId,
|
||||
applicationId: candidate.applicationId,
|
||||
messageRecordId: candidate.messageRecordId,
|
||||
messageId,
|
||||
matchStatus: 'matched',
|
||||
matchReason: `人工认领:${candidate.reason ?? candidate.matchSource}`,
|
||||
},
|
||||
});
|
||||
await this.prisma.smsUplinkMatchCandidate.updateMany({
|
||||
where: {
|
||||
uplinkMessageId,
|
||||
id: { not: candidate.id },
|
||||
status: 'pending',
|
||||
},
|
||||
data: { status: 'rejected' },
|
||||
});
|
||||
await this.prisma.smsUplinkMatchCandidate.update({
|
||||
where: { id: candidate.id },
|
||||
data: {
|
||||
status: 'claimed',
|
||||
claimedAt,
|
||||
claimedById: operatorId,
|
||||
},
|
||||
});
|
||||
await this.prisma.operationLog.create({
|
||||
data: {
|
||||
tenantId: candidate.tenantId,
|
||||
userId: operatorId,
|
||||
action: 'gateway.uplink_manual_claim',
|
||||
resource: 'sms_uplink_message',
|
||||
resourceId: uplinkMessageId,
|
||||
detail: {
|
||||
candidateId: candidate.id,
|
||||
applicationId: candidate.applicationId,
|
||||
applicationName: candidate.application.name,
|
||||
messageRecordId: candidate.messageRecordId,
|
||||
messageId,
|
||||
matchStatus: 'matched',
|
||||
matchReason: `人工认领:${candidate.reason ?? candidate.matchSource}`,
|
||||
matchSource: candidate.matchSource,
|
||||
phoneNumber: candidate.uplinkMessage.phoneNumber,
|
||||
destId: candidate.uplinkMessage.destId,
|
||||
},
|
||||
}),
|
||||
this.prisma.smsUplinkMatchCandidate.updateMany({
|
||||
where: {
|
||||
uplinkMessageId,
|
||||
id: { not: candidate.id },
|
||||
status: 'pending',
|
||||
},
|
||||
data: { status: 'rejected' },
|
||||
}),
|
||||
this.prisma.smsUplinkMatchCandidate.update({
|
||||
where: { id: candidate.id },
|
||||
data: {
|
||||
status: 'claimed',
|
||||
claimedAt,
|
||||
claimedById: operatorId,
|
||||
},
|
||||
}),
|
||||
this.prisma.operationLog.create({
|
||||
data: {
|
||||
tenantId: candidate.tenantId,
|
||||
userId: operatorId,
|
||||
action: 'gateway.uplink_manual_claim',
|
||||
resource: 'sms_uplink_message',
|
||||
resourceId: uplinkMessageId,
|
||||
detail: {
|
||||
candidateId: candidate.id,
|
||||
applicationId: candidate.applicationId,
|
||||
applicationName: candidate.application.name,
|
||||
messageRecordId: candidate.messageRecordId,
|
||||
messageId,
|
||||
matchSource: candidate.matchSource,
|
||||
phoneNumber: candidate.uplinkMessage.phoneNumber,
|
||||
destId: candidate.uplinkMessage.destId,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
},
|
||||
});
|
||||
|
||||
await this.facade.queueAndTryDownstreamDelivery({
|
||||
tenantId: candidate.tenantId,
|
||||
@@ -278,7 +305,10 @@ export class SendDownstreamDeliveryService {
|
||||
skipDuplicates: true,
|
||||
});
|
||||
const retained = await this.prisma.cmppDownstreamDelivery.findUniqueOrThrow({ where: { dedupeKey } });
|
||||
if (retained.messageRecordId !== data.messageRecordId || retained.applicationId !== data.applicationId)
|
||||
if (
|
||||
(retained.messageRecordId ?? null) !== (data.messageRecordId ?? null) ||
|
||||
retained.applicationId !== data.applicationId
|
||||
)
|
||||
throw new Error('completion_notification_identity_mismatch');
|
||||
return retained;
|
||||
}
|
||||
@@ -377,107 +407,12 @@ export class SendDownstreamDeliveryService {
|
||||
tenantId?: string;
|
||||
applicationId?: string;
|
||||
messageRecordId?: string;
|
||||
messageId?: string;
|
||||
matchStatus: string;
|
||||
matchReason: string;
|
||||
candidates: UplinkMatchCandidateInput[];
|
||||
}> {
|
||||
if (data.messageId) {
|
||||
const message = await this.prisma.smsMessageRecord.findUnique({ where: { messageId: data.messageId } });
|
||||
if (message?.tenantId) {
|
||||
return {
|
||||
tenantId: message.tenantId,
|
||||
applicationId: message.applicationId ?? undefined,
|
||||
messageRecordId: message.id,
|
||||
matchStatus: message.applicationId ? 'matched' : 'unmatched',
|
||||
matchReason: message.applicationId ? 'messageId 精确匹配' : 'messageId 匹配到下发记录但无应用',
|
||||
candidates: [],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const accessNumber = data.destId || channel.srcId || '';
|
||||
const accessRoutes = accessNumber
|
||||
? await this.prisma.channelRouteRule.findMany({
|
||||
where: {
|
||||
applicationId: { not: null },
|
||||
status: 'active',
|
||||
group: { items: { some: { channelId: channel.id, channel: { srcId: accessNumber } } } },
|
||||
},
|
||||
select: { applicationId: true },
|
||||
take: 10,
|
||||
})
|
||||
: [];
|
||||
const accessApplicationIds = [
|
||||
...new Set(accessRoutes.map((route) => route.applicationId).filter((value): value is string => Boolean(value))),
|
||||
];
|
||||
const accessApplications =
|
||||
accessApplicationIds.length > 0
|
||||
? await this.prisma.smsApplication.findMany({
|
||||
where: { id: { in: accessApplicationIds }, status: 'active' },
|
||||
select: { id: true, tenantId: true, name: true },
|
||||
})
|
||||
: [];
|
||||
if (accessApplications.length === 1) {
|
||||
return {
|
||||
tenantId: accessApplications[0].tenantId,
|
||||
applicationId: accessApplications[0].id,
|
||||
matchStatus: 'matched',
|
||||
matchReason: '接入号唯一匹配应用',
|
||||
candidates: [],
|
||||
};
|
||||
}
|
||||
if (accessApplications.length > 1) {
|
||||
return {
|
||||
matchStatus: 'ambiguous',
|
||||
matchReason: '接入号匹配多个应用',
|
||||
candidates: accessApplications.map((application) => ({
|
||||
tenantId: application.tenantId,
|
||||
applicationId: application.id,
|
||||
matchSource: 'access_number',
|
||||
confidence: 70,
|
||||
reason: `接入号 ${accessNumber} 可匹配应用 ${application.name}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
const windowHours = Number(process.env.UPLINK_MATCH_WINDOW_HOURS ?? 72);
|
||||
const since = new Date(Date.now() - Math.max(1, windowHours) * 60 * 60 * 1000);
|
||||
const recentMessages = await this.prisma.smsMessageRecord.findMany({
|
||||
where: {
|
||||
phoneNumber: data.phoneNumber,
|
||||
tenantId: { not: null },
|
||||
applicationId: { not: null },
|
||||
submittedAt: { gte: since },
|
||||
},
|
||||
orderBy: { submittedAt: 'desc' },
|
||||
take: 2,
|
||||
});
|
||||
const matchableRecentMessages = recentMessages.filter((message) => message.tenantId && message.applicationId);
|
||||
if (matchableRecentMessages.length === 1) {
|
||||
return {
|
||||
tenantId: matchableRecentMessages[0].tenantId ?? undefined,
|
||||
applicationId: matchableRecentMessages[0].applicationId ?? undefined,
|
||||
messageRecordId: matchableRecentMessages[0].id,
|
||||
matchStatus: 'matched',
|
||||
matchReason: `手机号 ${windowHours} 小时窗口唯一匹配`,
|
||||
candidates: [],
|
||||
};
|
||||
}
|
||||
if (matchableRecentMessages.length > 1) {
|
||||
return {
|
||||
matchStatus: 'ambiguous',
|
||||
matchReason: `手机号 ${windowHours} 小时窗口匹配多条下发记录`,
|
||||
candidates: matchableRecentMessages.map((message) => ({
|
||||
tenantId: String(message.tenantId),
|
||||
applicationId: String(message.applicationId),
|
||||
messageRecordId: message.id,
|
||||
matchSource: 'phone_window',
|
||||
confidence: 55,
|
||||
reason: `手机号 ${windowHours} 小时窗口候选下发 ${message.messageId}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
return { matchStatus: 'unmatched', matchReason: '未匹配到应用或下发记录', candidates: [] };
|
||||
return resolveUplinkMatch(this.prisma, data, channel);
|
||||
}
|
||||
|
||||
async recordCmppFailureReceipt(
|
||||
|
||||
Reference in New Issue
Block a user