Skip to content

feat(payment): 新增 Jeepay 聚合支付充值接入 - #4099

Closed
jeequan wants to merge 7 commits into
QuantumNous:mainfrom
jeequan:feature/jeepay-topup-v2
Closed

feat(payment): 新增 Jeepay 聚合支付充值接入#4099
jeequan wants to merge 7 commits into
QuantumNous:mainfrom
jeequan:feature/jeepay-topup-v2

Conversation

@jeequan

@jeequan jeequan commented Apr 5, 2026

Copy link
Copy Markdown

背景

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

本次改动

1. 支付下单能力

  • 新增 Jeepay 充值下单能力(MD5 签名)
  • 复用现有 top_up 支付链路,降低整体接入成本
  • 可使用 Jeepay 开源版,也可使用计全官方通道对接

2. 异步通知处理

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

3. 支付方式支持

  • QR_CASHIER - 聚合扫码
  • WEB_CASHIER - 收银台
  • WX_NATIVE - 微信扫码
  • ALI_QR - 支付宝扫码

4. 订单查询与超时控制

  • 新增订单状态轮询接口
  • 新增订单超时时间配置
  • 扫码界面超时自动提示二维码过期

5. 前后端相关调整

  • 前端新增扫码支付弹窗(含倒计时、过期态)
  • 后台新增 Jeepay 设置页
  • 单元测试覆盖签名和通知流程

相比 #4098 的改进

