Skip to content

feat: waffo pancake pay#4089

Draft
seefs001 wants to merge 1 commit intoQuantumNous:mainfrom
seefs001:feature/waffo-pay
Draft

feat: waffo pancake pay#4089
seefs001 wants to merge 1 commit intoQuantumNous:mainfrom
seefs001:feature/waffo-pay

Conversation

@seefs001
Copy link
Copy Markdown
Collaborator

@seefs001 seefs001 commented Apr 4, 2026

⚠️ 提交警告 / PR Warning

请注意: 请提供人工撰写的简洁摘要。包含大量 AI 灌水内容、逻辑混乱或无视模版的 PR 可能会被无视或直接关闭


💡 沟通提示 / Pre-submission

重大功能变更? 请先提交 Issue 交流,避免无效劳动。

📝 变更描述 / Description

新增Waffo Pancake集成

TODO:优化支付配置项

🚀 变更类型 / Type of change

  • 🐛 Bug 修复 (Bug fix)
  • ✨ 新功能 (New feature) - 重大特性建议先 Issue 沟通
  • ⚡ 性能优化 / 重构 (Refactor)
  • 📝 文档更新 (Documentation)

🔗 关联任务 / Related Issue

  • Closes # (如有)

✅ 提交前检查项 / Checklist

  • 人工确认: 我已亲自撰写此描述,去除了 AI 原始输出的冗余。
  • 深度理解: 我已完全理解这些更改的工作原理及潜在影响。
  • 范围聚焦: 本 PR 未包含任何与当前任务无关的代码改动。
  • 本地验证: 已在本地运行并通过了测试或手动验证。
  • 安全合规: 代码中无敏感凭据,且符合项目代码规范。

📸 运行证明 / Proof of Work

TODO

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 4, 2026

Walkthrough

Introduces the "Waffo Pancake" payment method for user top-ups, adding configuration settings, payment request/webhook handlers, checkout session creation, signature verification, transaction management, API routes, and frontend UI components to support this new payment gateway integration.

Changes

Cohort / File(s) Summary
Settings & Configuration
setting/payment_waffo_pancake.go
New package-level variables for Waffo Pancake integration (enabled, sandbox mode, merchant/store/product IDs, webhook keys, currency, unit price, minimum top-up).
Model Layer
model/option.go, model/topup.go
Extended InitOptionMap and updateOptionMap to recognize and persist Waffo Pancake settings; added RechargeWaffoPancake(tradeNo) function mirroring existing top-up completion flow with transaction locking and quota updates.
Backend Controller & Handlers
controller/topup.go, controller/topup_waffo_pancake.go
Updated top-up info endpoint to include Waffo Pancake enable flag and minimum amount; added new RequestWaffoPancakePay and WaffoPancakeWebhook handlers for payment initiation and webhook processing.
API Routes
router/api-router.go
Registered two new POST endpoints: webhook listener and rate-limited user payment route.
Service Layer
service/waffo_pancake.go
Implements checkout session creation with SHA-256/RSA signing, webhook signature verification with timestamp validation, and trade-number extraction from webhook events.
Frontend Payment UI
web/src/components/topup/index.jsx, web/src/components/topup/RechargeCard.jsx
Added Waffo Pancake payment flow with async handler, state management for enable/min-topup flags, UI card for payment entry, and callback integration.
Frontend Settings
web/src/components/settings/PaymentSetting.jsx, web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx
Extended payment settings to import and render Waffo Pancake configuration form; new component handles form state, validation, and option persistence with RSA key and numeric field handling.
Localization
web/src/i18n/locales/zh-CN.json
Added 38 zh-CN translation keys for Waffo Pancake UI labels, field descriptions, validation messages, and configuration help text.

Sequence Diagrams

