fix(proxy-order): remove mock pay button, use real WeChat pay

This commit is contained in:
2026-08-02 13:28:09 +08:00
parent ab3b230d01
commit 4ba51c502b
3 changed files with 183 additions and 81 deletions
@@ -63,7 +63,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
const [created, setCreated] = useState<ProxyOrderCreateResponse | null>(null);
const [codeUrl, setCodeUrl] = useState<string | null>(null);
const [payLoading, setPayLoading] = useState(false);
const [mockConfirming, setMockConfirming] = useState(false);
const pollRef = useRef<number | null>(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<PayStatus>(`/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' ? (
<Space>
<Button onClick={handleClose}></Button>
<Button type="primary" loading={mockConfirming} onClick={() => void handleMockConfirm()}>
</Button>
</Space>
) : (
<Space>
@@ -285,7 +261,7 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
) : codeUrl ? (
<Space direction="vertical" size={12} align="center">
<QRCode value={codeUrl} size={200} />
<Typography.Text type="secondary">使</Typography.Text>
<Typography.Text type="secondary">使</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-all' }}>
{codeUrl}
</Typography.Text>
@@ -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<TrackResult | null>(null);
const [trackError, setTrackError] = useState('');
const [paying, setPaying] = useState(false);
const [payMsg, setPayMsg] = useState('');
const [payMethod, setPayMethod] = useState<ProxyPayMethod>('NATIVE');
const [codeUrl, setCodeUrl] = useState<string | null>(null);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
const pollRef = useRef<number | null>(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<PartnerProxyOrderListItem>('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`, {
if (method === 'JSAPI') {
if (!isWechatEnv()) {
throw new Error('请在微信内打开合伙人端以使用微信代付');
}
const profile = await fetchPartnerProfile();
if (!partnerHasWechatBinding(profile)) {
throw new Error('请先绑定微信后再代付');
}
}
const pay = await request<ProxyOrderPayResponse>(
'PARTNER_H5',
`/partner/proxy-orders/${id}/pay`,
{
method: 'POST',
body: '{}',
body: JSON.stringify({ payMethod: method }),
silent: true,
});
},
);
if (pay.mode === 'mock') {
toastSuccess('支付成功');
const refreshed = await request<PartnerProxyOrderListItem>(
'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() {
</p>
</section>
{order.payStatus === 'UNPAID' || order.status === 'PENDING_PAY' ? (
{unpaid ? (
<section className="partner-form-card" style={{ marginTop: 12 }}>
<h3 className="headline-md" style={{ marginBottom: 12 }}></h3>
<div className="partner-proxy-mode-tabs">
<button
type="button"
className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
onClick={() => void startPay('NATIVE')}
>
</button>
<button
type="button"
className={`partner-proxy-mode-tab${payMethod === 'JSAPI' ? ' is-active' : ''}`}
onClick={() => void startPay('JSAPI')}
>
</button>
</div>
{payMethod === 'NATIVE' ? (
<div style={{ textAlign: 'center', marginTop: 16 }}>
{qrDataUrl ? (
<img src={qrDataUrl} alt="收款码" width={220} height={220} style={{ margin: '0 auto' }} />
) : (
<p className="label-md text-muted">{paying ? '生成收款码中…' : '暂无收款码'}</p>
)}
<p className="label-md text-muted" style={{ marginTop: 8 }}>
使
</p>
<button
type="button"
className="btn btn-block"
style={{ marginTop: 12 }}
disabled={paying}
onClick={() => void startPay('NATIVE')}
>
{paying ? '生成中…' : '重新生成收款码'}
</button>
</div>
) : (
<div style={{ marginTop: 16 }}>
<p className="label-md text-muted"></p>
<button
type="button"
className="btn btn-primary btn-block"
style={{ marginTop: 16 }}
style={{ marginTop: 12 }}
disabled={paying}
onClick={() => void continuePay()}
onClick={() => void startPay('JSAPI')}
>
{paying ? '处理中…' : '继续支付(模拟)'}
{paying ? '支付中…' : '调起微信代付'}
</button>
</div>
)}
{payMsg ? (
<p className="partner-form-error" role="alert" style={{ marginTop: 12 }}>
{payMsg}
</p>
) : null}
</section>
) : null}
{!isOnSite && order.payStatus === 'PAID' ? (
+5 -33
View File
@@ -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() {
<p className="label-md text-muted">{paying ? '生成收款码中…' : '暂无收款码'}</p>
)}
<p className="label-md text-muted" style={{ marginTop: 8 }}>
使
使
</p>
<button
type="button"
className="btn btn-primary btn-block"
className="btn btn-block"
style={{ marginTop: 16 }}
disabled={paying}
onClick={() => void handleMockConfirm()}
onClick={() => void startPay(created.id, 'NATIVE')}
>
{paying ? '处理中…' : '模拟支付成功'}
{paying ? '生成中…' : '重新生成收款码'}
</button>
</div>
) : (
<div style={{ marginTop: 16 }}>
<p className="label-md text-muted"> Mock </p>
<p className="label-md text-muted"></p>
<button
type="button"
className="btn btn-primary btn-block"
@@ -406,15 +387,6 @@ export default function ProxyOrderPage() {
>
{paying ? '支付中…' : '重新调起微信代付'}
</button>
<button
type="button"
className="btn btn-block"
style={{ marginTop: 8 }}
disabled={paying}
onClick={() => void handleMockConfirm()}
>
</button>
</div>
)}