Skip to content

新增Jeepay支付接入 - #4098

Closed
jeequan wants to merge 26 commits into
QuantumNous:mainfrom
jeequan:main
Closed

新增Jeepay支付接入#4098
jeequan wants to merge 26 commits into
QuantumNous:mainfrom
jeequan:main

Conversation

@jeequan

@jeequan jeequan commented Apr 5, 2026

Copy link
Copy Markdown

背景

new-api 中新增 Jeepay 充值支付接入,整体复用现有 top_up 充值链路,以最少改动完成 Jeepay 支付能力集成。

本次改动

1. 支付下单能力

  • 新增 Jeepay 充值下单能力
  • 复用现有 top_up 支付链路,降低整体接入成本
  • 可使用jeepay开源版,也可以使用jeepay官方通道对接,用户自选使用

2. 异步通知处理

  • 新增 Jeepay 异步通知处理逻辑
  • 通知兼容 JSON / form / query 三种数据格式

3. 支付方式支持

  • 支持多支付方式:
    • QR_CASHIER - 聚合扫码
    • WEB_CASHIER - 收银台
    • WX_NATIVE - 微信扫码
    • ALI_QR - 支付宝扫码

4. 订单查询与超时控制

  • 新增 Jeepay 订单状态查询接口
  • 新增订单超时时间配置
  • 扫码界面订单超时会自动提示二维码过期等逻辑

5. 前后端相关调整

  • 前端新增扫码支付弹窗
  • 支持支付状态轮询、倒计时与过期态处理
  • 后台新增 Jeepay 设置页
  • 优化相关文案、链接与图标展示

已验证流程

  • 收银台支付流程
  • 聚合扫码支付流程
  • 微信扫码支付流程
  • 支付宝扫码支付流程
  • Jeepay 异步通知入账流程
  • 扫码支付成功后前端自动提示成功
  • 二维码超时后进入过期态
  • 页面根据后台配置超时时间倒计时显示

重点评审

请重点关注以下内容:

  1. 是否符合现有 top_up / 支付接入 的整体风格
  2. 下单参数、签名 处理是否合理
  3. 异步通知验签、订单状态更新与入账逻辑是否足够安全
  4. 前端轮询、倒计时、过期态处理是否存在边界问题
  5. 配置项设计是否简洁,是否仍存在冗余

影响范围

  • new-api Jeepay 充值支付接入相关后端逻辑
  • 支付通知处理逻辑
  • 订单状态查询与超时控制
  • 后台 Jeepay 配置页面
  • 前端收银台扫码支付相关交互

测试说明

已完成基础流程联调与关键支付场景验证,重点覆盖:

  • 下单
  • 扫码支付
  • 异步通知
  • 入账
  • 支付成功提示
  • 超时过期处理

Summary by CodeRabbit

  • New Features
    • Added Jeepay as a payment method for account top-ups with QR code support
    • Added Jeepay payment configuration settings for administrators
    • Integrated real-time payment status monitoring for Jeepay transactions

Calcium-Ion and others added 26 commits April 4, 2026 09:16
feat(payment): integrate Jeepay top-up flow
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request integrates Jeepay as a new payment gateway for top-up functionality. It adds Jeepay-specific controllers for payment initiation, status polling, and webhook notifications, introduces configuration settings and database models to support the integration, registers API routes, and extends the frontend with a QR code payment modal, settings form, and conditional rendering for Jeepay payment options.

Changes

