新增Jeepay支付接入 - #4098
Conversation
feat(payment): integrate Jeepay top-up flow
WalkthroughThis 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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
为什么要伪造提交人 |
There was a problem hiding this comment.
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 | 🟠 MajorKeep 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 againstdata.min_topupinstead ofdata.jeepay_min_topup, which means users either get a false client-side block or a late/api/user/jeepay/payrejection 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: KeepJeepayOrderTimeoutMinutesin 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
RechargeWaffowith 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.godefinesJeepayNotifyURL/JeepayReturnURL, andcontroller/topup_jeepay.goalready 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
⛔ Files ignored due to path filters (1)
web/src/assets/jeepay.svgis excluded by!**/*.svg
📒 Files selected for processing (14)
controller/topup.gocontroller/topup_jeepay.gocontroller/topup_jeepay_test.gomodel/option.gomodel/topup.gorouter/api-router.gosetting/payment_jeepay.goweb/src/components/settings/PaymentSetting.jsxweb/src/components/topup/RechargeCard.jsxweb/src/components/topup/index.jsxweb/src/components/topup/modals/JeepayQRCodeModal.jsxweb/src/components/topup/modals/PaymentConfirmModal.jsxweb/src/components/topup/modals/TopupHistoryModal.jsxweb/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx
| 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 |
There was a problem hiding this comment.
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.
| 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") |
There was a problem hiding this comment.
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.
| 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), | ||
| }) | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| case "JeepayMinTopUp": | ||
| setting.JeepayMinTopUp, _ = strconv.Atoi(value) | ||
| case "JeepayOrderTimeoutMinutes": | ||
| setting.JeepayOrderTimeoutMinutes, _ = strconv.Atoi(value) |
There was a problem hiding this comment.
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.
| 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.
| } else if (status === 'failed' || status === 'expired') { | ||
| if (pollTimerRef.current) { | ||
| clearInterval(pollTimerRef.current); | ||
| pollTimerRef.current = null; | ||
| } | ||
| if (status === 'expired') { | ||
| markExpired(); | ||
| } else { | ||
| showError(t('订单状态已变更,请重新下单')); | ||
| } |
There was a problem hiding this comment.
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.
| 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' }, | ||
| ]; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n web/src/pages/Setting/Payment/SettingsPaymentGatewayJeepay.jsx | head -200Repository: 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 -5Repository: 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 -20Repository: 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 -30Repository: 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.jsonRepository: 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.jsonRepository: 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 zhRepository: 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.jsonRepository: 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 -80Repository: 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 -40Repository: 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 -5Repository: 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 -50Repository: 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.jsxRepository: 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 jsxRepository: 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.
|
为什么要伪造成我是提交人? |
背景
在
new-api中新增 Jeepay 充值支付接入,整体复用现有top_up充值链路,以最少改动完成 Jeepay 支付能力集成。本次改动
1. 支付下单能力
2. 异步通知处理
JSON / form / query三种数据格式3. 支付方式支持
QR_CASHIER- 聚合扫码WEB_CASHIER- 收银台WX_NATIVE- 微信扫码ALI_QR- 支付宝扫码4. 订单查询与超时控制
5. 前后端相关调整
已验证流程
重点评审
请重点关注以下内容:
top_up / 支付接入的整体风格影响范围
new-apiJeepay 充值支付接入相关后端逻辑测试说明
已完成基础流程联调与关键支付场景验证,重点覆盖:
Summary by CodeRabbit