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
+54 -1
View File
@@ -1,13 +1,66 @@
import './lib/text-encoding-polyfill';
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useRef } from 'react';
import Taro, { useDidShow } from '@tarojs/taro';
import WechatShareBootstrap from './components/WechatShareBootstrap';
import { patchTaroH5Hooks } from './lib/patch-taro-h5-hooks';
import { handleWechatOrderConfirmShow } from './lib/wechat-order-confirm';
import './app.css';
// H5:在首屏 page hooks 执行前,把 Taro.useDidShow 等绑到与 createReactApp 同一份 runtime
patchTaroH5Hooks();
function App({ children }: PropsWithChildren) {
const handlingRef = useRef(false);
useDidShow((options?: {
referrerInfo?: {
appId?: string;
extraData?: { status?: string; errormsg?: string; req_extradata?: Record<string, string> };
};
}) => {
if (process.env.TARO_ENV !== 'weapp') return;
if (handlingRef.current) return;
const referrerInfo =
options?.referrerInfo ||
(typeof Taro.getEnterOptionsSync === 'function'
? (
Taro.getEnterOptionsSync() as {
referrerInfo?: {
appId?: string;
extraData?: {
status?: string;
errormsg?: string;
req_extradata?: Record<string, string>;
};
};
}
).referrerInfo
: undefined);
if (!referrerInfo?.appId) return;
handlingRef.current = true;
void handleWechatOrderConfirmShow({ referrerInfo })
.then((result) => {
if (result.redirectUrl) {
Taro.redirectTo({ url: result.redirectUrl }).catch(() => {
Taro.reLaunch({ url: result.redirectUrl! });
});
return;
}
if (!result.handled || !result.orderId) return;
const pages = Taro.getCurrentPages();
const cur = pages[pages.length - 1] as { route?: string } | undefined;
const route = cur?.route || '';
if (route.includes('pickup-receive')) {
Taro.redirectTo({ url: '/pages/orders/index?tab=done' }).catch(() => {});
}
})
.finally(() => {
handlingRef.current = false;
});
});
return (
<>
<WechatShareBootstrap />
@@ -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';
}
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { View, Text } from '@tarojs/components';
import Taro, { useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import Taro, { useDidShow, useRouter, useShareAppMessage, useShareTimeline } from '@tarojs/taro';
import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import ShareNavButton from '../../components/ShareNavButton';
@@ -9,6 +9,7 @@ import ContactCsButton from '../../components/ContactCsButton';
import { request, toast } from '../../lib/api';
import { buildPayUrl } from '../../lib/checkout-nav';
import { maskPhone } from '../../lib/phone';
import { confirmOrderReceive, type WechatConfirmPayload } from '../../lib/wechat-order-confirm';
import {
DEFAULT_SHARE_DESC,
DEFAULT_SHARE_TITLE,
@@ -43,6 +44,7 @@ type OrderDetail = {
isProxyOrder?: boolean;
proxyPartnerName?: string | null;
items?: OrderItem[];
wechatConfirm?: WechatConfirmPayload | null;
};
const STATUS_LABELS: Record<string, string> = {
@@ -82,6 +84,14 @@ export default function OrderDetailPage() {
.catch((e) => toast(e instanceof Error ? e.message : '加载失败'));
}, [orderId]);
useDidShow(() => {
if (!orderId) return;
// 从微信确认收货组件返回后刷新
request<OrderDetail>(`/trade/orders/${orderId}`)
.then(setOrder)
.catch(() => {});
});
const isReship = !!order?.originOrderId;
const isProxy = !!order && (order.isProxyOrder || order.orderType === 'PROXY');
const canPay = !!order && order.status === 'PENDING_PAY' && !isReship;
@@ -127,7 +137,9 @@ export default function OrderDetailPage() {
const { confirm } = await Taro.showModal({
title: '确认收货?',
content: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
content: isWeapp
? '将打开微信确认收货,完成后订单即完结,无需再点服务通知。'
: '请确认已收到商品。确认后订单将完成,好客权益可正常使用。',
confirmText: '确认收货',
cancelText: '再想想',
});
@@ -135,12 +147,19 @@ export default function OrderDetailPage() {
setConfirming(true);
try {
const updated = await request<OrderDetail>(`/trade/orders/${order.id}/confirm-receive`, {
method: 'POST',
data: {},
const mode = await confirmOrderReceive({
orderId: order.id,
wechatConfirm: order.wechatConfirm,
onLocalSuccess: async () => {
const updated = await request<OrderDetail>(`/trade/orders/${order.id}`);
setOrder(updated);
toast('已确认收货');
},
});
setOrder(updated);
toast('已确认收货');
if (mode === 'wechat') {
// 回跳后由 App.onShow / 本页 useDidShow 处理
return;
}
} catch (e) {
toast(e instanceof Error ? e.message : '确认收货失败');
} finally {
@@ -5,6 +5,9 @@ import PageShell from '../../components/PageShell';
import SubPageHeader from '../../components/SubPageHeader';
import { request, toast } from '../../lib/api';
import { getProductMainImage } from '../../lib/product-images';
import { confirmOrderReceive, type WechatConfirmPayload } from '../../lib/wechat-order-confirm';
const isWeapp = process.env.TARO_ENV === 'weapp';
type OrderDetail = {
id: string;
@@ -22,6 +25,7 @@ type OrderDetail = {
};
imageUrl?: string | null;
mainImageUrl?: string | null;
wechatConfirm?: WechatConfirmPayload | null;
};
export default function PickupReceivePage() {
@@ -45,17 +49,33 @@ export default function PickupReceivePage() {
});
async function confirmReceive() {
if (!orderId || submitting) return;
if (!orderId || !order || submitting) return;
if (isWeapp) {
const { confirm } = await Taro.showModal({
title: '确认收货?',
content: '将打开微信确认收货,完成后订单即完结,无需再点服务通知。',
confirmText: '确认收货',
cancelText: '再想想',
});
if (!confirm) return;
}
setSubmitting(true);
try {
await request(`/trade/orders/${orderId}/confirm-receive`, {
method: 'POST',
data: {},
const mode = await confirmOrderReceive({
orderId,
wechatConfirm: order.wechatConfirm,
onSitePickup: true,
redirectUrl: '/pages/orders/index?tab=done',
onLocalSuccess: async () => {
toast('确认收货成功', 'success');
setTimeout(() => {
Taro.redirectTo({ url: '/pages/orders/index?tab=done' });
}, 500);
},
});
toast('确认收货成功', 'success');
setTimeout(() => {
Taro.redirectTo({ url: '/pages/orders/index?tab=done' });
}, 500);
if (mode === 'wechat') return;
} catch (e) {
toast(e instanceof Error ? e.message : '确认失败');
} finally {