Cohort / File(s) Summary
Backend Controllers
controller/topup.go, controller/topup_jeepay.go, controller/topup_jeepay_test.go
Extended GetTopUpInfo to conditionally include Jeepay as a payment method when configured. Implemented three new handlers: RequestJeepayPay for initiating unified orders, GetJeepayPayStatus for polling payment status with expiration checking, and JeepayNotify for processing webhook notifications with MD5 signature validation and transaction-based state updates. Added comprehensive test suite covering signature generation, notification handling, and invalid signature rejection.
Backend Configuration & Models
setting/payment_jeepay.go, model/option.go, model/topup.go
Introduced JeepayBaseURL, merchant credentials, routing codes, and numeric timeouts as exported settings. Extended option.go to recognize and persist Jeepay options. Added RechargeJeepay() function implementing transactional quota top-up with idempotency and logging.
Backend Routing
router/api-router.go
Registered three new endpoints: public webhook POST /api/jeepay/notify, authenticated user endpoints POST /self/jeepay/pay (with critical rate limiting) and GET /self/jeepay/status/:tradeNo.
Frontend Settings
web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx, web/src/components/settings/PaymentSetting.jsx
Created new SettingsPaymentGatewayJeepay component for admin Jeepay configuration form with concurrent option updates. Extended PaymentSetting to import/render the new gateway form and parse JeepayMinTopUp as numeric.
Frontend Top-up Components
web/src/components/topup/index.jsx, web/src/components/topup/RechargeCard.jsx
Updated top-up flow to call /api/user/jeepay/pay, detect QR-code way codes to open modal instead of redirect, and attach way_code to Jeepay pay methods. Extended RechargeCard to conditionally enable/render Jeepay alongside existing providers with custom SVG icon.
Frontend Payment Modals
web/src/components/topup/modals/JeepayQRCodeModal.jsx, web/src/components/topup/modals/PaymentConfirmModal.jsx, web/src/components/topup/modals/TopupHistoryModal.jsx
Added JeepayQRCodeModal component with countdown timer and 3-second polling loop to /api/user/jeepay/status endpoint, handling expiration and success transitions. Updated PaymentConfirmModal to render Jeepay icon for payment method display. Extended TopupHistoryModal's PAYMENT_METHOD_MAP to label Jeepay.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Browser as Frontend<br/>(React)
    participant Backend as Backend<br/>(Go API)
    participant Jeepay as Jeepay<br/>Payment Service
    participant Webhook as Webhook<br/>Receiver

    User->>Browser: Select Jeepay & enter amount
    Browser->>Backend: POST /api/user/jeepay/pay<br/>(payMethod, amount)
    Backend->>Backend: Validate config & amount<br/>Generate trade_no
    Backend->>Backend: Build & sign unified<br/>order request
    Backend->>Jeepay: POST unified order API<br/>(trade_no, amount, sign)
    Jeepay-->>Backend: Return payment_url, qr_code
    Backend->>Backend: Create TopUp record<br/>(status=Pending)
    Backend-->>Browser: Return qr_code_url,<br/>order_id, expiry
    Browser->>Browser: Open QR Modal<br/>Start countdown & polling
    Browser->>Backend: GET /api/user/jeepay/status<br/>/:trade_no (3s interval)
    Backend-->>Browser: Return status, expiry
    Note over Browser: Show QR code<br/>User scans & pays on Jeepay
    Jeepay->>Webhook: POST /api/jeepay/notify<br/>(trade_no, amount, sign)
    Webhook->>Backend: Handle notification
    Backend->>Backend: Verify MD5 signature
    Backend->>Backend: Row-lock TopUp,<br/>validate amount
    Backend->>Backend: Update TopUp status<br/>to Success
    Backend->>Backend: Increment user quota<br/>atomically
    Backend->>Backend: Log recharge<br/>completion
    Backend-->>Webhook: Return "success"
    Browser->>Backend: Poll status again
    Backend-->>Browser: status=success
    Browser->>Browser: Show success toast<br/>Close modal
    User->>Browser: See quota updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • seefs001

Poem

🐰 Hops with glee, a payment way so new!
Jeepay joins the crew, QR codes come through,
Signatures verified, quotas bloom fair,
Top-ups ascending through the digital air! ✨💳

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.94% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add Jeepay payment integration' directly matches the main purpose of this PR, which is comprehensive Jeepay payment support including order creation, async notifications, status querying, and frontend/backend UI integration.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@jeequan jeequan mentioned this pull request Apr 5, 2026
@seefs001

seefs001 commented Apr 5, 2026

Copy link
Copy Markdown
Collaborator

为什么要伪造提交人

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
web/src/components/topup/index.jsx (1)

512-533: ⚠️ Potential issue | 🟠 Major

Keep the minimum top-up bound to the selected gateway.

Line 515 still collapses the screen back to one minTopUpValue, and the shared state written at Lines 532-533 is what Line 199 and Line 222 enforce later. If generic online top-up and Jeepay are both enabled, Jeepay can be validated against data.min_topup instead of data.jeepay_min_topup, which means users either get a false client-side block or a late /api/user/jeepay/pay rejection depending on which limit is larger.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/index.jsx` around lines 512 - 533, The minTopUpValue
computation collapses multiple gateway limits into one value causing Jeepay to
be validated against the wrong minimum; instead compute and store per-gateway
minima and only call setMinTopUp/setTopUpCount when a specific gateway is
selected. Update the initialization to set dedicated states (e.g.,
setWaffoMinTopUp already exists; add or use setStripeMinTopUp,
setJeepayMinTopUp, setOnlineMinTopUp) from data.{stripe_min_topup,
jeepay_min_topup, min_topup, waffo_min_topup} and remove the chained ternary
that creates minTopUpValue; then change logic around setMinTopUp(setTopUpCount)
to use the selected gateway's min (based on the selected gateway state) rather
than the ambiguous minTopUpValue.
🧹 Nitpick comments (5)
web/src/components/settings/PaymentSetting.jsx (1)

53-60: Keep JeepayOrderTimeoutMinutes in the typed Jeepay state as well.

This file seeds and coerces JeepayMinTopUp, but the timeout field is missing from both places. That leaves the Jeepay options shape inconsistent, with one numeric field stored as a number and the other coming through as raw string/undefined.

💡 Proposed fix
     JeepayAPIKey: '',
     JeepayWayCode: 'WEB_CASHIER',
     JeepayNotifyURL: '',
     JeepayReturnURL: '',
     JeepayMinTopUp: 1,
+    JeepayOrderTimeoutMinutes: 5,
   });
@@
           case 'StripeUnitPrice':
           case 'StripeMinTopUp':
           case 'JeepayMinTopUp':
+          case 'JeepayOrderTimeoutMinutes':
             newInputs[item.key] = parseFloat(item.value);
             break;

Also applies to: 105-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/PaymentSetting.jsx` around lines 53 - 60, The
Jeepay settings are missing JeepayOrderTimeoutMinutes in the seeded/typed state
and in the coercion that normalizes numeric fields; add
JeepayOrderTimeoutMinutes alongside JeepayMinTopUp in the initial settings
object in PaymentSetting.jsx and update the coercion logic (the same place that
coerces JeepayMinTopUp) to normalize JeepayOrderTimeoutMinutes to a number with
a safe default (e.g., Number(...) or parseInt with fallback) so both fields
share the same numeric type and defaults.
router/api-router.go (1)

