小飞侠接口联调
- 通过 HQ 后台直接调用后端封装的小飞侠 API(cmd 100101~100301)。凭证请在
+ 通过 HQ 后台直接调用后端封装的小飞侠 API(cmd 100101~100301,含签收图片 100108)。凭证请在
仓配管理
中配置;联调会优先使用仓配管理里启用的小飞侠承运商。
@@ -273,6 +298,25 @@ export default function XiaofeixiaTestPage() {
),
},
+ {
+ key: 'signPhotos',
+ label: '签收图片 (100108)',
+ children: (
+
+
+
+
+
+
+
+
+ ),
+ },
{
key: 'cancel',
label: '取消订单 (100103)',
@@ -290,6 +334,24 @@ export default function XiaofeixiaTestPage() {
+ {signPhotoPreview.length > 0 ? (
+
+ 签收照片预览({signPhotoPreview.length})
+
+
+ {signPhotoPreview.map((url, index) => (
+
+ ))}
+
+
+
+ ) : null}
diff --git a/packages/shared-types/src/fulfillment-provider.ts b/packages/shared-types/src/fulfillment-provider.ts
index f117800..63d2937 100644
--- a/packages/shared-types/src/fulfillment-provider.ts
+++ b/packages/shared-types/src/fulfillment-provider.ts
@@ -41,6 +41,7 @@ export interface FulfillmentProviderDto {
capabilities?: {
createShipment?: boolean;
getTrack?: boolean;
+ getSignPhotos?: boolean;
callback?: boolean;
cancel?: boolean;
} | null;
diff --git a/server/dukang-api/src/integrations/courier/courier-log.util.ts b/server/dukang-api/src/integrations/courier/courier-log.util.ts
index 0908cf4..c802d4e 100644
--- a/server/dukang-api/src/integrations/courier/courier-log.util.ts
+++ b/server/dukang-api/src/integrations/courier/courier-log.util.ts
@@ -47,13 +47,27 @@ export function sceneForXfxCmd(cmd: string) {
return CMD_SCENE[cmd] ?? `XFX_CMD_${cmd}`;
}
+/** 签收照等大字段入库前截断,避免撑爆 JSON / 影响后续业务返回 */
+function sanitizeCourierResponseBody(scene: string, body: unknown): Record
| undefined {
+ if (body === undefined) return undefined;
+ if (typeof body !== 'object' || body === null) {
+ return { value: body };
+ }
+ const root = { ...(body as Record) };
+ if (scene === 'GET_SIGN_PHOTOS' && Array.isArray(root.data)) {
+ const photos = root.data as unknown[];
+ root.data = photos.map((item) => {
+ if (typeof item !== 'string') return item;
+ if (item.length <= 120) return item;
+ return `${item.slice(0, 80)}…(len=${item.length})`;
+ });
+ root.dataCount = photos.length;
+ }
+ return root;
+}
+
export async function logCourierCall(prisma: PrismaService, input: LogCourierCallInput) {
- const responseBody =
- input.responseBody === undefined
- ? undefined
- : typeof input.responseBody === 'object' && input.responseBody !== null
- ? (input.responseBody as Record)
- : { value: input.responseBody };
+ const responseBody = sanitizeCourierResponseBody(input.scene, input.responseBody);
const row = await prisma.logThirdParty.create({
data: {
diff --git a/server/dukang-api/src/modules/fulfillment/fulfillment-provider.service.ts b/server/dukang-api/src/modules/fulfillment/fulfillment-provider.service.ts
index 6538dc9..44c7881 100644
--- a/server/dukang-api/src/modules/fulfillment/fulfillment-provider.service.ts
+++ b/server/dukang-api/src/modules/fulfillment/fulfillment-provider.service.ts
@@ -140,6 +140,7 @@ export class FulfillmentProviderService {
? JSON.stringify({
createShipment: true,
getTrack: true,
+ getSignPhotos: true,
callback: true,
cancel: true,
})
diff --git a/server/dukang-api/src/modules/fulfillment/fulfillment.service.ts b/server/dukang-api/src/modules/fulfillment/fulfillment.service.ts
index cfe62ee..01d9ad5 100644
--- a/server/dukang-api/src/modules/fulfillment/fulfillment.service.ts
+++ b/server/dukang-api/src/modules/fulfillment/fulfillment.service.ts
@@ -313,8 +313,9 @@ export class FulfillmentService {
trackingNumber: order.delivery.trackingNo ?? undefined,
outNumber: order.orderNo,
};
+ const cachedSignPhotoUrl = order.delivery.signPhotoResource?.url ?? null;
- const [trackResult, signPhotoDataUris] = await Promise.all([
+ const [trackResult, signPhotoResult] = await Promise.all([
this.courier
.getTrack(shipmentQuery, options)
.then((nodes) => ({ nodes: Array.isArray(nodes) ? nodes : [], error: null as string | null }))
@@ -322,12 +323,30 @@ export class FulfillmentService {
nodes: [] as TrackNode[],
error: err instanceof Error ? err.message : '查询路由失败',
})),
- this.courier.getSignPhotos(shipmentQuery, options).catch(() => [] as string[]),
+ cachedSignPhotoUrl
+ ? Promise.resolve({ dataUris: [] as string[], error: null as string | null })
+ : this.courier
+ .getSignPhotos(shipmentQuery, options)
+ .then((dataUris) => ({
+ dataUris: Array.isArray(dataUris) ? dataUris : [],
+ error: null as string | null,
+ }))
+ .catch((err: unknown) => ({
+ dataUris: [] as string[],
+ error: err instanceof Error ? err.message : '查询签收照片失败',
+ })),
]);
base.nodes = this.sortTrackNodesOldestFirst(trackResult.nodes);
base.queryError = trackResult.error;
- base.signPhotoUrls = await this.resolveSignPhotoUrls(order, signPhotoDataUris);
+ if (cachedSignPhotoUrl) {
+ base.signPhotoUrls = [cachedSignPhotoUrl];
+ } else {
+ base.signPhotoUrls = await this.resolveSignPhotoUrls(order, signPhotoResult.dataUris);
+ if (base.signPhotoUrls.length === 0 && signPhotoResult.error && !base.queryError) {
+ base.queryError = signPhotoResult.error;
+ }
+ }
if (this.shouldFetchEstimatedArrival(order.status, base.nodes)) {
const toAddress = this.buildReceiverAddress(order);
@@ -431,43 +450,62 @@ export class FulfillmentService {
},
dataUris: string[],
): Promise {
- if (!order.delivery || dataUris.length === 0) {
- return order.delivery?.signPhotoResource?.url ? [order.delivery.signPhotoResource.url] : [];
+ if (!order.delivery) return [];
+ if (order.delivery.signPhotoResource?.url) {
+ return [order.delivery.signPhotoResource.url];
}
+ if (dataUris.length === 0) return [];
const urls: string[] = [];
let firstResourceId: bigint | null = order.delivery.signPhotoResourceId;
for (let i = 0; i < dataUris.length; i += 1) {
- const parsed = this.parseDataUri(dataUris[i]);
+ const raw = dataUris[i];
+ const parsed = this.parseDataUri(raw);
if (!parsed) continue;
- const result = await this.oss.putObject({
- bizType: 'SIGN_PHOTO',
- mediaType: 'IMAGE',
- fileName: `sign-${order.id}-${i + 1}.${parsed.ext}`,
- buffer: parsed.buffer,
- mimeType: parsed.mimeType,
- });
- urls.push(result.url);
-
- if (!firstResourceId) {
- const resource = await this.prisma.commonResource.create({
- data: {
- ownerType: 'ORDER',
- ownerId: order.id,
- bizType: 'SIGN_PHOTO',
- mediaType: 'IMAGE',
- ossBucket: result.bucket,
- ossKey: result.ossKey,
- url: result.url,
- fileName: `sign-${order.id}-${i + 1}.${parsed.ext}`,
- fileSize: BigInt(parsed.buffer.length),
- mimeType: parsed.mimeType,
- status: 'ACTIVE',
- },
+ try {
+ if (!this.oss.isEnabled()) {
+ throw new Error('OSS 未配置');
+ }
+ const result = await this.oss.putObject({
+ bizType: 'SIGN_PHOTO',
+ mediaType: 'IMAGE',
+ fileName: `sign-${order.id}-${i + 1}.${parsed.ext}`,
+ buffer: parsed.buffer,
+ mimeType: parsed.mimeType,
});
- firstResourceId = resource.id;
+ urls.push(result.url);
+
+ if (!firstResourceId) {
+ const resource = await this.prisma.commonResource.create({
+ data: {
+ ownerType: 'ORDER',
+ ownerId: order.id,
+ bizType: 'SIGN_PHOTO',
+ mediaType: 'IMAGE',
+ ossBucket: result.bucket,
+ ossKey: result.ossKey,
+ url: result.url,
+ fileName: `sign-${order.id}-${i + 1}.${parsed.ext}`,
+ fileSize: BigInt(parsed.buffer.length),
+ mimeType: parsed.mimeType,
+ status: 'ACTIVE',
+ },
+ });
+ firstResourceId = resource.id;
+ }
+ } catch (err) {
+ // OSS 失败不丢图:回退 data URI,保证 C 端 / HQ 仍可预览
+ this.logger.warn(
+ `签收照上传 OSS 失败,回退 dataURI:orderId=${order.id} err=${
+ err instanceof Error ? err.message : String(err)
+ }`,
+ );
+ const dataUri = raw.startsWith('data:')
+ ? raw
+ : `data:${parsed.mimeType};base64,${parsed.buffer.toString('base64')}`;
+ urls.push(dataUri);
}
}
diff --git a/server/dukang-api/src/modules/ops/admin-xiaofeixia.controller.ts b/server/dukang-api/src/modules/ops/admin-xiaofeixia.controller.ts
index f5bc32c..1ffe853 100644
--- a/server/dukang-api/src/modules/ops/admin-xiaofeixia.controller.ts
+++ b/server/dukang-api/src/modules/ops/admin-xiaofeixia.controller.ts
@@ -59,4 +59,9 @@ export class AdminXiaofeixiaController {
getTrack(@Body() body: XiaofeixiaShipmentQueryDto) {
return this.service.getTrack(body);
}
+
+ @Post('get-sign-photos')
+ getSignPhotos(@Body() body: XiaofeixiaShipmentQueryDto) {
+ return this.service.getSignPhotos(body);
+ }
}
diff --git a/server/dukang-api/src/modules/ops/admin-xiaofeixia.service.ts b/server/dukang-api/src/modules/ops/admin-xiaofeixia.service.ts
index 4c2bc46..623dd5a 100644
--- a/server/dukang-api/src/modules/ops/admin-xiaofeixia.service.ts
+++ b/server/dukang-api/src/modules/ops/admin-xiaofeixia.service.ts
@@ -101,6 +101,19 @@ export class AdminXiaofeixiaService {
return this.wrap(() => this.courier.getTrack(query, options));
}
+ async getSignPhotos(dto: XiaofeixiaShipmentQueryDto) {
+ const query = this.mapShipmentQuery(dto);
+ const options = await this.callOptions();
+ return this.wrap(async () => {
+ const dataUris = await this.courier.getSignPhotos(query, options);
+ return {
+ count: dataUris.length,
+ // 联调预览用:完整 dataURI;日志侧已截断
+ data: dataUris,
+ };
+ });
+ }
+
private async callOptions() {
const fromDb = await this.fulfillmentProviderService.resolveDefaultXiaofeixiaConfig();
return fromDb ? { xiaofeixia: fromDb } : undefined;