Просмотр исходного кода

🔎 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
Apple\Apple 5 месяцев назад
Родитель
Сommit
a8c9b24c7e
3 измененных файлов с 110 добавлено и 8 удалено
  1. 24 4
      controller/topup.go
  2. 68 0
      model/topup.go
  3. 18 4
      web/src/components/topup/modals/TopupHistoryModal.jsx

+ 24 - 4
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)
-
-	topups, total, err := model.GetUserTopUps(userId, pageInfo)
+	keyword := c.Query("keyword")
+
+	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)
-
-	topups, total, err := model.GetAllTopUps(pageInfo)
+	keyword := c.Query("keyword")
+
+	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

+ 68 - 0
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 == "" {

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