94-95: Bound the Jeepay status polling route.

Line 95 is the endpoint the QR modal polls, and it is currently uncapped. A broken client can hit it indefinitely, so a light read-oriented limiter or a short-lived status cache would protect this top-up query path without breaking normal polling.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@router/api-router.go` around lines 94 - 95, The GET polling endpoint
selfRoute.GET("/jeepay/status/:tradeNo") (handler controller.GetJeepayPayStatus)
is currently uncapped; add a lightweight read-oriented rate limit or short-lived
cache to protect against infinite polling. Update the route to include a
read-safe limiter middleware (similar to middleware.CriticalRateLimit but with
higher allowance and lower penalty) or wrap controller.GetJeepayPayStatus with a
short TTL status cache lookup before hitting backend logic; ensure the new
middleware/cache is applied to the same selfRoute GET registration so normal
polling is allowed but abusive/faulty clients are throttled.
model/topup.go (1)

439-495: Extract the shared “complete pending amount top-up” flow.

Lines 439-495 are effectively a copy of RechargeWaffo with the provider name changed. The row lock, status transitions, quota math, and quota update path are now duplicated again, which makes the next bugfix in this settlement flow easy to land for one gateway and miss for another.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/topup.go` around lines 439 - 495, The RechargeJeepay function
duplicates the same "lock row, validate status, compute quota, mark success,
save, and increment user quota" flow used by RechargeWaffo; extract that shared
flow into a single helper (e.g., finalizePendingTopUp or completePendingTopUp)
that accepts a gorm.Tx and a tradeNo or TopUp pointer and returns the updated
TopUp and quotaToAdd (or an error), reusing existing symbols TopUp, User,
common.TopUpStatusPending/Success, and RecordLog; then replace the transaction
block in RechargeJeepay (and RechargeWaffo) with a call to this helper and keep
only provider-specific actions (like provider-specific logging message) in each
gateway function.
web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx (1)

37-45: Expose the callback override fields or remove them from the option model.

setting/payment_jeepay.go defines JeepayNotifyURL / JeepayReturnURL, and controller/topup_jeepay.go already consumes them when building the webhook and return URLs. This form never loads or saves those keys, so admins cannot fix callback routing from the new settings page when the derived public URL is wrong behind a proxy or subpath.