针对上次 review 反馈的 6 个问题已全部修复:

  • [Critical] 下单失败时保持订单 pending,避免 webhook 无法入账
  • [Major] 支持非 HTTP 二维码链接(如 weixin://...
  • [Major] topup info 接口返回规范化的 Jeepay 限额
  • [Minor] 数值配置解析失败时返回错误,不再静默置零
  • [Minor] 轮询返回 failed 状态时隐藏二维码
  • [Minor] 支付方式选项支持 i18n,占位符使用中文源字符串

已验证流程

  • 收银台支付流程
  • 聚合扫码支付流程
  • 微信扫码支付流程
  • 支付宝扫码支付流程
  • 异步通知入账流程
  • 扫码支付成功后前端自动提示成功
  • 二维码超时后进入过期态

测试

  • go test ./controller/ -run TestJeepay -v 全部通过
  • 前端构建无错误
  • 手动测试:创建 Jeepay 订单,验证 QR 显示
  • 手动测试:模拟通知,验证入账

Summary by CodeRabbit

  • New Features

    • Added Jeepay payment gateway for account top-ups with support for QR code, web cashier, and native payment methods
    • New admin configuration panel to manage Jeepay settings
    • Real-time payment status tracking with automatic order timeout handling
  • Tests

    • Added test coverage for Jeepay payment flows and webhook processing

jeequan added 7 commits April 5, 2026 18:02
- Add Jeepay unified order creation with MD5 signature
- Support multiple pay modes: QR_CASHIER, WEB_CASHIER, WX_NATIVE, ALI_QR
- Add async notification handler with JSON/form/query parsing
- Add order status polling endpoint
- Add QR code payment modal with countdown and expiry
- Add admin Jeepay settings page
- Add unit tests for signature and notification flow
临时错误(超时、网络)不应将订单标记为 failed,
因为 webhook 可能仍会到达,而 RechargeJeepay 仅处理 pending 订单。
同时修复测试中 Redis 未初始化导致的 panic。
WX_NATIVE 的 codeUrl 使用 weixin:// scheme,优先提取 codeUrl
且不要求 HTTP 前缀,payUrl 和 cashierUrl 仍保持 HTTP 校验。
使用 getJeepayMinTopup() 和 getJeepayOrderTimeoutMinutes() 替代
原始 setting 值,确保前端获取到与后端一致的有效限额。
避免 JeepayMinTopUp/JeepayOrderTimeoutMinutes 在数据库值
格式异常时被静默置为 0,改为返回解析错误以便记录日志。
failed 和 expired 状态均调用 markExpired() 进入终态,
避免订单被拒后二维码仍然可扫。
将 jeepayWayCodeOptions 移入组件内以使用 t() 包裹标签,
占位符统一使用中文源字符串,符合项目 i18n 规范。
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

This pull request integrates Jeepay payment gateway support into the platform by adding backend controllers for payment requests and webhook handling, configurable settings, database models for transaction completion, new API routes, and frontend components for payment flows including QR code displays and status polling.

Changes

Cohort / File(s) Summary
Backend Jeepay Controller
controller/topup_jeepay.go
Implements payment request handler (RequestJeepayPay), status endpoint (GetJeepayPayStatus), webhook receiver (JeepayNotify), and helpers for MD5 signing, amount parsing, way-code validation, and URL extraction from Jeepay API responses.
Top-up Controller & Routes
controller/topup.go, router/api-router.go
Conditionally enables Jeepay in GetTopUpInfo by checking configuration and appending Jeepay method to response; adds three new API routes (POST /api/jeepay/notify, POST /api/user/jeepay/pay, GET /api/user/jeepay/status/:tradeNo).
Data Model & Configuration
model/topup.go, model/option.go, setting/payment_jeepay.go
Adds RechargeJeepay() transaction handler for completing top-ups with quota calculation; extends option map with Jeepay configuration keys and parsing logic; declares exported configuration variables (base URL, merchant credentials, way code, timeouts, minimum amounts).
Frontend Top-up Integration
web/src/components/topup/index.jsx, web/src/components/topup/RechargeCard.jsx
Extends top-up flow with Jeepay enablement state, payment method routing, and QR/web payment branching; adds Jeepay props and configuration fetching to RechargeCard.
Frontend Payment UI Components
web/src/components/topup/modals/JeepayQRCodeModal.jsx, web/src/components/topup/modals/PaymentConfirmModal.jsx, web/src/components/topup/modals/TopupHistoryModal.jsx
New QR code modal with countdown and status polling; updated payment icon rendering and history display for Jeepay method.
Payment Configuration UI
web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx, web/src/components/settings/PaymentSetting.jsx
New settings form component for Jeepay configuration (credentials, URLs, timeouts); imports and renders the new component in payment settings.
Test Coverage
controller/topup_jeepay_test.go
Adds helpers for DB initialization, signature validation tests, and webhook notify tests covering valid/invalid signature scenarios.

Sequence Diagram

sequenceDiagram
    actor User as User
    participant FE as Frontend
    participant BE as Backend
    participant JP as Jeepay API
    participant WH as Webhook Handler

    User->>FE: Initiate Jeepay top-up
    FE->>BE: POST /api/user/jeepay/pay<br/>(paymentMethod, amount, wayCode)
    BE->>JP: POST unified-order<br/>(mchNo, appId, signature, amount, timeout)
    JP-->>BE: Return paymentUrl, qrUrl
    BE-->>FE: Return urls + orderId + expireAt
    
    alt QR Payment (QR_CASHIER, WX_NATIVE, ALI_QR)
        FE->>FE: Display QR Code Modal
        FE->>BE: GET /api/user/jeepay/status/:orderId<br/>(polling every 3s)
        BE-->>FE: Return pending/success/failed/expired
    else Web Payment
        FE->>JP: Open paymentUrl in new tab
    end
    
    JP->>WH: POST /api/jeepay/notify<br/>(state, amount, sign, orderId)
    WH->>BE: Verify MD5 signature
    BE->>BE: RechargeJeepay(tradeNo)<br/>Lock order, update status,<br/>increment quota
    WH-->>JP: Return "success"
    BE-->>FE: Status poll returns success
    FE->>FE: Show success, close modal
    User->>User: Quota updated
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion
  • seefs001
  • creamlike1024

Poem

🐰 A new Jeepay path appears,
QR codes bloom with transient cheers,
Signatures signed, webhooks ring,
Quotas rise on digital wing—
Hop along, the payment's done! 🎉

🚥 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
Title check ✅ Passed The title accurately describes the main change: adding Jeepay aggregated payment top-up integration. It directly relates to the primary objective and is clear and specific.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

@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: 3

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)

511-533: ⚠️ Potential issue | 🟠 Major

Keep minTopUp payment-method specific.

Lines 515-523 choose a single shared minimum by gateway priority, but Lines 199-201 and 222-224 validate every payment against that shared minTopUp. If online top-up and Jeepay are both enabled, the UI can still open the Jeepay flow below data.jeepay_min_topup, and Lines 168-170 of controller/topup_jeepay.go then reject it server-side.

🤖 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 511 - 533, The code
currently computes a single shared minTopUpValue (minTopUpValue) and calls
setMinTopUp/setTopUpCount with it, which causes cross-gateway validation issues;
instead compute and store per-method minimums from data (e.g. jeepay_min_topup,
stripe_min_topup, min_topup for online, waffo_min_topup) and set corresponding
state variables (e.g. setMinTopUpJeepay, setMinTopUpStripe, setMinTopUpOnline,
setWaffoMinTopUp) while leaving setMinTopUp for a UI-wide fallback only; update
the initial setTopUpCount to use the minimum appropriate to the currently
selected gateway (or the smallest of enabled minima if none selected) and ensure
the per-gateway validation code that reads minTopUp (used in the
Jeepay/Stripe/online flows) uses the new method-specific state names instead of
the shared minTopUpValue.
🧹 Nitpick comments (3)
router/api-router.go (1)

