From a8c9b24c7eb71b755924a5793166ab5fe42d44a3 Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Tue, 7 Oct 2025 00:55:01 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=8E=20feat(topup):=20add=20order=20num?= =?UTF-8?q?ber=20search=20for=20billing=20history=20(admin=20and=20user)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- controller/topup.go | 24 ++++++- model/topup.go | 68 +++++++++++++++++++ .../topup/modals/TopupHistoryModal.jsx | 22 ++++-- 3 files changed, 108 insertions(+), 6 deletions(-) diff --git a/controller/topup.go b/controller/topup.go index 8626695f3..76a2521d9 100644 --- a/controller/topup.go +++ b/controller/topup.go @@ -318,8 +318,18 @@ func RequestAmount(c *gin.Context) { func GetUserTopUps(c *gin.Context) { userId := c.GetInt("id") pageInfo := common.GetPageQuery(c) + keyword := c.Query("keyword") - topups, total, err := model.GetUserTopUps(userId, pageInfo) + var ( + topups []*model.TopUp + total int64 + err error + ) + if keyword != "" { + topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo) + } else { + topups, total, err = model.GetUserTopUps(userId, pageInfo) + } if err != nil { common.ApiError(c, err) return @@ -333,8 +343,18 @@ func GetUserTopUps(c *gin.Context) { // GetAllTopUps 管理员获取全平台充值记录 func GetAllTopUps(c *gin.Context) { pageInfo := common.GetPageQuery(c) + keyword := c.Query("keyword") - topups, total, err := model.GetAllTopUps(pageInfo) + var ( + topups []*model.TopUp + total int64 + err error + ) + if keyword != "" { + topups, total, err = model.SearchAllTopUps(keyword, pageInfo) + } else { + topups, total, err = model.GetAllTopUps(pageInfo) + } if err != nil { common.ApiError(c, err) return diff --git a/model/topup.go b/model/topup.go index e46a5a88f..380f5851d 100644 --- a/model/topup.go +++ b/model/topup.go @@ -165,6 +165,74 @@ func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err return topups, total, nil } +// SearchUserTopUps 按订单号搜索某用户的充值记录 +func SearchUserTopUps(userId int, keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}).Where("user_id = ?", userId) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + +// SearchAllTopUps 按订单号搜索全平台充值记录(管理员使用) +func SearchAllTopUps(keyword string, pageInfo *common.PageInfo) (topups []*TopUp, total int64, err error) { + tx := DB.Begin() + if tx.Error != nil { + return nil, 0, tx.Error + } + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + + query := tx.Model(&TopUp{}) + if keyword != "" { + like := "%%" + keyword + "%%" + query = query.Where("trade_no LIKE ?", like) + } + + if err = query.Count(&total).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = query.Order("id desc").Limit(pageInfo.GetPageSize()).Offset(pageInfo.GetStartIdx()).Find(&topups).Error; err != nil { + tx.Rollback() + return nil, 0, err + } + + if err = tx.Commit().Error; err != nil { + return nil, 0, err + } + return topups, total, nil +} + // ManualCompleteTopUp 管理员手动完成订单并给用户充值 func ManualCompleteTopUp(tradeNo string) error { if tradeNo == "" { diff --git a/web/src/components/topup/modals/TopupHistoryModal.jsx b/web/src/components/topup/modals/TopupHistoryModal.jsx index fe50c6864..57916a9aa 100644 --- a/web/src/components/topup/modals/TopupHistoryModal.jsx +++ b/web/src/components/topup/modals/TopupHistoryModal.jsx @@ -25,12 +25,14 @@ import { Toast, Empty, Button, + Input, } from '@douyinfe/semi-ui'; import { IllustrationNoResult, IllustrationNoResultDark, } from '@douyinfe/semi-illustrations'; import { Coins } from 'lucide-react'; +import { IconSearch } from '@douyinfe/semi-icons'; import { API, timestamp2string } from '../../../helpers'; import { isAdmin } from '../../../helpers/utils'; import { useIsMobile } from '../../../hooks/common/useIsMobile'; @@ -57,15 +59,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [pageSize, setPageSize] = useState(10); + const [keyword, setKeyword] = useState(''); const isMobile = useIsMobile(); const loadTopups = async (currentPage, currentPageSize) => { setLoading(true); try { - const endpoint = isAdmin() - ? `/api/user/topup?p=${currentPage}&page_size=${currentPageSize}` - : `/api/user/topup/self?p=${currentPage}&page_size=${currentPageSize}`; + const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self'; + const qs = + `p=${currentPage}&page_size=${currentPageSize}` + + (keyword ? `&keyword=${encodeURIComponent(keyword)}` : ''); + const endpoint = `${base}?${qs}`; const res = await API.get(endpoint); const { success, message, data } = res.data; if (success) { @@ -86,7 +91,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { if (visible) { loadTopups(page, pageSize); } - }, [visible, page, pageSize]); + }, [visible, page, pageSize, keyword]); const handlePageChange = (currentPage) => { setPage(currentPage); @@ -221,6 +226,15 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => { footer={null} size={isMobile ? 'full-width' : 'large'} > +
+ } + placeholder={t('订单号')} + value={keyword} + onChange={setKeyword} + showClear + /> +