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:
@@ -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';
|
||||
}
|
||||
Reference in New Issue
Block a user