fix(courier): parse XFX multipart/form-data track callbacks

Xiaofeixia posts multipart form fields; previously mis-read as urlencoded empty payload. Parse multipart text parts and map into order status transitions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-07 19:08:03 +08:00
parent 8726cf14e3
commit 0692bebf07
4 changed files with 164 additions and 44 deletions
+2 -28
View File
@@ -4,7 +4,6 @@ import { loadAppConfig } from '@dukang/shared-types';
import { NestExpressApplication } from '@nestjs/platform-express';
import { ValidationPipe } from '@nestjs/common';
import { json, urlencoded, type NextFunction, type Request, type Response } from 'express';
import { parse as parseQueryString } from 'node:querystring';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { ResponseInterceptor } from './common/interceptors/response.interceptor';
@@ -12,6 +11,7 @@ import { LoggingInterceptor } from './common/logging/logging.interceptor';
import { preloadSystemConfigEnv } from './common/system-config/system-config.env';
import { AlertService } from './common/alert/alert.service';
import { initSentryIfConfigured } from './integrations/sentry/sentry.bootstrap';
import { parseCourierCallbackBody } from './integrations/courier/xiaofeixia/xiaofeixia-callback-body';
function isCourierTrackCallbackUrl(url?: string): boolean {
if (!url) return false;
@@ -22,33 +22,7 @@ function isCourierTrackCallbackUrl(url?: string): boolean {
);
}
function parseCourierCallbackBody(
rawText: string,
contentType: string,
): Record<string, unknown> {
const ct = contentType.toLowerCase();
const trimmed = rawText.trim();
if (!trimmed) return {};
if (ct.includes('application/json') || trimmed.startsWith('{') || trimmed.startsWith('[')) {
try {
const parsed = JSON.parse(trimmed) as unknown;
return parsed && typeof parsed === 'object'
? (parsed as Record<string, unknown>)
: { value: parsed };
} catch {
return { _rawText: rawText.slice(0, 4000) };
}
}
if (ct.includes('application/x-www-form-urlencoded') || /[=&]/.test(trimmed)) {
return parseQueryString(trimmed) as Record<string, unknown>;
}
return { _rawText: rawText.slice(0, 4000) };
}
/** 小飞侠回调:无论 Content-Type,先吃下 rawBody 再尽力解析(便于第三方日志排查) */
/** 小飞侠回调:无论 Content-Type,先吃下 rawBody 再尽力解析(含 multipart/form-data */
function courierTrackRawBodyMiddleware(req: Request, _res: Response, next: NextFunction) {
if (req.method !== 'POST' || !isCourierTrackCallbackUrl(req.originalUrl || req.url)) {
return next();