feat(mini-user): logistics sign photos, dial, ETA and courier callbacks (v3.4.13)

Extend Courier adapter for XFX sign photos and route callbacks; mini-user logistics UI with timeline, phone dial, and estimated arrival.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-05 23:34:09 +08:00
parent 62652cd072
commit 345d5db9c9
19 changed files with 710 additions and 140 deletions
@@ -0,0 +1,101 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from '../common/prisma/prisma.module';
import { CourierService } from '../integrations/courier/courier.service';
import { XiaofeixiaProvider } from '../integrations/courier/xiaofeixia/xiaofeixia.provider';
import { logCourierCall } from '../integrations/courier/courier-log.util';
import { TradeService } from '../modules/trade/trade.service';
const XFX_PROVIDER_ALIASES = new Set(['xfx', 'xiaofeixia']);
@Injectable()
export class DeliveryCallbackService {
constructor(
private readonly prisma: PrismaService,
private readonly courier: CourierService,
private readonly xiaofeixiaProvider: XiaofeixiaProvider,
private readonly tradeService: TradeService,
) {}
async handleTrackCallback(providerKey: string, body: unknown, requestUrl: string) {
const normalized = providerKey.trim().toLowerCase();
const baseLog = {
scene: 'TRACK_CALLBACK',
requestUrl,
requestBody: (body && typeof body === 'object' ? body : { value: body }) as Record<
string,
unknown
>,
};
if (!XFX_PROVIDER_ALIASES.has(normalized) && normalized !== 'logistics') {
await logCourierCall(this.prisma, {
...baseLog,
status: 'FAILED',
errorMessage: `不支持的承运商: ${providerKey}`,
});
return this.courier.buildTrackCallbackResponse(false);
}
if (normalized === 'logistics') {
await logCourierCall(this.prisma, {
...baseLog,
status: 'SUCCESS',
errorMessage: '跨城物流回调暂未接入,已记录',
});
return this.courier.buildTrackCallbackResponse(true);
}
const payload = this.xiaofeixiaProvider.parseTrackCallback(body);
if (!payload) {
await logCourierCall(this.prisma, {
...baseLog,
status: 'FAILED',
errorMessage: '回调体解析失败',
});
return this.courier.buildTrackCallbackResponse(false);
}
const order = payload.outNumber
? await this.prisma.order.findUnique({ where: { orderNo: payload.outNumber } })
: payload.trackingNumber
? await this.prisma.order.findFirst({
where: { delivery: { trackingNo: payload.trackingNumber } },
})
: null;
if (!order) {
await logCourierCall(this.prisma, {
...baseLog,
status: 'FAILED',
errorMessage: '订单不存在',
externalNo: payload.outNumber || payload.trackingNumber,
});
return this.courier.buildTrackCallbackResponse(false);
}
const targetStatus = this.xiaofeixiaProvider.mapTrackStatus(payload.status);
let applied = false;
if (targetStatus && targetStatus !== order.status) {
await this.tradeService.applyStatusTransition(
order.id,
order.status,
targetStatus,
'COURIER_TRACK_CALLBACK',
payload.trackInfo || payload.statusName,
);
applied = true;
}
const response = this.courier.buildTrackCallbackResponse(true);
await logCourierCall(this.prisma, {
...baseLog,
responseBody: response,
status: 'SUCCESS',
externalNo: order.orderNo,
ref: { refType: 'ORDER', refId: order.id },
errorMessage: applied ? undefined : '状态未变更',
});
return response;
}
}