Also applies to: 50-58, 71-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx` around lines
37 - 45, The form currently omits the callback override keys so admins can’t set
JeepayNotifyURL/JeepayReturnURL; add these two keys to the component state
initialization (inputs: JeepayNotifyURL, JeepayReturnURL), load their values
from the settings API into inputs when fetching existing config, render bound
form fields for both (where other Jeepay inputs are rendered around lines
50-84), and include them in the save/update payload so the backend option names
JeepayNotifyURL and JeepayReturnURL (used by controller/topup_jeepay.go) are
persisted; alternatively remove these option keys from the server model if you
intend them to be truly derived.
controller/topup_jeepay_test.go (1)

54-115: Add form/query and replay webhook cases.

The new handler supports JSON, form, and query payloads, and its safety depends on duplicate success notifications staying idempotent. These tests only cover the JSON happy path and a bad signature, so the newly added parser branches and replay behavior can regress unnoticed.

Also applies to: 117-164

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_jeepay_test.go` around lines 54 - 115, Add tests covering
form and query payload parsing and replay/idempotency: create new tests (e.g.,
TestJeepayNotify_FormPayload, TestJeepayNotify_QueryPayload,
TestJeepayNotify_ReplayIdempotency) that reuse the setup in TestJeepayNotify and
call JeepayNotify with a form-encoded body (Content-Type:
application/x-www-form-urlencoded) and a GET/POST with parameters in the URL
query; build signatures with buildJeepaySign for each case and assert
http.StatusOK and "success" response. For replay behavior send the same
successful notification twice and assert the top-up record (GetTopUpByTradeNo /
topUp.Status) becomes Success after the first and the user's Quota (model.User)
does not increase after the second call. Ensure also to include a negative case
for bad signature on form/query similar to the JSON bad-sign test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@controller/topup_jeepay.go`:
- Around line 480-514: The code currently only accepts payment URLs that start
with "http", which rejects scheme-based QR payloads like "weixin://..." (causing
valid Jeepay WX_NATIVE responses to fail); update the checks in all three places
(the top-level loop over keys, the payData string branch that parses JSON, and
the payData map branch) to accept either HTTP URLs or any scheme-like payload by
replacing strings.HasPrefix(value, "http") with a predicate such as
strings.HasPrefix(value, "http") || strings.Contains(value, "://") (or an
equivalent helper) so codeUrl values like "weixin://..." are treated as valid.
- Around line 259-265: The current catch-all error branch after calling
createJeepayOrder marks the local topUp as failed, which makes later valid
webhooks irrecoverable; change the logic in the handler so that only definitive
Jeepay rejections set topUp.Status = common.TopUpStatusFailed and call
topUp.Update(), while transient/ambiguous errors (context.DeadlineExceeded,
net.Error timeouts/temporary, transport errors) should be logged but leave the
order as pending (do not modify topUp.Status or call Update). Implement this by
inspecting err returned from createJeepayOrder (e.g. check for
context.DeadlineExceeded, errors.As to net.Error and
net.Error.Timeout()/Temporary()) and only performing the failed transition for
explicit rejection errors returned by createJeepayOrder or a clearly typed
rejection value; keep existing logging and return the JSON error response
without changing DB state for transient cases so RechargeJeepay can still
process pending orders.

In `@controller/topup.go`:
- Around line 81-98: The UI is showing raw Jeepay config values instead of the
normalized/validated limits; replace uses of setting.JeepayMinTopUp and
setting.JeepayOrderTimeoutMinutes in controller/topup.go with the normalized
accessors getJeepayMinTopUp() and getJeepayOrderTimeoutMinutes() (the same
functions used in controller/topup_jeepay.go) wherever you build payMethods
entries or return Jeepay limits so the frontend receives the effective limits
the backend actually enforces.

In `@model/option.go`:
- Around line 400-403: The current code silently ignores strconv.Atoi errors
when setting setting.JeepayMinTopUp and setting.JeepayOrderTimeoutMinutes;
change the logic in the switch cases that handle "JeepayMinTopUp" and
"JeepayOrderTimeoutMinutes" to check the strconv.Atoi error and avoid
overwriting the existing value on failure (or return/log the parse error): call
strconv.Atoi, if err != nil then log/return the error and keep the prior setting
value; only assign to setting.JeepayMinTopUp or
setting.JeepayOrderTimeoutMinutes when err == nil.

In `@web/src/components/topup/modals/JeepayQRCodeModal.jsx`:
- Around line 101-110: When polling returns status === 'failed' you clear the
timer but don’t flip the terminal state, so the QR JSX (which checks isExpired)
continues to render; treat 'failed' the same as 'expired' by calling
markExpired() or otherwise setting isExpired to true when status === 'failed'
(in the same branch that clears pollTimerRef.current) and still call
showError(t(...)) as needed so the component will render the terminal state
instead of the scannable QR; update the status handling in the poll result code
(referencing pollTimerRef, markExpired, showError, isExpired, status)
accordingly.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx`:
- Around line 27-32: jeepayWayCodeOptions is defined outside the component with
hard-coded Chinese labels and must be moved inside the component so you can wrap
labels with t(), e.g. build the array inside the component and call t() for each
label; also replace the placeholder keys used in the form inputs (the places
currently calling t('Jeepay mchNo') and t('Jeepay appId')) with the Chinese
source strings used elsewhere in the project so they use consistent i18n
keys/phrasing (update the placeholder arguments to the Chinese text and keep
using t()); ensure you only change the array location and the two placeholder
t(...) calls and nothing else.

---

Outside diff comments:
In `@web/src/components/topup/index.jsx`:
- Around line 512-533: The minTopUpValue computation collapses multiple gateway
limits into one value causing Jeepay to be validated against the wrong minimum;
instead compute and store per-gateway minima and only call
setMinTopUp/setTopUpCount when a specific gateway is selected. Update the
initialization to set dedicated states (e.g., setWaffoMinTopUp already exists;
add or use setStripeMinTopUp, setJeepayMinTopUp, setOnlineMinTopUp) from
data.{stripe_min_topup, jeepay_min_topup, min_topup, waffo_min_topup} and remove
the chained ternary that creates minTopUpValue; then change logic around
setMinTopUp(setTopUpCount) to use the selected gateway's min (based on the
selected gateway state) rather than the ambiguous minTopUpValue.

---

