diff --git a/src/pages/payment-detail/index.tsx b/src/pages/payment-detail/index.tsx index 22878ee..1eb46bd 100644 --- a/src/pages/payment-detail/index.tsx +++ b/src/pages/payment-detail/index.tsx @@ -2,15 +2,20 @@ import { useCallback, useEffect, useState } from 'react' import Taro, { useRouter } from '@tarojs/taro' import { View, Text, Image } from '@tarojs/components' import { Empty } from '@antmjs/vantui' -import { getPaymentDetailApi, PAY_METHOD_NAMES, PAY_STATUS_NAMES } from '@/services/payment' +import { + getPaymentDetailApi, + getPayStatusName, + PAY_METHOD_NAMES, + queryOnlinePaymentApi, +} from '@/services/payment' import { resolveFileUrl } from '@/utils/format' import type { PaymentDetail } from '@/services/payment' import './index.less' /** * 支付详情页 - * 支付信息 + 汇款凭证(可预览)+ 合并付款的账单(可下钻账单详情) - * 已拒绝时底部提供「重新发起付款」(账单已由后台释放,可重新合并提交) + * 线下凭证单:支付信息 + 汇款凭证(可预览)+ 合并付款的账单(可下钻账单详情),审核拒绝后可重新发起付款 + * 在线支付单:无凭证,待支付时可「刷新支付结果」主动同步网关结果(后台通知延迟/丢失时的兜底) */ export default function PaymentDetailPage() { const router = useRouter() @@ -18,16 +23,25 @@ export default function PaymentDetailPage() { const [detail, setDetail] = useState(null) const [loading, setLoading] = useState(false) + const [syncing, setSyncing] = useState(false) - useEffect(() => { + const loadDetail = useCallback(async () => { if (!id) return setLoading(true) - getPaymentDetailApi(id) - .then(res => setDetail(res.data)) - .catch(() => {}) - .finally(() => setLoading(false)) + try { + const res = await getPaymentDetailApi(id) + setDetail(res.data) + } catch { + // 错误已由 request 层 toast + } finally { + setLoading(false) + } }, [id]) + useEffect(() => { + loadDetail() + }, [loadDetail]) + /** 预览凭证图片 */ const previewVoucher = useCallback((current: string) => { const urls = (detail?.payment.voucher_urls ?? []).map(resolveFileUrl) @@ -39,13 +53,35 @@ export default function PaymentDetailPage() { Taro.navigateTo({ url: `/pages/bill-detail/index?id=${billId}` }) }, []) - /** 已拒绝 → 携带本组账单重新发起付款 */ + /** 已拒绝 / 支付失败 → 携带本组账单重新发起付款(账单已由后台释放) */ const handleRepay = useCallback(() => { if (!detail) return const ids = detail.bills.map(b => b.id).join(',') Taro.redirectTo({ url: `/pages/payment/index?ids=${ids}` }) }, [detail]) + /** 在线支付待支付 → 主动查询网关同步结果(已支付则后端立即结账),随后刷新详情 */ + const handleSync = useCallback(async () => { + if (!detail || syncing) return + setSyncing(true) + try { + const res = await queryOnlinePaymentApi(detail.payment.payment_no) + if (res.data.status === 1) { + Taro.showToast({ title: '支付成功', icon: 'success' }) + loadDetail() + } else if (res.data.status === 2) { + Taro.showToast({ title: '支付失败,账单已释放', icon: 'none' }) + loadDetail() + } else { + Taro.showToast({ title: '暂未查询到支付结果,请稍后再试', icon: 'none' }) + } + } catch { + // 错误已由 request 层 toast + } finally { + setSyncing(false) + } + }, [detail, syncing, loadDetail]) + if (loading && !detail) { return } @@ -55,6 +91,8 @@ export default function PaymentDetailPage() { const { payment, bills } = detail const vouchers = payment.voucher_urls.map(resolveFileUrl) + /** 在线支付单(旺铺网关):状态语义与线下凭证单不同,无凭证 */ + const isOnline = payment.pay_type === 2 return ( @@ -63,16 +101,23 @@ export default function PaymentDetailPage() { {payment.payment_no} - {PAY_STATUS_NAMES[payment.status]} + {getPayStatusName(payment)} ¥{payment.amount} - {payment.status === 0 && ( + {payment.status === 0 && !isOnline && ( 付款申请已提交,商家审核通过后账单将置为已支付 )} + {payment.status === 0 && isOnline && ( + + 账单已锁定,等待支付结果确认;如已完成支付,可点击下方「刷新支付结果」 + + )} {payment.status === 2 && ( - 审核未通过{payment.audit_remark ? `:${payment.audit_remark}` : ''},账单已释放,可重新发起付款 + {isOnline + ? '支付失败,账单已释放,可重新发起付款' + : `审核未通过${payment.audit_remark ? `:${payment.audit_remark}` : ''},账单已释放,可重新发起付款`} )} @@ -89,6 +134,18 @@ export default function PaymentDetailPage() { {payment.audited_at} )} + {isOnline && payment.paid_at && ( + + 支付时间 + {payment.paid_at} + + )} + {isOnline && payment.trade_no && ( + + 交易单号 + {payment.trade_no} + + )} {payment.remark && ( 付款备注 @@ -97,22 +154,24 @@ export default function PaymentDetailPage() { )} - {/* ===== 汇款凭证 ===== */} - - 汇款凭证({vouchers.length}) - - {vouchers.map((url, i) => ( - previewVoucher(url)} - /> - ))} + {/* ===== 汇款凭证(在线支付单无凭证) ===== */} + {!isOnline && ( + + 汇款凭证({vouchers.length}) + + {vouchers.map((url, i) => ( + previewVoucher(url)} + /> + ))} + + {vouchers.length === 0 && } - {vouchers.length === 0 && } - + )} {/* ===== 合并账单 ===== */} @@ -132,12 +191,21 @@ export default function PaymentDetailPage() { {bills.length === 0 && } - {/* ===== 已拒绝 → 重新付款 ===== */} + {/* ===== 已拒绝 / 支付失败 → 重新付款 ===== */} {payment.status === 2 && ( 重新发起付款 )} + + {/* ===== 在线支付待支付 → 主动同步支付结果 ===== */} + {isOnline && payment.status === 0 && ( + + + {syncing ? '查询中...' : '刷新支付结果'} + + + )} ) } diff --git a/src/pages/payment-records/index.tsx b/src/pages/payment-records/index.tsx index 54441a5..7dd6f88 100644 --- a/src/pages/payment-records/index.tsx +++ b/src/pages/payment-records/index.tsx @@ -3,7 +3,7 @@ import Taro, { useDidShow, useReachBottom } from '@tarojs/taro' import { View, Text, ScrollView } from '@tarojs/components' import { Empty } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' -import { getPaymentListApi, PAY_METHOD_NAMES, PAY_STATUS_NAMES } from '@/services/payment' +import { getPaymentListApi, getPayStatusName, PAY_METHOD_NAMES } from '@/services/payment' import type { Payment, PayStatus } from '@/services/payment' import './index.less' @@ -115,7 +115,7 @@ export default function PaymentRecordsPage() { {record.payment_no} - {PAY_STATUS_NAMES[record.status]} + {getPayStatusName(record)} @@ -128,7 +128,7 @@ export default function PaymentRecordsPage() { 合并 {record.bills_count ?? 0} 张账单 - {record.status === 2 && !!record.audit_remark && ( + {record.status === 2 && record.pay_type !== 2 && !!record.audit_remark && ( 拒绝原因:{record.audit_remark} )} diff --git a/src/pages/payment/index.tsx b/src/pages/payment/index.tsx index 4b23750..a034c9b 100644 --- a/src/pages/payment/index.tsx +++ b/src/pages/payment/index.tsx @@ -4,7 +4,7 @@ import { View, Text, Image, Textarea } from '@tarojs/components' import { Empty, Icon } from '@antmjs/vantui' import useAuthStore from '@/stores/auth/useAuthStore' import { getBillListApi } from '@/services/bill' -import { createPaymentApi, getPaymentConfigApi } from '@/services/payment' +import { createOnlinePaymentApi, createPaymentApi, getPaymentConfigApi, queryOnlinePaymentApi } from '@/services/payment' import { chooseAndUploadImages } from '@/utils/upload' import { resolveFileUrl } from '@/utils/format' import type { Bill } from '@/services/bill' @@ -17,8 +17,14 @@ const PAGE_SIZE = 20 /** 凭证最多上传张数 */ const MAX_VOUCHERS = 3 -/** 支付方式选项 */ +/** 在线支付(调起 wx.requestPayment)仅微信小程序支持 */ +const IS_WEAPP = process.env.TARO_ENV === 'weapp' + +/** 支付方式选项(在线支付仅小程序端展示,排在最前) */ const PAY_METHODS: Array<{ value: PayMethod; label: string; icon: string; desc: string }> = [ + ...(IS_WEAPP + ? [{ value: 4 as PayMethod, label: '微信在线支付', icon: 'wechat', desc: '小程序内直接付款,免上传凭证' }] + : []), { value: 1, label: '微信支付', icon: 'wechat', desc: '扫码完成转账' }, { value: 2, label: '支付宝', icon: 'alipay', desc: '扫码完成转账' }, { value: 3, label: '对公汇款', icon: 'credit-pay', desc: '银行转账至对公账户' }, @@ -26,7 +32,9 @@ const PAY_METHODS: Array<{ value: PayMethod; label: string; icon: string; desc: /** * 发起付款页(合并付款) - * 选择本店可付款账单(?payable=1)→ 选择支付方式(展示收款码 / 对公账户)→ 上传汇款凭证 → 提交,后台审核 + * 选择本店可付款账单(?payable=1)→ 选择支付方式 → 提交: + * - 在线支付(仅小程序):wx.login 取 code → 后端经旺铺网关下单 → 调起微信支付 → 主动查询同步结果(回调兜底) + * - 线下凭证:展示收款码 / 对公账户 → 上传汇款凭证 → 提交,后台审核 * 支持 ?ids=1,2 预选账单(账单详情页"去付款"跳转) */ export default function PaymentPage() { @@ -50,12 +58,15 @@ export default function PaymentPage() { const loadingRef = useRef(false) const [config, setConfig] = useState(null) - const [payMethod, setPayMethod] = useState(1) + const [payMethod, setPayMethod] = useState(IS_WEAPP ? 4 : 1) const [vouchers, setVouchers] = useState([]) const [remark, setRemark] = useState('') const [uploading, setUploading] = useState(false) const [submitting, setSubmitting] = useState(false) + /** 在线支付(旺铺网关 JSAPI):免凭证,调起微信支付 */ + const isOnline = payMethod === 4 + /** 拉取可付款账单(首次加载应用路由预选) */ const loadBills = useCallback( async (pageNum: number, reset: boolean) => { @@ -152,8 +163,8 @@ export default function PaymentPage() { Taro.setClipboardData({ data: config.bank_info }) }, [config]) - /** 提交付款申请 */ - const handleSubmit = useCallback(async () => { + /** 提交线下凭证付款申请(后台审核) */ + const handleVoucherSubmit = useCallback(async () => { if (submitting) return if (selectedIds.length === 0) { Taro.showToast({ title: '请选择要付款的账单', icon: 'none' }) @@ -183,8 +194,94 @@ export default function PaymentPage() { } }, [submitting, selectedIds, vouchers, payMethod, remark, loadBills]) + /** + * 在线支付:wx.login 取 code → 后端经旺铺网关下单 → 调起微信支付 → 主动查询同步结果 + * 无论支付成功/取消都跳转支付详情(待支付单可在详情页刷新同步结果) + */ + const handleOnlinePay = useCallback(async () => { + if (submitting) return + if (!IS_WEAPP) { + Taro.showToast({ title: '请在微信小程序中使用在线支付', icon: 'none' }) + return + } + if (selectedIds.length === 0) { + Taro.showToast({ title: '请选择要付款的账单', icon: 'none' }) + return + } + setSubmitting(true) + try { + // 1. 获取微信登录凭证(后端换付款人 openid) + const { code } = await Taro.login() + if (!code) { + Taro.showToast({ title: '微信登录失败,请稍后重试', icon: 'none' }) + return + } + // 2. 后端下单(创建支付单并锁定账单) + const res = await createOnlinePaymentApi({ + bill_ids: selectedIds, + code, + remark: remark.trim() || undefined, + }) + const { id, payment_no, pay_params } = res.data + // 3. 调起微信支付(pay_params 为网关透传的调起参数) + try { + await Taro.requestPayment({ + timeStamp: String(pay_params.timeStamp || ''), + nonceStr: String(pay_params.nonceStr || ''), + package: String(pay_params.package || ''), + signType: (pay_params.signType || 'RSA') as 'MD5' | 'HMAC-SHA256' | 'RSA', + paySign: String(pay_params.paySign || ''), + }) + } catch (e: any) { + // 用户取消或调起失败:账单仍锁定在支付单中,进详情页可刷新同步/稍后处理 + const errMsg = e?.errMsg || '' + Taro.showToast({ + title: errMsg.includes('cancel') ? '已取消支付' : '支付调起失败,请稍后重试', + icon: 'none', + }) + setTimeout(() => { + Taro.redirectTo({ url: `/pages/payment-detail/index?id=${id}` }) + }, 800) + return + } + // 4. 主动查询同步结果(网关后台通知延迟/丢失时的兜底结账) + 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) + } + }, [submitting, selectedIds, remark, loadBills]) + + /** 提交入口:按支付方式分发 */ + const handleSubmit = useCallback(() => { + if (isOnline) { + handleOnlinePay() + } else { + handleVoucherSubmit() + } + }, [isOnline, handleOnlinePay, handleVoucherSubmit]) + /** 当前支付方式的收款展示 */ const renderMethodContent = () => { + if (isOnline) { + return ( + + 确认支付后将调起微信支付,支付成功后账单自动结清 + + ) + } if (payMethod === 3) { return config?.bank_info ? ( @@ -284,37 +381,39 @@ export default function PaymentPage() { {renderMethodContent()} - {/* ========== 汇款凭证 ========== */} - - - 汇款凭证 - 转账截图或回单,最多 {MAX_VOUCHERS} 张 - - - {vouchers.map((v, i) => { - const url = resolveFileUrl(v.url) - return ( - - previewImage(vouchers.map(x => resolveFileUrl(x.url)), url)} - /> - handleRemoveVoucher(i)}> - + {/* ========== 汇款凭证(在线支付免凭证) ========== */} + {!isOnline && ( + + + 汇款凭证 + 转账截图或回单,最多 {MAX_VOUCHERS} 张 + + + {vouchers.map((v, i) => { + const url = resolveFileUrl(v.url) + return ( + + previewImage(vouchers.map(x => resolveFileUrl(x.url)), url)} + /> + handleRemoveVoucher(i)}> + + + ) + })} + {vouchers.length < MAX_VOUCHERS && ( + + + {uploading ? '上传中' : '上传凭证'} - ) - })} - {vouchers.length < MAX_VOUCHERS && ( - - - {uploading ? '上传中' : '上传凭证'} - - )} + )} + - + )} {/* ========== 备注 ========== */} @@ -323,7 +422,7 @@ export default function PaymentPage() { className='pay-remark' value={remark} maxlength={255} - placeholder='如:汇款人姓名、转账时间等' + placeholder={isOnline ? '可填写付款说明' : '如:汇款人姓名、转账时间等'} onInput={e => setRemark(e.detail.value)} /> @@ -339,7 +438,7 @@ export default function PaymentPage() { className={`pay-bar__btn ${selectedIds.length === 0 || submitting ? 'disabled' : ''}`} onClick={handleSubmit} > - {submitting ? '提交中...' : '提交付款'} + {submitting ? (isOnline ? '支付中...' : '提交中...') : isOnline ? '立即支付' : '提交付款'} )} diff --git a/src/services/payment.ts b/src/services/payment.ts index dc56e1d..f01127a 100644 --- a/src/services/payment.ts +++ b/src/services/payment.ts @@ -1,16 +1,20 @@ import { get, post } from '@/utils/request' import type { PaginatedData } from '@/types/api' -/** 支付方式:1 微信 / 2 支付宝 / 3 对公汇款 */ -export type PayMethod = 1 | 2 | 3 +/** 支付方式:1 微信 / 2 支付宝 / 3 对公汇款 / 4 旺铺支付(小程序在线支付) */ +export type PayMethod = 1 | 2 | 3 | 4 export const PAY_METHOD_NAMES: Record = { 1: '微信支付', 2: '支付宝', 3: '对公汇款', + 4: '微信在线支付', } -/** 支付状态:0 待审核 / 1 已通过 / 2 已拒绝 */ +/** 支付类型:1 线下凭证支付 / 2 在线支付(旧数据可能缺省,缺省按线下处理) */ +export type PayType = 1 | 2 + +/** 支付状态:0 待审核 / 1 已通过 / 2 已拒绝(线下凭证支付单语义) */ export type PayStatus = 0 | 1 | 2 export const PAY_STATUS_NAMES: Record = { @@ -19,6 +23,20 @@ export const PAY_STATUS_NAMES: Record = { 2: '已拒绝', } +/** 在线支付状态:0 待支付 / 1 支付成功 / 2 支付失败(与线下同字段,按 pay_type 区分语义) */ +export type OnlinePayStatus = 0 | 1 | 2 + +export const ONLINE_PAY_STATUS_NAMES: Record = { + 0: '待支付', + 1: '支付成功', + 2: '支付失败', +} + +/** 支付单状态展示名(在线支付单与线下凭证单同字段不同语义,按 pay_type 取名) */ +export function getPayStatusName(payment: { status: PayStatus; pay_type?: PayType }): string { + return payment.pay_type === 2 ? ONLINE_PAY_STATUS_NAMES[payment.status] : PAY_STATUS_NAMES[payment.status] +} + /** 支付配置(付款页展示,图片为解析后的预览地址,可能为空串) */ export interface PaymentConfig { wechat_qrcode: string @@ -35,17 +53,23 @@ export interface Payment { user_id: number /** 合并付款总金额 */ amount: string + /** 支付类型:1 线下凭证 / 2 在线支付(旺铺网关) */ + pay_type?: PayType pay_method: PayMethod - /** 凭证图片 ID 数组(模型 casts 为 array) */ + /** 凭证图片 ID 数组(模型 casts 为 array,在线支付单为空) */ voucher_ids: number[] status: PayStatus /** 提交备注 */ remark: string - /** 审核时间 */ + /** 审核时间(线下凭证) */ audited_at: string | null auditor_id: number | null - /** 审核备注(拒绝原因) */ + /** 审核备注(拒绝原因,线下凭证) */ audit_remark: string | null + /** 在线支付成功时间(在线支付单非空) */ + paid_at?: string | null + /** 网关交易号(在线支付单非空) */ + trade_no?: string | null created_at: string /** 列表返回:合并账单数 */ bills_count?: number @@ -98,3 +122,46 @@ export function createPaymentApi(data: { export function getPaymentDetailApi(id: number) { return get(`/mini/payment/${id}`) } + +/** 在线支付下单返回(pay_params 为旺铺网关透传的 wx.requestPayment 调起参数,以网关实际返回为准) */ +export interface OnlinePaymentCreateResult { + id: number + /** 支付单号(ZF 前缀,= 上送网关的商户订单号 mer_order_id),查询/对账用 */ + payment_no: string + /** 应付金额(= 所选账单总额合计,元) */ + amount: string + pay_params: { + timeStamp?: string + nonceStr?: string + package?: string + signType?: string + paySign?: string + [key: string]: any + } +} + +/** 在线支付结果查询返回 */ +export interface OnlinePaymentQueryResult { + payment_no: string + /** 0 待支付 / 1 支付成功(账单已置已支付)/ 2 支付失败(账单已释放) */ + status: OnlinePayStatus + status_name: string + paid_at: string | null + trade_no: string | null +} + +/** + * 发起在线支付(合并账单下单):POST /mini/payment/online + * code 为 wx.login() 返回的登录凭证(后端换付款人 openid) + */ +export function createOnlinePaymentApi(data: { bill_ids: number[]; code: string; remark?: string }) { + return post('/mini/payment/online', data) +} + +/** + * 主动查询在线支付结果(网关后台通知延迟/丢失时的兜底):GET /mini/payment/online/{payment_no}/query + * 网关返回已支付则立即结账(与后台通知同一幂等逻辑) + */ +export function queryOnlinePaymentApi(paymentNo: string) { + return get(`/mini/payment/online/${paymentNo}/query`) +} diff --git a/src/utils/request.ts b/src/utils/request.ts index 1093a8a..df6beb4 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -10,8 +10,8 @@ const LOGIN_PATH = '/pages/login/index' /** 默认请求超时(ms) */ const DEFAULT_TIMEOUT = 15000 /** 接口根地址(uploadFile 等原生请求同样使用) */ -export const BASE_URL = "http://localhost:8000/index.php" -// export const BASE_URL = "https://purchase.henanklkj.com/index.php" +// export const BASE_URL = "http://localhost:8000/index.php" +export const BASE_URL = "https://purchase.henanklkj.com/index.php" /** * HTTP 状态码 → 错误提示映射