Files
lislgosms/api/src/send-chain/downstream-receipt-targets.ts
T
hectorzhao 001d5f2cbd
CSS quality / css-quality (push) Has been cancelled
fix: 修复 CMPP 协议字段容量与版本兼容性
2026-09-20 15:49:37 +08:00

139 lines
4.6 KiB
TypeScript

import { parseProtocolSequence } from '../common/protocol-uint32';
import type { PrismaService } from '../prisma/prisma.service';
export type FinalReceiptMessage = {
id: string;
tenantId?: string | null;
applicationId?: string | null;
messageId: string;
phoneNumber: string;
cmppSubmitSequenceId?: string | null;
cmppSubmitGroupMessageId?: string | null;
cmppRegisteredDelivery?: boolean | null;
};
export type DownstreamDeliveryQueueRequest = {
tenantId: string;
applicationId?: string | null;
messageRecordId?: string | null;
messageId?: string | null;
deliveryType: 'receipt' | 'uplink';
payload: Record<string, unknown>;
receiptDedupeKey?: string;
queueHttpWebhook?: boolean;
queueCmppDelivery?: boolean;
allowBusinessRejectionCmppDelivery?: boolean;
propagateHttpQueueError?: boolean;
};
type ClientReceiptTarget = {
segmentIndex: number;
segmentTotal: number;
submitSequenceId: number;
submitGroupMessageId: string;
registeredDelivery: boolean;
};
async function resolveClientReceiptTargets(
prisma: PrismaService,
message: FinalReceiptMessage,
): Promise<ClientReceiptTarget[]> {
if (message.cmppSubmitGroupMessageId) {
const group = await prisma.cmppInboundLongMessage.findFirst({
where: { messageId: message.cmppSubmitGroupMessageId },
include: { segments: { orderBy: { segmentIndex: 'asc' } } },
});
if (group?.segments.length) {
return group.segments.flatMap((segment) => {
const submitSequenceId = parseProtocolSequence(segment.sequenceId);
if (submitSequenceId === undefined) return [];
return [
{
segmentIndex: segment.segmentIndex,
segmentTotal: group.segmentTotal,
submitSequenceId,
submitGroupMessageId: group.messageId,
registeredDelivery: segment.registeredDelivery,
},
];
});
}
}
const submitSequenceId = parseProtocolSequence(message.cmppSubmitSequenceId);
if (submitSequenceId === undefined) return [];
return [
{
segmentIndex: 1,
segmentTotal: 1,
submitSequenceId,
submitGroupMessageId: message.cmppSubmitGroupMessageId ?? message.messageId,
// Null means a historical CMPP record created before this field existed.
registeredDelivery: message.cmppRegisteredDelivery !== false,
},
];
}
/**
* Queue one business-level HTTP callback and one CMPP status report for every
* original client fragment that requested Registered_Delivery. Internal retry,
* refund and billing remain message-level; only protocol delivery is expanded.
*/
export async function queueFinalReceiptDeliveries(
prisma: PrismaService,
queue: (request: DownstreamDeliveryQueueRequest) => Promise<unknown>,
data: {
message: FinalReceiptMessage;
payload: Record<string, unknown>;
segmentPayloads?: Record<number, Record<string, unknown>>;
allowBusinessRejectionCmppDelivery?: boolean;
propagateHttpQueueError?: boolean;
},
) {
const { message } = data;
if (!message.tenantId || !message.applicationId) {
return { queued: false, cmppTargetCount: 0 };
}
// HTTP submissions have one client message identity, so their webhook stays
// message-level even when the carrier internally split the SMS into segments.
await queue({
tenantId: message.tenantId,
applicationId: message.applicationId,
messageRecordId: message.id,
messageId: message.messageId,
deliveryType: 'receipt',
payload: data.payload,
queueHttpWebhook: true,
queueCmppDelivery: false,
propagateHttpQueueError: data.propagateHttpQueueError,
});
const targets = (await resolveClientReceiptTargets(prisma, message)).filter((target) => target.registeredDelivery);
for (const target of targets) {
const isSingleFragment = target.segmentTotal === 1;
await queue({
tenantId: message.tenantId,
applicationId: message.applicationId,
messageRecordId: message.id,
messageId: message.messageId,
deliveryType: 'receipt',
payload: {
...data.payload,
...data.segmentPayloads?.[target.segmentIndex],
submitSequenceId: target.submitSequenceId,
submitGroupMessageId: target.submitGroupMessageId,
clientSegmentIndex: target.segmentIndex,
clientSegmentTotal: target.segmentTotal,
},
receiptDedupeKey: isSingleFragment
? `receipt:${message.id}`
: `receipt:${message.id}:segment:${target.segmentIndex}`,
queueHttpWebhook: false,
queueCmppDelivery: true,
allowBusinessRejectionCmppDelivery: data.allowBusinessRejectionCmppDelivery,
});
}
return { queued: true, cmppTargetCount: targets.length };
}