sequenceDiagram
    actor User
    participant Frontend as Frontend UI
    participant Controller as Payment Controller
    participant Service as Waffo Service
    participant WaffoDB as Database
    participant WaffoPay as Waffo API

    User->>Frontend: Enter top-up amount
    Frontend->>Frontend: Validate amount >= minTopUp
    Frontend->>Controller: POST /api/user/waffo-pancake/pay<br/>(amount)
    Controller->>WaffoDB: Create pending TopUp record<br/>(generate tradeNo)
    Controller->>Service: CreateWaffoPancakeCheckoutSession<br/>(params with tradeNo metadata)
    Service->>Service: Sign request<br/>(SHA-256 + RSA/PKCS#1)
    Service->>WaffoPay: POST checkout session<br/>(signed payload)
    WaffoPay-->>Service: Return sessionID, checkoutURL, expiresAt
    Service-->>Controller: Return session details
    Controller-->>Frontend: Return checkoutURL
    Frontend->>User: Open checkout URL (new window)
    
    rect rgba(200, 150, 100, 0.5)
        Note over User,WaffoPay: User completes payment on Waffo platform
    end

    WaffoPay->>Controller: POST /api/waffo-pancake/webhook<br/>(order.completed event + signature)
    Controller->>Service: VerifyConfiguredWaffoPancakeWebhook<br/>(payload + header signature)
    Service->>Service: Verify timestamp tolerance
    Service->>Service: Verify RSA signature<br/>(against configured pubkey)
    Service-->>Controller: Return verified event
    Controller->>Service: ExtractWaffoPancakeTradeNo(event)
    Service-->>Controller: Return tradeNo
    Controller->>WaffoDB: LockOrder(tradeNo)
    Controller->>WaffoDB: RechargeWaffoPancake(tradeNo)
    WaffoDB->>WaffoDB: Update TopUp to Success<br/>Increment user quota
    WaffoDB-->>Controller: Success
    Controller-->>WaffoPay: HTTP 200 OK
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related PRs

Suggested reviewers

  • Calcium-Ion
  • creamlike1024

Poem

🐰 A carrot path of payments new,
With Pancakes topped in Waffo's hue,
RSA keys dance, signatures spin,
Webhooks whisper, credits win!
Frontend and backend, hand in paw—
The sweetest payment integration of all! 🥕✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.33% 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 'feat: waffo pancake pay' directly and accurately summarizes the main change: adding support for Waffo Pancake as a new payment method for top-ups across the application.
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.

@seefs001 seefs001 marked this pull request as draft April 4, 2026 11:11
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (7)
model/topup.go (1)

439-494: Consolidate duplicated Waffo recharge transaction logic.

RechargeWaffo and RechargeWaffoPancake now share almost identical idempotency + transaction + quota-credit code. This is drift-prone when one path gets patched and the other is missed.

♻️ Proposed refactor sketch
+func rechargeWaffoByTradeNo(tradeNo string) (topUp *TopUp, quotaToAdd int, err error) {
+  // shared lock/query/status/quota/update logic
+}
+
 func RechargeWaffo(tradeNo string) (err error) {
-  // duplicated logic...
+  topUp, quotaToAdd, err := rechargeWaffoByTradeNo(tradeNo)
+  // provider-specific logging
 }
 
 func RechargeWaffoPancake(tradeNo string) (err error) {
-  // duplicated logic...
+  topUp, quotaToAdd, err := rechargeWaffoByTradeNo(tradeNo)
+  // provider-specific logging
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@model/topup.go` around lines 439 - 494, Both RechargeWaffo and
RechargeWaffoPancake duplicate the same idempotency + transaction + quota-credit
logic; extract that common flow into a single helper (e.g., processWaffoTopUp or
applyTopUpTransaction) that accepts the identifying tradeNo and any
source-specific metadata, performs the FOR UPDATE lookup on TopUp (mirroring the
current refCol logic), validates status, computes quotaToAdd, updates TopUp
(CompleteTime/Status) and increments User.quota inside one DB.Transaction, and
returns the updated TopUp, quotaToAdd and an error; then simplify RechargeWaffo
and RechargeWaffoPancake to call this helper and only handle post-success
logging (RecordLog) and error mapping. Ensure the helper preserves existing
behaviors: PostgreSQL quoting, idempotent success return (no-op if already
success), same error messages, and calling tx.Save / tx.Model updates as
currently implemented.
controller/topup_waffo_pancake.go (2)

169-171: Ignored error when updating TopUp status to failed.

If topUp.Update() fails, the database will have a stale pending record that never transitions to failed. Consider logging the error to aid debugging.

Proposed fix
 		log.Printf("create Waffo Pancake checkout session failed: %v", err)
 		topUp.Status = common.TopUpStatusFailed
-		_ = topUp.Update()
+		if updateErr := topUp.Update(); updateErr != nil {
+			log.Printf("failed to mark Waffo Pancake topup as failed: %v, trade_no=%s", updateErr, tradeNo)
+		}
 		c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup_waffo_pancake.go` around lines 169 - 171, The call to
topUp.Update() ignores errors causing stale pending records; change the code
around setting topUp.Status = common.TopUpStatusFailed to capture the returned
error from topUp.Update(), handle it (e.g., log via your logger or
c.Error/c.JSON with an internal error message), and only proceed to respond
after ensuring the update succeeded or after logging the update error; reference
the topUp variable and the topUp.Update() method and ensure the failure path
logs the error before sending c.JSON(200, gin.H{...}).

206-216: Silently returning OK for missing trade number may hide webhook issues.

When tradeNo is empty, the handler logs and returns 200 OK. This prevents retries, but if the metadata is malformed due to a bug, the order will never be fulfilled. Consider whether this should be a 400 to trigger investigation, or add monitoring/alerting for this case.

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

In `@controller/topup_waffo_pancake.go` around lines 206 - 216, The handler
currently logs and returns 200 OK when tradeNo is empty (in the block using
service.ExtractWaffoPancakeTradeNo and event.NormalizedEventType), which
suppresses retries and hides malformed webhook metadata; change this to return a
client/error status (e.g., c.String(400, "missing trade no") or 422) instead of
200, enrich the log (replace log.Printf call) to include event.ID and raw
payload/metadata, and emit/record a monitoring metric or alert (e.g., call
metrics.IncWebhookMalformed or monitoring.Alert) so missing trade numbers are
visible and actionable rather than silently ignored.
web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx (3)

300-307: min={0} allows zero unit price, but validation requires > 0.

The Form.InputNumber for WaffoPancakeUnitPrice sets min={0}, but the submit validation at line 141 rejects values <= 0. Set min={0.01} to provide consistent feedback and prevent user confusion.

Proposed fix
               <Form.InputNumber
                 field='WaffoPancakeUnitPrice'
                 precision={2}
                 label={t('充值价格(x元/美金)')}
                 placeholder='1.0'
-                min={0}
+                min={0.01}
               />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx` around
lines 300 - 307, The Form.InputNumber for WaffoPancakeUnitPrice currently sets
min={0} which contradicts the form submit validation that rejects values <= 0;
update the InputNumber to use min={0.01} so the UI prevents zero input and
matches the submit-side check (locate the Form.InputNumber with
field='WaffoPancakeUnitPrice' in SettingsPaymentGatewayWaffoPancake.jsx and
align it with the submit validation logic that enforces > 0).

180-193: Parallel API calls may result in partial updates without clear feedback.

Using Promise.all means all requests run in parallel. If some succeed and some fail, the user sees error toasts for failures but no indication of what succeeded. The early return at line 192 also prevents props.refresh?.() and the success toast, leaving the UI potentially out of sync.

Consider using Promise.allSettled to handle partial success more gracefully, or switch to sequential updates if atomicity is important.

Alternative approach using Promise.allSettled
-      const results = await Promise.all(
-        options.map((opt) =>
-          API.put('/api/option/', {
-            key: opt.key,
-            value: opt.value,
-          }),
-        ),
-      );
-
-      const errorResults = results.filter((res) => !res.data.success);
-      if (errorResults.length > 0) {
-        errorResults.forEach((res) => showError(res.data.message));
-        return;
-      }
+      const results = await Promise.allSettled(
+        options.map((opt) =>
+          API.put('/api/option/', {
+            key: opt.key,
+            value: opt.value,
+          }),
+        ),
+      );
+
+      const failures = results.filter(
+        (r) => r.status === 'rejected' || !r.value?.data?.success,
+      );
+      if (failures.length > 0) {
+        failures.forEach((r) => {
+          const msg = r.status === 'rejected' ? r.reason : r.value?.data?.message;
+          showError(msg || t('更新失败'));
+        });
+        if (failures.length < results.length) {
+          showSuccess(t('部分设置已更新'));
+          props.refresh?.();
+        }
+        return;
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx` around
lines 180 - 193, The current bulk update uses Promise.all with options.map and
then filters results into errorResults and early returns, which can leave
partial updates and skip props.refresh?.() and the success toast; change the
implementation to use Promise.allSettled for the API.put calls (or perform them
sequentially if you require atomicity), inspect settled results to separate
fulfilled vs rejected (check result.status and result.value.data.success), call
showError for each failure and show a single success toast if at least one
update succeeded, then always call props.refresh?.() so the UI is consistent;
update references to results, errorResults, API.put, showError, and
props.refresh?.() in the function accordingly.

95-98: Use parseInt for integer field WaffoPancakeMinTopUp.

WaffoPancakeMinTopUp represents a count (minimum number of units), so parseFloat may introduce floating-point precision issues. Use parseInt to match the field's semantics.

Proposed fix
       WaffoPancakeMinTopUp:
         props.options.WaffoPancakeMinTopUp !== undefined
-          ? parseFloat(props.options.WaffoPancakeMinTopUp)
+          ? parseInt(props.options.WaffoPancakeMinTopUp, 10)
           : 1,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx` around
lines 95 - 98, The WaffoPancakeMinTopUp value is an integer count but currently
uses parseFloat; change the parsing to parseInt with an explicit radix (10) so
the field is interpreted as an integer and falls back to 1 when undefined or NaN
(update the code around WaffoPancakeMinTopUp in
SettingsPaymentGatewayWaffoPancake.jsx to use
parseInt(props.options.WaffoPancakeMinTopUp, 10) and ensure a default of 1 if
the parsed result is NaN).
web/src/components/topup/RechargeCard.jsx (1)

426-426: Empty catch block silently swallows errors.

Consider logging the error or removing the try-catch if the default usdRate = 7 fallback is intentional for all failure cases.

Proposed fix
-                      } catch (e) {}
+                      } catch (e) {
+                        console.warn('Failed to parse status for USD rate:', e);
+                      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/topup/RechargeCard.jsx` at line 426, The empty catch in
RechargeCard.jsx silently swallows errors when computing usdRate (the try-catch
around the usdRate assignment), so either remove the try-catch if you intend to
always use the fallback usdRate = 7, or log the error inside the catch (e.g.,
console.error or your app logger) and then keep the fallback; update the catch
block in the function/component that sets usdRate to include a meaningful log
message (reference variable usdRate and the try block that computes it) rather
than an empty catch.
🤖 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_waffo_pancake.go`:
- Around line 218-219: The current in-process LockOrder/UnlockOrder (used in
handlers like the Waffo webhook where LockOrder(tradeNo) and defer
UnlockOrder(tradeNo) are called) uses a process-local sync.Map and won’t prevent
races in multi-instance deployments; replace this with a distributed lock
(preferred: Redis-based lock using the existing Redis in the stack, or DB
row-level locking via a SELECT ... FOR UPDATE inside a transaction that scopes
the webhook processing) so that concurrent webhook requests for the same tradeNo
across instances are serialized; alternatively, if you intend to keep the
process-local LockOrder/UnlockOrder, add clear documentation of the
single-instance deployment constraint and change usages (e.g., in the Waffo
handler) to use the new distributed lock API instead of LockOrder/UnlockOrder.

In `@controller/topup.go`:
- Around line 82-86: The enableWaffoPancake boolean currently only checks
merchant/private/store/product IDs; include the webhook public-key readiness in
this gating by adding a non-empty trimmed check for the webhook public key (e.g.
strings.TrimSpace(setting.WaffoPancakeWebhookPublicKey) != "") into the same
expression that defines enableWaffoPancake so callback verification won't be
skipped when the key is missing; update the enableWaffoPancake assignment
accordingly (and optionally add a log/warning elsewhere if the webhook key is
missing).

In `@service/waffo_pancake.go`:
- Around line 334-343: The current verifyWaffoPancakeWebhookWithResolvedKeys
function falls back to test when opts.environment is empty, allowing test-signed
webhooks in prod; change it to require an explicit environment and remove the
prod->test fallback: have verifyWaffoPancakeWebhookWithResolvedKeys accept only
"prod" or "test" (return a clear error for any other/empty value), call
resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys) once and
pass that to verifyWaffoPancakeWebhookWithKey, and ensure callers (e.g.,
VerifyConfiguredWaffoPancakeWebhook) always provide opts.environment.
- Around line 173-181: VerifyConfiguredWaffoPancakeWebhook currently omits the
sandbox environment flag so verifyWaffoPancakeWebhook falls back to trying both
keys; update VerifyConfiguredWaffoPancakeWebhook to set opts.environment using
setting.WaffoPancakeSandbox (or equivalent) so the
waffoPancakeWebhookVerifyOptions.environment field is populated, ensuring
verifyWaffoPancakeWebhookWithResolvedKeys uses only the correct environment
(Test or Prod) when resolving publicKeys and prevents acceptance of the
hardcoded test key in production.
- Around line 154-158: The code is using http.DefaultClient.Do(req) which has no
timeout and can hang; replace it by creating a dedicated http.Client with a
sensible Timeout (e.g., 10s) and use client.Do(req) instead of
http.DefaultClient.Do; update the scope where resp and err are obtained (the
block using req, resp, err and defer resp.Body.Close()) so it uses this new
client (refer to the variables req, resp, err and the call currently to
http.DefaultClient.Do) and ensure any context-based cancellation is preserved if
the surrounding function accepts a context.

In `@web/src/components/settings/PaymentSetting.jsx`:
- Line 140: The current updater setInputs((prev) => ({ ...prev, ...newInputs }))
preserves prior values for keys that the backend omits, which can re-submit
stale Waffo Pancake credentials; change the update logic in the PaymentSetting
component so that when handling Waffo Pancake payloads you either replace the
credentials object entirely or explicitly clear sensitive fields omitted by
newInputs (e.g., merchantKey, privateKey) instead of merging with prev—locate
the call to setInputs and make the merge conditional: if newInputs contains a
Waffo Pancake credentials object, use that object as the source of truth (or set
omitted sensitive keys to null/empty) rather than shallow-merging with prev.

In `@web/src/i18n/locales/zh-CN.json`:
- Line 2345: Remove the duplicate translation entry for the key "未启用" so only a
single mapping for that key remains in the JSON; locate both occurrences of the
string "未启用" in the locale file, delete the later duplicate (or merge if
translations differ), and validate the JSON afterwards to ensure no trailing
commas or syntax errors.

---

Nitpick comments:
In `@controller/topup_waffo_pancake.go`:
- Around line 169-171: The call to topUp.Update() ignores errors causing stale
pending records; change the code around setting topUp.Status =
common.TopUpStatusFailed to capture the returned error from topUp.Update(),
handle it (e.g., log via your logger or c.Error/c.JSON with an internal error
message), and only proceed to respond after ensuring the update succeeded or
after logging the update error; reference the topUp variable and the
topUp.Update() method and ensure the failure path logs the error before sending
c.JSON(200, gin.H{...}).
- Around line 206-216: The handler currently logs and returns 200 OK when
tradeNo is empty (in the block using service.ExtractWaffoPancakeTradeNo and
event.NormalizedEventType), which suppresses retries and hides malformed webhook
metadata; change this to return a client/error status (e.g., c.String(400,
"missing trade no") or 422) instead of 200, enrich the log (replace log.Printf
call) to include event.ID and raw payload/metadata, and emit/record a monitoring
metric or alert (e.g., call metrics.IncWebhookMalformed or monitoring.Alert) so
missing trade numbers are visible and actionable rather than silently ignored.

In `@model/topup.go`:
- Around line 439-494: Both RechargeWaffo and RechargeWaffoPancake duplicate the
same idempotency + transaction + quota-credit logic; extract that common flow
into a single helper (e.g., processWaffoTopUp or applyTopUpTransaction) that
accepts the identifying tradeNo and any source-specific metadata, performs the
FOR UPDATE lookup on TopUp (mirroring the current refCol logic), validates
status, computes quotaToAdd, updates TopUp (CompleteTime/Status) and increments
User.quota inside one DB.Transaction, and returns the updated TopUp, quotaToAdd
and an error; then simplify RechargeWaffo and RechargeWaffoPancake to call this
helper and only handle post-success logging (RecordLog) and error mapping.
Ensure the helper preserves existing behaviors: PostgreSQL quoting, idempotent
success return (no-op if already success), same error messages, and calling
tx.Save / tx.Model updates as currently implemented.

In `@web/src/components/topup/RechargeCard.jsx`:
- Line 426: The empty catch in RechargeCard.jsx silently swallows errors when
computing usdRate (the try-catch around the usdRate assignment), so either
remove the try-catch if you intend to always use the fallback usdRate = 7, or
log the error inside the catch (e.g., console.error or your app logger) and then
keep the fallback; update the catch block in the function/component that sets
usdRate to include a meaningful log message (reference variable usdRate and the
try block that computes it) rather than an empty catch.

In `@web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx`:
- Around line 300-307: The Form.InputNumber for WaffoPancakeUnitPrice currently
sets min={0} which contradicts the form submit validation that rejects values <=
0; update the InputNumber to use min={0.01} so the UI prevents zero input and
matches the submit-side check (locate the Form.InputNumber with
field='WaffoPancakeUnitPrice' in SettingsPaymentGatewayWaffoPancake.jsx and
align it with the submit validation logic that enforces > 0).
- Around line 180-193: The current bulk update uses Promise.all with options.map
and then filters results into errorResults and early returns, which can leave
partial updates and skip props.refresh?.() and the success toast; change the
implementation to use Promise.allSettled for the API.put calls (or perform them
sequentially if you require atomicity), inspect settled results to separate
fulfilled vs rejected (check result.status and result.value.data.success), call
showError for each failure and show a single success toast if at least one
update succeeded, then always call props.refresh?.() so the UI is consistent;
update references to results, errorResults, API.put, showError, and
props.refresh?.() in the function accordingly.
- Around line 95-98: The WaffoPancakeMinTopUp value is an integer count but
currently uses parseFloat; change the parsing to parseInt with an explicit radix
(10) so the field is interpreted as an integer and falls back to 1 when
undefined or NaN (update the code around WaffoPancakeMinTopUp in
SettingsPaymentGatewayWaffoPancake.jsx to use
parseInt(props.options.WaffoPancakeMinTopUp, 10) and ensure a default of 1 if
the parsed result is NaN).
🪄 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: b7f7f4f5-7cf3-41e1-8a73-b58cab233604

📥 Commits

Reviewing files that changed from the base of the PR and between 495e4f5 and 4072891.

📒 Files selected for processing (12)
  • controller/topup.go
  • controller/topup_waffo_pancake.go
  • model/option.go
  • model/topup.go
  • router/api-router.go
  • service/waffo_pancake.go
  • setting/payment_waffo_pancake.go
  • web/src/components/settings/PaymentSetting.jsx
  • web/src/components/topup/RechargeCard.jsx
  • web/src/components/topup/index.jsx
  • web/src/i18n/locales/zh-CN.json
  • web/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsx

Comment on lines +218 to +219
LockOrder(tradeNo)
defer UnlockOrder(tradeNo)
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how other webhook handlers in the codebase handle locking
rg -n -A5 "LockOrder|UnlockOrder" --type=go

Repository: QuantumNous/new-api

Length of output: 5240


🏁 Script executed:

# View the complete LockOrder/UnlockOrder implementation
sed -n '278,310p' controller/topup.go

Repository: QuantumNous/new-api

Length of output: 678


🏁 Script executed:

# Check if there's documentation or deployment configuration
fd -e md -e yaml -e yml -e json -e toml | head -20

Repository: QuantumNous/new-api

Length of output: 459


🏁 Script executed:

# Search for deployment or distributed deployment references
rg -i "(distributed|deployment|multi.instance|cluster)" --type=go -l | head -10

Repository: QuantumNous/new-api

Length of output: 312


🏁 Script executed:

# Check if Redis is configured and used elsewhere in the codebase
rg -i "redis" --type=go -l | head -10

Repository: QuantumNous/new-api

Length of output: 282


🏁 Script executed:

# Check docker-compose.yml for multi-instance setup
cat docker-compose.yml

Repository: QuantumNous/new-api

Length of output: 3338


🏁 Script executed:

# Check README for deployment guidance
head -100 README.md | grep -i -A5 "deploy\|instance\|distributed"

Repository: QuantumNous/new-api

Length of output: 201


🏁 Script executed:

# Search for any comments about single-instance assumptions in topup.go
rg -B5 -A5 "LockOrder|orderLocks" controller/topup.go | head -50

Repository: QuantumNous/new-api

Length of output: 953


In-process locking is insufficient for distributed deployments.

The LockOrder/UnlockOrder implementation uses a process-local sync.Map. The docker-compose.yml includes a comment indicating multi-node deployment is a supported scenario (SESSION_SECRET=random_string # multi-node deployment, set this to a random string). If multiple instances are deployed, concurrent webhook requests for the same tradeNo across instances can race.

This pattern is pervasive across all webhook handlers (Epay, Stripe, Creem, Waffo, etc.). For multi-instance deployments, switch to database-level locking (e.g., SELECT ... FOR UPDATE) or Redis-based locking (Redis is already in the stack). If single-instance deployment is the only supported mode, explicitly document this constraint.

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

In `@controller/topup_waffo_pancake.go` around lines 218 - 219, The current
in-process LockOrder/UnlockOrder (used in handlers like the Waffo webhook where
LockOrder(tradeNo) and defer UnlockOrder(tradeNo) are called) uses a
process-local sync.Map and won’t prevent races in multi-instance deployments;
replace this with a distributed lock (preferred: Redis-based lock using the
existing Redis in the stack, or DB row-level locking via a SELECT ... FOR UPDATE
inside a transaction that scopes the webhook processing) so that concurrent
webhook requests for the same tradeNo across instances are serialized;
alternatively, if you intend to keep the process-local LockOrder/UnlockOrder,
add clear documentation of the single-instance deployment constraint and change
usages (e.g., in the Waffo handler) to use the new distributed lock API instead
of LockOrder/UnlockOrder.

Comment on lines +82 to +86
enableWaffoPancake := setting.WaffoPancakeEnabled &&
strings.TrimSpace(setting.WaffoPancakeMerchantID) != "" &&
strings.TrimSpace(setting.WaffoPancakePrivateKey) != "" &&
strings.TrimSpace(setting.WaffoPancakeStoreID) != "" &&
strings.TrimSpace(setting.WaffoPancakeProductID) != ""
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

Include webhook public-key readiness in enableWaffoPancake gating.

Current checks only validate merchant/private/store/product IDs. If webhook verify key is missing, users can pay but callback verification may fail and quota won’t be credited.

🔐 Suggested gating update
 enableWaffoPancake := setting.WaffoPancakeEnabled &&
   strings.TrimSpace(setting.WaffoPancakeMerchantID) != "" &&
   strings.TrimSpace(setting.WaffoPancakePrivateKey) != "" &&
   strings.TrimSpace(setting.WaffoPancakeStoreID) != "" &&
-  strings.TrimSpace(setting.WaffoPancakeProductID) != ""
+  strings.TrimSpace(setting.WaffoPancakeProductID) != "" &&
+  ((!setting.WaffoPancakeSandbox &&
+    strings.TrimSpace(setting.WaffoPancakeProdWebhookPublicKey) != "") ||
+    (setting.WaffoPancakeSandbox &&
+      strings.TrimSpace(setting.WaffoPancakeTestWebhookPublicKey) != ""))
📝 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
enableWaffoPancake := setting.WaffoPancakeEnabled &&
strings.TrimSpace(setting.WaffoPancakeMerchantID) != "" &&
strings.TrimSpace(setting.WaffoPancakePrivateKey) != "" &&
strings.TrimSpace(setting.WaffoPancakeStoreID) != "" &&
strings.TrimSpace(setting.WaffoPancakeProductID) != ""
enableWaffoPancake := setting.WaffoPancakeEnabled &&
strings.TrimSpace(setting.WaffoPancakeMerchantID) != "" &&
strings.TrimSpace(setting.WaffoPancakePrivateKey) != "" &&
strings.TrimSpace(setting.WaffoPancakeStoreID) != "" &&
strings.TrimSpace(setting.WaffoPancakeProductID) != "" &&
((!setting.WaffoPancakeSandbox &&
strings.TrimSpace(setting.WaffoPancakeProdWebhookPublicKey) != "") ||
(setting.WaffoPancakeSandbox &&
strings.TrimSpace(setting.WaffoPancakeTestWebhookPublicKey) != ""))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@controller/topup.go` around lines 82 - 86, The enableWaffoPancake boolean
currently only checks merchant/private/store/product IDs; include the webhook
public-key readiness in this gating by adding a non-empty trimmed check for the
webhook public key (e.g. strings.TrimSpace(setting.WaffoPancakeWebhookPublicKey)
!= "") into the same expression that defines enableWaffoPancake so callback
verification won't be skipped when the key is missing; update the
enableWaffoPancake assignment accordingly (and optionally add a log/warning
elsewhere if the webhook key is missing).

Comment on lines +154 to +158
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request Waffo Pancake checkout session: %w", err)
}
defer resp.Body.Close()
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

HTTP client without timeout may hang indefinitely.

http.DefaultClient has no timeout configured. If the Waffo Pancake API is slow or unresponsive, the goroutine will block indefinitely, potentially exhausting resources.

Proposed fix: Use a client with timeout
+var waffoPancakeHTTPClient = &http.Client{
+	Timeout: 30 * time.Second,
+}
+
 func CreateWaffoPancakeCheckoutSession(ctx context.Context, params *WaffoPancakeCreateSessionParams) (*WaffoPancakeCheckoutSession, error) {
 	// ... existing code ...
 
-	resp, err := http.DefaultClient.Do(req)
+	resp, err := waffoPancakeHTTPClient.Do(req)
 	if err != nil {
 		return nil, fmt.Errorf("request Waffo Pancake checkout session: %w", err)
 	}
📝 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
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request Waffo Pancake checkout session: %w", err)
}
defer resp.Body.Close()
var waffoPancakeHTTPClient = &http.Client{
Timeout: 30 * time.Second,
}
func CreateWaffoPancakeCheckoutSession(ctx context.Context, params *WaffoPancakeCreateSessionParams) (*WaffoPancakeCheckoutSession, error) {
// ... existing code ...
resp, err := waffoPancakeHTTPClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request Waffo Pancake checkout session: %w", err)
}
defer resp.Body.Close()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/waffo_pancake.go` around lines 154 - 158, The code is using
http.DefaultClient.Do(req) which has no timeout and can hang; replace it by
creating a dedicated http.Client with a sensible Timeout (e.g., 10s) and use
client.Do(req) instead of http.DefaultClient.Do; update the scope where resp and
err are obtained (the block using req, resp, err and defer resp.Body.Close()) so
it uses this new client (refer to the variables req, resp, err and the call
currently to http.DefaultClient.Do) and ensure any context-based cancellation is
preserved if the surrounding function accepts a context.

Comment on lines +173 to +181
func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string) (*waffoPancakeWebhookEvent, error) {
return verifyWaffoPancakeWebhook(payload, signatureHeader, waffoPancakeWebhookVerifyOptions{
toleranceMs: waffoPancakeDefaultTolerance.Milliseconds(),
publicKeys: waffoPancakeWebhookPublicKeys{
Test: setting.WaffoPancakeTestWebhookPublicKey,
Prod: setting.WaffoPancakeProdWebhookPublicKey,
},
})
}
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

Critical: Sandbox mode setting is ignored in webhook verification.

VerifyConfiguredWaffoPancakeWebhook does not pass setting.WaffoPancakeSandbox to determine the environment. The opts.environment field is left empty, causing verifyWaffoPancakeWebhookWithResolvedKeys (lines 334-343) to fall back to trying both production and test keys.

This allows an attacker to forge webhooks using the hardcoded test public key (lines 25-33) even when the system is configured for production mode, potentially crediting accounts without actual payment.

🔒 Proposed fix: Pass sandbox setting to determine environment
 func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string) (*waffoPancakeWebhookEvent, error) {
+	env := "prod"
+	if setting.WaffoPancakeSandbox {
+		env = "test"
+	}
 	return verifyWaffoPancakeWebhook(payload, signatureHeader, waffoPancakeWebhookVerifyOptions{
+		environment: env,
 		toleranceMs: waffoPancakeDefaultTolerance.Milliseconds(),
 		publicKeys: waffoPancakeWebhookPublicKeys{
 			Test: setting.WaffoPancakeTestWebhookPublicKey,
 			Prod: setting.WaffoPancakeProdWebhookPublicKey,
 		},
 	})
 }
📝 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
func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string) (*waffoPancakeWebhookEvent, error) {
return verifyWaffoPancakeWebhook(payload, signatureHeader, waffoPancakeWebhookVerifyOptions{
toleranceMs: waffoPancakeDefaultTolerance.Milliseconds(),
publicKeys: waffoPancakeWebhookPublicKeys{
Test: setting.WaffoPancakeTestWebhookPublicKey,
Prod: setting.WaffoPancakeProdWebhookPublicKey,
},
})
}
func VerifyConfiguredWaffoPancakeWebhook(payload string, signatureHeader string) (*waffoPancakeWebhookEvent, error) {
env := "prod"
if setting.WaffoPancakeSandbox {
env = "test"
}
return verifyWaffoPancakeWebhook(payload, signatureHeader, waffoPancakeWebhookVerifyOptions{
environment: env,
toleranceMs: waffoPancakeDefaultTolerance.Milliseconds(),
publicKeys: waffoPancakeWebhookPublicKeys{
Test: setting.WaffoPancakeTestWebhookPublicKey,
Prod: setting.WaffoPancakeProdWebhookPublicKey,
},
})
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/waffo_pancake.go` around lines 173 - 181,
VerifyConfiguredWaffoPancakeWebhook currently omits the sandbox environment flag
so verifyWaffoPancakeWebhook falls back to trying both keys; update
VerifyConfiguredWaffoPancakeWebhook to set opts.environment using
setting.WaffoPancakeSandbox (or equivalent) so the
waffoPancakeWebhookVerifyOptions.environment field is populated, ensuring
verifyWaffoPancakeWebhookWithResolvedKeys uses only the correct environment
(Test or Prod) when resolving publicKeys and prevents acceptance of the
hardcoded test key in production.

Comment on lines +334 to +343
func verifyWaffoPancakeWebhookWithResolvedKeys(signatureInput string, signaturePart string, opts waffoPancakeWebhookVerifyOptions) error {
if opts.environment == "prod" || opts.environment == "test" {
return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys))
}