94-95: Consider adding rate limiting to the status polling endpoint.

The /jeepay/pay endpoint correctly uses CriticalRateLimit(), but the /jeepay/status/:tradeNo endpoint has no rate limiting. While it's protected by UserAuth(), a malicious authenticated user could still abuse the polling endpoint. Consider adding a lighter rate limit (e.g., SearchRateLimit()) to prevent excessive polling.

💡 Suggested change
 selfRoute.POST("/jeepay/pay", middleware.CriticalRateLimit(), controller.RequestJeepayPay)
-selfRoute.GET("/jeepay/status/:tradeNo", controller.GetJeepayPayStatus)
+selfRoute.GET("/jeepay/status/:tradeNo", middleware.SearchRateLimit(), controller.GetJeepayPayStatus)
🤖 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, Add a lighter rate limit
middleware to the Jeepay status polling route to prevent abusive polling: update
the route that registers controller.GetJeepayPayStatus (the
selfRoute.GET("/jeepay/status/:tradeNo", controller.GetJeepayPayStatus) line) to
include a suitable limiter like middleware.SearchRateLimit() (or another defined
non-critical limiter) before the handler so the endpoint stays protected by
UserAuth() while enforcing a lower request rate.
web/src/components/topup/modals/JeepayQRCodeModal.jsx (1)

8-19: Use useTranslation() inside the modal.

Receiving t as a prop here couples this leaf component to its parent and keeps the modal off the repo’s standard i18n path. Resolve translations locally and drop t from this component’s public API instead.

As per coding guidelines, "Frontend i18n: Use i18next + react-i18next + i18next-browser-languagedetector. Translation files in web/src/i18n/locales/{lang}.json must be flat JSON with Chinese source strings as keys. Use useTranslation() hook and call t('中文key') in components."

🤖 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 8 - 19,
The component JeepayQRCodeModal should stop accepting the translation function
prop t and instead import and call useTranslation() inside the component; remove
t from the prop list in the JeepayQRCodeModal signature and any places that pass
it in, add import { useTranslation } from 'react-i18next' and call const { t } =
useTranslation() at the top of the component, then replace any parent-provided
t(...) uses with local t('中文 key') calls that match the flat JSON keys in
web/src/i18n/locales/{lang}.json; ensure the public API (props: visible,
onCancel, qrCodeUrl, orderId, wayCode, money, expiredTime, expireAt, onPaid) no
longer includes t and update all callers to stop passing t.
controller/topup_jeepay_test.go (1)

55-165: Cover the form/query notify branches too.

parseJeepayNotifyPayload in controller/topup_jeepay.go has separate JSON, form, and query parsing paths, but this suite only exercises JSON requests. Adding one application/x-www-form-urlencoded case and one query-string case would keep the advertised compatibility from regressing silently.

