From 5cde1bcc59bf195423c358e36897440ff0232c7b Mon Sep 17 00:00:00 2001 From: erio Date: Sun, 5 Apr 2026 22:17:56 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20code=20quality=20audit=20=E2=80=94=20cri?= =?UTF-8?q?tical=20bugs=20+=20convention=20compliance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Critical: - Refund idempotency keys now include UnixNano timestamp (alipay/wxpay) - PaymentView passes qr/pay_url params to QR code page - Fix dead code branch in order result handling Quality: - Currency symbol $ → ¥ in admin stats and refund dialog - StripePaymentView setTimeout cleanup on unmount - Webhook body size limited to 1MB (io.LimitReader) - SubscriptionPlan features type documented --- backend/internal/handler/payment_webhook_handler.go | 2 +- backend/internal/payment/provider/alipay.go | 3 ++- backend/internal/payment/provider/wxpay.go | 3 ++- .../src/components/admin/payment/AdminRefundDialog.vue | 8 ++++---- .../src/components/admin/payment/OrderStatsCards.vue | 6 +++--- frontend/src/types/payment.ts | 1 + frontend/src/views/user/PaymentView.vue | 9 ++++++--- frontend/src/views/user/StripePaymentView.vue | 9 +++++++-- 8 files changed, 26 insertions(+), 15 deletions(-) diff --git a/backend/internal/handler/payment_webhook_handler.go b/backend/internal/handler/payment_webhook_handler.go index 160699c753..66eae259ad 100644 --- a/backend/internal/handler/payment_webhook_handler.go +++ b/backend/internal/handler/payment_webhook_handler.go @@ -51,7 +51,7 @@ func (h *PaymentWebhookHandler) StripeWebhook(c *gin.Context) { // handleNotify is the shared logic for all provider webhook handlers. func (h *PaymentWebhookHandler) handleNotify(c *gin.Context, providerKey string) { - body, err := io.ReadAll(c.Request.Body) + body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) // 1MB limit if err != nil { log.Printf("[Payment Webhook] failed to read body for %s: %v", providerKey, err) c.String(http.StatusBadRequest, "failed to read body") diff --git a/backend/internal/payment/provider/alipay.go b/backend/internal/payment/provider/alipay.go index 910b27c1a9..55c119b232 100644 --- a/backend/internal/payment/provider/alipay.go +++ b/backend/internal/payment/provider/alipay.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/Wei-Shaw/sub2api/internal/payment" "github.com/smartwalle/alipay/v3" @@ -207,7 +208,7 @@ func (a *Alipay) Refund(ctx context.Context, req payment.RefundRequest) (*paymen OutTradeNo: req.OrderID, RefundAmount: req.Amount, RefundReason: req.Reason, - OutRequestNo: req.OrderID + "-refund", + OutRequestNo: fmt.Sprintf("%s-refund-%d", req.OrderID, time.Now().UnixNano()), }) if err != nil { return nil, fmt.Errorf("alipay TradeRefund: %w", err) diff --git a/backend/internal/payment/provider/wxpay.go b/backend/internal/payment/provider/wxpay.go index 4fdc70fc48..e1946b7c40 100644 --- a/backend/internal/payment/provider/wxpay.go +++ b/backend/internal/payment/provider/wxpay.go @@ -10,6 +10,7 @@ import ( "strconv" "strings" "sync" + "time" "github.com/Wei-Shaw/sub2api/internal/payment" "github.com/wechatpay-apiv3/wechatpay-go/core" @@ -272,7 +273,7 @@ func (w *Wxpay) Refund(ctx context.Context, req payment.RefundRequest) (*payment cur := "CNY" res, _, err := rs.Create(ctx, refunddomestic.CreateRequest{ OutTradeNo: core.String(req.OrderID), - OutRefundNo: core.String(fmt.Sprintf("refund-%s", req.OrderID)), + OutRefundNo: core.String(fmt.Sprintf("%s-refund-%d", req.OrderID, time.Now().UnixNano())), Reason: core.String(req.Reason), Amount: &refunddomestic.AmountReq{Refund: core.Int64(rf), Total: core.Int64(tf), Currency: &cur}, }) diff --git a/frontend/src/components/admin/payment/AdminRefundDialog.vue b/frontend/src/components/admin/payment/AdminRefundDialog.vue index 0f78375cad..286f81d7b6 100644 --- a/frontend/src/components/admin/payment/AdminRefundDialog.vue +++ b/frontend/src/components/admin/payment/AdminRefundDialog.vue @@ -14,11 +14,11 @@
{{ t('payment.orders.amount') }} - ${{ order?.pay_amount?.toFixed(2) }} + ¥{{ order?.pay_amount?.toFixed(2) }}
{{ t('payment.admin.alreadyRefunded') }} - ${{ order.refund_amount.toFixed(2) }} + ¥{{ order.refund_amount.toFixed(2) }}
@@ -26,7 +26,7 @@
- $ + ¥

- {{ t('payment.admin.maxRefundable') }}: ${{ maxRefundable.toFixed(2) }} + {{ t('payment.admin.maxRefundable') }}: ¥{{ maxRefundable.toFixed(2) }}

diff --git a/frontend/src/components/admin/payment/OrderStatsCards.vue b/frontend/src/components/admin/payment/OrderStatsCards.vue index 0f7ec956d9..52faf38263 100644 --- a/frontend/src/components/admin/payment/OrderStatsCards.vue +++ b/frontend/src/components/admin/payment/OrderStatsCards.vue @@ -8,7 +8,7 @@

{{ t('payment.admin.todayRevenue') }}

-

${{ formatMoney(stats.today_amount) }}

+

¥{{ formatMoney(stats.today_amount) }}

{{ stats.today_count }} {{ t('payment.admin.orders') }}

@@ -24,7 +24,7 @@

{{ t('payment.admin.totalRevenue') }}

-

${{ formatMoney(stats.total_amount) }}

+

¥{{ formatMoney(stats.total_amount) }}

{{ stats.total_count }} {{ t('payment.admin.orders') }}

@@ -53,7 +53,7 @@

{{ t('payment.admin.avgAmount') }}

-

${{ formatMoney(stats.avg_amount) }}

+

¥{{ formatMoney(stats.avg_amount) }}

diff --git a/frontend/src/types/payment.ts b/frontend/src/types/payment.ts index e2d63b2072..9d24ac2307 100644 --- a/frontend/src/types/payment.ts +++ b/frontend/src/types/payment.ts @@ -79,6 +79,7 @@ export interface SubscriptionPlan { original_price?: number validity_days: number validity_unit: string + /** Stored as JSON string in backend; API layer should parse before use */ features: string[] for_sale: boolean sort_order: number diff --git a/frontend/src/views/user/PaymentView.vue b/frontend/src/views/user/PaymentView.vue index 348e90b407..190971f0e8 100644 --- a/frontend/src/views/user/PaymentView.vue +++ b/frontend/src/views/user/PaymentView.vue @@ -214,12 +214,15 @@ async function createOrder(orderAmount: number, orderType: string, planId?: numb order_type: orderType, plan_id: planId, }) - if (selectedMethod.value === 'stripe' && result.client_secret) { + if (result.client_secret) { router.push({ path: '/payment/stripe', query: { order_id: String(result.order_id), client_secret: result.client_secret } }) - } else if (result.qr_code || result.pay_url) { - router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id) } }) + } else if (result.qr_code) { + router.push({ path: '/payment/qrcode', query: { order_id: String(result.order_id), qr: result.qr_code || '', pay_url: result.pay_url || '' } }) } else if (result.pay_url) { window.location.href = result.pay_url + } else { + errorMessage.value = t('payment.result.failed') + appStore.showError(errorMessage.value) } } catch (err: any) { errorMessage.value = err.response?.data?.detail || err.message || t('payment.result.failed') diff --git a/frontend/src/views/user/StripePaymentView.vue b/frontend/src/views/user/StripePaymentView.vue index c596bc870c..481cf547db 100644 --- a/frontend/src/views/user/StripePaymentView.vue +++ b/frontend/src/views/user/StripePaymentView.vue @@ -43,7 +43,7 @@