mirror of
https://github.com/QuantumNous/new-api.git
synced 2026-04-19 11:48:38 +00:00
🔎 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:
@@ -318,8 +318,18 @@ func RequestAmount(c *gin.Context) {
|
|||||||
func GetUserTopUps(c *gin.Context) {
|
func GetUserTopUps(c *gin.Context) {
|
||||||
userId := c.GetInt("id")
|
userId := c.GetInt("id")
|
||||||
pageInfo := common.GetPageQuery(c)
|
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 {
|
if err != nil {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
@@ -333,8 +343,18 @@ func GetUserTopUps(c *gin.Context) {
|
|||||||
// GetAllTopUps 管理员获取全平台充值记录
|
// GetAllTopUps 管理员获取全平台充值记录
|
||||||
func GetAllTopUps(c *gin.Context) {
|
func GetAllTopUps(c *gin.Context) {
|
||||||
pageInfo := common.GetPageQuery(c)
|
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 {
|
if err != nil {
|
||||||
common.ApiError(c, err)
|
common.ApiError(c, err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -165,6 +165,74 @@ func GetAllTopUps(pageInfo *common.PageInfo) (topups []*TopUp, total int64, err
|
|||||||
return topups, total, nil
|
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 管理员手动完成订单并给用户充值
|
// ManualCompleteTopUp 管理员手动完成订单并给用户充值
|
||||||
func ManualCompleteTopUp(tradeNo string) error {
|
func ManualCompleteTopUp(tradeNo string) error {
|
||||||
if tradeNo == "" {
|
if tradeNo == "" {
|
||||||
|
|||||||
@@ -25,12 +25,14 @@ import {
|
|||||||
Toast,
|
Toast,
|
||||||
Empty,
|
Empty,
|
||||||
Button,
|
Button,
|
||||||
|
Input,
|
||||||
} from '@douyinfe/semi-ui';
|
} from '@douyinfe/semi-ui';
|
||||||
import {
|
import {
|
||||||
IllustrationNoResult,
|
IllustrationNoResult,
|
||||||
IllustrationNoResultDark,
|
IllustrationNoResultDark,
|
||||||
} from '@douyinfe/semi-illustrations';
|
} from '@douyinfe/semi-illustrations';
|
||||||
import { Coins } from 'lucide-react';
|
import { Coins } from 'lucide-react';
|
||||||
|
import { IconSearch } from '@douyinfe/semi-icons';
|
||||||
import { API, timestamp2string } from '../../../helpers';
|
import { API, timestamp2string } from '../../../helpers';
|
||||||
import { isAdmin } from '../../../helpers/utils';
|
import { isAdmin } from '../../../helpers/utils';
|
||||||
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
import { useIsMobile } from '../../../hooks/common/useIsMobile';
|
||||||
@@ -57,15 +59,18 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
|||||||
const [total, setTotal] = useState(0);
|
const [total, setTotal] = useState(0);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
const [pageSize, setPageSize] = useState(10);
|
const [pageSize, setPageSize] = useState(10);
|
||||||
|
const [keyword, setKeyword] = useState('');
|
||||||
|
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
|
|
||||||
const loadTopups = async (currentPage, currentPageSize) => {
|
const loadTopups = async (currentPage, currentPageSize) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const endpoint = isAdmin()
|
const base = isAdmin() ? '/api/user/topup' : '/api/user/topup/self';
|
||||||
? `/api/user/topup?p=${currentPage}&page_size=${currentPageSize}`
|
const qs =
|
||||||
: `/api/user/topup/self?p=${currentPage}&page_size=${currentPageSize}`;
|
`p=${currentPage}&page_size=${currentPageSize}` +
|
||||||
|
(keyword ? `&keyword=${encodeURIComponent(keyword)}` : '');
|
||||||
|
const endpoint = `${base}?${qs}`;
|
||||||
const res = await API.get(endpoint);
|
const res = await API.get(endpoint);
|
||||||
const { success, message, data } = res.data;
|
const { success, message, data } = res.data;
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -86,7 +91,7 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
|||||||
if (visible) {
|
if (visible) {
|
||||||
loadTopups(page, pageSize);
|
loadTopups(page, pageSize);
|
||||||
}
|
}
|
||||||
}, [visible, page, pageSize]);
|
}, [visible, page, pageSize, keyword]);
|
||||||
|
|
||||||
const handlePageChange = (currentPage) => {
|
const handlePageChange = (currentPage) => {
|
||||||
setPage(currentPage);
|
setPage(currentPage);
|
||||||
@@ -221,6 +226,15 @@ const TopupHistoryModal = ({ visible, onCancel, t }) => {
|
|||||||
footer={null}
|
footer={null}
|
||||||
size={isMobile ? 'full-width' : 'large'}
|
size={isMobile ? 'full-width' : 'large'}
|
||||||
>
|
>
|
||||||
|
<div className='mb-3'>
|
||||||
|
<Input
|
||||||
|
prefix={<IconSearch />}
|
||||||
|
placeholder={t('订单号')}
|
||||||
|
value={keyword}
|
||||||
|
onChange={setKeyword}
|
||||||
|
showClear
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<Table
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={topups}
|
dataSource={topups}
|
||||||
|
|||||||
Reference in New Issue
Block a user