feat(trade): WeChat confirm-receive component + admin status log columns

Open weappOrderConfirm in mini-user so users confirm in-app instead of
service notice; verify via get_order before completing. Show operator/remark
on admin order status timeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-29 16:48:44 +08:00
parent 05dffbfa15
commit 1a0afb6d39
14 changed files with 505 additions and 32 deletions
@@ -0,0 +1,173 @@
import Taro from '@tarojs/taro';
import { request, toast } from './api';
/** 微信确认收货组件来源 AppId(官方固定) */
export const WECHAT_ORDER_CONFIRM_APPID = 'wx1183b055aeec94d1';
const PENDING_KEY = 'pending_wechat_order_confirm';
export type WechatConfirmPayload = {
merchantId?: string | null;
merchantTradeNo?: string | null;
transactionId?: string | null;
};
type PendingConfirm = {
orderId: string;
redirectUrl?: string;
};
type OpenBusinessViewFn = (opts: {
businessType: string;
extraData: Record<string, string>;
success?: () => void;
fail?: (err: { errMsg?: string }) => void;
}) => void;
function getOpenBusinessView(): OpenBusinessViewFn | null {
if (process.env.TARO_ENV !== 'weapp') return null;
const taroAny = Taro as unknown as { openBusinessView?: OpenBusinessViewFn };
if (typeof taroAny.openBusinessView === 'function') return taroAny.openBusinessView.bind(Taro);
const wxAny = (globalThis as { wx?: { openBusinessView?: OpenBusinessViewFn } }).wx;
if (wxAny && typeof wxAny.openBusinessView === 'function') {
return wxAny.openBusinessView.bind(wxAny);
}
return null;
}
export function savePendingWechatOrderConfirm(pending: PendingConfirm) {
Taro.setStorageSync(PENDING_KEY, JSON.stringify(pending));
}
export function takePendingWechatOrderConfirm(): PendingConfirm | null {
try {
const raw = Taro.getStorageSync(PENDING_KEY);
if (!raw) return null;
Taro.removeStorageSync(PENDING_KEY);
return typeof raw === 'string' ? (JSON.parse(raw) as PendingConfirm) : (raw as PendingConfirm);
} catch {
Taro.removeStorageSync(PENDING_KEY);
return null;
}
}
/**
* 拉起微信「确认收货」半屏组件,资金侧确认与自家订单同步。
* @see https://developers.weixin.qq.com/miniprogram/dev/platform-capabilities/business-capabilities/order-shipping/order-shipping-half.html
*/
export function openWechatOrderConfirm(opts: {
orderId: string;
payload: WechatConfirmPayload;
redirectUrl?: string;
}): Promise<'opened' | 'unsupported' | 'missing_pay_ref'> {
const open = getOpenBusinessView();
if (!open) return Promise.resolve('unsupported');
const transactionId = opts.payload.transactionId?.trim();
const merchantId = opts.payload.merchantId?.trim();
const merchantTradeNo = opts.payload.merchantTradeNo?.trim();
if (!transactionId && !(merchantId && merchantTradeNo)) {
return Promise.resolve('missing_pay_ref');
}
const extraData: Record<string, string> = {};
if (transactionId) extraData.transaction_id = transactionId;
if (merchantId) extraData.merchant_id = merchantId;
if (merchantTradeNo) extraData.merchant_trade_no = merchantTradeNo;
savePendingWechatOrderConfirm({
orderId: opts.orderId,
redirectUrl: opts.redirectUrl,
});
return new Promise((resolve) => {
open({
businessType: 'weappOrderConfirm',
extraData,
success: () => resolve('opened'),
fail: (err) => {
Taro.removeStorageSync(PENDING_KEY);
toast(err?.errMsg || '无法打开微信确认收货,请升级微信后重试');
resolve('unsupported');
},
});
});
}
type ReferrerExtra = {
status?: string;
errormsg?: string;
req_extradata?: Record<string, string>;
};
/**
* 处理确认收货组件回跳(App/页面 onShow)。
* 成功则调用后端同步订单,并返回是否已处理。
*/
export async function handleWechatOrderConfirmShow(options?: {
referrerInfo?: { appId?: string; extraData?: ReferrerExtra };
}): Promise<{ handled: boolean; orderId?: string; redirectUrl?: string }> {
const info = options?.referrerInfo;
if (!info?.appId || info.appId !== WECHAT_ORDER_CONFIRM_APPID) {
return { handled: false };
}
const pending = takePendingWechatOrderConfirm();
const status = info.extraData?.status;
if (status === 'cancel') {
toast('已取消确认收货');
return { handled: true, orderId: pending?.orderId };
}
if (status === 'fail') {
toast(info.extraData?.errormsg || '微信确认收货失败');
return { handled: true, orderId: pending?.orderId };
}
if (status !== 'success' || !pending?.orderId) {
return { handled: true };
}
try {
await request(`/trade/orders/${pending.orderId}/confirm-receive`, {
method: 'POST',
data: { source: 'WECHAT_COMPONENT' },
});
toast('确认收货成功', 'success');
return {
handled: true,
orderId: pending.orderId,
redirectUrl: pending.redirectUrl,
};
} catch (e) {
toast(e instanceof Error ? e.message : '同步订单失败');
return { handled: true, orderId: pending.orderId };
}
}
/** 统一入口:小程序走微信组件;H5/无能力时降级为本地确认 */
export async function confirmOrderReceive(opts: {
orderId: string;
wechatConfirm?: WechatConfirmPayload | null;
onSitePickup?: boolean;
redirectUrl?: string;
/** 降级本地确认成功后的回调(不经过微信回跳) */
onLocalSuccess?: () => void | Promise<void>;
}): Promise<'wechat' | 'local'> {
const mode = await openWechatOrderConfirm({
orderId: opts.orderId,
payload: opts.wechatConfirm || {},
redirectUrl: opts.redirectUrl,
});
if (mode === 'opened') return 'wechat';
// Mock / H5 / 缺支付单号:本地确认(不通知微信资金侧)
await request(`/trade/orders/${opts.orderId}/confirm-receive`, {
method: 'POST',
data: {
onSitePickup: !!opts.onSitePickup,
source: 'USER',
},
});
await opts.onLocalSuccess?.();
return 'local';
}