Nitpick comments:
In `@controller/topup_jeepay_test.go`:
- Around line 54-115: Add tests covering form and query payload parsing and
replay/idempotency: create new tests (e.g., TestJeepayNotify_FormPayload,
TestJeepayNotify_QueryPayload, TestJeepayNotify_ReplayIdempotency) that reuse
the setup in TestJeepayNotify and call JeepayNotify with a form-encoded body
(Content-Type: application/x-www-form-urlencoded) and a GET/POST with parameters
in the URL query; build signatures with buildJeepaySign for each case and assert
http.StatusOK and "success" response. For replay behavior send the same
successful notification twice and assert the top-up record (GetTopUpByTradeNo /
topUp.Status) becomes Success after the first and the user's Quota (model.User)
does not increase after the second call. Ensure also to include a negative case
for bad signature on form/query similar to the JSON bad-sign test.

In `@model/topup.go`:
- Around line 439-495: The RechargeJeepay function duplicates the same "lock
row, validate status, compute quota, mark success, save, and increment user
quota" flow used by RechargeWaffo; extract that shared flow into a single helper
(e.g., finalizePendingTopUp or completePendingTopUp) that accepts a gorm.Tx and
a tradeNo or TopUp pointer and returns the updated TopUp and quotaToAdd (or an
error), reusing existing symbols TopUp, User, common.TopUpStatusPending/Success,
and RecordLog; then replace the transaction block in RechargeJeepay (and
RechargeWaffo) with a call to this helper and keep only provider-specific
actions (like provider-specific logging message) in each gateway function.

In `@router/api-router.go`:
- Around line 94-95: The GET polling endpoint
selfRoute.GET("/jeepay/status/:tradeNo") (handler controller.GetJeepayPayStatus)
is currently uncapped; add a lightweight read-oriented rate limit or short-lived
cache to protect against infinite polling. Update the route to include a
read-safe limiter middleware (similar to middleware.CriticalRateLimit but with
higher allowance and lower penalty) or wrap controller.GetJeepayPayStatus with a
short TTL status cache lookup before hitting backend logic; ensure the new
middleware/cache is applied to the same selfRoute GET registration so normal
polling is allowed but abusive/faulty clients are throttled.

In `@web/src/components/settings/PaymentSetting.jsx`:
- Around line 53-60: The Jeepay settings are missing JeepayOrderTimeoutMinutes
in the seeded/typed state and in the coercion that normalizes numeric fields;
add JeepayOrderTimeoutMinutes alongside JeepayMinTopUp in the initial settings
object in PaymentSetting.jsx and update the coercion logic (the same place that
coerces JeepayMinTopUp) to normalize JeepayOrderTimeoutMinutes to a number with
a safe default (e.g., Number(...) or parseInt with fallback) so both fields
share the same numeric type and defaults.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx`:
- Around line 37-45: The form currently omits the callback override keys so
admins can’t set JeepayNotifyURL/JeepayReturnURL; add these two keys to the
component state initialization (inputs: JeepayNotifyURL, JeepayReturnURL), load
their values from the settings API into inputs when fetching existing config,
render bound form fields for both (where other Jeepay inputs are rendered around
lines 50-84), and include them in the save/update payload so the backend option
names JeepayNotifyURL and JeepayReturnURL (used by controller/topup_jeepay.go)
are persisted; alternatively remove these option keys from the server model if
you intend them to be truly derived.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7db29931-dfe6-4c41-8187-b665543f22d2

📥 Commits

Reviewing files that changed from the base of the PR and between b195065 and 90fcc13.

⛔ Files ignored due to path filters (1)
  • web/src/assets/jeepay.svg is excluded by !**/*.svg
📒 Files selected for processing (14)
  • controller/topup.go
  • controller/topup_jeepay.go
  • controller/topup_jeepay_test.go
  • model/option.go
  • model/topup.go
  • router/api-router.go
  • setting/payment_jeepay.go
  • web/src/components/settings/PaymentSetting.jsx
  • web/src/components/topup/RechargeCard.jsx
  • web/src/components/topup/index.jsx
  • web/src/components/topup/modals/JeepayQRCodeModal.jsx
  • web/src/components/topup/modals/PaymentConfirmModal.jsx
  • web/src/components/topup/modals/TopupHistoryModal.jsx
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx

Comment on lines +259 to +265
paymentURL, err := createJeepayOrder(c.Request.Context(), &orderReq)
if err != nil {
log.Printf("Jeepay 下单失败 - 订单号: %s, wayCode: %s, amountFen: %d, expiredTime: %d, err: %v", tradeNo, orderReq.WayCode, orderReq.Amount, orderReq.ExpiredTime, err)
topUp.Status = common.TopUpStatusFailed
_ = topUp.Update()
c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("Jeepay下单返回:%s", err.Error())})
return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Don’t fail the local order on ambiguous upstream errors.

This catch-all branch handles timeouts and transport errors as well as explicit Jeepay rejections. For the ambiguous cases, moving the row to failed makes a later valid webhook unrecoverable because RechargeJeepay only credits pending orders.

⚠️ Safer direction
  paymentURL, err := createJeepayOrder(c.Request.Context(), &orderReq)
  if err != nil {
    log.Printf("Jeepay 下单失败 - 订单号: %s, wayCode: %s, amountFen: %d, expiredTime: %d, err: %v", tradeNo, orderReq.WayCode, orderReq.Amount, orderReq.ExpiredTime, err)
-   topUp.Status = common.TopUpStatusFailed
-   _ = topUp.Update()
+   // Keep the order pending unless you have an explicit terminal refusal from Jeepay.
+   // Otherwise a remotely-created order can still succeed via webhook, but local settlement
+   // becomes impossible once the status leaves `pending`.
    c.JSON(http.StatusOK, gin.H{"message": "error", "data": fmt.Sprintf("Jeepay下单返回:%s", err.Error())})
    return
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_jeepay.go` around lines 259 - 265, The current catch-all
error branch after calling createJeepayOrder marks the local topUp as failed,
which makes later valid webhooks irrecoverable; change the logic in the handler
so that only definitive Jeepay rejections set topUp.Status =
common.TopUpStatusFailed and call topUp.Update(), while transient/ambiguous
errors (context.DeadlineExceeded, net.Error timeouts/temporary, transport
errors) should be logged but leave the order as pending (do not modify
topUp.Status or call Update). Implement this by inspecting err returned from
createJeepayOrder (e.g. check for context.DeadlineExceeded, errors.As to
net.Error and net.Error.Timeout()/Temporary()) and only performing the failed
transition for explicit rejection errors returned by createJeepayOrder or a
clearly typed rejection value; keep existing logging and return the JSON error
response without changing DB state for transient cases so RechargeJeepay can
still process pending orders.

