-
-
Notifications
You must be signed in to change notification settings - Fork 2.5k
新增 creem 支付 #1823
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
新增 creem 支付 #1823
Conversation
WalkthroughAdds a Creem payment integration: backend controller and webhook handlers, a Creem adaptor and checkout flow, a model RechargeCreem function, new settings and option keys, API routes for payment and webhook, top-up response exposure, and an admin web UI plus i18n entries for Creem configuration. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant FE as Frontend (Top-up)
participant BE as Controller (RequestCreemPay)
participant S as Settings
participant DB as DB
participant Creem as Creem API
participant WH as Controller (CreemWebhook)
participant M as Model (RechargeCreem)
User->>FE: select Creem product & pay
FE->>BE: POST /api/user/creem/pay {productId, paymentMethod:"creem"}
BE->>S: read Creem settings & products
BE->>DB: create pending TopUp (referenceId)
BE->>Creem: create checkout (x-api-key, metadata)
Creem-->>BE: {checkout_url, order_id}
BE-->>FE: {checkout_url, order_id}
note over User,Creem: User completes payment at Creem
Creem-->>WH: POST /api/creem/webhook (checkout.completed)
WH->>WH: verify signature (if configured)
WH->>DB: find TopUp by referenceId
alt pending & paid
WH->>M: RechargeCreem(referenceId,email,name)
M->>DB: tx: mark success, add quota, set email if empty
DB-->>M: ok
M-->>WH: ok
else invalid
WH-->>Creem: ignore / 400
end
sequenceDiagram
autonumber
actor Admin
participant FE as Web (SettingsPaymentGatewayCreem)
participant API as Backend Options API
participant SET as setting / model.option
Admin->>FE: edit Creem settings/products
FE->>API: PUT options {CreemApiKey, CreemWebhookSecret, CreemTestMode, CreemProducts}
API->>SET: updateOptionMap(...)
SET-->>API: persisted
API-->>FE: per-option success responses
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ 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.
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 (10)
setting/payment_creem.go (1)
1-6: Consider adding validation constants and documentation.The configuration variables lack documentation and validation constants. Consider adding comments to explain their purpose and define validation constants for better maintainability.
package setting +// Creem payment gateway configuration +const ( + // DefaultCreemProducts represents the default empty product list + DefaultCreemProducts = "[]" +) + +// CreemApiKey is the API key for authenticating with Creem payment gateway var CreemApiKey = "" -var CreemProducts = "[]" + +// CreemProducts is a JSON string containing the list of available Creem products +var CreemProducts = DefaultCreemProducts + +// CreemTestMode enables test mode for Creem payment processing var CreemTestMode = false + +// CreemWebhookSecret is used to verify webhook signatures from Creem var CreemWebhookSecret = ""web/src/pages/TopUp/index.js (1)
304-346: Consider adding error boundaries for payment flows.The Creem payment implementation mirrors the Stripe flow well. However, both implementations open new tabs/windows without handling potential popup blocker scenarios.
const processCreemCallback = (data) => { - // 与 Stripe 保持一致的实现方式 - window.open(data.checkout_url, '_blank'); + // 与 Stripe 保持一致的实现方式 + const newWindow = window.open(data.checkout_url, '_blank'); + if (!newWindow || newWindow.closed || typeof newWindow.closed === 'undefined') { + showError(t('请允许弹出窗口以完成支付')); + } };Apply the same improvement to
processStripeCallbackfor consistency.router/api-router.go (1)
42-42: Security: Creem webhook is signature-verified in handler — consider optional hardeningCreemWebhook in controller/topup_creem.go calls verifyCreemSignature(string(bodyBytes), signature, setting.CreemWebhookSecret) and aborts with 401 on failure.
- Confirm verifyCreemSignature uses a secure HMAC and constant-time comparison and that the webhook secret is stored/rotated securely.
- Optional: add middleware for rate-limiting / IP allowlist / centralized logging or extract the signature check into reusable middleware.
controller/misc.go (1)
77-78: Validate CreemProducts JSON before exposing to the frontend.controller/misc.go currently uses a string check (setting.CreemProducts != "[]") which doesn't validate JSON; invalid JSON can reach the frontend. controller/topup_creem.go already json.Unmarshals CreemProducts and handles errors, but misc.go should parse/validate before setting the enable flag.
- "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]", + "enable_creem_topup": isCreemEnabled(), "creem_products": setting.CreemProducts,Suggested helper (add to an appropriate package/file and import encoding/json):
func isCreemEnabled() bool { if setting.CreemApiKey == "" { return false } var products []interface{} if err := json.Unmarshal([]byte(setting.CreemProducts), &products); err != nil { return false } return len(products) > 0 }model/topup.go (1)
136-154: Comment drift.Comment says “优先使用邮箱,如果邮箱为空则使用用户名” but only email is considered. Align the comment or implement the username fallback.
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (3)
78-85: Secrets cannot be cleared; send only if changed (including empty).Currently secrets are only sent if non-empty, preventing clearing. Compare with origin and push when changed.
- if (inputs.CreemApiKey && inputs.CreemApiKey !== '') { - options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey }); - } + if (inputs.CreemApiKey !== originInputs.CreemApiKey) { + options.push({ key: 'CreemApiKey', value: inputs.CreemApiKey }); + } @@ - if (inputs.CreemWebhookSecret && inputs.CreemWebhookSecret !== '') { - options.push({ key: 'CreemWebhookSecret', value: inputs.CreemWebhookSecret }); - } + if (inputs.CreemWebhookSecret !== originInputs.CreemWebhookSecret) { + options.push({ key: 'CreemWebhookSecret', value: inputs.CreemWebhookSecret }); + }
86-91: Keep origin snapshot in sync with saved products.Persist the JSON you actually saved so change detection stays correct.
- options.push({ key: 'CreemProducts', value: JSON.stringify(products) }); + const productsJson = JSON.stringify(products); + options.push({ key: 'CreemProducts', value: productsJson }); @@ - setOriginInputs({ ...inputs }); + setOriginInputs({ ...inputs, CreemProducts: productsJson });Also applies to: 110-113
195-196: Normalize price display.Ensure two decimals for currency display.
- render: (price, record) => `${record.currency === 'EUR' ? '€' : '$'}${price}`, + render: (price, record) => `${record.currency === 'EUR' ? '€' : '$'}${Number(price).toFixed(2)}`,controller/topup_creem.go (2)
97-105: Harden request path: auth check and reduce noisy logs.Verify user id exists before creating orders; avoid logging entire request bodies.
- id := c.GetInt("id") + id := c.GetInt("id") + if id <= 0 { + c.JSON(http.StatusUnauthorized, gin.H{"message": "error", "data": "未登录"}) + return + }Optionally drop the body echo in RequestCreemPay to DEBUG only.
Also applies to: 140-164
371-399: Set Accept header and shorter user-agent (optional).Some APIs require Accept; adding a UA aids vendor debugging.
req.Header.Set("Content-Type", "application/json") req.Header.Set("x-api-key", setting.CreemApiKey) + req.Header.Set("Accept", "application/json") + req.Header.Set("User-Agent", "new-api/creem-integration")
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
controller/misc.go(1 hunks)controller/topup_creem.go(1 hunks)model/option.go(2 hunks)model/topup.go(1 hunks)router/api-router.go(2 hunks)setting/payment_creem.go(1 hunks)web/src/components/settings/PaymentSetting.js(4 hunks)web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js(1 hunks)web/src/pages/TopUp/index.js(8 hunks)
🧰 Additional context used
🧬 Code graph analysis (8)
router/api-router.go (2)
controller/topup_creem.go (2)
CreemWebhook(237-284)RequestCreemPay(140-164)middleware/rate-limit.go (1)
CriticalRateLimit(103-105)
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (2)
web/src/components/settings/PaymentSetting.js (1)
inputs(13-33)web/src/helpers/utils.js (3)
showError(94-123)showSuccess(129-131)a(231-231)
model/option.go (2)
common/constants.go (1)
OptionMap(36-36)setting/payment_creem.go (4)
CreemApiKey(3-3)CreemProducts(4-4)CreemTestMode(5-5)CreemWebhookSecret(6-6)
web/src/components/settings/PaymentSetting.js (1)
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (2)
SettingsPaymentGatewayCreem(25-387)inputs(28-33)
web/src/pages/TopUp/index.js (3)
web/src/helpers/utils.js (1)
showError(94-123)web/src/helpers/api.js (6)
res(186-186)res(187-187)res(228-228)res(229-229)API(5-13)API(5-13)web/src/helpers/render.js (1)
renderQuotaWithAmount(856-864)
model/topup.go (7)
common/database.go (1)
UsingPostgreSQL(10-10)model/main.go (1)
DB(63-63)common/constants.go (2)
TopUpStatusPending(198-198)TopUpStatusSuccess(199-199)common/utils.go (1)
GetTimestamp(192-194)model/user.go (1)
User(18-47)model/log.go (1)
RecordLog(76-92)common/logger.go (1)
FormatQuota(107-113)
controller/misc.go (1)
setting/payment_creem.go (2)
CreemApiKey(3-3)CreemProducts(4-4)
controller/topup_creem.go (5)
setting/payment_creem.go (4)
CreemProducts(4-4)CreemWebhookSecret(6-6)CreemApiKey(3-3)CreemTestMode(5-5)model/user.go (1)
GetUserById(224-236)common/hash.go (1)
Sha1(22-24)model/topup.go (3)
TopUp(11-20)GetTopUpByTradeNo(44-52)RechargeCreem(102-171)common/constants.go (1)
TopUpStatusPending(198-198)
🔇 Additional comments (14)
router/api-router.go (1)
68-68: Good use of rate limiting for payment endpoints.The use of
CriticalRateLimitmiddleware for the Creem payment endpoint is appropriate and consistent with other payment methods.model/option.go (2)
84-87: CreemTestMode boolean-string conversion is handled correctly.The implementation properly converts CreemTestMode between boolean and string representations for storage and retrieval, maintaining consistency with other boolean options.
333-340: LGTM! Proper handling of Creem configuration updates.The update logic correctly handles all Creem-related settings, including the string-to-boolean conversion for CreemTestMode.
web/src/pages/TopUp/index.js (4)
449-456: Good defensive JSON parsing with fallback.The code properly handles potential JSON parsing errors for CreemProducts with a safe fallback to an empty array.
1017-1026: Banner logic correctly handles all payment methods.The warning banner now properly checks all three payment methods (online, Stripe, and Creem) before displaying the "no payment methods enabled" warning.
1182-1253: UI implementation for Creem products is well-structured.The Creem product selection UI is clean, responsive, and consistent with the overall design. The separate desktop and mobile layouts ensure good user experience across devices.
1421-1424: Good attention to mobile UX with bottom spacing.Adding bottom spacing to prevent content from being hidden behind the fixed payment area shows attention to detail.
model/topup.go (2)
115-131: Row-level lock style is legacy gorm; confirm on v2.
Set("gorm:query_option", "FOR UPDATE")is a v1 idiom. If you’re on gorm v2, preferClauses(clause.Locking{Strength: "UPDATE"}). If the codebase standardizes on the current pattern, keep as-is.Do you want me to scan the repo to confirm gorm version and lock usage consistency?
168-169: LGTM on transactional integrity and idempotency.Status check + FOR UPDATE + single commit give good idempotency and race safety for webhook replays.
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (1)
47-67: LGTM on initial load and robust product parsing.Good defensive JSON parse with fallback and proper form hydration.
controller/topup_creem.go (4)
287-308: Consider validating paid amount/product against the pending order.Before crediting, compare webhook
order.amount_paid/product.idwith the pendingTopUp.Money/Amountto detect mismatches or tampering.Do you want me to add a guarded check (with currency minor-unit handling) that fails fast on discrepancies?
Also applies to: 333-351
131-138: LGTM on order creation and reference binding.Binding
request_idto our hashed reference and locking on webhook side gives a clean idempotent path.
347-349: Update call site after removingcustomerNamefrom model.If you accept the model signature change, adjust here.
- err := model.RechargeCreem(referenceId, customerEmail, customerName) + err := model.RechargeCreem(referenceId, customerEmail)
22-25: Constant naming LGTM.Clear, scoped, and consistent with existing patterns.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 3
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
controller/topup_creem.go(1 hunks)model/topup.go(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
model/topup.go (7)
common/database.go (1)
UsingPostgreSQL(10-10)model/main.go (1)
DB(63-63)common/constants.go (2)
TopUpStatusPending(198-198)TopUpStatusSuccess(199-199)common/utils.go (1)
GetTimestamp(192-194)model/user.go (1)
User(18-47)model/log.go (2)
RecordLog(76-92)LogTypeTopup(41-41)common/logger.go (1)
FormatQuota(107-113)
controller/topup_creem.go (5)
setting/payment_creem.go (4)
CreemTestMode(5-5)CreemProducts(4-4)CreemWebhookSecret(6-6)CreemApiKey(3-3)model/user.go (1)
GetUserById(224-236)common/hash.go (1)
Sha1(22-24)model/topup.go (3)
TopUp(11-20)GetTopUpByTradeNo(44-52)RechargeCreem(101-170)common/constants.go (1)
TopUpStatusPending(198-198)
🔇 Additional comments (1)
controller/topup_creem.go (1)
317-360: Remove PII from checkout-completed logs.Lines 317 and 358 still emit raw customer emails (and previously the customer name), which was already flagged earlier. These logs violate our “no PII in logs” policy. Please redact or drop the email/name before logging.
- log.Printf("处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s, 客户邮箱: %s, 产品: %s", - referenceId, - event.Object.Order.Id, - event.Object.Order.AmountPaid, - event.Object.Order.Currency, - event.Object.Customer.Email, - event.Object.Product.Name) + log.Printf("处理Creem支付完成 - 订单号: %s, Creem订单ID: %s, 支付金额: %d %s", + referenceId, + event.Object.Order.Id, + event.Object.Order.AmountPaid, + event.Object.Order.Currency) @@ - log.Printf("Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f, 客户邮箱: %s, 客户姓名: %s", - referenceId, topUp.Amount, topUp.Money, customerEmail, customerName) + log.Printf("Creem充值成功 - 订单号: %s, 充值额度: %d, 支付金额: %.2f", + referenceId, topUp.Amount, topUp.Money)
| err := model.RechargeCreem(referenceId, customerEmail, customerName) | ||
| if err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix RechargeCreem call signature.
Line 351 still invokes model.RechargeCreem with three parameters, but the function now takes only (referenceId, customerEmail). This won’t compile. Drop the extra argument.
- err := model.RechargeCreem(referenceId, customerEmail, customerName)
+ err := model.RechargeCreem(referenceId, customerEmail)📝 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.
| err := model.RechargeCreem(referenceId, customerEmail, customerName) | |
| if err != nil { | |
| err := model.RechargeCreem(referenceId, customerEmail) | |
| if err != nil { | |
| // handle error... | |
| } |
🤖 Prompt for AI Agents
In controller/topup_creem.go around lines 351 to 352, the call to
model.RechargeCreem is passing three arguments but the function signature now
accepts only (referenceId, customerEmail); remove the extra customerName
argument so the call matches the updated signature and the code will compile.
| log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s", | ||
| apiUrl, product.ProductId, email, referenceId) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redact email in payment request log.
Line 422 logs the payer’s email address when hitting the Creem API. That’s PII we don’t want in logs; omit or mask it.
- log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s",
- apiUrl, product.ProductId, email, referenceId)
+ log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 订单号: %s",
+ apiUrl, product.ProductId, referenceId)📝 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.
| log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s", | |
| apiUrl, product.ProductId, email, referenceId) | |
| log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 订单号: %s", | |
| apiUrl, product.ProductId, referenceId) |
🤖 Prompt for AI Agents
In controller/topup_creem.go around lines 422 to 424, the payment request log
currently includes the payer’s full email (PII); remove or mask the email before
logging. Replace the email argument with a redacted value (e.g., "[REDACTED]" or
a masked string that preserves minimal context like first character and domain)
so the log call no longer contains the full address while retaining the other
fields (apiUrl, product.ProductId, referenceId).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (8)
web/src/pages/TopUp/index.js (5)
304-311: Guard against invalid product before opening modalAdd a quick field check (productId) to fail fast and avoid opening the modal with a misconfigured product.
const creemPreTopUp = async (product) => { if (!enableCreemTopUp) { showError(t('管理员未开启 Creem 充值!')); return; } + if (!product?.productId) { + showError(t('产品配置错误,请联系管理员')); + return; + } setSelectedCreemProduct(product); setCreemOpen(true); };
551-553: Ensure Stripe amount reflects server pricingSetting stripeAmount = preset.value can display a wrong amount. Recompute via API instead.
setStripeTopUpCount(preset.value); - setStripeAmount(preset.value); + getStripeAmount(preset.value);
675-684: Modal title: avoid hardcoded $“确定要充值 $” may be misleading. Use a neutral title for Stripe, amount is shown below.
- <Modal - title={t('确定要充值 $')} + <Modal + title={t('充值确认')}
694-719: Modal title: reflect Creem context or remove $Same as Stripe: avoid the literal “$”. Prefer a generic title; details below already show currency/price.
- <Modal - title={t('确定要充值 $')} + <Modal + title={t('充值确认')}
454-461: Harden parsing of creem_productsConsider validating entries (required keys and types) after JSON.parse to prevent broken items from being clickable.
try { - const products = JSON.parse(statusState.status.creem_products || '[]'); - setCreemProducts(products); + const products = JSON.parse(statusState.status.creem_products || '[]'); + const valid = Array.isArray(products) + ? products.filter(p => p && p.name && p.productId && p.price > 0 && p.quota > 0) + : []; + setCreemProducts(valid); } catch (e) { setCreemProducts([]); }web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (3)
92-101: Batch update options to avoid partial writesMultiple parallel PUTs can leave settings inconsistent if one fails. Prefer a single batch endpoint or transactional update.
- Option A: implement API to accept an array of {key,value} and apply atomically.
- Option B: sequence requests and abort on first failure with rollback (if supported).
Also applies to: 100-118
175-179: Confirm before deleting a productAvoid accidental deletion by adding a confirmation dialog.
-const deleteProduct = (productId) => { - const newProducts = products.filter(p => p.productId !== productId); - setProducts(newProducts); -}; +const deleteProduct = (productId) => { + Modal.confirm({ + title: t('确认删除'), + content: t('删除后不可恢复,是否继续?'), + onOk: () => { + setProducts(products.filter(p => p.productId !== productId)); + }, + }); +};
78-85: Secrets cannot be cleared via UIBecause empty values are skipped, there’s no way to intentionally remove a key/secret. Confirm this is desired; otherwise, add an explicit “Clear” action.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
controller/topup_creem.go(1 hunks)model/topup.go(1 hunks)web/src/components/settings/PaymentSetting.js(4 hunks)web/src/i18n/locales/en.json(1 hunks)web/src/i18n/locales/zh.json(1 hunks)web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js(1 hunks)web/src/pages/TopUp/index.js(9 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
web/src/components/settings/PaymentSetting.js (1)
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (2)
SettingsPaymentGatewayCreem(25-385)inputs(28-33)
web/src/pages/TopUp/index.js (4)
web/src/helpers/utils.js (1)
showError(94-123)web/src/helpers/api.js (6)
res(186-186)res(187-187)res(228-228)res(229-229)API(5-13)API(5-13)web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (1)
products(35-35)web/src/helpers/render.js (1)
renderQuotaWithAmount(856-864)
web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (2)
web/src/components/settings/PaymentSetting.js (1)
inputs(13-33)web/src/helpers/utils.js (3)
showError(94-123)showSuccess(129-131)a(231-231)
model/topup.go (7)
common/database.go (1)
UsingPostgreSQL(10-10)model/main.go (1)
DB(63-63)common/constants.go (2)
TopUpStatusPending(198-198)TopUpStatusSuccess(199-199)common/utils.go (1)
GetTimestamp(192-194)model/user.go (1)
User(18-47)model/log.go (2)
RecordLog(76-92)LogTypeTopup(41-41)common/logger.go (1)
FormatQuota(107-113)
controller/topup_creem.go (6)
setting/payment_creem.go (4)
CreemTestMode(5-5)CreemProducts(4-4)CreemWebhookSecret(6-6)CreemApiKey(3-3)model/user.go (1)
GetUserById(224-236)common/hash.go (1)
Sha1(22-24)web/src/pages/TopUp/index.js (1)
TopUp(42-1548)model/topup.go (3)
TopUp(11-20)GetTopUpByTradeNo(44-52)RechargeCreem(101-169)common/constants.go (1)
TopUpStatusPending(198-198)
🔇 Additional comments (21)
model/topup.go (5)
101-169: Correct function signature based on past review.The past review comment indicates that the
RechargeCreemfunction signature was updated to take only two parameters(referenceId string, customerEmail string), but the current implementation still has three parameters includingcustomerName. This creates a compilation mismatch between the function definition and where it's called in the controller.Apply this fix:
-func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) { +func RechargeCreem(referenceId string, customerEmail string) (err error) {Additionally, update the function body to not reference the removed
customerNameparameter.
106-106: LGTM: Proper integer quota handling.Good change to use
int64for quota to maintain consistency with theTopUp.Amountfield type and avoid float precision issues.
131-133: LGTM: Direct quota assignment from amount.This approach correctly uses the integer amount directly as quota, avoiding the float conversion that could introduce precision drift. This aligns with the Creem payment model where products have fixed amounts.
139-152: LGTM: Conditional email update logic.The logic properly checks if the user's current email is empty before updating it with the customer email from the payment provider. This prevents overwriting existing user emails while still allowing new users to have their payment email set.
166-166: Remove PII from log as noted in past review.The log message still includes
customerEmailwhich is PII and should be removed based on the past review comment. Also, since thecustomerNameparameter is being removed, this log should be updated accordingly.Apply this fix:
-RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", common.FormatQuota(int(quota)), topUp.Money)) +RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", common.FormatQuota(int(quota)), topUp.Money))controller/topup_creem.go (8)
352-352: Fix function call signature mismatch.This call still passes three arguments to
model.RechargeCreem, but according to the past review comments, the function signature was updated to only accept two parameters. The call needs to match the updated signature.Apply this fix:
-err := model.RechargeCreem(referenceId, customerEmail, customerName) +err := model.RechargeCreem(referenceId, customerEmail)
155-156: Remove PII logging as noted in past review.The debug logging still prints the raw request body which may contain sensitive user information like email addresses.
Apply this fix:
-// 打印body内容 -log.Printf("creem pay request body: %s", string(bodyBytes))
423-424: Redact email in payment request log.Line 423 logs the payer's email address, which is PII that shouldn't be stored in logs.
Apply this fix:
-log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 用户邮箱: %s, 订单号: %s", - apiUrl, product.ProductId, email, referenceId) +log.Printf("发送Creem支付请求 - URL: %s, 产品ID: %s, 订单号: %s", + apiUrl, product.ProductId, referenceId)
22-25: LGTM: Well-defined constants.Good practice to define payment method and signature header constants for consistency across the codebase.
30-49: LGTM: Secure signature verification with test mode bypass.The signature verification properly uses HMAC-SHA256 and correctly implements the test mode bypass only when explicitly enabled, addressing the security concern from past reviews.
104-106: LGTM: Proper unique reference ID generation.The reference ID generation using timestamp, user ID, and random string provides good uniqueness and traceability.
378-461: LGTM: Comprehensive Creem API integration.The
genCreemLinkfunction properly handles:
- API key validation
- Test vs production environment selection
- Complete request data construction with customer info
- Proper HTTP client with timeout
- Comprehensive error handling
- Response validation
The implementation correctly accepts any 2xx status codes as mentioned in past reviews.
295-362: LGTM: Robust webhook event processing.The
handleCheckoutCompletedfunction implements proper validation:
- Order status verification (
paid)- Order type validation (
onetime)- Local order existence check
- Pending status verification to prevent duplicate processing
- Comprehensive error handling and logging
Good defensive programming practices throughout.
web/src/i18n/locales/zh.json (1)
12-14: LGTM: Proper localization additions.The new translation entries for Creem integration are properly added:
- Correct trailing comma after existing entry
- Appropriate Chinese translations for the Creem introduction and configuration tips
- Good integration with the existing localization structure
web/src/components/settings/PaymentSetting.js (2)
52-54: Fix inconsistent CreemProducts handling as noted in past review.The try-catch block doesn't actually parse JSON but just assigns the value directly, which is inconsistent with other similar cases like TopupGroupRatio.
Apply this fix:
case 'CreemProducts': - newInputs[item.key] = item.value || '[]'; + try { + newInputs[item.key] = item.value ? JSON.parse(item.value) : []; + } catch (error) { + console.error('解析CreemProducts出错:', error); + newInputs[item.key] = []; + } break;
6-6: LGTM: Clean integration of Creem settings component.The integration properly:
- Imports the new Creem settings component
- Initializes Creem-related state fields with appropriate defaults
- Renders the component in a new card with proper styling and props
The implementation follows the established pattern used for other payment gateways.
Also applies to: 30-32, 104-106
web/src/i18n/locales/en.json (1)
1784-1816: LGTM: Comprehensive English translations for Creem integration.The translation additions are well-structured and provide:
- Clear, professional English translations for all Creem-related UI elements
- Proper context-aware translations (e.g., currency symbols, examples)
- Consistent terminology throughout the Creem feature set
- Good integration with the existing translation structure
web/src/pages/TopUp/index.js (3)
313-346: Validation added for Creem payment request — looks goodChecks for selection and productId are in place; this addresses the earlier review ask.
1187-1254: Creem product grid UX is clearGood presentation of name/quota/price and consistent currency symbol handling.
313-346: Creem pay response schema matches frontend assumptions
Confirmed controller/topup_creem.go returns"message": "success"and a"data"object containingcheckout_url.web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js (1)
150-173: Basic product validation is goodPresence and >0 checks for price/quota are in place; unique productId enforced; disabling productId edit prevents accidental ID changes.
| <InputNumber | ||
| value={productForm.price} | ||
| onChange={(value) => setProductForm({ ...productForm, price: value })} | ||
| placeholder={t('例如:4.99')} | ||
| min={0.01} | ||
| precision={2} | ||
| size='large' | ||
| className='w-full' | ||
| defaultValue={4.49} | ||
| /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Controlled InputNumber should not use defaultValue
You’re providing value plus defaultValue, which is conflicting. Remove defaultValue.
<InputNumber
value={productForm.price}
onChange={(value) => setProductForm({ ...productForm, price: value })}
placeholder={t('例如:4.99')}
min={0.01}
precision={2}
size='large'
className='w-full'
- defaultValue={4.49}
/>🤖 Prompt for AI Agents
In web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.js around lines 356
to 365, the InputNumber is using both value and defaultValue which conflicts for
a controlled component; remove the defaultValue prop from the InputNumber and,
if you want an initial value, set productForm.price to 4.49 in the component's
state initialization so the input remains fully controlled.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
🧹 Nitpick comments (6)
model/topup.go (4)
110-117: Use GORM locking clause and avoid manual identifier quoting.Prefer the v2 API with explicit row locking; also drop manual backtick/quote handling for portability and clarity.
- refCol := "`trade_no`" - if common.UsingPostgreSQL { - refCol = `"trade_no"` - } - err = DB.Transaction(func(tx *gorm.DB) error { - err := tx.Set("gorm:query_option", "FOR UPDATE").Where(refCol+" = ?", referenceId).First(topUp).Error + err = DB.Transaction(func(tx *gorm.DB) error { + err := tx. + Clauses(clause.Locking{Strength: "UPDATE"}). + Where("trade_no = ?", referenceId). + First(topUp).ErrorAdd import:
import "gorm.io/gorm/clause"
135-139: Fix misleading comment to match behavior.Comment says “use username if email empty,” but code only conditionally updates email.
- // 构建更新字段,优先使用邮箱,如果邮箱为空则使用用户名 + // 构建更新字段;若用户当前邮箱为空且回调提供了邮箱,则更新邮箱
140-156: Avoid extra SELECT; conditionally set email in a single UPDATE.You can atomically update quota and set email only if blank, avoiding the read and reducing contention inside the transaction.
- // 如果有客户邮箱,尝试更新用户邮箱(仅当用户邮箱为空时) - if customerEmail != "" { - // 先检查用户当前邮箱是否为空 - var user User - err = tx.Where("id = ?", topUp.UserId).First(&user).Error - if err != nil { - return err - } - // 如果用户邮箱为空,则更新为支付时使用的邮箱 - if user.Email == "" { - updateFields["email"] = customerEmail - } - } - err = tx.Model(&User{}).Where("id = ?", topUp.UserId).Updates(updateFields).Error + // 原子更新:增加配额;若邮箱为空(或NULL)则写入支付邮箱 + err = tx.Model(&User{}). + Where("id = ?", topUp.UserId). + Updates(map[string]interface{}{ + "quota": gorm.Expr("quota + ?", quota), + "email": gorm.Expr("CASE WHEN (email = '' OR email IS NULL) AND ? <> '' THEN ? ELSE email END", customerEmail, customerEmail), + }).Error
167-167: Unify quota formatting in logs.Use the existing quota formatter for consistency with other flows.
- RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money)) + RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", logger.FormatQuota(int(quota)), topUp.Money))model/option.go (1)
341-343: Parse boolean robustly.Use
strconv.ParseBoolto accept “1/0”, “t/f”, etc., and avoid brittle string compares.-case "CreemTestMode": - setting.CreemTestMode = value == "true" +case "CreemTestMode": + if b, err2 := strconv.ParseBool(value); err2 == nil { + setting.CreemTestMode = b + }router/api-router.go (1)
42-42: Webhook hardening: consider scoped rate limit and body size limit.Public route without auth is correct for webhooks, but add a tight rate limit and max body bytes to reduce abuse; keep signature checks in controller.
Would you like a lightweight middleware snippet (per‑IP small window, e.g., 30 req/min) and a max‑bytes reader (e.g., 256KB) for this endpoint?
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
controller/topup.go(1 hunks)model/option.go(2 hunks)model/topup.go(1 hunks)router/api-router.go(2 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
router/api-router.go (1)
controller/topup_creem.go (2)
CreemWebhook(240-292)RequestCreemPay(144-167)
controller/topup.go (1)
setting/payment_creem.go (2)
CreemApiKey(3-3)CreemProducts(4-4)
model/topup.go (6)
common/database.go (1)
UsingPostgreSQL(10-10)model/main.go (1)
DB(63-63)common/constants.go (2)
TopUpStatusPending(199-199)TopUpStatusSuccess(200-200)common/utils.go (1)
GetTimestamp(272-274)model/user.go (1)
User(19-48)model/log.go (1)
RecordLog(78-94)
model/option.go (2)
common/constants.go (1)
OptionMap(36-36)setting/payment_creem.go (4)
CreemApiKey(3-3)CreemProducts(4-4)CreemTestMode(5-5)CreemWebhookSecret(6-6)
🔇 Additional comments (2)
model/option.go (1)
85-88: Wiring new Creem options into OptionMap looks good.Confirm these keys are masked/hidden appropriately in any admin UI exports and audit logs (especially CreemApiKey, CreemWebhookSecret).
model/topup.go (1)
102-106: Remove unused parametercustomerName(compile-time error) and update callers.
customerNameis not used in the function body; in Go, unused parameters cause a compile error. This was also flagged earlier and marked addressed, but it’s still present. Please drop it and adjust all call sites.Apply:
-func RechargeCreem(referenceId string, customerEmail string, customerName string) (err error) { +func RechargeCreem(referenceId string, customerEmail string) (err error) {Then verify/rewrite all usages:
#!/bin/bash # Find all call sites for RechargeCreem and show surrounding context rg -nP -C3 '\bRechargeCreem\s*\('
| "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]", | ||
| "creem_products": setting.CreemProducts, | ||
| "pay_methods": payMethods, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Compute enable_creem_topup from parsed products; return parsed products, not a JSON string.
Current check setting.CreemProducts != "[]" can be true for invalid JSON; returning a raw JSON string forces clients to re‑parse. Parse once, validate, and return typed JSON.
- "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]",
- "creem_products": setting.CreemProducts,
+ "enable_creem_topup": setting.CreemApiKey != "" && len(creemProducts) > 0,
+ "creem_products": creemProducts,Add before building data:
// at top of GetTopUpInfo
var creemProducts []CreemProduct
if err := json.Unmarshal([]byte(setting.CreemProducts), &creemProducts); err != nil {
creemProducts = nil
}Add import:
import "encoding/json"Please confirm the frontend expects an array, not a string.
🏁 Script executed:
#!/bin/bash
# Grep UI for 'creem_products' usage to confirm expected type
rg -nP -C2 '\bcreem_products\b'Length of output: 963
Return creem_products as an array and update frontend parsing
-
In
controller/topup.go, unmarshalsetting.CreemProductsbefore buildingdataand switch to:// parse once at top of GetTopUpInfo var creemProducts []CreemProduct if err := json.Unmarshal([]byte(setting.CreemProducts), &creemProducts); err != nil { creemProducts = nil } // when constructing response - "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]", - "creem_products": setting.CreemProducts, + "enable_creem_topup": setting.CreemApiKey != "" && len(creemProducts) > 0, + "creem_products": creemProducts,and add
import "encoding/json"at the top. -
In
web/src/components/topup/index.jsx(around line 383), remove the JSON.parse and consume the array directly:- const products = JSON.parse(data.creem_products || '[]'); + const products = data.creem_products || []; setCreemProducts(products);
This ensures the backend supplies a validated array and the UI no longer re-parses a JSON string.
🤖 Prompt for AI Agents
In controller/topup.go around lines 53 to 55, the code currently returns
setting.CreemProducts as a raw JSON string; unmarshal setting.CreemProducts into
a Go slice (e.g., []string) before assembling the response, add import
"encoding/json" at the top of the file, handle unmarshal errors (return or log
and fall back to empty slice) and set "creem_products" in the response map to
that slice (not the original string); also update
web/src/components/topup/index.jsx (around line ~383) to remove JSON.parse usage
and consume the array directly from the backend.
| selfRoute.POST("/amount", controller.RequestAmount) | ||
| selfRoute.POST("/stripe/pay", middleware.CriticalRateLimit(), controller.RequestStripePay) | ||
| selfRoute.POST("/stripe/amount", controller.RequestStripeAmount) | ||
| selfRoute.POST("/creem/pay", middleware.CriticalRateLimit(), controller.RequestCreemPay) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
🧩 Analysis chain
Drop full request body logging in RequestCreemPay; may leak PII.
Per snippet controller/topup_creem.go Lines 143‑166, the entire JSON body is logged unconditionally. That likely includes email/metadata. Restrict logs to minimal fields and only in test mode.
Proposed change in controller/topup_creem.go:
- // 打印body内容
- log.Printf("creem pay request body: %s", string(bodyBytes))
+ // 仅在测试模式下打印有限字段,避免泄露PII
+ if setting.CreemTestMode {
+ var peek struct {
+ ProductId string `json:"product_id"`
+ }
+ _ = json.Unmarshal(bodyBytes, &peek)
+ log.Printf("creem pay request (test) product_id=%s", peek.ProductId)
+ }Search to confirm and remove any other full-body logs:
🏁 Script executed:
#!/bin/bash
rg -nP -C1 'log\.Printf\(\s*"creem (pay request body|Webhook - Signature:)' controllerLength of output: 164
Restrict full request-body logging to test mode only
Remove the unconditional log.Printf("creem pay request body: …") in controller/topup_creem.go and replace it with a test-mode–gated, minimal-field log. For example:
@@ controller/topup_creem.go:155-157
- // 打印body内容
- log.Printf("creem pay request body: %s", string(bodyBytes))
+ if setting.CreemTestMode {
+ var peek struct {
+ ProductId string `json:"product_id"`
+ }
+ _ = json.Unmarshal(bodyBytes, &peek)
+ log.Printf("creem pay request (test) product_id=%s", peek.ProductId)
+ }This change prevents inadvertent leakage of PII in production.
🤖 Prompt for AI Agents
In router/api-router.go around line 70 and controller/topup_creem.go where the
current unconditional log.Printf("creem pay request body: …") is emitted, remove
the unconditional full-body logging and replace it with a test-mode guarded,
minimal-field log: detect test mode via the existing config/env flag (e.g., an
IsTest or APP_ENV == "test"), and only when that flag is true log a small,
non-PII subset of the request (transaction id, amount, status) or a single
masked summary; in non-test (production) flows do not log the full request
body—ensure PII fields are excluded or masked and keep the logging conditional
so production cannot leak sensitive data.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
♻️ Duplicate comments (1)
model/topup.go (1)
308-308: Remove the unusedcustomerNameparameter.The
customerNameparameter is declared but never used in the function body. Remove it from the signature and update all call sites accordingly.Based on past review comments.
🧹 Nitpick comments (1)
model/topup.go (1)
373-373: Format quota consistently with other recharge functions.Use
logger.FormatQuotato format the quota value, maintaining consistency withRecharge(line 101) andManualCompleteTopUp(line 305).Apply this diff:
- RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", quota, topUp.Money)) + RecordLog(topUp.UserId, LogTypeTopup, fmt.Sprintf("使用Creem充值成功,充值额度: %v,支付金额:%.2f", logger.FormatQuota(int(quota)), topUp.Money))
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (4)
controller/topup.go(1 hunks)model/option.go(2 hunks)model/topup.go(1 hunks)router/api-router.go(2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
- controller/topup.go
- model/option.go
- router/api-router.go
🧰 Additional context used
🧬 Code graph analysis (1)
model/topup.go (6)
common/database.go (1)
UsingPostgreSQL(10-10)model/main.go (1)
DB(64-64)common/constants.go (2)
TopUpStatusPending(200-200)TopUpStatusSuccess(201-201)common/utils.go (1)
GetTimestamp(272-274)model/user.go (1)
User(20-49)model/log.go (2)
RecordLog(81-97)LogTypeTopup(45-45)
🔇 Additional comments (1)
model/topup.go (1)
338-339: Original review comment is incorrect and based on flawed assumptions.The code is not inconsistent. The
Amountfield has intentionally different semantics across payment methods:
- Non-Creem methods (Stripe/Manual):
Amount= raw money value → multiply byQuotaPerUnitat completion- Creem:
Amount= pre-calculated quota from product configuration → use directlyIn
RequestPay(line ~105), the TopUp is created with:Amount: selectedProduct.Quota // Already final quota (int64) Money: selectedProduct.Price // Actual payment amountSince Creem products are pre-configured with both
QuotaandPricefields, the quota conversion happens once at product setup, not at transaction completion. Line 339's direct assignment is correct and the comment accurately describes this intentional difference.Likely an incorrect or invalid review comment.
| common.OptionMap["StripeWebhookSecret"] = setting.StripeWebhookSecret | ||
| common.OptionMap["StripePriceId"] = setting.StripePriceId | ||
| common.OptionMap["StripeUnitPrice"] = strconv.FormatFloat(setting.StripeUnitPrice, 'f', -1, 64) | ||
| common.OptionMap["CreemApiKey"] = setting.CreemApiKey |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
需要参考一下 setting目录下设置项的使用来,不能继续在这里堆不好维护的代码了
通过creem的产品购买功能,实现充值功能。
在支付设置新增creem配置,支持: screet , sign screet, test mode 选项,以及产品配置。
通过产品Id, 展示价格,amout 定义产品,并展示在 个人中心。
当用户点击 产品,会创建订单到 topup,通过在creem预定的product id,唤起creem 支付页面,
用户支付后,回调new-api 地址: https://my.new-api/api/creem/webhook 在回调中验证请求。
请求验证后,通过 callback中的 订单id 找到 topup 的未完成订单, 按照订单中设置 amout 值 加到 用户的 quote 上。
Summary by CodeRabbit