Files
dukang/apps/mini-user/src/lib/wechat-order-confirm.ts
T
jacy 2cd4e25682 feat(ops): add HQ admin proxy order and mini-user store session fixes
Align HQ orders page with partner dual-SMS offline proxy flow; improve mini-user stores session and WeChat confirm-receive handling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 09:18:19 +08:00

300 lines
8.7 KiB
TypeScript

import Taro from '@tarojs/taro';
import { request, toast } from './api';
import { fetchClientConfig } from './pay-wechat';
/** 微信确认收货组件来源 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 OpenBusinessViewOptions = {
businessType: string;
extraData: Record<string, string>;
success?: () => void;
fail?: (err: { errMsg?: string }) => void;
complete?: () => void;
};
type MiniWx = {
openBusinessView?: (opts: OpenBusinessViewOptions) => void;
};
/**
* 取小程序原生 wx.openBusinessView。
* 官方兼容写法:`if (wx.openBusinessView) { ... }`(不要用 canIUse 挡业务组件)。
* Taro 未封装该 API;模块作用域下可能读不到全局 wx,需多重回退。
*/
function getOpenBusinessView(): ((opts: OpenBusinessViewOptions) => void) | null {
if (process.env.TARO_ENV !== 'weapp') return null;
const candidates: Array<MiniWx | null | undefined> = [];
try {
// eslint-disable-next-line no-undef
if (typeof wx !== 'undefined') candidates.push(wx as MiniWx);
} catch {
/* ignore */
}
const g = globalThis as typeof globalThis & { wx?: MiniWx };
candidates.push(g.wx);
try {
// 跳出 bundler 模块作用域,读微信运行时全局
const fromRuntime = new Function(
'return typeof wx !== "undefined" ? wx : null',
)() as MiniWx | null;
candidates.push(fromRuntime);
} catch {
/* ignore */
}
for (const api of candidates) {
if (api && typeof api.openBusinessView === 'function') {
return api.openBusinessView.bind(api);
}
}
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;
}
}
function normalizePayload(payload?: WechatConfirmPayload | null): WechatConfirmPayload {
return {
merchantId: payload?.merchantId?.trim() || undefined,
merchantTradeNo: payload?.merchantTradeNo?.trim() || undefined,
transactionId: payload?.transactionId?.trim() || undefined,
};
}
async function resolveConfirmPayload(
orderId: string,
hint?: WechatConfirmPayload | null,
): Promise<WechatConfirmPayload> {
const fromHint = normalizePayload(hint);
if (fromHint.transactionId || (fromHint.merchantId && fromHint.merchantTradeNo)) {
return fromHint;
}
const order = await request<{
orderNo?: string;
payExternalNo?: string | null;
payment?: { externalNo?: string | null } | null;
wechatConfirm?: WechatConfirmPayload | null;
}>(`/trade/orders/${orderId}`);
const fromApi = normalizePayload(order.wechatConfirm);
if (fromApi.transactionId || (fromApi.merchantId && fromApi.merchantTradeNo)) {
return fromApi;
}
const transactionId =
order.payExternalNo?.trim() || order.payment?.externalNo?.trim() || undefined;
return normalizePayload({
transactionId,
merchantTradeNo: order.orderNo,
merchantId: fromApi.merchantId,
});
}
/**
* 拉起微信「确认收货」半屏组件。
* @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) {
console.warn('[wechat-order-confirm] openBusinessView unavailable', {
taroEnv: process.env.TARO_ENV,
});
return Promise.resolve('unsupported');
}
const payload = normalizePayload(opts.payload);
const transactionId = payload.transactionId;
const merchantId = payload.merchantId;
const merchantTradeNo = payload.merchantTradeNo;
if (!transactionId && !(merchantId && merchantTradeNo)) {
console.warn('[wechat-order-confirm] missing pay ref', payload);
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) => {
let settled = false;
const done = (mode: 'opened' | 'unsupported' | 'missing_pay_ref') => {
if (settled) return;
settled = true;
resolve(mode);
};
try {
open({
businessType: 'weappOrderConfirm',
extraData,
success: () => done('opened'),
fail: (err) => {
Taro.removeStorageSync(PENDING_KEY);
console.error('[wechat-order-confirm] openBusinessView fail', err, extraData);
toast(err?.errMsg || '无法打开微信确认收货,请稍后重试');
done('unsupported');
},
});
} catch (err) {
Taro.removeStorageSync(PENDING_KEY);
console.error('[wechat-order-confirm] openBusinessView throw', err);
toast('无法打开微信确认收货组件');
done('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 };
}
}
async function confirmLocally(opts: {
orderId: string;
onSitePickup?: boolean;
onLocalSuccess?: () => void | Promise<void>;
}): Promise<'local'> {
await request(`/trade/orders/${opts.orderId}/confirm-receive`, {
method: 'POST',
data: {
onSitePickup: !!opts.onSitePickup,
source: 'USER',
},
});
await opts.onLocalSuccess?.();
return 'local';
}
/**
* 统一入口:
* - 小程序 + 真实支付:必须拉起 weappOrderConfirm,禁止静默降级
* - Mock / H5:本地确认
*/
export async function confirmOrderReceive(opts: {
orderId: string;
wechatConfirm?: WechatConfirmPayload | null;
onSitePickup?: boolean;
redirectUrl?: string;
onLocalSuccess?: () => void | Promise<void>;
}): Promise<'wechat' | 'local'> {
const isWeapp = process.env.TARO_ENV === 'weapp';
if (!isWeapp) {
return confirmLocally(opts);
}
let mockPay = false;
try {
const cfg = await fetchClientConfig();
mockPay = !!cfg.mockPay;
} catch {
mockPay = false;
}
if (mockPay) {
return confirmLocally(opts);
}
const payload = await resolveConfirmPayload(opts.orderId, opts.wechatConfirm);
const mode = await openWechatOrderConfirm({
orderId: opts.orderId,
payload,
redirectUrl: opts.redirectUrl,
});
if (mode === 'opened') return 'wechat';
if (mode === 'missing_pay_ref') {
throw new Error('缺少微信支付单号,无法打开确认收货组件');
}
throw new Error('当前环境无法打开微信确认收货组件,请用微信最新版打开小程序后重试');
}