fix: close receipt delivery workflows

This commit is contained in:
hectorzhao
2026-07-24 14:40:57 +08:00
parent 2ee39056eb
commit 91f04f5288
22 changed files with 829 additions and 48 deletions
+24 -12
View File
@@ -11,9 +11,9 @@ import { SendChainService } from '../send-chain/send-chain.service';
import { decryptSecret, encryptSecret } from './open-api.crypto';
import type { OpenApiAuthContext } from './open-api.types';
import { ProtocolLogsService } from '../protocol-logs/protocol-logs.service';
import { automaticDeliveryMode } from './delivery-mode';
const WEBHOOK_QUEUE = 'http-webhook-delivery';
const DELIVERY_MODES = ['cmpp', 'http', 'both', 'none'] as const;
const RETRY_DELAYS_SECONDS = [0, 60, 300, 900, 3600, 21600, 86400];
export type HttpConfigInput = {
@@ -75,7 +75,7 @@ export class OpenApiService implements OnModuleInit, OnModuleDestroy {
async updateConfig(applicationId: string, input: HttpConfigInput, tenantId?: string) {
const application = await this.requireApplication(applicationId, tenantId);
const data = normalizeConfig(input, application.httpConfig);
const data = normalizeConfig(input, application.httpConfig, application.interfaceEnabled !== false);
const ipAllowlist = normalizeIpAllowlist(input.ipAllowlist);
const [config] = await this.prisma.$transaction([
this.prisma.smsApplicationHttpConfig.upsert({
@@ -144,6 +144,18 @@ export class OpenApiService implements OnModuleInit, OnModuleDestroy {
async upsertWebhookEndpoint(applicationId: string, eventType: string, data: { url: string; rotateSecret?: boolean; status?: string }, tenantId?: string) {
const application = await this.requireApplication(applicationId, tenantId);
if (!['receipt', 'uplink'].includes(eventType)) throw new BadRequestException('eventType only supports receipt or uplink');
if (!String(data.url ?? '').trim()) {
await this.prisma.httpWebhookEndpoint.deleteMany({ where: { applicationId, eventType } });
return {
applicationId,
eventType,
url: '',
secretLast4: '',
status: 'inactive',
updatedAt: new Date(),
deleted: true,
};
}
const url = await validateWebhookUrl(data.url, application.httpConfig?.requireHttps ?? true);
const existing = await this.prisma.httpWebhookEndpoint.findUnique({ where: { applicationId_eventType: { applicationId, eventType } } });
const secret = !existing || data.rotateSecret ? randomBytes(32).toString('base64url') : undefined;
@@ -299,9 +311,8 @@ export class OpenApiService implements OnModuleInit, OnModuleDestroy {
if (!data.applicationId) return null;
const application = await this.prisma.smsApplication.findUnique({ where: { id: data.applicationId }, include: { httpConfig: true } });
const config = application?.httpConfig;
const mode = data.eventType === 'receipt' ? config?.receiptDeliveryMode : config?.uplinkDeliveryMode;
const enabled = data.eventType === 'receipt' ? config?.receiptWebhookEnabled : config?.uplinkWebhookEnabled;
if (!config?.enabled || !enabled || !['http', 'both'].includes(mode ?? '')) return null;
if (!config?.enabled || !enabled) return null;
const endpoint = await this.prisma.httpWebhookEndpoint.findUnique({ where: { applicationId_eventType: { applicationId: data.applicationId, eventType: data.eventType } } });
if (!endpoint || endpoint.status !== 'active') return null;
const event = await this.prisma.httpWebhookEvent.create({
@@ -410,7 +421,11 @@ function normalizeOpenApiFailure(error: unknown) {
return { httpStatus: 500, code: 'INTERNAL_ERROR', responseBody: { code: 'INTERNAL_ERROR', message: 'Internal server error' } as Prisma.InputJsonValue };
}
function normalizeConfig(input: HttpConfigInput, existing?: { enabled?: boolean } | null) {
function normalizeConfig(
input: HttpConfigInput,
existing: { enabled?: boolean } | null | undefined,
cmppEnabled: boolean,
) {
const enabling = input.enabled === true && existing?.enabled !== true;
const effective = enabling ? {
sendEnabled: true,
@@ -419,13 +434,10 @@ function normalizeConfig(input: HttpConfigInput, existing?: { enabled?: boolean
uplinkWebhookEnabled: true,
uplinkQueryEnabled: true,
credentialSelfServiceEnabled: true,
receiptDeliveryMode: 'http',
uplinkDeliveryMode: 'http',
...input,
} : input;
for (const mode of [effective.receiptDeliveryMode, effective.uplinkDeliveryMode]) {
if (mode !== undefined && !DELIVERY_MODES.includes(mode as typeof DELIVERY_MODES[number])) throw new BadRequestException('投递模式仅支持 cmpp、http、both、none');
}
const httpEnabled = effective.enabled ?? existing?.enabled ?? false;
const deliveryMode = automaticDeliveryMode(cmppEnabled, httpEnabled);
return {
enabled: effective.enabled,
sendEnabled: effective.sendEnabled,
@@ -440,8 +452,8 @@ function normalizeConfig(input: HttpConfigInput, existing?: { enabled?: boolean
uplinkRetentionDays: bounded(effective.uplinkRetentionDays, 1, 365, '上行保留天数'),
maxQueryRangeDays: bounded(effective.maxQueryRangeDays, 1, 90, '查询跨度'),
maxPageSize: bounded(effective.maxPageSize, 10, 500, '分页上限'),
receiptDeliveryMode: effective.receiptDeliveryMode,
uplinkDeliveryMode: effective.uplinkDeliveryMode,
receiptDeliveryMode: deliveryMode,
uplinkDeliveryMode: deliveryMode,
webhookRetryEnabled: effective.webhookRetryEnabled,
webhookMaxAttempts: bounded(effective.webhookMaxAttempts, 1, 7, '回调重试次数'),
webhookTimeoutSeconds: bounded(effective.webhookTimeoutSeconds, 1, 30, '回调超时'),