diff --git a/apps/admin-web/src/components/ProxyOrderModal.tsx b/apps/admin-web/src/components/ProxyOrderModal.tsx index 55ef4df..66f5ccb 100644 --- a/apps/admin-web/src/components/ProxyOrderModal.tsx +++ b/apps/admin-web/src/components/ProxyOrderModal.tsx @@ -63,7 +63,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder const [created, setCreated] = useState(null); const [codeUrl, setCodeUrl] = useState(null); const [payLoading, setPayLoading] = useState(false); - const [mockConfirming, setMockConfirming] = useState(false); const pollRef = useRef(null); const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); @@ -215,26 +214,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder } } - async function handleMockConfirm() { - if (!created) return; - setMockConfirming(true); - try { - const st = await request(`/admin/proxy-orders/${created.id}/pay/mock-confirm`, { - method: 'POST', - body: '{}', - }); - stopPoll(); - message.success(`支付成功:${st.orderNo}`); - const payload = { id: st.id, orderNo: st.orderNo }; - resetForm(); - onSuccess(payload); - } catch (e) { - message.error(e instanceof Error ? e.message : '模拟支付失败'); - } finally { - setMockConfirming(false); - } - } - const deliveryLabel = preview?.deliveryType === 'ON_SITE_PICKUP' ? '现场提货' @@ -253,9 +232,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder step === 'pay' ? ( - ) : ( @@ -285,7 +261,7 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder ) : codeUrl ? ( - 请使用微信扫一扫完成支付(本地可用下方模拟支付) + 请使用微信扫一扫完成支付,成功后自动关闭 {codeUrl} diff --git a/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx b/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx index 83ab15b..49af50c 100644 --- a/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx +++ b/apps/h5-partner/src/pages/ProxyOrderDetailPage.tsx @@ -1,14 +1,18 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; +import QRCode from 'qrcode'; import PageHeader from '@dukang/shared-ui/PageHeader'; -import type { PartnerProxyOrderListItem } from '@dukang/shared-types'; -import { request } from '../lib/api'; +import { invokeWechatPay } from '@dukang/weixin-sdk'; +import type { PartnerProxyOrderListItem, ProxyOrderPayResponse, ProxyPayMethod } from '@dukang/shared-types'; +import { getToken, request } from '../lib/api'; import { proxyOrderPayLabel, proxyOrderStatusColor, proxyOrderStatusLabel, } from '../lib/proxyOrderStatus'; import { toastError, toastSuccess } from '../lib/toast'; +import { isWechatEnv } from '../lib/weixin'; +import { fetchPartnerProfile, partnerHasWechatBinding } from '../lib/wechat-auth'; function fmtMoney(n: number) { return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); @@ -51,6 +55,20 @@ export default function ProxyOrderDetailPage() { const [track, setTrack] = useState(null); const [trackError, setTrackError] = useState(''); const [paying, setPaying] = useState(false); + const [payMsg, setPayMsg] = useState(''); + const [payMethod, setPayMethod] = useState('NATIVE'); + const [codeUrl, setCodeUrl] = useState(null); + const [qrDataUrl, setQrDataUrl] = useState(null); + const pollRef = useRef(null); + + function stopPoll() { + if (pollRef.current != null) { + window.clearInterval(pollRef.current); + pollRef.current = null; + } + } + + useEffect(() => () => stopPoll(), []); useEffect(() => { document.title = '代下单详情'; @@ -75,28 +93,113 @@ export default function ProxyOrderDetailPage() { .catch((e) => setTrackError(e instanceof Error ? e.message : '物流暂不可用')); }, [id, order]); - async function continuePay() { + useEffect(() => { + if (!codeUrl) { + setQrDataUrl(null); + return; + } + void QRCode.toDataURL(codeUrl, { width: 220, margin: 1 }) + .then(setQrDataUrl) + .catch(() => setQrDataUrl(null)); + }, [codeUrl]); + + const unpaid = order?.payStatus === 'UNPAID' || order?.status === 'PENDING_PAY'; + + function startPoll(orderId: string) { + stopPoll(); + pollRef.current = window.setInterval(() => { + void request<{ payStatus: string; orderNo: string }>( + 'PARTNER_H5', + `/partner/proxy-orders/${orderId}/pay-status`, + { silent: true }, + ) + .then((st) => { + if (st.payStatus === 'PAID') { + stopPoll(); + toastSuccess(`支付成功:${st.orderNo}`); + void request('PARTNER_H5', `/partner/proxy-orders/${orderId}`) + .then(setOrder) + .catch(() => undefined); + setCodeUrl(null); + } + }) + .catch(() => undefined); + }, 2000); + } + + async function startPay(method: ProxyPayMethod) { if (!id) return; setPaying(true); + setPayMsg(''); + setPayMethod(method); try { - await request('PARTNER_H5', `/partner/proxy-orders/${id}/pay/mock-confirm`, { - method: 'POST', - body: '{}', - silent: true, - }); - toastSuccess('支付成功'); - const refreshed = await request( + if (method === 'JSAPI') { + if (!isWechatEnv()) { + throw new Error('请在微信内打开合伙人端以使用微信代付'); + } + const profile = await fetchPartnerProfile(); + if (!partnerHasWechatBinding(profile)) { + throw new Error('请先绑定微信后再代付'); + } + } + + const pay = await request( 'PARTNER_H5', - `/partner/proxy-orders/${id}`, + `/partner/proxy-orders/${id}/pay`, + { + method: 'POST', + body: JSON.stringify({ payMethod: method }), + silent: true, + }, ); - setOrder(refreshed); + + if (pay.mode === 'mock') { + toastSuccess('支付成功'); + const refreshed = await request( + 'PARTNER_H5', + `/partner/proxy-orders/${id}`, + ); + setOrder(refreshed); + setCodeUrl(null); + return; + } + + if (pay.mode === 'native' && pay.codeUrl) { + setCodeUrl(pay.codeUrl); + startPoll(id); + return; + } + + if (pay.mode === 'jsapi' && pay.prepay) { + setCodeUrl(null); + await invokeWechatPay(pay.prepay, { + apiBase: '/api/v1', + clientApp: 'PARTNER_H5', + getAccessToken: getToken, + platform: 'wechat-h5', + }); + startPoll(id); + return; + } + + throw new Error('支付发起失败'); } catch (e) { + setPayMsg(e instanceof Error ? e.message : '支付失败'); toastError(e instanceof Error ? e.message : '支付失败'); } finally { setPaying(false); } } + const autoPayStartedRef = useRef(false); + useEffect(() => { + if (!id || !unpaid || autoPayStartedRef.current) return; + autoPayStartedRef.current = true; + void startPay('NATIVE'); + // 仅首次进入未支付详情时自动拉收款码 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [id, unpaid]); + const img = order?.imageResource?.url || ''; const isOnSite = String(order?.deliveryType || '').toUpperCase() === 'ON_SITE_PICKUP'; @@ -175,16 +278,67 @@ export default function ProxyOrderDetailPage() {

- {order.payStatus === 'UNPAID' || order.status === 'PENDING_PAY' ? ( - + {unpaid ? ( +
+

继续支付

+
+ + +
+ + {payMethod === 'NATIVE' ? ( +
+ {qrDataUrl ? ( + 收款码 + ) : ( +

{paying ? '生成收款码中…' : '暂无收款码'}

+ )} +

+ 请使用微信扫码支付,支付成功后自动更新 +

+ +
+ ) : ( +
+

将调起微信支付,由合伙人微信完成代付

+ +
+ )} + + {payMsg ? ( +

+ {payMsg} +

+ ) : null} +
) : null} {!isOnSite && order.payStatus === 'PAID' ? ( diff --git a/apps/h5-partner/src/pages/ProxyOrderPage.tsx b/apps/h5-partner/src/pages/ProxyOrderPage.tsx index 5a68563..9616fee 100644 --- a/apps/h5-partner/src/pages/ProxyOrderPage.tsx +++ b/apps/h5-partner/src/pages/ProxyOrderPage.tsx @@ -305,25 +305,6 @@ export default function ProxyOrderPage() { } } - async function handleMockConfirm() { - if (!created) return; - setPaying(true); - try { - await request('PARTNER_H5', `/partner/proxy-orders/${created.id}/pay/mock-confirm`, { - method: 'POST', - body: '{}', - silent: true, - }); - stopPoll(); - toastSuccess('支付成功'); - navigate(`/center/proxy-orders/${created.id}`, { replace: true }); - } catch (e) { - setMsg(e instanceof Error ? e.message : '模拟支付失败'); - } finally { - setPaying(false); - } - } - const selectedProduct = options?.products.find((p) => p.id === productId); const deliveryLabel = preview?.deliveryType === 'ON_SITE_PICKUP' @@ -382,21 +363,21 @@ export default function ProxyOrderPage() {

{paying ? '生成收款码中…' : '暂无收款码'}

)}

- 请使用微信扫码支付 + 请使用微信扫码支付,支付成功后自动跳转

) : (
-

将调起微信支付(本地 Mock 可能直接入账)

+

将调起微信支付,由合伙人微信完成代付

-
)}