Comment on lines +480 to +514
for _, key := range []string{"payUrl", "payData", "codeUrl", "cashierUrl"} {
value := strings.TrimSpace(jeepayValueToString(data[key]))
if value != "" && strings.HasPrefix(value, "http") {
return value, nil
}
}

if payData, ok := data["payData"].(string); ok {
trimmed := strings.TrimSpace(payData)
if strings.HasPrefix(trimmed, "http") {
return trimmed, nil
}
if strings.HasPrefix(trimmed, "{") {
var nested map[string]interface{}
if err := common.Unmarshal([]byte(trimmed), &nested); err == nil {
for _, key := range []string{"payUrl", "cashierUrl", "codeUrl"} {
value := strings.TrimSpace(jeepayValueToString(nested[key]))
if value != "" && strings.HasPrefix(value, "http") {
return value, nil
}
}
}
}
}

if payData, ok := data["payData"].(map[string]interface{}); ok {
for _, key := range []string{"payUrl", "cashierUrl", "codeUrl"} {
value := strings.TrimSpace(jeepayValueToString(payData[key]))
if value != "" && strings.HasPrefix(value, "http") {
return value, nil
}
}
}

return "", fmt.Errorf("payment url not found")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Allow non-HTTP QR payloads.

codeUrl for WX_NATIVE is commonly a scheme string like weixin://..., not an HTTP URL. Requiring http here means a successful Jeepay response can still fail with payment url not found. The same relaxation is needed in the nested payData branches as well.

💡 Possible fix
- for _, key := range []string{"payUrl", "payData", "codeUrl", "cashierUrl"} {
-   value := strings.TrimSpace(jeepayValueToString(data[key]))
-   if value != "" && strings.HasPrefix(value, "http") {
-     return value, nil
-   }
- }
+ if value := strings.TrimSpace(jeepayValueToString(data["codeUrl"])); value != "" {
+   return value, nil
+ }
+ for _, key := range []string{"payUrl", "cashierUrl"} {
+   value := strings.TrimSpace(jeepayValueToString(data[key]))
+   if value != "" && strings.HasPrefix(value, "http") {
+     return value, nil
+   }
+ }