🤖 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 55 - 165, Add two tests that
exercise the form and query parsing branches used by parseJeepayNotifyPayload:
create copies of TestJeepayNotify that build payloads as (1)
application/x-www-form-urlencoded (use url.Values, encode to body, set
Content-Type to "application/x-www-form-urlencoded") and (2) query-string params
(attach the same key/value pairs to the request URL). In both cases include the
same required fields (mchNo, appId, mchOrderNo, state, amount, signType,
timestamps as needed), compute the signature with buildJeepaySign using
settingJeepayApiKeyForTest, and assert the handler JeepayNotify returns
http.StatusOK/"success" and that model.GetTopUpByTradeNo shows
Status==common.TopUpStatusSuccess and the user quota updated; use the existing
helpers (setupJeepayTestDB, settingJeepayForTest, buildJeepaySign) and the same
trade numbers to locate records.
🤖 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_test.go`:
- Around line 18-34: The helper setupJeepayTestDB mutates globals (model.DB,
model.LOG_DB, common.UsingSQLite, common.UsingMySQL, common.UsingPostgreSQL,
common.RedisEnabled, common.LogConsumeEnabled, common.QuotaPerUnit and the
setting.Jeepay* globals referenced later) without restoring them; update
setupJeepayTestDB (and the other helper at the other block) to capture the
original values at the start and call t.Cleanup to restore each original value
(close and nil the temporary sqlite DB and reassign model.DB/model.LOG_DB and
each common.* and setting.Jeepay* back to their originals) so tests no longer
leak state or become order-dependent.

In `@controller/topup_jeepay.go`:
- Around line 268-275: Compute and persist a single canonical expiry when the
Jeepay order is created and use that persisted value for both response payloads
instead of computing expire_at from time.Now() or recomputing from CreateTime +
getJeepayExpiredTime(); specifically, when creating the order save an explicit
ExpireAt (e.g., derived from the upstream order response or from
topUp.CreateTime + getJeepayExpiredTime()) and then replace the ephemeral
calculation that sets expireAt in responseData (payment_url/expire_at block) and
the later logic that uses topUp.CreateTime + getJeepayExpiredTime() to read that
persisted ExpireAt field so both endpoints return the identical expiry.

In `@web/src/components/topup/modals/JeepayQRCodeModal.jsx`:
- Around line 82-117: The pollStatus function can overlap when setInterval fires
before a prior async API.get completes; add a reentrancy guard (e.g., a local
ref like isPollingRef or inFlight boolean) and early-return if a poll is already
in progress, ensure you set isPollingRef.current = true before awaiting API.get
and set it back to false in a finally block; also when you detect terminal
states ('success', 'failed', 'expired') clear the pollTimerRef and set
isPollingRef.current = false before calling Toast.success, showError, or onPaid
to avoid duplicate callbacks; update references to pollStatus, pollTimerRef,
expireAtRef, markExpired, onPaid, Toast.success, and showError accordingly.

---

Outside diff comments:
In `@web/src/components/topup/index.jsx`:
- Around line 511-533: The code currently computes a single shared minTopUpValue
(minTopUpValue) and calls setMinTopUp/setTopUpCount with it, which causes
cross-gateway validation issues; instead compute and store per-method minimums
from data (e.g. jeepay_min_topup, stripe_min_topup, min_topup for online,
waffo_min_topup) and set corresponding state variables (e.g. setMinTopUpJeepay,
setMinTopUpStripe, setMinTopUpOnline, setWaffoMinTopUp) while leaving
setMinTopUp for a UI-wide fallback only; update the initial setTopUpCount to use
the minimum appropriate to the currently selected gateway (or the smallest of
enabled minima if none selected) and ensure the per-gateway validation code that
reads minTopUp (used in the Jeepay/Stripe/online flows) uses the new
method-specific state names instead of the shared minTopUpValue.

---

Nitpick comments:
In `@controller/topup_jeepay_test.go`:
- Around line 55-165: Add two tests that exercise the form and query parsing
branches used by parseJeepayNotifyPayload: create copies of TestJeepayNotify
that build payloads as (1) application/x-www-form-urlencoded (use url.Values,
encode to body, set Content-Type to "application/x-www-form-urlencoded") and (2)
query-string params (attach the same key/value pairs to the request URL). In
both cases include the same required fields (mchNo, appId, mchOrderNo, state,
amount, signType, timestamps as needed), compute the signature with
buildJeepaySign using settingJeepayApiKeyForTest, and assert the handler
JeepayNotify returns http.StatusOK/"success" and that model.GetTopUpByTradeNo
shows Status==common.TopUpStatusSuccess and the user quota updated; use the
existing helpers (setupJeepayTestDB, settingJeepayForTest, buildJeepaySign) and
the same trade numbers to locate records.

In `@router/api-router.go`:
- Around line 94-95: Add a lighter rate limit middleware to the Jeepay status
polling route to prevent abusive polling: update the route that registers
controller.GetJeepayPayStatus (the selfRoute.GET("/jeepay/status/:tradeNo",
controller.GetJeepayPayStatus) line) to include a suitable limiter like
middleware.SearchRateLimit() (or another defined non-critical limiter) before
the handler so the endpoint stays protected by UserAuth() while enforcing a
lower request rate.

In `@web/src/components/topup/modals/JeepayQRCodeModal.jsx`:
- Around line 8-19: The component JeepayQRCodeModal should stop accepting the
translation function prop t and instead import and call useTranslation() inside
the component; remove t from the prop list in the JeepayQRCodeModal signature
and any places that pass it in, add import { useTranslation } from
'react-i18next' and call const { t } = useTranslation() at the top of the
component, then replace any parent-provided t(...) uses with local t('中文 key')
calls that match the flat JSON keys in web/src/i18n/locales/{lang}.json; ensure
the public API (props: visible, onCancel, qrCodeUrl, orderId, wayCode, money,
expiredTime, expireAt, onPaid) no longer includes t and update all callers to
stop passing t.
🪄 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: 0e5c5ad1-258a-4ef7-8f11-d603f108a46a

📥 Commits

Reviewing files that changed from the base of the PR and between 677d02f and 81753b0.

⛔ Files ignored due to path filters (2)
  • web/bun.lock is excluded by !**/*.lock
  • 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 +18 to +34
func setupJeepayTestDB(t *testing.T) {
t.Helper()

db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
require.NoError(t, err)

model.DB = db
model.LOG_DB = db
common.UsingSQLite = true
common.UsingMySQL = false
common.UsingPostgreSQL = false
common.RedisEnabled = false
common.LogConsumeEnabled = true
common.QuotaPerUnit = 1

require.NoError(t, db.AutoMigrate(&model.User{}, &model.TopUp{}, &model.Log{}))
}

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

Restore mutated globals with t.Cleanup().

These helpers overwrite model.DB, model.LOG_DB, multiple common.* flags, and setting.Jeepay* globals without restoring them. That makes this file order-dependent with other controller package tests.

Also applies to: 173-178

🤖 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 18 - 34, The helper
setupJeepayTestDB mutates globals (model.DB, model.LOG_DB, common.UsingSQLite,
common.UsingMySQL, common.UsingPostgreSQL, common.RedisEnabled,
common.LogConsumeEnabled, common.QuotaPerUnit and the setting.Jeepay* globals
referenced later) without restoring them; update setupJeepayTestDB (and the
other helper at the other block) to capture the original values at the start and
call t.Cleanup to restore each original value (close and nil the temporary
sqlite DB and reassign model.DB/model.LOG_DB and each common.* and
setting.Jeepay* back to their originals) so tests no longer leak state or become
order-dependent.

Comment on lines +268 to +275
expireAt := time.Now().Add(time.Duration(orderReq.ExpiredTime) * time.Second).Unix()
responseData := gin.H{
"payment_url": paymentURL,
"order_id": tradeNo,
"way_code": orderReq.WayCode,
"money": payMoney,
"expired_time": orderReq.ExpiredTime,
"expire_at": expireAt,

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

Use one persisted expiry source for both endpoints.

Lines 268-275 return expire_at from time.Now() after the upstream order call completes, but Lines 301-305 later derive expiry from topUp.CreateTime + getJeepayExpiredTime(). Those values can drift on slow upstream calls and whenever JeepayOrderTimeoutMinutes changes after the order is created, so the modal countdown and the status API can disagree.

Also applies to: 301-305

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

In `@controller/topup_jeepay.go` around lines 268 - 275, Compute and persist a
single canonical expiry when the Jeepay order is created and use that persisted
value for both response payloads instead of computing expire_at from time.Now()
or recomputing from CreateTime + getJeepayExpiredTime(); specifically, when
creating the order save an explicit ExpireAt (e.g., derived from the upstream
order response or from topUp.CreateTime + getJeepayExpiredTime()) and then
replace the ephemeral calculation that sets expireAt in responseData
(payment_url/expire_at block) and the later logic that uses topUp.CreateTime +
getJeepayExpiredTime() to read that persisted ExpireAt field so both endpoints
return the identical expiry.

Comment on lines +82 to +117
const pollStatus = async () => {
if (Date.now() > expireAtRef.current) {
markExpired();
return;
}

try {
const res = await API.get(`/api/user/jeepay/status/${encodeURIComponent(orderId)}`);
if (!res?.data?.success) {
return;
}
const status = res.data?.data?.status;
if (status === 'success') {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
Toast.success({ content: t('支付成功') });
onPaid?.();
} else if (status === 'failed' || status === 'expired') {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current);
pollTimerRef.current = null;
}
markExpired();
if (status === 'failed') {
showError(t('订单状态已变更,请重新下单'));
}
}
} catch (error) {
// ignore transient polling errors
}
};

pollStatus();
pollTimerRef.current = setInterval(pollStatus, 3000);

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

Prevent overlapping status polls.

Lines 82-117 run an async pollStatus under setInterval, so a slow request can still be in flight when the next tick starts. That makes duplicate Toast.success, showError, or onPaid calls possible after the modal has already reached a terminal state.

🤖 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 82 - 117,
The pollStatus function can overlap when setInterval fires before a prior async
API.get completes; add a reentrancy guard (e.g., a local ref like isPollingRef
or inFlight boolean) and early-return if a poll is already in progress, ensure
you set isPollingRef.current = true before awaiting API.get and set it back to
false in a finally block; also when you detect terminal states ('success',
'failed', 'expired') clear the pollTimerRef and set isPollingRef.current = false
before calling Toast.success, showError, or onPaid to avoid duplicate callbacks;
update references to pollStatus, pollTimerRef, expireAtRef, markExpired, onPaid,
Toast.success, and showError accordingly.

@Calcium-Ion Calcium-Ion closed this Apr 5, 2026
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.

2 participants