feat(courier): 小飞侠签收照联调与 OSS 失败回退
CI / verify (push) Has been cancelled

补齐 admin 100108 联调入口;track 优先用缓存签收照,上传失败回退 dataURI,日志截断大图。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-02 17:56:56 +08:00
parent c6b01def4c
commit e5fe8b903a
8 changed files with 183 additions and 40 deletions
@@ -140,6 +140,7 @@ export class FulfillmentProviderService {
? JSON.stringify({
createShipment: true,
getTrack: true,
getSignPhotos: true,
callback: true,
cancel: true,
})
@@ -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<string[]> {
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 失败,回退 dataURIorderId=${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);
}
}
@@ -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);
}
}
@@ -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;