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; 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 = []; 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 { 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 = {}; 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; }; /** * 处理确认收货组件回跳(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; }): 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; }): 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('当前环境无法打开微信确认收货组件,请用微信最新版打开小程序后重试'); }