- 提取通用 calcPayMoney/calcMinTopup 函数,消除微信和支付宝控制器中的重复计费逻辑 - 合并 RechargeAlipay/RechargeWechat 为 rechargeByQRCodePayment 内部函数,删除 model/topup_alipay.go - 复用已有的 wrapAsPEM 替换支付宝专用的 wrapAlipayPublicKey - 删除被 QRCodePayModal 替代的 WechatPayQRCodeModal.jsx - 修复 controller/topup.go 中支付宝代码块的缩进错误 净减 264 行代码。 Co-Authored-By: Claude <noreply@anthropic.com>master
| @@ -72,25 +72,25 @@ func GetTopUpInfo(c *gin.Context) { | |||||
| payMethods = append(payMethods, wechatMethod) | payMethods = append(payMethods, wechatMethod) | ||||
| } | } | ||||
| } | } | ||||
| // 如果启用了支付宝支付,添加到支付方法列表 | |||||
| if setting.IsAlipayConfigured() { | |||||
| hasAlipay := false | |||||
| for _, method := range payMethods { | |||||
| if method["type"] == PaymentMethodAlipay { | |||||
| hasAlipay = true | |||||
| break | |||||
| } | |||||
| // 如果启用了支付宝支付,添加到支付方法列表 | |||||
| if setting.IsAlipayConfigured() { | |||||
| hasAlipay := false | |||||
| for _, method := range payMethods { | |||||
| if method["type"] == PaymentMethodAlipay { | |||||
| hasAlipay = true | |||||
| break | |||||
| } | } | ||||
| if !hasAlipay { | |||||
| alipayMethod := map[string]string{ | |||||
| "name": "Alipay", | |||||
| "type": PaymentMethodAlipay, | |||||
| "color": "rgba(var(--semi-blue-5), 1)", | |||||
| "min_topup": strconv.Itoa(setting.AlipayMinTopUp), | |||||
| } | |||||
| payMethods = append(payMethods, alipayMethod) | |||||
| } | |||||
| if !hasAlipay { | |||||
| alipayMethod := map[string]string{ | |||||
| "name": "Alipay", | |||||
| "type": PaymentMethodAlipay, | |||||
| "color": "rgba(var(--semi-blue-5), 1)", | |||||
| "min_topup": strconv.Itoa(setting.AlipayMinTopUp), | |||||
| } | } | ||||
| payMethods = append(payMethods, alipayMethod) | |||||
| } | } | ||||
| } | |||||
| data := gin.H{ | data := gin.H{ | ||||
| "enable_online_topup": enableOnlineTopup, | "enable_online_topup": enableOnlineTopup, | ||||
| @@ -164,15 +164,37 @@ func getPayMoney(amount int64, group string) float64 { | |||||
| } | } | ||||
| func getMinTopup() int64 { | func getMinTopup() int64 { | ||||
| minTopup := operation_setting.MinTopUp | |||||
| return calcMinTopup(operation_setting.MinTopUp) | |||||
| } | |||||
| // calcMinTopup 计算最低充值数量(考虑 QuotaDisplayType 换算) | |||||
| func calcMinTopup(baseMinTopup int) int64 { | |||||
| minTopup := baseMinTopup | |||||
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | ||||
| dMinTopup := decimal.NewFromInt(int64(minTopup)) | |||||
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) | |||||
| minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart()) | |||||
| minTopup = minTopup * int(common.QuotaPerUnit) | |||||
| } | } | ||||
| return int64(minTopup) | return int64(minTopup) | ||||
| } | } | ||||
| // calcPayMoney 计算应付金额(元),使用指定的单价和最低充值 | |||||
| func calcPayMoney(amount float64, group string, unitPrice float64) float64 { | |||||
| originalAmount := amount | |||||
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | |||||
| amount = amount / common.QuotaPerUnit | |||||
| } | |||||
| topupGroupRatio := common.GetTopupGroupRatio(group) | |||||
| if topupGroupRatio == 0 { | |||||
| topupGroupRatio = 1 | |||||
| } | |||||
| discount := 1.0 | |||||
| if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok { | |||||
| if ds > 0 { | |||||
| discount = ds | |||||
| } | |||||
| } | |||||
| return amount * unitPrice * topupGroupRatio * discount | |||||
| } | |||||
| func RequestEpay(c *gin.Context) { | func RequestEpay(c *gin.Context) { | ||||
| var req EpayRequest | var req EpayRequest | ||||
| err := c.ShouldBindJSON(&req) | err := c.ShouldBindJSON(&req) | ||||
| @@ -3,19 +3,16 @@ package controller | |||||
| import ( | import ( | ||||
| "context" | "context" | ||||
| "encoding/base64" | "encoding/base64" | ||||
| "encoding/pem" | |||||
| "fmt" | "fmt" | ||||
| "log" | "log" | ||||
| "net/http" | "net/http" | ||||
| "strconv" | "strconv" | ||||
| "strings" | |||||
| "sync" | "sync" | ||||
| "time" | "time" | ||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/setting" | "github.com/QuantumNous/new-api/setting" | ||||
| "github.com/QuantumNous/new-api/setting/operation_setting" | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| "github.com/go-pay/gopay" | "github.com/go-pay/gopay" | ||||
| @@ -64,8 +61,11 @@ func getAlipayClient() (*alipay.Client, error) { | |||||
| SetNotifyUrl(setting.AlipayNotifyURL) | SetNotifyUrl(setting.AlipayNotifyURL) | ||||
| // 设置支付宝公钥(用于回调验签) | // 设置支付宝公钥(用于回调验签) | ||||
| pubKeyPEM := wrapAlipayPublicKey(setting.AlipayPublicKey) | |||||
| client.AutoVerifySign([]byte(pubKeyPEM)) | |||||
| pubKeyBytes, err := base64.StdEncoding.DecodeString(setting.AlipayPublicKey) | |||||
| if err != nil { | |||||
| pubKeyBytes = []byte(setting.AlipayPublicKey) | |||||
| } | |||||
| client.AutoVerifySign([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY"))) | |||||
| alipayClient = client | alipayClient = client | ||||
| return alipayClient, nil | return alipayClient, nil | ||||
| @@ -107,7 +107,7 @@ func RequestAlipayPayAmount(c *gin.Context) { | |||||
| return | return | ||||
| } | } | ||||
| minTopup := getAlipayMinTopup() | |||||
| minTopup := calcMinTopup(setting.AlipayMinTopUp) | |||||
| if req.Amount < minTopup { | if req.Amount < minTopup { | ||||
| c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) | c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) | ||||
| return | return | ||||
| @@ -120,7 +120,7 @@ func RequestAlipayPayAmount(c *gin.Context) { | |||||
| return | return | ||||
| } | } | ||||
| payMoney := getAlipayPayMoney(float64(req.Amount), group) | |||||
| payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice) | |||||
| if payMoney <= 0.01 { | if payMoney <= 0.01 { | ||||
| c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) | c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) | ||||
| return | return | ||||
| @@ -137,7 +137,7 @@ func RequestAlipayPay(c *gin.Context) { | |||||
| return | return | ||||
| } | } | ||||
| minTopup := getAlipayMinTopup() | |||||
| minTopup := calcMinTopup(setting.AlipayMinTopUp) | |||||
| if req.Amount < minTopup { | if req.Amount < minTopup { | ||||
| c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) | c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) | ||||
| return | return | ||||
| @@ -175,8 +175,8 @@ func RequestAlipayPay(c *gin.Context) { | |||||
| tradeNo := fmt.Sprintf("ali%d%s", time.Now().UnixMilli(), randstr.String(8)) | tradeNo := fmt.Sprintf("ali%d%s", time.Now().UnixMilli(), randstr.String(8)) | ||||
| payMoney := getAlipayPayMoney(float64(req.Amount), group) | |||||
| totalAmount := strconv.FormatFloat(payMoney, 'f', 2, 64) // 支付宝金额单位是元 | |||||
| payMoney := calcPayMoney(float64(req.Amount), group, setting.AlipayUnitPrice) | |||||
| totalAmount := strconv.FormatFloat(payMoney, 'f', 2, 64) | |||||
| qrCode, err := createAlipayPrecreateOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalAmount) | qrCode, err := createAlipayPrecreateOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalAmount) | ||||
| if err != nil { | if err != nil { | ||||
| @@ -222,7 +222,6 @@ func AlipayPayStatus(c *gin.Context) { | |||||
| return | return | ||||
| } | } | ||||
| // 验证订单属于当前用户 | |||||
| userId := c.GetInt("id") | userId := c.GetInt("id") | ||||
| if topUp.UserId != userId { | if topUp.UserId != userId { | ||||
| c.JSON(200, gin.H{"message": "error", "data": "订单不存在"}) | c.JSON(200, gin.H{"message": "error", "data": "订单不存在"}) | ||||
| @@ -261,7 +260,6 @@ func AlipayPayWebhook(c *gin.Context) { | |||||
| tradeStatus := notifyReq.Get("trade_status") | tradeStatus := notifyReq.Get("trade_status") | ||||
| if tradeStatus != "TRADE_SUCCESS" { | if tradeStatus != "TRADE_SUCCESS" { | ||||
| log.Printf("支付宝回调非成功状态: %s", tradeStatus) | |||||
| c.String(http.StatusOK, "success") | c.String(http.StatusOK, "success") | ||||
| return | return | ||||
| } | } | ||||
| @@ -280,54 +278,3 @@ func AlipayPayWebhook(c *gin.Context) { | |||||
| log.Printf("支付宝充值成功: %s", tradeNo) | log.Printf("支付宝充值成功: %s", tradeNo) | ||||
| c.String(http.StatusOK, "success") | c.String(http.StatusOK, "success") | ||||
| } | } | ||||
| // getAlipayPayMoney 计算支付宝应付金额(元) | |||||
| func getAlipayPayMoney(amount float64, group string) float64 { | |||||
| originalAmount := amount | |||||
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | |||||
| amount = amount / common.QuotaPerUnit | |||||
| } | |||||
| topupGroupRatio := common.GetTopupGroupRatio(group) | |||||
| if topupGroupRatio == 0 { | |||||
| topupGroupRatio = 1 | |||||
| } | |||||
| discount := 1.0 | |||||
| if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok { | |||||
| if ds > 0 { | |||||
| discount = ds | |||||
| } | |||||
| } | |||||
| payMoney := amount * setting.AlipayUnitPrice * topupGroupRatio * discount | |||||
| return payMoney | |||||
| } | |||||
| // getAlipayMinTopup 获取支付宝最低充值数量 | |||||
| func getAlipayMinTopup() int64 { | |||||
| minTopup := setting.AlipayMinTopUp | |||||
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | |||||
| minTopup = minTopup * int(common.QuotaPerUnit) | |||||
| } | |||||
| return int64(minTopup) | |||||
| } | |||||
| // wrapAlipayPublicKey 将支付宝公钥包装为 PEM 格式 | |||||
| // 输入可能是:原始 Base64 字符串 或 已有 PEM 格式 | |||||
| func wrapAlipayPublicKey(pubKey string) string { | |||||
| if strings.Contains(pubKey, "-----BEGIN") { | |||||
| return pubKey | |||||
| } | |||||
| // 去除空白字符 | |||||
| cleaned := strings.ReplaceAll(pubKey, "\n", "") | |||||
| cleaned = strings.ReplaceAll(cleaned, "\r", "") | |||||
| cleaned = strings.TrimSpace(cleaned) | |||||
| // Base64 解码为 DER 字节 | |||||
| derBytes, err := base64.StdEncoding.DecodeString(cleaned) | |||||
| if err != nil { | |||||
| // 如果解码失败,原样返回让上层报错 | |||||
| return pubKey | |||||
| } | |||||
| return string(pem.EncodeToMemory(&pem.Block{ | |||||
| Type: "PUBLIC KEY", | |||||
| Bytes: derBytes, | |||||
| })) | |||||
| } | |||||
| @@ -18,7 +18,6 @@ import ( | |||||
| "github.com/QuantumNous/new-api/common" | "github.com/QuantumNous/new-api/common" | ||||
| "github.com/QuantumNous/new-api/model" | "github.com/QuantumNous/new-api/model" | ||||
| "github.com/QuantumNous/new-api/setting" | "github.com/QuantumNous/new-api/setting" | ||||
| "github.com/QuantumNous/new-api/setting/operation_setting" | |||||
| "github.com/gin-gonic/gin" | "github.com/gin-gonic/gin" | ||||
| "github.com/go-pay/gopay" | "github.com/go-pay/gopay" | ||||
| @@ -355,31 +354,10 @@ func WechatPayWebhook(c *gin.Context) { | |||||
| c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) | c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) | ||||
| } | } | ||||
| // getWechatPayMoney 计算微信支付应付金额(元) | |||||
| func getWechatPayMoney(amount float64, group string) float64 { | func getWechatPayMoney(amount float64, group string) float64 { | ||||
| originalAmount := amount | |||||
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | |||||
| amount = amount / common.QuotaPerUnit | |||||
| } | |||||
| topupGroupRatio := common.GetTopupGroupRatio(group) | |||||
| if topupGroupRatio == 0 { | |||||
| topupGroupRatio = 1 | |||||
| } | |||||
| discount := 1.0 | |||||
| if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok { | |||||
| if ds > 0 { | |||||
| discount = ds | |||||
| } | |||||
| } | |||||
| payMoney := amount * setting.WechatPayUnitPrice * topupGroupRatio * discount | |||||
| return payMoney | |||||
| return calcPayMoney(amount, group, setting.WechatPayUnitPrice) | |||||
| } | } | ||||
| // getWechatMinTopup 获取微信支付最低充值数量 | |||||
| func getWechatMinTopup() int64 { | func getWechatMinTopup() int64 { | ||||
| minTopup := setting.WechatPayMinTopUp | |||||
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { | |||||
| minTopup = minTopup * int(common.QuotaPerUnit) | |||||
| } | |||||
| return int64(minTopup) | |||||
| return calcMinTopup(setting.WechatPayMinTopUp) | |||||
| } | } | ||||
| @@ -1,80 +0,0 @@ | |||||
| package model | |||||
| import ( | |||||
| "errors" | |||||
| "fmt" | |||||
| "github.com/QuantumNous/new-api/common" | |||||
| "github.com/QuantumNous/new-api/logger" | |||||
| "github.com/shopspring/decimal" | |||||
| "gorm.io/gorm" | |||||
| ) | |||||
| // RechargeAlipay 支付宝支付充值完成(由回调触发) | |||||
| // 与 RechargeWechat 类似,使用事务+行锁保证幂等 | |||||
| func RechargeAlipay(tradeNo string) error { | |||||
| if tradeNo == "" { | |||||
| return errors.New("未提供支付单号") | |||||
| } | |||||
| var quotaToAdd int64 | |||||
| var payMoney float64 | |||||
| var userId int | |||||
| refCol := "`trade_no`" | |||||
| if common.UsingPostgreSQL { | |||||
| refCol = `"trade_no"` | |||||
| } | |||||
| err := DB.Transaction(func(tx *gorm.DB) error { | |||||
| topUp := &TopUp{} | |||||
| if err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", tradeNo).First(topUp).Error; err != nil { | |||||
| return errors.New("充值订单不存在") | |||||
| } | |||||
| if topUp.Status == common.TopUpStatusSuccess { | |||||
| // 已处理,幂等返回 | |||||
| return nil | |||||
| } | |||||
| if topUp.Status != common.TopUpStatusPending { | |||||
| return errors.New("充值订单状态错误") | |||||
| } | |||||
| topUp.CompleteTime = common.GetTimestamp() | |||||
| topUp.Status = common.TopUpStatusSuccess | |||||
| if err := tx.Save(topUp).Error; err != nil { | |||||
| return err | |||||
| } | |||||
| // 支付宝充值额度计算: | |||||
| // topUp.Money = req.Amount * topUpGroupRatio * unitPrice * discount(经分组倍率和折扣调整后的实际支付金额) | |||||
| // 充值额度 = topUp.Money * QuotaPerUnit | |||||
| dMoney := decimal.NewFromFloat(topUp.Money) | |||||
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) | |||||
| quotaToAdd = dMoney.Mul(dQuotaPerUnit).IntPart() | |||||
| if quotaToAdd <= 0 { | |||||
| return errors.New("无效的充值额度") | |||||
| } | |||||
| if err := tx.Model(&User{}).Where("id = ?", topUp.UserId).Update("quota", gorm.Expr("quota + ?", quotaToAdd)).Error; err != nil { | |||||
| return err | |||||
| } | |||||
| userId = topUp.UserId | |||||
| payMoney = topUp.Money | |||||
| return nil | |||||
| }) | |||||
| if err != nil { | |||||
| return err | |||||
| } | |||||
| if quotaToAdd > 0 { | |||||
| RecordLog(userId, LogTypeTopup, fmt.Sprintf("使用支付宝充值成功,充值金额: %v,支付金额:%.2f", logger.FormatQuota(int(quotaToAdd)), payMoney)) | |||||
| } | |||||
| return nil | |||||
| } | |||||
| @@ -12,8 +12,18 @@ import ( | |||||
| ) | ) | ||||
| // RechargeWechat 微信支付充值完成(由回调触发) | // RechargeWechat 微信支付充值完成(由回调触发) | ||||
| // 与 Recharge/RechargeCreem 类似,使用事务+行锁保证幂等 | |||||
| func RechargeWechat(tradeNo string) error { | func RechargeWechat(tradeNo string) error { | ||||
| return rechargeByQRCodePayment(tradeNo, "微信支付") | |||||
| } | |||||
| // RechargeAlipay 支付宝充值完成(由回调触发) | |||||
| func RechargeAlipay(tradeNo string) error { | |||||
| return rechargeByQRCodePayment(tradeNo, "支付宝") | |||||
| } | |||||
| // rechargeByQRCodePayment 扫码支付充值完成(微信/支付宝通用) | |||||
| // 使用事务+行锁保证幂等 | |||||
| func rechargeByQRCodePayment(tradeNo string, paymentMethod string) error { | |||||
| if tradeNo == "" { | if tradeNo == "" { | ||||
| return errors.New("未提供支付单号") | return errors.New("未提供支付单号") | ||||
| } | } | ||||
| @@ -34,7 +44,6 @@ func RechargeWechat(tradeNo string) error { | |||||
| } | } | ||||
| if topUp.Status == common.TopUpStatusSuccess { | if topUp.Status == common.TopUpStatusSuccess { | ||||
| // 已处理,幂等返回 | |||||
| return nil | return nil | ||||
| } | } | ||||
| @@ -48,9 +57,6 @@ func RechargeWechat(tradeNo string) error { | |||||
| return err | return err | ||||
| } | } | ||||
| // 微信支付充值额度计算: | |||||
| // topUp.Money = req.Amount * topUpGroupRatio(经分组倍率调整后的数量) | |||||
| // 充值额度 = topUp.Money * QuotaPerUnit(与 Stripe 的 Recharge 逻辑一致) | |||||
| dMoney := decimal.NewFromFloat(topUp.Money) | dMoney := decimal.NewFromFloat(topUp.Money) | ||||
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) | dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) | ||||
| quotaToAdd = dMoney.Mul(dQuotaPerUnit).IntPart() | quotaToAdd = dMoney.Mul(dQuotaPerUnit).IntPart() | ||||
| @@ -73,7 +79,7 @@ func RechargeWechat(tradeNo string) error { | |||||
| } | } | ||||
| if quotaToAdd > 0 { | if quotaToAdd > 0 { | ||||
| RecordLog(userId, LogTypeTopup, fmt.Sprintf("使用微信支付充值成功,充值金额: %v,支付金额:%.2f", logger.FormatQuota(int(quotaToAdd)), payMoney)) | |||||
| RecordLog(userId, LogTypeTopup, fmt.Sprintf("使用%s充值成功,充值金额: %v,支付金额:%.2f", paymentMethod, logger.FormatQuota(int(quotaToAdd)), payMoney)) | |||||
| } | } | ||||
| return nil | return nil | ||||
| @@ -1,136 +0,0 @@ | |||||
| import React, { useEffect, useState, useRef, useCallback } from 'react'; | |||||
| import { Modal, Typography, Spin, Button } from '@douyinfe/semi-ui'; | |||||
| import { QRCodeSVG } from 'qrcode.react'; | |||||
| import { API } from '../../helpers'; | |||||
| import { useTranslation } from 'react-i18next'; | |||||
| const { Text } = Typography; | |||||
| const POLL_INTERVAL = 2000; | |||||
| export default function WechatPayQRCodeModal({ visible, qrCodeUrl, tradeNo, onClose, onSuccess }) { | |||||
| const { t } = useTranslation(); | |||||
| const [status, setStatus] = useState('pending'); | |||||
| const [countdown, setCountdown] = useState(300); | |||||
| const timerRef = useRef(null); | |||||
| const pollRef = useRef(null); | |||||
| const pollStatus = useCallback(async () => { | |||||
| if (!tradeNo) return; | |||||
| try { | |||||
| const res = await API.get(`/api/user/wechat/pay/status?trade_no=${tradeNo}`); | |||||
| const { data } = res.data; | |||||
| if (data?.status === 'success') { | |||||
| setStatus('success'); | |||||
| clearInterval(pollRef.current); | |||||
| clearInterval(timerRef.current); | |||||
| if (onSuccess) { | |||||
| setTimeout(onSuccess, 1000); | |||||
| } | |||||
| } else if (data?.status === 'expired') { | |||||
| setStatus('expired'); | |||||
| clearInterval(pollRef.current); | |||||
| clearInterval(timerRef.current); | |||||
| } | |||||
| } catch (e) { | |||||
| // 忽略轮询错误 | |||||
| } | |||||
| }, [tradeNo, onSuccess]); | |||||
| useEffect(() => { | |||||
| if (visible && tradeNo && status === 'pending') { | |||||
| pollRef.current = setInterval(pollStatus, POLL_INTERVAL); | |||||
| timerRef.current = setInterval(() => { | |||||
| setCountdown((prev) => { | |||||
| if (prev <= 1) { | |||||
| setStatus('expired'); | |||||
| clearInterval(pollRef.current); | |||||
| clearInterval(timerRef.current); | |||||
| return 0; | |||||
| } | |||||
| return prev - 1; | |||||
| }); | |||||
| }, 1000); | |||||
| } | |||||
| return () => { | |||||
| clearInterval(pollRef.current); | |||||
| clearInterval(timerRef.current); | |||||
| }; | |||||
| }, [visible, tradeNo, status, pollStatus]); | |||||
| useEffect(() => { | |||||
| if (visible) { | |||||
| setStatus('pending'); | |||||
| setCountdown(300); | |||||
| } | |||||
| }, [visible]); | |||||
| const formatCountdown = (seconds) => { | |||||
| const m = Math.floor(seconds / 60); | |||||
| const s = seconds % 60; | |||||
| return `${m}:${s.toString().padStart(2, '0')}`; | |||||
| }; | |||||
| const renderContent = () => { | |||||
| if (status === 'success') { | |||||
| return ( | |||||
| <div style={{ textAlign: 'center', padding: '20px 0' }}> | |||||
| <Text style={{ fontSize: 18, color: '#28a745' }}> | |||||
| {t('支付成功!')} | |||||
| </Text> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| if (status === 'expired') { | |||||
| return ( | |||||
| <div style={{ textAlign: 'center', padding: '20px 0' }}> | |||||
| <Text style={{ fontSize: 16, color: '#dc3545' }}> | |||||
| {t('二维码已过期,请重新发起支付')} | |||||
| </Text> | |||||
| <div style={{ marginTop: 16 }}> | |||||
| <Button onClick={onClose}>{t('关闭')}</Button> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| return ( | |||||
| <div style={{ textAlign: 'center' }}> | |||||
| <div style={{ display: 'inline-block', padding: 16, background: '#fff', borderRadius: 8 }}> | |||||
| <QRCodeSVG | |||||
| value={qrCodeUrl} | |||||
| size={256} | |||||
| level='M' | |||||
| includeMargin={false} | |||||
| /> | |||||
| </div> | |||||
| <div style={{ marginTop: 16 }}> | |||||
| <Text type="secondary"> | |||||
| {t('请使用微信扫描二维码完成支付')} | |||||
| </Text> | |||||
| </div> | |||||
| <div style={{ marginTop: 8 }}> | |||||
| <Spin size="small" /> | |||||
| <Text type="secondary" style={{ marginLeft: 8 }}> | |||||
| {t('等待支付中...')} ({formatCountdown(countdown)}) | |||||
| </Text> | |||||
| </div> | |||||
| </div> | |||||
| ); | |||||
| }; | |||||
| return ( | |||||
| <Modal | |||||
| title={t('微信支付')} | |||||
| visible={visible} | |||||
| onCancel={onClose} | |||||
| footer={null} | |||||
| width={400} | |||||
| centered | |||||
| > | |||||
| {renderContent()} | |||||
| </Modal> | |||||
| ); | |||||
| } | |||||
| @@ -38,7 +38,6 @@ import InvitationCard from './InvitationCard'; | |||||
| import TransferModal from './modals/TransferModal'; | import TransferModal from './modals/TransferModal'; | ||||
| import PaymentConfirmModal from './modals/PaymentConfirmModal'; | import PaymentConfirmModal from './modals/PaymentConfirmModal'; | ||||
| import TopupHistoryModal from './modals/TopupHistoryModal'; | import TopupHistoryModal from './modals/TopupHistoryModal'; | ||||
| import WechatPayQRCodeModal from './WechatPayQRCodeModal'; | |||||
| import QRCodePayModal from './QRCodePayModal'; | import QRCodePayModal from './QRCodePayModal'; | ||||
| const TopUp = () => { | const TopUp = () => { | ||||