2cd4e25682
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>
300 lines
8.7 KiB
TypeScript
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('当前环境无法打开微信确认收货组件,请用微信最新版打开小程序后重试');
|
|
}
|