if err := verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey("prod", opts.publicKeys)); err == nil {
return nil
}
return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey("test", opts.publicKeys))
}
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

Fallback to both keys weakens security guarantees.

When opts.environment is empty (which is always the case currently), the code tries the production key first, then falls back to the test key. This means a webhook signed with the test key will be accepted in production.

Even after fixing VerifyConfiguredWaffoPancakeWebhook to pass the environment, this fallback logic should be removed to enforce strict environment separation.

🔒 Proposed fix: Remove fallback, require explicit environment
 func verifyWaffoPancakeWebhookWithResolvedKeys(signatureInput string, signaturePart string, opts waffoPancakeWebhookVerifyOptions) error {
-	if opts.environment == "prod" || opts.environment == "test" {
-		return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys))
+	if opts.environment == "" {
+		return fmt.Errorf("webhook environment not specified")
 	}
-
-	if err := verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey("prod", opts.publicKeys)); err == nil {
-		return nil
-	}
-	return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey("test", opts.publicKeys))
+	return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys))
 }
📝 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
func verifyWaffoPancakeWebhookWithResolvedKeys(signatureInput string, signaturePart string, opts waffoPancakeWebhookVerifyOptions) error {
if opts.environment == "prod" || opts.environment == "test" {
return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys))
}
if err := verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey("prod", opts.publicKeys)); err == nil {
return nil
}
return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey("test", opts.publicKeys))
}
func verifyWaffoPancakeWebhookWithResolvedKeys(signatureInput string, signaturePart string, opts waffoPancakeWebhookVerifyOptions) error {
if opts.environment == "" {
return fmt.Errorf("webhook environment not specified")
}
return verifyWaffoPancakeWebhookWithKey(signatureInput, signaturePart, resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys))
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@service/waffo_pancake.go` around lines 334 - 343, The current
verifyWaffoPancakeWebhookWithResolvedKeys function falls back to test when
opts.environment is empty, allowing test-signed webhooks in prod; change it to
require an explicit environment and remove the prod->test fallback: have
verifyWaffoPancakeWebhookWithResolvedKeys accept only "prod" or "test" (return a
clear error for any other/empty value), call
resolveWaffoPancakeWebhookPublicKey(opts.environment, opts.publicKeys) once and
pass that to verifyWaffoPancakeWebhookWithKey, and ensure callers (e.g.,
VerifyConfiguredWaffoPancakeWebhook) always provide opts.environment.

});

setInputs(newInputs);
setInputs((prev) => ({ ...prev, ...newInputs }));
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

Avoid preserving stale Waffo Pancake credentials when option payload is partial.

setInputs((prev) => ({ ...prev, ...newInputs })) keeps old values for keys omitted by the backend response, which can accidentally resubmit outdated merchant/private key settings.

🔧 Suggested safe merge for Waffo Pancake keys
-      setInputs((prev) => ({ ...prev, ...newInputs }));
+      setInputs((prev) => ({
+        ...prev,
+        WaffoPancakeEnabled: false,
+        WaffoPancakeSandbox: false,
+        WaffoPancakeMerchantID: '',
+        WaffoPancakePrivateKey: '',
+        WaffoPancakeStoreID: '',
+        WaffoPancakeProductID: '',
+        WaffoPancakeReturnURL: '',
+        WaffoPancakeCurrency: 'USD',
+        WaffoPancakeUnitPrice: 1.0,
+        WaffoPancakeMinTopUp: 1,
+        WaffoPancakeProdWebhookPublicKey: '',
+        WaffoPancakeTestWebhookPublicKey: '',
+        ...newInputs,
+      }));
📝 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
setInputs((prev) => ({ ...prev, ...newInputs }));
setInputs((prev) => ({
...prev,
WaffoPancakeEnabled: false,
WaffoPancakeSandbox: false,
WaffoPancakeMerchantID: '',
WaffoPancakePrivateKey: '',
WaffoPancakeStoreID: '',
WaffoPancakeProductID: '',
WaffoPancakeReturnURL: '',
WaffoPancakeCurrency: 'USD',
WaffoPancakeUnitPrice: 1.0,
WaffoPancakeMinTopUp: 1,
WaffoPancakeProdWebhookPublicKey: '',
WaffoPancakeTestWebhookPublicKey: '',
...newInputs,
}));
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@web/src/components/settings/PaymentSetting.jsx` at line 140, The current
updater setInputs((prev) => ({ ...prev, ...newInputs })) preserves prior values
for keys that the backend omits, which can re-submit stale Waffo Pancake
credentials; change the update logic in the PaymentSetting component so that
when handling Waffo Pancake payloads you either replace the credentials object
entirely or explicitly clear sensitive fields omitted by newInputs (e.g.,
merchantKey, privateKey) instead of merging with prev—locate the call to
setInputs and make the merge conditional: if newInputs contains a Waffo Pancake
credentials object, use that object as the source of truth (or set omitted
sensitive keys to null/empty) rather than shallow-merging with prev.

"充值完成后跳回的页面": "充值完成后跳回的页面",
"启用后会按测试环境保存这组配置": "启用后会按测试环境保存这组配置",
"更新 Waffo Pancake 设置": "更新 Waffo Pancake 设置",
"未启用": "未启用",
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

Remove duplicated translation key 未启用.

This key is already defined earlier in the same JSON. Duplicate keys make resolution order-dependent and can cause confusing i18n behavior/lint failures.

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

In `@web/src/i18n/locales/zh-CN.json` at line 2345, Remove the duplicate
translation entry for the key "未启用" so only a single mapping for that key
remains in the JSON; locate both occurrences of the string "未启用" in the locale
file, delete the later duplicate (or merge if translations differ), and validate
the JSON afterwards to ensure no trailing commas or syntax errors.

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.

1 participant