From 74aee003e7e79016711650947e50301bd0fc4724 Mon Sep 17 00:00:00 2001 From: liu <2302563948@qq.com> Date: Fri, 4 Sep 2026 21:54:10 +0800 Subject: [PATCH] =?UTF-8?q?H5=E6=94=AF=E4=BB=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.html | 1 + src/pages/payment/index.tsx | 181 ++++++++++++++++++++++++++++++++++-- src/services/payment.ts | 15 ++- 3 files changed, 187 insertions(+), 10 deletions(-) diff --git a/src/index.html b/src/index.html index 49264a4..31a1bc6 100644 --- a/src/index.html +++ b/src/index.html @@ -10,6 +10,7 @@ 订货采购 + diff --git a/src/pages/payment/index.tsx b/src/pages/payment/index.tsx index c2e4342..d8dc94c 100644 --- a/src/pages/payment/index.tsx +++ b/src/pages/payment/index.tsx @@ -20,10 +20,68 @@ const MAX_VOUCHERS = 3 /** 在线支付(调起 wx.requestPayment)仅微信小程序支持 */ const IS_WEAPP = process.env.TARO_ENV === 'weapp' -/** 支付方式选项(在线支付仅小程序端展示,排在最前) */ +/** H5 端处于微信内置浏览器时,可走公众号网页授权 + JSAPI 在线支付 */ +const IS_H5_WECHAT = + process.env.TARO_ENV === 'h5' && + typeof navigator !== 'undefined' && + /micromessenger/i.test(navigator.userAgent) + +/** 在线支付(旺铺网关 JSAPI)是否可用:小程序 / 微信内 H5 */ +const ONLINE_PAY_AVAILABLE = IS_WEAPP || IS_H5_WECHAT + +/** H5 公众号支付草稿存储 key(授权跳转前暂存账单选择,回跳后恢复) */ +const H5_PAY_DRAFT_KEY = 'h5_online_pay_draft' + +/** H5 公众号支付草稿(授权回跳页面重载,勾选状态经 sessionStorage 恢复) */ +interface H5PayDraft { + bill_ids: number[] + remark?: string +} + +/** + * 调起公众号 JSAPI 支付(WeixinJSBridge 未注入时等待 WeixinJSBridgeReady 事件) + * resolve: 'ok' 支付成功 / 'cancel' 用户取消 / 'fail' 调起失败 + */ +function invokeWechatJsapiPay(payParams: Record): Promise<'ok' | 'cancel' | 'fail'> { + return new Promise(resolve => { + const invoke = () => { + ;(window as any).WeixinJSBridge.invoke( + 'getBrandWCPayRequest', + { + appId: String(payParams.appId || ''), + timeStamp: String(payParams.timeStamp || ''), + nonceStr: String(payParams.nonceStr || ''), + package: String(payParams.package || ''), + signType: String(payParams.signType || 'RSA'), + paySign: String(payParams.paySign || ''), + }, + (res: any) => { + const msg: string = res?.err_msg || '' + if (msg === 'get_brand_wcpay_request:ok') resolve('ok') + else if (msg === 'get_brand_wcpay_request:cancel') resolve('cancel') + else resolve('fail') + }, + ) + } + if ((window as any).WeixinJSBridge) { + invoke() + } else { + document.addEventListener('WeixinJSBridgeReady', invoke, { once: true }) + } + }) +} + +/** 支付方式选项(在线支付仅小程序/微信内 H5 展示,排在最前) */ const PAY_METHODS: Array<{ value: PayMethod; label: string; icon: string; desc: string }> = [ - ...(IS_WEAPP - ? [{ value: 4 as PayMethod, label: '微信在线支付', icon: 'wechat', desc: '小程序内直接付款,免上传凭证' }] + ...(ONLINE_PAY_AVAILABLE + ? [ + { + value: 4 as PayMethod, + label: '微信在线支付', + icon: 'wechat', + desc: IS_WEAPP ? '小程序内直接付款,免上传凭证' : '微信内直接付款,免上传凭证', + }, + ] : []), { value: 1, label: '微信支付', icon: 'wechat', desc: '扫码完成转账' }, { value: 2, label: '支付宝', icon: 'alipay', desc: '扫码完成转账' }, @@ -45,7 +103,7 @@ export default function PaymentPage() { const loadingRef = useRef(false) const [config, setConfig] = useState(null) - const [payMethod, setPayMethod] = useState(IS_WEAPP ? 4 : 1) + const [payMethod, setPayMethod] = useState(ONLINE_PAY_AVAILABLE ? 4 : 1) const [vouchers, setVouchers] = useState([]) const [remark, setRemark] = useState('') const [uploading, setUploading] = useState(false) @@ -247,14 +305,123 @@ export default function PaymentPage() { } }, [submitting, selectedIds, remark, loadBills]) - /** 提交入口:按支付方式分发 */ + /** + * H5 公众号支付主流程(授权回跳后执行): + * 授权 code 下单(scene=mp,后端换付款人 openid)→ WeixinJSBridge 调起支付 → 主动查询同步结果 + * 与小程序端一致:无论成功/取消都跳转支付详情(待支付单可在详情页刷新同步结果) + */ + const runH5OnlinePay = useCallback( + async (billIds: number[], code: string, remarkText?: string) => { + setSubmitting(true) + try { + const res = await createOnlinePaymentApi({ + bill_ids: billIds, + code, + scene: 'mp', + remark: remarkText, + }) + const { id, payment_no, pay_params } = res.data + const result = await invokeWechatJsapiPay(pay_params) + if (result !== 'ok') { + // 用户取消或调起失败:账单仍锁定在支付单中,进详情页可刷新同步/稍后处理 + Taro.showToast({ + title: result === 'cancel' ? '已取消支付' : '支付调起失败,请稍后重试', + icon: 'none', + }) + setTimeout(() => { + Taro.redirectTo({ url: `/pages/payment-detail/index?id=${id}` }) + }, 800) + return + } + // 主动查询同步结果(网关后台通知延迟/丢失时的兜底结账) + let paid = false + try { + const q = await queryOnlinePaymentApi(payment_no) + paid = q.data.status === 1 + } catch { + // 查询失败不阻断,进详情页可手动刷新 + } + Taro.showToast({ title: paid ? '支付成功' : '支付结果确认中', icon: paid ? 'success' : 'none' }) + setTimeout(() => { + Taro.redirectTo({ url: `/pages/payment-detail/index?id=${id}` }) + }, 800) + } catch { + // 下单失败:账单可能已被其他端付款/锁定,刷新列表 + loadBills(1, true) + } finally { + setSubmitting(false) + } + }, + [loadBills], + ) + + /** H5 公众号支付:处理微信授权回跳(URL 携带 code 且本地存在支付草稿时自动继续支付) */ + const h5CallbackRef = useRef(false) + useEffect(() => { + if (!IS_H5_WECHAT || h5CallbackRef.current || !loggedIn) return + const code = new URLSearchParams(window.location.search).get('code') + if (!code) return + // 清理地址栏授权参数,避免刷新/分享带出已失效的 code + window.history.replaceState(null, '', window.location.pathname) + let draft: H5PayDraft | null = null + try { + draft = JSON.parse(window.sessionStorage.getItem(H5_PAY_DRAFT_KEY) || 'null') + window.sessionStorage.removeItem(H5_PAY_DRAFT_KEY) + } catch { + draft = null + } + if (!draft || !Array.isArray(draft.bill_ids) || draft.bill_ids.length === 0) return + h5CallbackRef.current = true + setSelectedIds(draft.bill_ids) + if (draft.remark) setRemark(draft.remark) + runH5OnlinePay(draft.bill_ids, code, draft.remark) + }, [loggedIn, runH5OnlinePay]) + + /** + * H5 公众号支付入口:暂存支付草稿 → 跳转微信网页授权(snsapi_base 静默授权) + * 授权后回跳本页携带 code,由上方 effect 恢复草稿并继续支付 + */ + const handleH5OnlinePay = useCallback(async () => { + if (submitting) return + if (selectedIds.length === 0) { + Taro.showToast({ title: '请选择要付款的账单', icon: 'none' }) + return + } + setSubmitting(true) + try { + // mp_appid 可能尚未加载完成,兜底重新拉取 + let appid = config?.mp_appid + if (!appid) { + const res = await getPaymentConfigApi() + setConfig(res.data) + appid = res.data.mp_appid + } + if (!appid) { + Taro.showToast({ title: '公众号支付暂未开通,请选择其他支付方式', icon: 'none' }) + setSubmitting(false) + return + } + const draft: H5PayDraft = { bill_ids: selectedIds, remark: remark.trim() || undefined } + window.sessionStorage.setItem(H5_PAY_DRAFT_KEY, JSON.stringify(draft)) + const redirectUri = encodeURIComponent(window.location.origin + window.location.pathname) + window.location.href = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${appid}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base#wechat_redirect` + } catch { + setSubmitting(false) + } + }, [submitting, selectedIds, remark, config]) + + /** 提交入口:按支付方式与端分发(小程序 wx.requestPayment / H5 公众号 JSAPI / 线下凭证) */ const handleSubmit = useCallback(() => { if (isOnline) { - handleOnlinePay() + if (IS_H5_WECHAT) { + handleH5OnlinePay() + } else { + handleOnlinePay() + } } else { handleVoucherSubmit() } - }, [isOnline, handleOnlinePay, handleVoucherSubmit]) + }, [isOnline, handleOnlinePay, handleH5OnlinePay, handleVoucherSubmit]) /** 当前支付方式的收款展示 */ const renderMethodContent = () => { diff --git a/src/services/payment.ts b/src/services/payment.ts index f01127a..f9c9357 100644 --- a/src/services/payment.ts +++ b/src/services/payment.ts @@ -42,6 +42,8 @@ export interface PaymentConfig { wechat_qrcode: string alipay_qrcode: string bank_info: string + /** 公众号 appid(H5 网页授权取 code 拼授权链接用,配置了公众号支付才返回) */ + mp_appid?: string } /** 支付记录(列表行与详情的 payment 字段一致) */ @@ -123,7 +125,7 @@ export function getPaymentDetailApi(id: number) { return get(`/mini/payment/${id}`) } -/** 在线支付下单返回(pay_params 为旺铺网关透传的 wx.requestPayment 调起参数,以网关实际返回为准) */ +/** 在线支付下单返回(pay_params 为旺铺网关透传的调起参数:小程序给 wx.requestPayment,H5 公众号给 getBrandWCPayRequest,以网关实际返回为准) */ export interface OnlinePaymentCreateResult { id: number /** 支付单号(ZF 前缀,= 上送网关的商户订单号 mer_order_id),查询/对账用 */ @@ -131,6 +133,7 @@ export interface OnlinePaymentCreateResult { /** 应付金额(= 所选账单总额合计,元) */ amount: string pay_params: { + appId?: string timeStamp?: string nonceStr?: string package?: string @@ -152,9 +155,15 @@ export interface OnlinePaymentQueryResult { /** * 发起在线支付(合并账单下单):POST /mini/payment/online - * code 为 wx.login() 返回的登录凭证(后端换付款人 openid) + * code:小程序传 wx.login() 登录凭证(不传 scene); + * H5 公众号传网页授权回调 code,需带 scene: 'mp'(后端按场景换付款人 openid) */ -export function createOnlinePaymentApi(data: { bill_ids: number[]; code: string; remark?: string }) { +export function createOnlinePaymentApi(data: { + bill_ids: number[] + code: string + scene?: 'mp' + remark?: string +}) { return post('/mini/payment/online', data) }