Apply the same rule to payData.codeUrl in the nested branches below.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_jeepay.go` around lines 480 - 514, The code currently only
accepts payment URLs that start with "http", which rejects scheme-based QR
payloads like "weixin://..." (causing valid Jeepay WX_NATIVE responses to fail);
update the checks in all three places (the top-level loop over keys, the payData
string branch that parses JSON, and the payData map branch) to accept either
HTTP URLs or any scheme-like payload by replacing strings.HasPrefix(value,
"http") with a predicate such as strings.HasPrefix(value, "http") ||
strings.Contains(value, "://") (or an equivalent helper) so codeUrl values like
"weixin://..." are treated as valid.

Comment thread controller/topup.go
Comment on lines +81 to +98
enableJeepay := isJeepayConfigured()
if enableJeepay {
hasJeepay := false
for _, method := range payMethods {
if method["type"] == PaymentMethodJeepay {
hasJeepay = true
break
}
}
if !hasJeepay {
payMethods = append(payMethods, map[string]string{
"name": "Jeepay",
"type": PaymentMethodJeepay,
"color": "rgba(var(--semi-green-5), 1)",
"min_topup": strconv.Itoa(setting.JeepayMinTopUp),
})
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Expose the effective Jeepay limits here, not the raw option values.

Line 95 and Lines 106-117 return setting.JeepayMinTopUp / setting.JeepayOrderTimeoutMinutes directly, but the Jeepay flow already normalizes non-positive values via getJeepayMinTopUp() and getJeepayOrderTimeoutMinutes() in controller/topup_jeepay.go:66-80. When those stored values are unset or malformed, the UI will enable low amounts or show a zero-minute expiry while the backend enforces different limits.

💡 Proposed fix
 	if enableJeepay {
 		hasJeepay := false
 		for _, method := range payMethods {
 			if method["type"] == PaymentMethodJeepay {
@@
 		if !hasJeepay {
 			payMethods = append(payMethods, map[string]string{
 				"name":      "Jeepay",
 				"type":      PaymentMethodJeepay,
 				"color":     "rgba(var(--semi-green-5), 1)",
-				"min_topup": strconv.Itoa(setting.JeepayMinTopUp),
+				"min_topup": strconv.FormatInt(getJeepayMinTopUp(), 10),
 			})
 		}
 	}
@@
-		"enable_jeepay_topup":          enableJeepay,
-		"jeepay_way_code":              getJeepayWayCode(),
-		"jeepay_order_timeout_minutes": setting.JeepayOrderTimeoutMinutes,
+		"enable_jeepay_topup":          enableJeepay,
+		"jeepay_way_code":              getJeepayWayCode(),
+		"jeepay_order_timeout_minutes": getJeepayOrderTimeoutMinutes(),
@@
-		"jeepay_min_topup":    setting.JeepayMinTopUp,
+		"jeepay_min_topup":    getJeepayMinTopUp(),

Also applies to: 104-117

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup.go` around lines 81 - 98, The UI is showing raw Jeepay
config values instead of the normalized/validated limits; replace uses of
setting.JeepayMinTopUp and setting.JeepayOrderTimeoutMinutes in
controller/topup.go with the normalized accessors getJeepayMinTopUp() and
getJeepayOrderTimeoutMinutes() (the same functions used in
controller/topup_jeepay.go) wherever you build payMethods entries or return
Jeepay limits so the frontend receives the effective limits the backend actually
enforces.

Comment thread model/option.go
Comment on lines +400 to +403
case "JeepayMinTopUp":
setting.JeepayMinTopUp, _ = strconv.Atoi(value)
case "JeepayOrderTimeoutMinutes":
setting.JeepayOrderTimeoutMinutes, _ = strconv.Atoi(value)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Don’t silently zero Jeepay numeric config on parse errors.

Lines 401 and 403 discard strconv.Atoi errors, so a malformed DB value changes the in-memory Jeepay min-topup/timeout to 0 and hides the bad option. For payment limits and expiry, it is safer to surface the parse failure or keep the previous value intact.

💡 Proposed fix
 	case "JeepayMinTopUp":
-		setting.JeepayMinTopUp, _ = strconv.Atoi(value)
+		if parsedValue, convErr := strconv.Atoi(value); convErr != nil {
+			return convErr
+		} else {
+			setting.JeepayMinTopUp = parsedValue
+		}
 	case "JeepayOrderTimeoutMinutes":
