补齐 admin 100108 联调入口;track 优先用缓存签收照,上传失败回退 dataURI,日志截断大图。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -47,13 +47,27 @@ export function sceneForXfxCmd(cmd: string) {
|
||||
return CMD_SCENE[cmd] ?? `XFX_CMD_${cmd}`;
|
||||
}
|
||||
|
||||
/** 签收照等大字段入库前截断,避免撑爆 JSON / 影响后续业务返回 */
|
||||
function sanitizeCourierResponseBody(scene: string, body: unknown): Record<string, unknown> | undefined {
|
||||
if (body === undefined) return undefined;
|
||||
if (typeof body !== 'object' || body === null) {
|
||||
return { value: body };
|
||||
}
|
||||
const root = { ...(body as Record<string, unknown>) };
|
||||
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<string, unknown>)
|
||||
: { value: input.responseBody };
|
||||
const responseBody = sanitizeCourierResponseBody(input.scene, input.responseBody);
|
||||
|
||||
const row = await prisma.logThirdParty.create({
|
||||
data: {
|
||||
|
||||
@@ -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 失败,回退 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user