feat(trade): upload WeChat mini program shipping info

Hook SHIPPING and on-site pickup to upload_shipping_info for settlement unlock.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 14:35:21 +08:00
parent ca621c9db8
commit bf9cd1bffd
9 changed files with 435 additions and 0 deletions
@@ -8,6 +8,9 @@ import type {
WechatCodeSession,
WechatOAuthSession,
WechatWxaCodeUnlimitedInput,
WechatUploadShippingInfoInput,
WechatUploadShippingInfoResult,
WechatDeliveryCompany,
} from './wechat.interface';
import { logWechatAuth, type WechatActorRef } from './wechat-log.util';
import {
@@ -303,6 +306,86 @@ export class WechatApiProvider implements IWechatProvider {
return buf;
}
async uploadShippingInfo(input: WechatUploadShippingInfoInput): Promise<WechatUploadShippingInfoResult> {
const accessToken = await this.getMiniAccessToken();
const orderKey: Record<string, string | number> = {
order_number_type: input.orderNumberType,
};
if (input.orderNumberType === 2) {
if (!input.transactionId) {
throw new BadRequestException('微信支付单号不能为空');
}
orderKey.transaction_id = input.transactionId;
} else {
if (!input.mchId || !input.outTradeNo) {
throw new BadRequestException('商户号与商户单号不能为空');
}
orderKey.mchid = input.mchId;
orderKey.out_trade_no = input.outTradeNo;
}
const body = {
order_key: orderKey,
logistics_type: input.logisticsType,
delivery_mode: input.deliveryMode ?? 1,
shipping_list: input.shippingList.map((row) => {
const item: Record<string, unknown> = {
item_desc: row.itemDesc.slice(0, 120),
};
if (row.trackingNo) item.tracking_no = row.trackingNo;
if (row.expressCompany) item.express_company = row.expressCompany;
if (row.contact?.consignorContact || row.contact?.receiverContact) {
item.contact = {
...(row.contact.consignorContact
? { consignor_contact: row.contact.consignorContact }
: {}),
...(row.contact.receiverContact
? { receiver_contact: row.contact.receiverContact }
: {}),
};
}
return item;
}),
upload_time: input.uploadTime,
payer: { openid: input.payerOpenId },
};
const apiUrl = `https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info?access_token=${accessToken}`;
const data = await this.fetchJson<{ errcode?: number; errmsg?: string }>(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return {
errcode: data.errcode ?? -1,
errmsg: data.errmsg ?? 'unknown',
};
}
async getDeliveryList(): Promise<WechatDeliveryCompany[]> {
const accessToken = await this.getMiniAccessToken();
const apiUrl = `https://api.weixin.qq.com/cgi-bin/express/delivery/open_msg/get_delivery_list?access_token=${accessToken}`;
const data = await this.fetchJson<{
errcode?: number;
errmsg?: string;
delivery_list?: Array<{ delivery_id?: string; delivery_name?: string }>;
}>(apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: '{}',
});
if (data.errcode && data.errcode !== 0) {
this.logger.warn(`get_delivery_list failed: ${data.errcode} ${data.errmsg}`);
return [];
}
return (data.delivery_list ?? [])
.filter((row) => row.delivery_id && row.delivery_name)
.map((row) => ({
deliveryId: row.delivery_id!,
deliveryName: row.delivery_name!,
}));
}
async getPhoneNumberByCode(code: string, platform: 'mini' | 'h5', actorRef?: WechatActorRef): Promise<string> {
if (platform === 'h5') {
throw new InternalServerErrorException('H5 请使用短信绑定手机号');