-		setting.JeepayOrderTimeoutMinutes, _ = strconv.Atoi(value)
+		if parsedValue, convErr := strconv.Atoi(value); convErr != nil {
+			return convErr
+		} else {
+			setting.JeepayOrderTimeoutMinutes = parsedValue
+		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
case "JeepayMinTopUp":
setting.JeepayMinTopUp, _ = strconv.Atoi(value)
case "JeepayOrderTimeoutMinutes":
setting.JeepayOrderTimeoutMinutes, _ = strconv.Atoi(value)
case "JeepayMinTopUp":
if parsedValue, convErr := strconv.Atoi(value); convErr != nil {
return convErr
} else {
setting.JeepayMinTopUp = parsedValue
}
case "JeepayOrderTimeoutMinutes":
if parsedValue, convErr := strconv.Atoi(value); convErr != nil {
return convErr
} else {
setting.JeepayOrderTimeoutMinutes = parsedValue
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/option.go` around lines 400 - 403, The current code silently ignores
strconv.Atoi errors when setting setting.JeepayMinTopUp and
setting.JeepayOrderTimeoutMinutes; change the logic in the switch cases that
handle "JeepayMinTopUp" and "JeepayOrderTimeoutMinutes" to check the
strconv.Atoi error and avoid overwriting the existing value on failure (or
return/log the parse error): call strconv.Atoi, if err != nil then log/return
the error and keep the prior setting value; only assign to
setting.JeepayMinTopUp or setting.JeepayOrderTimeoutMinutes when err == nil.

Comment on lines +101 to +110
} else if (status === 'failed' || status === 'expired') {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
if (status === 'expired') {
markExpired();
} else {
showError(t('订单状态已变更,请重新下单'));
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Render a terminal state when polling returns failed.

Here you stop polling, but the JSX at Lines 153-176 still renders the QR path because isExpired stays false. That leaves a scannable code on screen after the backend has already rejected the order.

Also applies to: 153-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/modals/JeepayQRCodeModal.jsx` around lines 101 -
110, When polling returns status === 'failed' you clear the timer but don’t flip
the terminal state, so the QR JSX (which checks isExpired) continues to render;
treat 'failed' the same as 'expired' by calling markExpired() or otherwise
setting isExpired to true when status === 'failed' (in the same branch that
clears pollTimerRef.current) and still call showError(t(...)) as needed so the
component will render the terminal state instead of the scannable QR; update the
status handling in the poll result code (referencing pollTimerRef, markExpired,
showError, isExpired, status) accordingly.

Comment on lines +27 to +32
const jeepayWayCodeOptions = [
{ label: '聚合扫码(QR_CASHIER)', value: 'QR_CASHIER' },
{ label: '收银台(WEB_CASHIER)', value: 'WEB_CASHIER' },
{ label: '微信扫码(WX_NATIVE)', value: 'WX_NATIVE' },
{ label: '支付宝扫码(ALI_QR)', value: 'ALI_QR' },
];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx | head -200

Repository: QuantumNous/new-api

Length of output: 8047


🏁 Script executed:

cat -n web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx | sed -n '200,215p'

Repository: QuantumNous/new-api

Length of output: 580


🏁 Script executed:

# Check if translation files exist and examine the format
find web/src/i18n/locales -name "*.json" -type f | head -5

Repository: QuantumNous/new-api

Length of output: 207


🏁 Script executed:

# Check the en.json to see what translation keys actually exist
rg '"Jeepay' web/src/i18n/locales/en.json | head -20

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check if these keys exist in any translation file
rg 'Jeepay' web/src/i18n/locales/ | head -30

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check the structure of the en.json translation file
head -50 web/src/i18n/locales/en.json

Repository: QuantumNous/new-api

Length of output: 5007


🏁 Script executed:

# Check if 'Jeepay mchNo', 'Jeepay appId' keys exist in translation files
rg '"Jeepay' web/src/i18n/locales/en.json

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check the Chinese translation file (zh.json or similar)
ls -la web/src/i18n/locales/ | grep -i zh

Repository: QuantumNous/new-api

Length of output: 186


🏁 Script executed:

# Check for Jeepay keys in Chinese translation file
rg 'Jeepay' web/src/i18n/locales/zh-CN.json

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check the similar Creem payment gateway component for comparison
cat -n web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx | head -80

Repository: QuantumNous/new-api

Length of output: 3141


🏁 Script executed:

# Check how Creem component uses option lists, if any
rg -A 5 'optionList|options\s*=' web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx | head -40

Repository: QuantumNous/new-api

Length of output: 232


🏁 Script executed:

# Check other payment gateway components for option list patterns
ls web/src/pages/Setting/Payment/SettingsPaymentGateway*.jsx | head -5

Repository: QuantumNous/new-api

Length of output: 369


🏁 Script executed:

# Check Stripe gateway for similar option list patterns
rg -B 2 -A 5 'optionList|const.*Options' web/src/pages/Setting/Payment/SettingsPaymentGatewayStripe.jsx | head -50

Repository: QuantumNous/new-api

Length of output: 45


🏁 Script executed:

# Check how the code actually uses t() for placeholders in other fields on this component
grep -n "placeholder=" web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx

Repository: QuantumNous/new-api

Length of output: 322


🏁 Script executed:

# Check if these translation keys are used anywhere else in the codebase
rg "t\('Jeepay" web/src/ --type jsx

Repository: QuantumNous/new-api

Length of output: 90


🏁 Script executed:

# Check if these translation keys are used anywhere else in the codebase
rg "t\('Jeepay" web/src/

Repository: QuantumNous/new-api

Length of output: 483


Move option labels inside the component and use Chinese source keys for all placeholders.

jeepayWayCodeOptions at lines 27–32 has hard-coded Chinese labels that bypass t(), preventing translation. Additionally, placeholders at lines 155 and 167 use English keys (t('Jeepay mchNo'), t('Jeepay appId')) instead of Chinese source strings, inconsistent with the rest of the form. Move the options array inside the component to enable t() wrapping, and replace English placeholder keys with Chinese text to match the project's i18n convention.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx` around lines
27 - 32, jeepayWayCodeOptions is defined outside the component with hard-coded
Chinese labels and must be moved inside the component so you can wrap labels
with t(), e.g. build the array inside the component and call t() for each label;
also replace the placeholder keys used in the form inputs (the places currently
calling t('Jeepay mchNo') and t('Jeepay appId')) with the Chinese source strings
used elsewhere in the project so they use consistent i18n keys/phrasing (update
the placeholder arguments to the Chinese text and keep using t()); ensure you
only change the array location and the two placeholder t(...) calls and nothing
else.

@Calcium-Ion

Copy link
Copy Markdown
Member

为什么要伪造成我是提交人?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants