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 [created, setCreated] = useState<ProxyOrderCreateResponse | null>(null);
const [codeUrl, setCodeUrl] = useState<string | null>(null); const [codeUrl, setCodeUrl] = useState<string | null>(null);
const [payLoading, setPayLoading] = useState(false); const [payLoading, setPayLoading] = useState(false);
const [mockConfirming, setMockConfirming] = useState(false);
const pollRef = useRef<number | null>(null); const pollRef = useRef<number | null>(null);
const region = useMemo(() => parseRegionCodes(regionCodes), [regionCodes]); 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 = const deliveryLabel =
preview?.deliveryType === 'ON_SITE_PICKUP' preview?.deliveryType === 'ON_SITE_PICKUP'
? '现场提货' ? '现场提货'
@@ -253,9 +232,6 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
step === 'pay' ? ( step === 'pay' ? (
<Space> <Space>
<Button onClick={handleClose}></Button> <Button onClick={handleClose}></Button>
<Button type="primary" loading={mockConfirming} onClick={() => void handleMockConfirm()}>
</Button>
</Space> </Space>
) : ( ) : (
<Space> <Space>
@@ -285,7 +261,7 @@ export default function ProxyOrderModal({ open, onClose, onSuccess }: ProxyOrder
) : codeUrl ? ( ) : codeUrl ? (
<Space direction="vertical" size={12} align="center"> <Space direction="vertical" size={12} align="center">
<QRCode value={codeUrl} size={200} /> <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' }}> <Typography.Text type="secondary" style={{ fontSize: 12, wordBreak: 'break-all' }}>
{codeUrl} {codeUrl}
</Typography.Text> </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 { useNavigate, useParams } from 'react-router-dom';
import QRCode from 'qrcode';
import PageHeader from '@dukang/shared-ui/PageHeader'; import PageHeader from '@dukang/shared-ui/PageHeader';
import type { PartnerProxyOrderListItem } from '@dukang/shared-types'; import { invokeWechatPay } from '@dukang/weixin-sdk';
import { request } from '../lib/api'; import type { PartnerProxyOrderListItem, ProxyOrderPayResponse, ProxyPayMethod } from '@dukang/shared-types';
import { getToken, request } from '../lib/api';
import { import {
proxyOrderPayLabel, proxyOrderPayLabel,
proxyOrderStatusColor, proxyOrderStatusColor,
proxyOrderStatusLabel, proxyOrderStatusLabel,
} from '../lib/proxyOrderStatus'; } from '../lib/proxyOrderStatus';
import { toastError, toastSuccess } from '../lib/toast'; import { toastError, toastSuccess } from '../lib/toast';
import { isWechatEnv } from '../lib/weixin';
import { fetchPartnerProfile, partnerHasWechatBinding } from '../lib/wechat-auth';
function fmtMoney(n: number) { function fmtMoney(n: number) {
return n.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); 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 [track, setTrack] = useState<TrackResult | null>(null);
const [trackError, setTrackError] = useState(''); const [trackError, setTrackError] = useState('');
const [paying, setPaying] = useState(false); 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(() => { useEffect(() => {
document.title = '代下单详情'; document.title = '代下单详情';
@@ -75,28 +93,113 @@ export default function ProxyOrderDetailPage() {
.catch((e) => setTrackError(e instanceof Error ? e.message : '物流暂不可用')); .catch((e) => setTrackError(e instanceof Error ? e.message : '物流暂不可用'));
}, [id, order]); }, [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; if (!id) return;
setPaying(true); setPaying(true);
setPayMsg('');
setPayMethod(method);
try { try {
await request('PARTNER_H5', `/partner/proxy-orders/${id}/pay/mock-confirm`, { if (method === 'JSAPI') {
method: 'POST', if (!isWechatEnv()) {
body: '{}', throw new Error('请在微信内打开合伙人端以使用微信代付');
silent: true, }
}); const profile = await fetchPartnerProfile();
toastSuccess('支付成功'); if (!partnerHasWechatBinding(profile)) {
const refreshed = await request<PartnerProxyOrderListItem>( throw new Error('请先绑定微信后再代付');
}
}
const pay = await request<ProxyOrderPayResponse>(
'PARTNER_H5', '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<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) { } catch (e) {
setPayMsg(e instanceof Error ? e.message : '支付失败');
toastError(e instanceof Error ? e.message : '支付失败'); toastError(e instanceof Error ? e.message : '支付失败');
} finally { } finally {
setPaying(false); 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 img = order?.imageResource?.url || '';
const isOnSite = String(order?.deliveryType || '').toUpperCase() === 'ON_SITE_PICKUP'; const isOnSite = String(order?.deliveryType || '').toUpperCase() === 'ON_SITE_PICKUP';
@@ -175,16 +278,67 @@ export default function ProxyOrderDetailPage() {
</p> </p>
</section> </section>
{order.payStatus === 'UNPAID' || order.status === 'PENDING_PAY' ? ( {unpaid ? (
<button <section className="partner-form-card" style={{ marginTop: 12 }}>
type="button" <h3 className="headline-md" style={{ marginBottom: 12 }}></h3>
className="btn btn-primary btn-block" <div className="partner-proxy-mode-tabs">
style={{ marginTop: 16 }} <button
disabled={paying} type="button"
onClick={() => void continuePay()} className={`partner-proxy-mode-tab${payMethod === 'NATIVE' ? ' is-active' : ''}`}
> onClick={() => void startPay('NATIVE')}
{paying ? '处理中…' : '继续支付(模拟)'} >
</button>
</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: 12 }}
disabled={paying}
onClick={() => void startPay('JSAPI')}
>
{paying ? '支付中…' : '调起微信代付'}
</button>
</div>
)}
{payMsg ? (
<p className="partner-form-error" role="alert" style={{ marginTop: 12 }}>
{payMsg}
</p>
) : null}
</section>
) : null} ) : null}
{!isOnSite && order.payStatus === 'PAID' ? ( {!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 selectedProduct = options?.products.find((p) => p.id === productId);
const deliveryLabel = const deliveryLabel =
preview?.deliveryType === 'ON_SITE_PICKUP' 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">{paying ? '生成收款码中…' : '暂无收款码'}</p>
)} )}
<p className="label-md text-muted" style={{ marginTop: 8 }}> <p className="label-md text-muted" style={{ marginTop: 8 }}>
使 使
</p> </p>
<button <button
type="button" type="button"
className="btn btn-primary btn-block" className="btn btn-block"
style={{ marginTop: 16 }} style={{ marginTop: 16 }}
disabled={paying} disabled={paying}
onClick={() => void handleMockConfirm()} onClick={() => void startPay(created.id, 'NATIVE')}
> >
{paying ? '处理中…' : '模拟支付成功'} {paying ? '生成中…' : '重新生成收款码'}
</button> </button>
</div> </div>
) : ( ) : (
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<p className="label-md text-muted"> Mock </p> <p className="label-md text-muted"></p>
<button <button
type="button" type="button"
className="btn btn-primary btn-block" className="btn btn-primary btn-block"
@@ -406,15 +387,6 @@ export default function ProxyOrderPage() {
> >
{paying ? '支付中…' : '重新调起微信代付'} {paying ? '支付中…' : '重新调起微信代付'}
</button> </button>
<button
type="button"
className="btn btn-block"
style={{ marginTop: 8 }}
disabled={paying}
onClick={() => void handleMockConfirm()}
>
</button>
</div> </div>
)} )}