From e5fe8b903ad1b6c870c97b5bae3d330e0970aabd Mon Sep 17 00:00:00 2001 From: jacy <18049821889@163.com> Date: Wed, 2 Sep 2026 17:56:56 +0800 Subject: [PATCH] =?UTF-8?q?feat(courier):=20=E5=B0=8F=E9=A3=9E=E4=BE=A0?= =?UTF-8?q?=E7=AD=BE=E6=94=B6=E7=85=A7=E8=81=94=E8=B0=83=E4=B8=8E=20OSS=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=9B=9E=E9=80=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补齐 admin 100108 联调入口;track 优先用缓存签收照,上传失败回退 dataURI,日志截断大图。 Co-authored-by: Cursor --- .../src/components/OrderTrackDrawer.tsx | 9 ++ .../src/pages/XiaofeixiaTestPage.tsx | 68 +++++++++++- .../shared-types/src/fulfillment-provider.ts | 1 + .../integrations/courier/courier-log.util.ts | 26 +++-- .../fulfillment-provider.service.ts | 1 + .../fulfillment/fulfillment.service.ts | 100 ++++++++++++------ .../ops/admin-xiaofeixia.controller.ts | 5 + .../modules/ops/admin-xiaofeixia.service.ts | 13 +++ 8 files changed, 183 insertions(+), 40 deletions(-) diff --git a/apps/admin-web/src/components/OrderTrackDrawer.tsx b/apps/admin-web/src/components/OrderTrackDrawer.tsx index 143d837..ae504e6 100644 --- a/apps/admin-web/src/components/OrderTrackDrawer.tsx +++ b/apps/admin-web/src/components/OrderTrackDrawer.tsx @@ -1,5 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { + Alert, Button, Descriptions, Drawer, @@ -140,6 +141,14 @@ export default function OrderTrackDrawer({ + ) : nodes.some((n) => n.statusName?.includes('签收') || n.trackInfo?.includes('签收')) ? ( + ) : null} diff --git a/apps/admin-web/src/pages/XiaofeixiaTestPage.tsx b/apps/admin-web/src/pages/XiaofeixiaTestPage.tsx index de92dd3..d1400a0 100644 --- a/apps/admin-web/src/pages/XiaofeixiaTestPage.tsx +++ b/apps/admin-web/src/pages/XiaofeixiaTestPage.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Link } from 'react-router-dom'; import { - Alert, Button, Card, Col, Descriptions, Form, Input, InputNumber, Row, Select, Space, + Alert, Button, Card, Col, Descriptions, Form, Image, Input, InputNumber, Row, Select, Space, Tabs, Tag, Typography, message, } from 'antd'; import { request } from '../lib/api'; @@ -53,6 +53,22 @@ const CREATE_DEFAULTS = { function ResultPanel({ result }: { result: ApiResult | null }) { if (!result) return 点击「调用接口」后在此显示响应; + const display = (() => { + if (!result.data || typeof result.data !== 'object') return result; + const data = result.data as { count?: number; data?: string[] }; + if (!Array.isArray(data.data)) return result; + return { + ...result, + data: { + ...data, + data: data.data.map((item) => + typeof item === 'string' && item.length > 120 + ? `${item.slice(0, 80)}…(len=${item.length})` + : item, + ), + }, + }; + })(); return (
@@ -64,7 +80,7 @@ function ResultPanel({ result }: { result: ApiResult | null }) { margin: 0, padding: 12, background: '#f5f5f5', borderRadius: 4, maxHeight: 360, overflow: 'auto', fontSize: 12, }}> - {JSON.stringify(result, null, 2)} + {JSON.stringify(display, null, 2)}
); @@ -81,7 +97,9 @@ export default function XiaofeixiaTestPage() { const [queryForm] = Form.useForm(); const [batchForm] = Form.useForm(); const [trackForm] = Form.useForm(); + const [signPhotoForm] = Form.useForm(); const [cancelForm] = Form.useForm(); + const [signPhotoPreview, setSignPhotoPreview] = useState([]); useEffect(() => { void request('/admin/courier/xiaofeixia/config').then(setConfig); @@ -99,12 +117,19 @@ export default function XiaofeixiaTestPage() { body: JSON.stringify(body), }); setResult(res); + if (path.endsWith('/get-sign-photos')) { + const data = (res.data as { data?: string[] } | undefined)?.data; + setSignPhotoPreview(Array.isArray(data) ? data.filter((u) => typeof u === 'string') : []); + } else { + setSignPhotoPreview([]); + } if (res.ok) message.success('调用成功'); else message.warning(res.error || '调用失败'); } catch (e) { const err = e instanceof Error ? e.message : String(e); message.error(err); setResult({ ok: false, elapsedMs: 0, error: err }); + setSignPhotoPreview([]); } finally { setLoading(false); } @@ -118,7 +143,7 @@ export default function XiaofeixiaTestPage() {
小飞侠接口联调 - 通过 HQ 后台直接调用后端封装的小飞侠 API(cmd 100101~100301)。凭证请在 + 通过 HQ 后台直接调用后端封装的小飞侠 API(cmd 100101~100301,含签收图片 100108)。凭证请在 仓配管理 中配置;联调会优先使用仓配管理里启用的小飞侠承运商。 @@ -273,6 +298,25 @@ export default function XiaofeixiaTestPage() { ), }, + { + key: 'signPhotos', + label: '签收图片 (100108)', + children: ( +
void invoke('/admin/courier/xiaofeixia/get-sign-photos', v)} + > + + + + + + + +
+ ), + }, { 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;