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:
@@ -12,6 +12,7 @@ import { WechatApiProvider } from './wechat/wechat.api.provider';
|
|||||||
import { WechatDisabledProvider } from './wechat/wechat.disabled.provider';
|
import { WechatDisabledProvider } from './wechat/wechat.disabled.provider';
|
||||||
import { WechatMockProvider } from './wechat/wechat.mock.provider';
|
import { WechatMockProvider } from './wechat/wechat.mock.provider';
|
||||||
import { WechatRouterProvider } from './wechat/wechat.router.provider';
|
import { WechatRouterProvider } from './wechat/wechat.router.provider';
|
||||||
|
import { WechatOrderShippingService } from './wechat/wechat-order-shipping.service';
|
||||||
import { OssAliyunProvider } from './oss/oss.aliyun.provider';
|
import { OssAliyunProvider } from './oss/oss.aliyun.provider';
|
||||||
import { TencentLbsProvider } from './map/tencent-lbs.provider';
|
import { TencentLbsProvider } from './map/tencent-lbs.provider';
|
||||||
import {
|
import {
|
||||||
@@ -38,6 +39,7 @@ import { DELIVERY_QUEUE } from '../jobs/jobs.constants';
|
|||||||
WechatMockProvider,
|
WechatMockProvider,
|
||||||
WechatRouterProvider,
|
WechatRouterProvider,
|
||||||
{ provide: WECHAT_PROVIDER, useExisting: WechatRouterProvider },
|
{ provide: WECHAT_PROVIDER, useExisting: WechatRouterProvider },
|
||||||
|
WechatOrderShippingService,
|
||||||
PayMockProvider,
|
PayMockProvider,
|
||||||
PayWechatProvider,
|
PayWechatProvider,
|
||||||
PayRouterProvider,
|
PayRouterProvider,
|
||||||
@@ -55,6 +57,7 @@ import { DELIVERY_QUEUE } from '../jobs/jobs.constants';
|
|||||||
PAY_PROVIDER,
|
PAY_PROVIDER,
|
||||||
DELIVERY_PROVIDER,
|
DELIVERY_PROVIDER,
|
||||||
WECHAT_PROVIDER,
|
WECHAT_PROVIDER,
|
||||||
|
WechatOrderShippingService,
|
||||||
OSS_PROVIDER,
|
OSS_PROVIDER,
|
||||||
MAP_PROVIDER,
|
MAP_PROVIDER,
|
||||||
TencentLbsProvider,
|
TencentLbsProvider,
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { loadAppConfig } from '@dukang/shared-types';
|
||||||
|
import { PrismaService } from '../../common/prisma/prisma.module';
|
||||||
|
import { WECHAT_PROVIDER } from '../integrations.constants';
|
||||||
|
import type { IWechatProvider } from './wechat.interface';
|
||||||
|
import {
|
||||||
|
formatWechatUploadTime,
|
||||||
|
maskReceiverPhone,
|
||||||
|
resolveExpressCompanyId,
|
||||||
|
} from './wechat-order-shipping.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序发货信息管理:订单发货/自提后向微信录入发货信息,解冻交易资金。
|
||||||
|
* @see https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping.html
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class WechatOrderShippingService {
|
||||||
|
private readonly logger = new Logger(WechatOrderShippingService.name);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly prisma: PrismaService,
|
||||||
|
@Inject(WECHAT_PROVIDER) private readonly wechat: IWechatProvider,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** 异步安全上报,失败只记日志,不阻断主履约流程 */
|
||||||
|
uploadForOrderSafe(orderId: bigint) {
|
||||||
|
void this.uploadForOrder(orderId).catch((err) => {
|
||||||
|
const msg = err instanceof Error ? err.message : String(err);
|
||||||
|
this.logger.error(`upload shipping info failed order=${orderId}: ${msg}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async uploadForOrder(orderId: bigint): Promise<{ skipped?: string; ok?: boolean } | void> {
|
||||||
|
const cfg = loadAppConfig();
|
||||||
|
if (cfg.mockPay) {
|
||||||
|
return { skipped: 'MOCK_PAY' };
|
||||||
|
}
|
||||||
|
if (!this.wechat.isPayEnabled() && this.wechat.isMock()) {
|
||||||
|
return { skipped: 'MOCK_WECHAT' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const order = await this.prisma.order.findUnique({
|
||||||
|
where: { id: orderId },
|
||||||
|
include: {
|
||||||
|
delivery: true,
|
||||||
|
user: { select: { wxOpenId: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!order) return { skipped: 'ORDER_NOT_FOUND' };
|
||||||
|
if (order.payStatus !== 'PAID') return { skipped: 'NOT_PAID' };
|
||||||
|
|
||||||
|
const openId = order.user?.wxOpenId?.trim();
|
||||||
|
if (!openId) return { skipped: 'NO_OPENID' };
|
||||||
|
|
||||||
|
const transactionId = order.payExternalNo?.trim();
|
||||||
|
const mchId = this.wechat.getMchId()?.trim();
|
||||||
|
if (!transactionId && !(mchId && order.orderNo)) {
|
||||||
|
return { skipped: 'NO_PAY_REF' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已成功上报则跳过(幂等)
|
||||||
|
const existed = await this.prisma.logThirdParty.findFirst({
|
||||||
|
where: {
|
||||||
|
provider: 'WECHAT_PAY',
|
||||||
|
scene: 'UPLOAD_SHIPPING',
|
||||||
|
refType: 'ORDER',
|
||||||
|
refId: orderId,
|
||||||
|
status: 'SUCCESS',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existed) return { skipped: 'ALREADY_UPLOADED' };
|
||||||
|
|
||||||
|
const logisticsType = this.resolveLogisticsType(order.deliveryType, order.delivery);
|
||||||
|
const trackingNo = order.delivery?.trackingNo?.trim() || undefined;
|
||||||
|
let expressCompany = resolveExpressCompanyId(order.delivery?.logisticsCompany);
|
||||||
|
|
||||||
|
if (logisticsType === 1 && trackingNo && !expressCompany) {
|
||||||
|
try {
|
||||||
|
const list = await this.wechat.getDeliveryList();
|
||||||
|
expressCompany = resolveExpressCompanyId(order.delivery?.logisticsCompany, list);
|
||||||
|
} catch (err) {
|
||||||
|
this.logger.warn(
|
||||||
|
`get_delivery_list failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快递模式缺运力 ID 时降级为同城,避免接口硬失败
|
||||||
|
let finalLogisticsType = logisticsType;
|
||||||
|
if (finalLogisticsType === 1 && (!trackingNo || !expressCompany)) {
|
||||||
|
finalLogisticsType = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemDesc = `${order.productName}${order.productSpec ? `(${order.productSpec})` : ''}*${order.quantity}`
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.slice(0, 120);
|
||||||
|
|
||||||
|
const shippingItem: {
|
||||||
|
trackingNo?: string;
|
||||||
|
expressCompany?: string;
|
||||||
|
itemDesc: string;
|
||||||
|
contact?: { receiverContact: string };
|
||||||
|
} = { itemDesc };
|
||||||
|
|
||||||
|
if (finalLogisticsType === 1) {
|
||||||
|
shippingItem.trackingNo = trackingNo;
|
||||||
|
shippingItem.expressCompany = expressCompany;
|
||||||
|
if (expressCompany === 'SF') {
|
||||||
|
shippingItem.contact = {
|
||||||
|
receiverContact: maskReceiverPhone(order.receiverPhone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (trackingNo) {
|
||||||
|
// 同城也可附带运单信息
|
||||||
|
shippingItem.trackingNo = trackingNo;
|
||||||
|
if (expressCompany) shippingItem.expressCompany = expressCompany;
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = {
|
||||||
|
orderNumberType: (transactionId ? 2 : 1) as 1 | 2,
|
||||||
|
transactionId: transactionId || undefined,
|
||||||
|
mchId: transactionId ? undefined : mchId,
|
||||||
|
outTradeNo: transactionId ? undefined : order.orderNo,
|
||||||
|
logisticsType: finalLogisticsType,
|
||||||
|
deliveryMode: 1 as const,
|
||||||
|
shippingList: [shippingItem],
|
||||||
|
uploadTime: formatWechatUploadTime(),
|
||||||
|
payerOpenId: openId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const requestUrl = 'https://api.weixin.qq.com/wxa/sec/order/upload_shipping_info';
|
||||||
|
try {
|
||||||
|
const result = await this.wechat.uploadShippingInfo(input);
|
||||||
|
const ok = result.errcode === 0;
|
||||||
|
await this.prisma.logThirdParty.create({
|
||||||
|
data: {
|
||||||
|
provider: 'WECHAT_PAY',
|
||||||
|
scene: 'UPLOAD_SHIPPING',
|
||||||
|
refType: 'ORDER',
|
||||||
|
refId: orderId,
|
||||||
|
requestUrl,
|
||||||
|
requestBody: {
|
||||||
|
orderNo: order.orderNo,
|
||||||
|
logisticsType: finalLogisticsType,
|
||||||
|
trackingNo: shippingItem.trackingNo,
|
||||||
|
expressCompany: shippingItem.expressCompany,
|
||||||
|
orderNumberType: input.orderNumberType,
|
||||||
|
},
|
||||||
|
responseBody: { errcode: result.errcode, errmsg: result.errmsg },
|
||||||
|
externalNo: transactionId || order.orderNo,
|
||||||
|
status: ok ? 'SUCCESS' : 'FAILED',
|
||||||
|
errorMessage: ok ? undefined : `${result.errcode}:${result.errmsg}`.slice(0, 512),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!ok) {
|
||||||
|
this.logger.warn(
|
||||||
|
`WeChat upload_shipping_info order=${order.orderNo} ${result.errcode} ${result.errmsg}`,
|
||||||
|
);
|
||||||
|
return { ok: false };
|
||||||
|
}
|
||||||
|
this.logger.log(`WeChat upload_shipping_info ok order=${order.orderNo}`);
|
||||||
|
return { ok: true };
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
await this.prisma.logThirdParty.create({
|
||||||
|
data: {
|
||||||
|
provider: 'WECHAT_PAY',
|
||||||
|
scene: 'UPLOAD_SHIPPING',
|
||||||
|
refType: 'ORDER',
|
||||||
|
refId: orderId,
|
||||||
|
requestUrl,
|
||||||
|
requestBody: { orderNo: order.orderNo },
|
||||||
|
status: 'FAILED',
|
||||||
|
errorMessage: message.slice(0, 512),
|
||||||
|
externalNo: transactionId || order.orderNo,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private resolveLogisticsType(
|
||||||
|
deliveryType: string,
|
||||||
|
delivery: { trackingNo: string | null; logisticsCompany: string | null; provider: string } | null,
|
||||||
|
): 1 | 2 | 4 {
|
||||||
|
if (deliveryType === 'ON_SITE_PICKUP') return 4;
|
||||||
|
if (deliveryType === 'LOCAL') return 2;
|
||||||
|
// CROSS_CITY:有运单走快递,否则同城兜底
|
||||||
|
if (delivery?.trackingNo?.trim()) return 1;
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/** 常见快递公司名称 → 微信运力 ID(get_delivery_list 的 delivery_id) */
|
||||||
|
const STATIC_EXPRESS_MAP: Array<{ id: string; aliases: string[] }> = [
|
||||||
|
{ id: 'SF', aliases: ['sf', '顺丰', '顺丰速运', '顺丰快递'] },
|
||||||
|
{ id: 'STO', aliases: ['sto', '申通', '申通快递'] },
|
||||||
|
{ id: 'YTO', aliases: ['yto', '圆通', '圆通速递', '圆通快递'] },
|
||||||
|
{ id: 'ZTO', aliases: ['zto', '中通', '中通快递'] },
|
||||||
|
{ id: 'YD', aliases: ['yd', '韵达', '韵达速递', '韵达快递'] },
|
||||||
|
{ id: 'HTKY', aliases: ['htky', '百世', '百世快递', '汇通'] },
|
||||||
|
{ id: 'EMS', aliases: ['ems', '邮政', '中国邮政', '邮政快递'] },
|
||||||
|
{ id: 'JD', aliases: ['jd', '京东', '京东快递', '京东物流'] },
|
||||||
|
{ id: 'JTSD', aliases: ['jtsd', 'jt', '极兔', '极兔速递', '极兔快递'] },
|
||||||
|
{ id: 'UC', aliases: ['uc', '优速', '优速快递'] },
|
||||||
|
{ id: 'DBL', aliases: ['dbl', '德邦', '德邦快递', '德邦物流'] },
|
||||||
|
{ id: 'ANE', aliases: ['ane', '安能', '安能物流'] },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function maskReceiverPhone(phone: string): string {
|
||||||
|
const digits = phone.replace(/\D/g, '');
|
||||||
|
if (digits.length >= 7) {
|
||||||
|
return `${digits.slice(0, 3)}****${digits.slice(-4)}`;
|
||||||
|
}
|
||||||
|
return phone.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** RFC3339 +08:00,微信发货录入 upload_time 要求 */
|
||||||
|
export function formatWechatUploadTime(date = new Date()): string {
|
||||||
|
const fmt = new Intl.DateTimeFormat('en-CA', {
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
const parts = Object.fromEntries(fmt.formatToParts(date).map((p) => [p.type, p.value]));
|
||||||
|
const ms = String(date.getMilliseconds()).padStart(3, '0');
|
||||||
|
return `${parts.year}-${parts.month}-${parts.day}T${parts.hour}:${parts.minute}:${parts.second}.${ms}+08:00`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveExpressCompanyId(
|
||||||
|
logisticsCompany: string | null | undefined,
|
||||||
|
deliveryList?: Array<{ deliveryId: string; deliveryName: string }>,
|
||||||
|
): string | undefined {
|
||||||
|
const raw = (logisticsCompany || '').trim();
|
||||||
|
if (!raw) return undefined;
|
||||||
|
|
||||||
|
const upper = raw.toUpperCase();
|
||||||
|
// 已是运力 ID
|
||||||
|
if (/^[A-Z0-9_()-]{2,32}$/.test(upper) && !/[\u4e00-\u9fff]/.test(raw)) {
|
||||||
|
return upper;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lower = raw.toLowerCase();
|
||||||
|
for (const row of STATIC_EXPRESS_MAP) {
|
||||||
|
if (row.aliases.some((a) => lower.includes(a.toLowerCase()) || a === raw)) {
|
||||||
|
return row.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deliveryList?.length) {
|
||||||
|
const hit = deliveryList.find(
|
||||||
|
(d) =>
|
||||||
|
d.deliveryId.toUpperCase() === upper ||
|
||||||
|
d.deliveryName === raw ||
|
||||||
|
d.deliveryName.includes(raw) ||
|
||||||
|
raw.includes(d.deliveryName),
|
||||||
|
);
|
||||||
|
if (hit) return hit.deliveryId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
@@ -8,6 +8,9 @@ import type {
|
|||||||
WechatCodeSession,
|
WechatCodeSession,
|
||||||
WechatOAuthSession,
|
WechatOAuthSession,
|
||||||
WechatWxaCodeUnlimitedInput,
|
WechatWxaCodeUnlimitedInput,
|
||||||
|
WechatUploadShippingInfoInput,
|
||||||
|
WechatUploadShippingInfoResult,
|
||||||
|
WechatDeliveryCompany,
|
||||||
} from './wechat.interface';
|
} from './wechat.interface';
|
||||||
import { logWechatAuth, type WechatActorRef } from './wechat-log.util';
|
import { logWechatAuth, type WechatActorRef } from './wechat-log.util';
|
||||||
import {
|
import {
|
||||||
@@ -303,6 +306,86 @@ export class WechatApiProvider implements IWechatProvider {
|
|||||||
return buf;
|
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> {
|
async getPhoneNumberByCode(code: string, platform: 'mini' | 'h5', actorRef?: WechatActorRef): Promise<string> {
|
||||||
if (platform === 'h5') {
|
if (platform === 'h5') {
|
||||||
throw new InternalServerErrorException('H5 请使用短信绑定手机号');
|
throw new InternalServerErrorException('H5 请使用短信绑定手机号');
|
||||||
|
|||||||
@@ -58,4 +58,12 @@ export class WechatDisabledProvider implements IWechatProvider {
|
|||||||
getWxaCodeUnlimited(_input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
getWxaCodeUnlimited(_input: WechatWxaCodeUnlimitedInput): Promise<Buffer> {
|
||||||
return this.disabled();
|
return this.disabled();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadShippingInfo() {
|
||||||
|
return this.disabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeliveryList() {
|
||||||
|
return this.disabled();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,41 @@ export type WechatWxaCodeUnlimitedInput = {
|
|||||||
isHyaline?: boolean;
|
isHyaline?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** 小程序发货信息管理 — 发货信息录入 */
|
||||||
|
export type WechatUploadShippingInfoInput = {
|
||||||
|
/** 1=商户单号;2=微信支付单号 */
|
||||||
|
orderNumberType: 1 | 2;
|
||||||
|
transactionId?: string;
|
||||||
|
mchId?: string;
|
||||||
|
outTradeNo?: string;
|
||||||
|
/** 1 快递 2 同城 3 虚拟 4 自提 */
|
||||||
|
logisticsType: 1 | 2 | 3 | 4;
|
||||||
|
/** 1 统一发货 2 分拆发货 */
|
||||||
|
deliveryMode?: 1 | 2;
|
||||||
|
shippingList: Array<{
|
||||||
|
trackingNo?: string;
|
||||||
|
/** 微信运力 ID,如 SF / STO */
|
||||||
|
expressCompany?: string;
|
||||||
|
itemDesc: string;
|
||||||
|
contact?: {
|
||||||
|
consignorContact?: string;
|
||||||
|
receiverContact?: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
uploadTime: string;
|
||||||
|
payerOpenId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WechatUploadShippingInfoResult = {
|
||||||
|
errcode: number;
|
||||||
|
errmsg: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WechatDeliveryCompany = {
|
||||||
|
deliveryId: string;
|
||||||
|
deliveryName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export interface IWechatProvider {
|
export interface IWechatProvider {
|
||||||
isEnabled(): boolean;
|
isEnabled(): boolean;
|
||||||
|
|
||||||
@@ -95,4 +130,13 @@ export interface IWechatProvider {
|
|||||||
|
|
||||||
/** 获取不限制的小程序码(PNG Buffer),须服务端调用 */
|
/** 获取不限制的小程序码(PNG Buffer),须服务端调用 */
|
||||||
getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer>;
|
getWxaCodeUnlimited(input: WechatWxaCodeUnlimitedInput): Promise<Buffer>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 小程序发货信息录入(交易资金解冻前置)
|
||||||
|
* @see https://developers.weixin.qq.com/miniprogram/dev/server/API/order_shipping/api_uploadshippinginfo.html
|
||||||
|
*/
|
||||||
|
uploadShippingInfo(input: WechatUploadShippingInfoInput): Promise<WechatUploadShippingInfoResult>;
|
||||||
|
|
||||||
|
/** 获取运力公司列表(快递公司 delivery_id) */
|
||||||
|
getDeliveryList(): Promise<WechatDeliveryCompany[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,4 +94,18 @@ export class WechatMockProvider implements IWechatProvider {
|
|||||||
color: { dark: '#1f1a17', light: '#ffffff' },
|
color: { dark: '#1f1a17', light: '#ffffff' },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async uploadShippingInfo() {
|
||||||
|
return { errcode: 0, errmsg: 'ok' };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getDeliveryList() {
|
||||||
|
return [
|
||||||
|
{ deliveryId: 'SF', deliveryName: '顺丰速运' },
|
||||||
|
{ deliveryId: 'STO', deliveryName: '申通快递' },
|
||||||
|
{ deliveryId: 'YTO', deliveryName: '圆通速递' },
|
||||||
|
{ deliveryId: 'ZTO', deliveryName: '中通快递' },
|
||||||
|
{ deliveryId: 'YD', deliveryName: '韵达速递' },
|
||||||
|
];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,4 +90,12 @@ export class WechatRouterProvider implements IWechatProvider {
|
|||||||
getWxaCodeUnlimited(input: Parameters<IWechatProvider['getWxaCodeUnlimited']>[0]) {
|
getWxaCodeUnlimited(input: Parameters<IWechatProvider['getWxaCodeUnlimited']>[0]) {
|
||||||
return this.resolve().getWxaCodeUnlimited(input);
|
return this.resolve().getWxaCodeUnlimited(input);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadShippingInfo(input: Parameters<IWechatProvider['uploadShippingInfo']>[0]) {
|
||||||
|
return this.resolve().uploadShippingInfo(input);
|
||||||
|
}
|
||||||
|
|
||||||
|
getDeliveryList() {
|
||||||
|
return this.resolve().getDeliveryList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { extractClientIp } from '../../common/geo/client-ip.util';
|
|||||||
import { buildOrderStatusEvent, orderStatusLogWhere } from '../../common/event/event.helpers';
|
import { buildOrderStatusEvent, orderStatusLogWhere } from '../../common/event/event.helpers';
|
||||||
import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-compat';
|
import { mapOrderCompat, mapStatusLogCompat } from '../../common/compat/v31-compat';
|
||||||
import { FulfillmentService } from '../fulfillment/fulfillment.service';
|
import { FulfillmentService } from '../fulfillment/fulfillment.service';
|
||||||
|
import { WechatOrderShippingService } from '../../integrations/wechat/wechat-order-shipping.service';
|
||||||
import type { Request } from 'express';
|
import type { Request } from 'express';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -49,6 +50,7 @@ export class TradeService {
|
|||||||
private readonly promoCodeService: PromoCodeService,
|
private readonly promoCodeService: PromoCodeService,
|
||||||
@Inject(forwardRef(() => FulfillmentService))
|
@Inject(forwardRef(() => FulfillmentService))
|
||||||
private readonly fulfillmentService: FulfillmentService,
|
private readonly fulfillmentService: FulfillmentService,
|
||||||
|
private readonly wechatOrderShipping: WechatOrderShippingService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async preview(
|
async preview(
|
||||||
@@ -349,6 +351,8 @@ export class TradeService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (order.deliveryType === 'ON_SITE_PICKUP') {
|
if (order.deliveryType === 'ON_SITE_PICKUP') {
|
||||||
|
// 现场取货:支付后即向微信录入「用户自提」发货信息
|
||||||
|
this.wechatOrderShipping.uploadForOrderSafe(orderId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1021,6 +1025,11 @@ export class TradeService {
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 发货信息管理:进入 SHIPPING 时向微信录入(解冻结算前置)
|
||||||
|
if (targetStatus === 'SHIPPING' && currentStatus !== 'SHIPPING') {
|
||||||
|
this.wechatOrderShipping.uploadForOrderSafe(orderId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getPartnerProxyOrderOptions(partnerAccountId: bigint) {
|
async getPartnerProxyOrderOptions(partnerAccountId: bigint) {
|
||||||
|
|||||||
Reference in New Issue
Block a user