✨ feat: Add topup billing history with admin manual completion - #1976
Conversation
Implement comprehensive topup billing system with user history viewing and admin management capabilities.
## Features Added
### Frontend
- Add topup history modal with paginated billing records
- Display order details: trade number, payment method, amount, money, status, create time
- Implement empty state with proper illustrations
- Add payment method column with localized display (Stripe, Alipay, WeChat)
- Add admin manual completion feature for pending orders
- Add Coins icon for recharge amount display
- Integrate "Bills" button in RechargeCard header
- Optimize code quality by using shared utility functions (isAdmin)
- Extract constants for status and payment method mappings
- Use React.useMemo for performance optimization
### Backend
- Create GET `/api/user/topup/self` endpoint for user topup history with pagination
- Create POST `/api/user/topup/complete` endpoint for admin manual order completion
- Add `payment_method` field to TopUp model for tracking payment types
- Implement `GetUserTopUps` method with proper pagination and ordering
- Implement `ManualCompleteTopUp` with transaction safety and row-level locking
- Add application-level mutex locks to prevent concurrent order processing
- Record payment method in Epay and Stripe payment flows
- Ensure idempotency and data consistency with proper error handling
### Internationalization
- Add i18n keys for Chinese (zh), English (en), and French (fr)
- Support for billing-related UI text and status messages
## Technical Improvements
- Use database transactions with FOR UPDATE row-level locking
- Implement sync.Map-based mutex for order-level concurrency control
- Proper error handling and user-friendly toast notifications
- Follow existing codebase patterns for empty states and modals
- Maintain code quality with extracted render functions and constants
## Files Changed
- Backend: controller/topup.go, controller/topup_stripe.go, model/topup.go, router/api-router.go
- Frontend: web/src/components/topup/modals/TopupHistoryModal.jsx (new), web/src/components/topup/RechargeCard.jsx, web/src/components/topup/index.jsx
- i18n: web/src/i18n/locales/{zh,en,fr}.json
Allow administrators to view all platform topup orders and streamline admin-only routes. Frontend - TopupHistoryModal: dynamically switch endpoint by role - Admin → GET /api/user/topup (all orders) - Non-admin → GET /api/user/topup/self (own orders) - Use shared utils `isAdmin()`; keep logic centralized and DRY - Minor UI: set admin action button theme to outline for clarity Backend - model/topup.go: add GetAllTopUps(pageInfo) with pagination (ordered by id desc) - controller/topup.go: add GetAllTopUps handler returning PageInfo response - router/api-router.go: - Add admin route GET /api/user/topup (AdminAuth) - Move POST /api/user/topup/complete to adminRoute (keeps path stable, consolidates admin endpoints) Security/Behavior - Admin-only endpoints now reside under the admin route group with AdminAuth - No behavior change for regular users; no schema changes Affected files - model/topup.go - controller/topup.go - router/api-router.go - web/src/components/topup/modals/TopupHistoryModal.jsx
… user)
Enable searching topup records by trade_no across both admin-wide and user-only views.
Frontend
- TopupHistoryModal.jsx:
- Add search input with prefix icon (IconSearch) to filter by order number
- Send `keyword` query param to backend; works with both endpoints:
- Admin: GET /api/user/topup?p=1&page_size=10&keyword=...
- User: GET /api/user/topup/self?p=1&page_size=10&keyword=...
- Keep endpoint auto-switching based on role (isAdmin)
- Minor UI polish: outlined admin action button; keep Coins icon for amount
Backend
- model/topup.go:
- Add SearchUserTopUps(userId, keyword, pageInfo)
- Add SearchAllTopUps(keyword, pageInfo)
- Both support pagination and `trade_no LIKE %keyword%` filtering (ordered by id desc)
- controller/topup.go:
- GetUserTopUps / GetAllTopUps accept optional `keyword` and route to search functions when present
Routes
- No new endpoints; search is enabled via `keyword` on existing:
- GET /api/user/topup
- GET /api/user/topup/self
Affected files
- model/topup.go
- controller/topup.go
- web/src/components/topup/modals/TopupHistoryModal.jsx
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.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
controller/topup.go (1)
234-246: Critical: nil-pointer risk when Epay client is not configured.If GetEpayClient() returns nil, code writes "fail" but continues and calls client.Verify, causing a panic. Return early after writing the response.
Apply this diff:
if client == nil { log.Println("易支付回调失败 未找到配置信息") _, err := c.Writer.Write([]byte("fail")) - if err != nil { - log.Println("易支付回调写入失败") - return - } + if err != nil { + log.Println("易支付回调写入失败") + } + return }
🧹 Nitpick comments (7)
model/topup.go (6)
105-137: Optional: drop read-only transactions to reduce overhead.Counting + listing are independent reads; wrapping in a transaction isn’t necessary and can add lock contention.
139-166: Same as above: consider removing transaction in GetAllTopUps.
168-200: SearchUserTopUps: transaction optional; LIKE pattern OK.You can compute total and page in separate queries without a tx. Current approach is correct but can be simplified.
202-234: SearchAllTopUps: mirrors user search; transaction optional.
236-306: ManualCompleteTopUp: solid idempotence and row locking; minor GORM API tweak.
- Good: FOR UPDATE + status checks + decimal math.
- Replace raw query option with GORM clause locking for portability:
// import "gorm.io/gorm/clause" tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where(refCol+" = ?", tradeNo). First(topUp)- Both
logger.LogQuotaandlogger.FormatQuotaexist; consider standardizing on one API for quota formatting across the codebase.
14-23: Add composite and status indexes to TopUp modelGORM’s
AutoMigratealready includes&TopUp{}, soPaymentMethodwill be applied. To optimize common queries, add a composite index on(user_id, id DESC)and a single‐column index onstatus(via GORM tags in model/topup.go or a dedicated SQL migration).controller/topup.go (1)
372-389: Admin middleware in place; no changes needed.
- The route is already registered under
adminRoute.Use(middleware.AdminAuth()).Revisit lock cleanup strategy.
- Deleting the
orderLocksentry inUnlockOrdercan break mutual exclusion when a new caller races between unlock and lock.- Consider an eviction approach (TTL, background GC) or accept the map’s retention rather than removing entries directly.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (33)
controller/topup.go(2 hunks)controller/topup_stripe.go(1 hunks)model/topup.go(2 hunks)router/api-router.go(2 hunks)web/src/components/auth/LoginForm.jsx(2 hunks)web/src/components/common/examples/ChannelKeyViewExample.jsx(3 hunks)web/src/components/common/modals/SecureVerificationModal.jsx(8 hunks)web/src/components/settings/PersonalSetting.jsx(2 hunks)web/src/components/settings/SystemSetting.jsx(7 hunks)web/src/components/settings/personal/cards/AccountManagement.jsx(2 hunks)web/src/components/settings/personal/cards/NotificationSettings.jsx(2 hunks)web/src/components/setup/components/steps/DatabaseStep.jsx(1 hunks)web/src/components/table/channels/modals/EditTagModal.jsx(1 hunks)web/src/components/table/channels/modals/ModelTestModal.jsx(2 hunks)web/src/components/table/users/UsersColumnDefs.jsx(1 hunks)web/src/components/table/users/modals/ResetPasskeyModal.jsx(1 hunks)web/src/components/table/users/modals/ResetTwoFAModal.jsx(1 hunks)web/src/components/topup/RechargeCard.jsx(5 hunks)web/src/components/topup/index.jsx(5 hunks)web/src/components/topup/modals/TopupHistoryModal.jsx(1 hunks)web/src/constants/channel.constants.js(1 hunks)web/src/helpers/passkey.js(6 hunks)web/src/helpers/render.jsx(14 hunks)web/src/helpers/secureApiCall.js(2 hunks)web/src/hooks/channels/useChannelsData.jsx(1 hunks)web/src/hooks/common/useSecureVerification.jsx(6 hunks)web/src/hooks/users/useUsersData.jsx(1 hunks)web/src/i18n/locales/en.json(1 hunks)web/src/i18n/locales/fr.json(1 hunks)web/src/i18n/locales/zh.json(1 hunks)web/src/pages/Setting/Chat/SettingsChats.jsx(1 hunks)web/src/pages/Setting/Operation/SettingsLog.jsx(3 hunks)web/src/services/secureVerification.js(6 hunks)
🧰 Additional context used
🧬 Code graph analysis (14)
web/src/components/topup/index.jsx (1)
web/src/components/topup/modals/TopupHistoryModal.jsx (1)
TopupHistoryModal(56-266)
router/api-router.go (2)
controller/topup.go (3)
GetUserTopUps(318-341)GetAllTopUps(344-366)AdminCompleteTopUp(373-389)model/topup.go (2)
GetUserTopUps(105-137)GetAllTopUps(140-166)
web/src/components/settings/personal/cards/AccountManagement.jsx (1)
web/src/components/settings/PersonalSetting.jsx (2)
passkeyDeleteLoading(75-75)passkeyRegisterLoading(74-74)
web/src/components/table/channels/modals/ModelTestModal.jsx (1)
web/src/hooks/channels/useChannelsData.jsx (3)
testChannel(757-840)currentTestChannel(80-80)selectedEndpointType(87-87)
controller/topup.go (3)
model/topup.go (6)
GetUserTopUps(105-137)TopUp(13-23)SearchUserTopUps(169-200)GetAllTopUps(140-166)SearchAllTopUps(203-234)ManualCompleteTopUp(237-306)common/page_info.go (1)
GetPageQuery(41-82)common/gin.go (3)
ApiError(95-100)ApiSuccess(109-115)ApiErrorMsg(102-107)
web/src/components/topup/modals/TopupHistoryModal.jsx (1)
web/src/helpers/utils.jsx (2)
isAdmin(35-40)timestamp2string(192-218)
web/src/hooks/common/useSecureVerification.jsx (6)
web/src/components/common/examples/ChannelKeyViewExample.jsx (1)
useSecureVerification(38-56)web/src/components/table/channels/modals/EditChannelModal.jsx (1)
useSecureVerification(223-251)web/src/components/common/modals/SecureVerificationModal.jsx (2)
verificationMethods(62-62)verificationState(63-63)web/src/services/secureVerification.js (1)
SecureVerificationService(31-185)web/src/helpers/utils.jsx (2)
showError(122-151)showSuccess(157-159)web/src/helpers/secureApiCall.js (2)
error(33-33)isVerificationRequiredError(30-47)
web/src/components/settings/PersonalSetting.jsx (1)
web/src/helpers/passkey.js (5)
publicKey(55-62)publicKey(90-93)prepareCredentialCreationOptions(46-79)credential(108-108)buildRegistrationResult(105-126)
controller/topup_stripe.go (1)
common/constants.go (1)
TopUpStatusPending(200-200)
web/src/components/topup/RechargeCard.jsx (2)
web/src/components/topup/index.jsx (1)
amountLoading(69-69)web/src/helpers/render.jsx (11)
getCurrencyConfig(929-955)getCurrencyConfig(964-964)getCurrencyConfig(1133-1133)getCurrencyConfig(1392-1392)getCurrencyConfig(1492-1492)getCurrencyConfig(1689-1689)getCurrencyConfig(1833-1833)symbol(843-843)symbol(913-913)symbol(933-933)symbol(977-977)
web/src/components/auth/LoginForm.jsx (1)
web/src/helpers/passkey.js (3)
prepareCredentialRequestOptions(81-103)assertion(131-131)buildAssertionResult(128-148)
model/topup.go (8)
controller/topup.go (2)
GetUserTopUps(318-341)GetAllTopUps(344-366)model/main.go (1)
DB(63-63)common/database.go (1)
UsingPostgreSQL(10-10)common/constants.go (3)
TopUpStatusSuccess(201-201)TopUpStatusPending(200-200)QuotaPerUnit(21-21)common/utils.go (1)
GetTimestamp(272-274)model/user.go (1)
User(19-48)model/log.go (2)
RecordLog(78-94)LogTypeTopup(43-43)logger/logger.go (1)
FormatQuota(122-146)
web/src/services/secureVerification.js (1)
web/src/helpers/passkey.js (4)
isPasskeySupported(150-177)publicKey(55-62)publicKey(90-93)prepareCredentialRequestOptions(81-103)
web/src/pages/Setting/Chat/SettingsChats.jsx (1)
web/src/components/table/channels/modals/EditChannelModal.jsx (1)
isEdit(122-122)
🔇 Additional comments (19)
web/src/components/common/examples/ChannelKeyViewExample.jsx (1)
72-74: Formatting tidy-up looks good.Condensing the button props keeps things consistent without altering behavior. Nicely done.
web/src/components/table/channels/modals/EditTagModal.jsx (1)
122-134: LGTM! Formatting improvement enhances readability.The multi-line array formatting makes the model list more readable and easier to maintain. Future additions or removals will also produce cleaner git diffs.
web/src/components/table/channels/modals/ModelTestModal.jsx (3)
70-73: LGTM! Formatting improves readability.The multi-line formatting makes the long label string more readable.
75-78: LGTM! Consistent with the gemini option formatting.The multi-line formatting improves readability and maintains consistency with the previous option.
175-181: LGTM! Multi-line formatting improves readability.The expanded function call with trailing comma is more readable and will produce cleaner diffs if arguments are added or modified in the future.
web/src/components/settings/personal/cards/NotificationSettings.jsx (1)
624-626: LGTM! Minor i18n and formatting improvements.The changes improve consistency in the Gotify configuration section:
- Line 624-626: The validation message is now properly wrapped with
t()for internationalization.- Line 683: The configuration list item is consolidated to match the formatting of the other list items.
Both changes follow i18n best practices and improve readability without introducing any functional changes.
Also applies to: 683-683
web/src/pages/Setting/Operation/SettingsLog.jsx (1)
233-244: Modal affordances remain intact.Thanks for keeping the DatePicker helper text and destructive-action button aligned; the readability bump comes with no behavioral regressions.
web/src/helpers/render.jsx (2)
1180-1189: LGTM: Improved readability with multi-line formatting.The reformatting of
i18next.ttemplate calls improves readability by breaking long strings across multiple lines. These changes maintain the same functionality while making the code easier to read and maintain.Also applies to: 1317-1325, 1329-1337, 1597-1621, 1630-1638, 1776-1808
1525-1531: Ignore incorrect parameter removal suggestion –renderAudioModelPricedoes not take anaudioInputPriceparameter; it intentionally derives audio pricing frominputRatioPrice * audioRatio. No unused parameter to remove.Likely an incorrect or invalid review comment.
web/src/components/settings/personal/cards/AccountManagement.jsx (1)
552-556: LGTM! Correct loading state selection.The updated loading state logic now correctly displays the appropriate loading indicator based on the current action (delete vs. register), rather than showing loading if either operation is in progress. This prevents the button from incorrectly appearing busy when the opposite operation is loading.
web/src/components/settings/PersonalSetting.jsx (1)
215-217: LGTM! Robust fallback chain for passkey options.The expanded fallback chain (
data?.options || data?.publicKey || data) ensures the function can handle various response structures from the server, making the passkey registration more resilient.controller/topup_stripe.go (1)
85-92: LGTM! Correctly populates PaymentMethod field.The TopUp creation now includes the
PaymentMethodfield set toPaymentMethodStripe, which aligns with the PR's objective to add payment method tracking for billing history. The field population is consistent with the Stripe payment flow.web/src/i18n/locales/en.json (1)
2249-2262: LGTM: new billing/top-up i18n keys align with the feature.Keys read well and match the admin/manual-completion flow.
web/src/i18n/locales/fr.json (1)
2241-2254: LGTM: FR translations added for billing/top-up.Terminology is consistent with EN (Bills/Order/Complete).
web/src/components/table/users/modals/ResetTwoFAModal.jsx (1)
32-37: LGTM: formatting-only change.Multiline t() keeps spacing via {' '} and preserves logic.
controller/topup.go (3)
186-193: Persisting PaymentMethod on order creation is correct.This enables downstream manual-completion logic to compute quota accurately.
318-341: LGTM: user self top-ups listing with optional keyword search.Pagination and keyword flow are clear; consistent response via ApiSuccess.
344-366: LGTM: admin all top-ups listing with search.Matches the self endpoint structure; good reuse of model functions.
web/src/hooks/common/useSecureVerification.jsx (1)
34-121: LGTM: clearer async flow, state handling, and helper wrappers.
- startVerification/executeVerification now explicit async; good UX via loading/finally.
- withVerification wrapper is handy; null-return contract is clear.
No action needed.
Also applies to: 124-171, 174-181, 189-204, 208-218, 227-245, 268-273
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| useEffect(() => { | ||
| if (visible) { | ||
| loadTopups(page, pageSize); | ||
| } | ||
| }, [visible, page, pageSize, keyword]); | ||
|
|
||
| const handlePageChange = (currentPage) => { | ||
| setPage(currentPage); | ||
| }; | ||
|
|
||
| const handlePageSizeChange = (currentPageSize) => { | ||
| setPageSize(currentPageSize); | ||
| setPage(1); | ||
| }; | ||
|
|
There was a problem hiding this comment.
Reset pagination when applying a search keyword
Line [234]: when the search keyword changes we keep whatever page the user was previously on. If they were on page 5 and their new query only has one page of results, we end up requesting p=5 and render an empty table even though matches exist. Reset the page to 1 whenever the keyword updates so searches always start from the first page.
- const handlePageSizeChange = (currentPageSize) => {
- setPageSize(currentPageSize);
- setPage(1);
- };
+ const handlePageSizeChange = (currentPageSize) => {
+ setPageSize(currentPageSize);
+ setPage(1);
+ };
+
+ const handleKeywordChange = (value) => {
+ setKeyword(value);
+ setPage(1);
+ };
...
- <Input
+ <Input
prefix={<IconSearch />}
placeholder={t('订单号')}
- value={keyword}
- onChange={setKeyword}
+ value={keyword}
+ onChange={handleKeywordChange}
showClear
/>Also applies to: 230-236
🤖 Prompt for AI Agents
In web/src/components/topup/modals/TopupHistoryModal.jsx around lines 86-104,
the component keeps the current page when the search keyword changes which can
request an out-of-range page and render an empty table; update the logic so that
when the keyword changes the page is reset to 1 before loading results — e.g.,
on keyword change call setPage(1) (or set page to 1 and then call loadTopups
with page=1) so searches always start from the first page; ensure this does not
create an infinite loop by either performing the reset inside a separate effect
that watches keyword only or by applying the reset and loading explicitly in the
same effect.
✨ feat: Add topup billing history with admin manual completion
Summary by CodeRabbit
New Features
Bug Fixes