🔎 feat(topup): add order number search for billing history (admin and 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
This commit is contained in:
Apple\Apple
2025-10-07 00:55:01 +08:00
parent 2389dbafc5
commit a8c9b24c7e
3 changed files with 108 additions and 6 deletions

View File

@@ -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

View File

@@ -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 == "" {

View File

@@ -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'}
>
<div className='mb-3'>
<Input
prefix={<IconSearch />}
placeholder={t('订单号')}
value={keyword}
onChange={setKeyword}
showClear
/>
</div>
<Table
columns={columns}
dataSource={topups}