perf(admin): 下推商品资产筛选并统一工作台缓存刷新
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
package listing
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// applyAdminAssetSQLFilters 把资产与皮肤条件下推到 MySQL JSON 字段,避免先加载全表再在内存过滤。
|
||||
func applyAdminAssetSQLFilters(db *gorm.DB, query AdminListQuery) *gorm.DB {
|
||||
if len(query.Insurance) > 0 {
|
||||
db = db.Where(
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(a.asset_summary, '$.season_insurance')) IN ?",
|
||||
query.Insurance,
|
||||
)
|
||||
}
|
||||
if len(query.Stamina) > 0 {
|
||||
db = db.Where(
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(a.asset_summary, '$.stamina_level')) IN ?",
|
||||
query.Stamina,
|
||||
)
|
||||
}
|
||||
if len(query.Load) > 0 {
|
||||
db = db.Where(
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(a.asset_summary, '$.load_level')) IN ?",
|
||||
query.Load,
|
||||
)
|
||||
}
|
||||
|
||||
// 皮肤名称按 AND 语义过滤;同时选择多款皮肤时必须全部命中。
|
||||
if len(query.SkinName) > 0 {
|
||||
for _, skin := range query.SkinName {
|
||||
skin = strings.TrimSpace(skin)
|
||||
if skin == "" {
|
||||
continue
|
||||
}
|
||||
db = db.Where(
|
||||
"JSON_SEARCH(a.asset_summary, 'one', ?, '!', '$.skin_groups.*') IS NOT NULL",
|
||||
escapeJSONSearchPattern(skin),
|
||||
)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
if len(query.SkinGroup) > 0 {
|
||||
clauses := make([]string, 0, len(query.SkinGroup))
|
||||
args := make([]any, 0, len(query.SkinGroup))
|
||||
for _, group := range query.SkinGroup {
|
||||
group = strings.TrimSpace(group)
|
||||
if group == "" {
|
||||
continue
|
||||
}
|
||||
clauses = append(clauses, "JSON_LENGTH(JSON_EXTRACT(a.asset_summary, ?)) > 0")
|
||||
args = append(args, jsonMemberPath("$.skin_groups", group))
|
||||
}
|
||||
if len(clauses) == 0 {
|
||||
return db.Where("1 = 0")
|
||||
}
|
||||
db = db.Where("("+strings.Join(clauses, " OR ")+")", args...)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func jsonMemberPath(prefix string, key string) string {
|
||||
escaped := strings.ReplaceAll(key, `\`, `\\`)
|
||||
escaped = strings.ReplaceAll(escaped, `"`, `\"`)
|
||||
return prefix + `."` + escaped + `"`
|
||||
}
|
||||
|
||||
func escapeJSONSearchPattern(value string) string {
|
||||
replacer := strings.NewReplacer("!", "!!", "%", "!%", "_", "!_")
|
||||
return replacer.Replace(value)
|
||||
}
|
||||
@@ -129,6 +129,65 @@ func TestFilterAdminListingsByMultipleSkinsRequiresAll(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAdminListFiltersPushesAssetFiltersToSQL(t *testing.T) {
|
||||
db := database.NewDryRunDB()
|
||||
repo := &Repository{}
|
||||
|
||||
query := AdminListQuery{
|
||||
Insurance: []string{"3*3"},
|
||||
Stamina: []string{"7级"},
|
||||
Load: []string{"7级"},
|
||||
}
|
||||
sql := repo.applyAdminListFilters(db.Table("rental_listings AS l"), query).
|
||||
Find(&[]struct{}{}).Statement.SQL.String()
|
||||
|
||||
for _, fragment := range []string{
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(a.asset_summary, '$.season_insurance')) IN (?)",
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(a.asset_summary, '$.stamina_level')) IN (?)",
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(a.asset_summary, '$.load_level')) IN (?)",
|
||||
} {
|
||||
if !strings.Contains(sql, fragment) {
|
||||
t.Fatalf("期望资产筛选下推 SQL 包含 %q, 实际生成: %s", fragment, sql)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAdminListFiltersPushesSkinNameAndGroupFiltersToSQL(t *testing.T) {
|
||||
db := database.NewDryRunDB()
|
||||
repo := &Repository{}
|
||||
|
||||
skinNameSQL := repo.applyAdminListFilters(
|
||||
db.Table("rental_listings AS l"),
|
||||
AdminListQuery{SkinName: []string{"凌霄成卫", "M7棱镜攻势S2"}},
|
||||
).Find(&[]struct{}{}).Statement.SQL.String()
|
||||
if strings.Count(skinNameSQL, "JSON_SEARCH(a.asset_summary") != 2 {
|
||||
t.Fatalf("多皮肤筛选应生成两个 AND 条件, 实际: %s", skinNameSQL)
|
||||
}
|
||||
if !strings.Contains(skinNameSQL, "'$.skin_groups.*'") {
|
||||
t.Fatalf("皮肤名称筛选应遍历全部分组, 实际: %s", skinNameSQL)
|
||||
}
|
||||
|
||||
groupSQL := repo.applyAdminListFilters(
|
||||
db.Table("rental_listings AS l"),
|
||||
AdminListQuery{SkinGroup: []string{"operatorRed", "weapon"}},
|
||||
).Find(&[]struct{}{}).Statement.SQL.String()
|
||||
if !strings.Contains(groupSQL, "JSON_LENGTH(JSON_EXTRACT(a.asset_summary, ?)) > 0") {
|
||||
t.Fatalf("皮肤分组筛选应下推 JSON_LENGTH, 实际: %s", groupSQL)
|
||||
}
|
||||
if !strings.Contains(groupSQL, " OR ") {
|
||||
t.Fatalf("多皮肤分组应使用 OR 语义, 实际: %s", groupSQL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminAssetFilterHelpers(t *testing.T) {
|
||||
if got := jsonMemberPath("$.skin_groups", `operator"Red`); got != `$.skin_groups."operator\"Red"` {
|
||||
t.Fatalf("jsonMemberPath = %q", got)
|
||||
}
|
||||
if got := escapeJSONSearchPattern(`50%_暗星!`); got != `50!%!_暗星!!` {
|
||||
t.Fatalf("escapeJSONSearchPattern = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAdminListFiltersSmartRouting(t *testing.T) {
|
||||
db := database.NewDryRunDB()
|
||||
repo := &Repository{}
|
||||
|
||||
@@ -35,37 +35,6 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*Admi
|
||||
pageSize = 100
|
||||
}
|
||||
|
||||
if hasAdminAssetFilters(query) {
|
||||
var rows []listingRow
|
||||
err := r.applyAdminListFilters(r.baseQuery(ctx), query).
|
||||
Order("l.id DESC").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := filterAdminListings(rowsToDTO(rows), query)
|
||||
total := int64(len(items))
|
||||
start := (page - 1) * pageSize
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
if start >= len(items) {
|
||||
items = []ListingDTO{}
|
||||
} else {
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
items = items[start:end]
|
||||
}
|
||||
return &AdminListResult{
|
||||
Items: items,
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: pageSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
db := r.applyAdminListFilters(r.baseQuery(ctx), query)
|
||||
offset := (page - 1) * pageSize
|
||||
if offset < 0 {
|
||||
@@ -82,7 +51,12 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminListQuery) (*Admi
|
||||
if page == 1 && len(rows) < pageSize {
|
||||
total = int64(len(rows))
|
||||
} else {
|
||||
countDB := r.applyAdminListFilters(r.db.WithContext(ctx).Table("rental_listings AS l"), query)
|
||||
countDB := r.applyAdminListFilters(
|
||||
r.db.WithContext(ctx).
|
||||
Table("rental_listings AS l").
|
||||
Joins("JOIN game_accounts AS a ON a.id = l.account_id"),
|
||||
query,
|
||||
)
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -149,6 +123,7 @@ func (r *Repository) applyAdminListFilters(db *gorm.DB, query AdminListQuery) *g
|
||||
if query.MaxDeposit != nil {
|
||||
db = db.Where("l.deposit_amount_cent <= ?", yuanToAdminCent(*query.MaxDeposit))
|
||||
}
|
||||
db = applyAdminAssetSQLFilters(db, query)
|
||||
return db
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useState } from 'react'
|
||||
import { useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Button, Image, Input, InputNumber, Modal, Select, Space, Tag, Typography } from 'antd'
|
||||
import { CheckCircleOutlined } from '@ant-design/icons'
|
||||
|
||||
import { arbitrateAdminDispute } from '@/features/admin/api'
|
||||
import { invalidateAdminDashboard } from '@/features/admin/cache'
|
||||
import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import { handoffStatusMeta, orderStatusMap, readStatusMeta, settlementStatusMap } from '@/features/admin/labels'
|
||||
import {
|
||||
handoffStatusMeta,
|
||||
orderStatusMap,
|
||||
readStatusMeta,
|
||||
settlementStatusMap,
|
||||
} from '@/features/admin/labels'
|
||||
import {
|
||||
allowsAmount,
|
||||
arbitrationResultLabel,
|
||||
@@ -211,6 +218,7 @@ export function AdminDisputeDetailDrawer({
|
||||
onUpdated,
|
||||
}: AdminDisputeDetailDrawerProps) {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const canArbitrate = Boolean(
|
||||
permissions?.includes('*') || permissions?.includes('dispute:arbitrate')
|
||||
@@ -234,7 +242,8 @@ export function AdminDisputeDetailDrawer({
|
||||
)
|
||||
setCoinM(dispute.checkout_coin_consumed_m ?? null)
|
||||
setConsumableYuan(
|
||||
(dispute.checkout_consumable_amount_cent ?? dispute.extra_item_original_amount_cent ?? 0) / 100
|
||||
(dispute.checkout_consumable_amount_cent ?? dispute.extra_item_original_amount_cent ?? 0) /
|
||||
100
|
||||
)
|
||||
setModalOpen(true)
|
||||
}
|
||||
@@ -277,7 +286,7 @@ export function AdminDisputeDetailDrawer({
|
||||
remark: text,
|
||||
amount_cent:
|
||||
allowsAmount(result) && amountYuan != null ? Math.round(amountYuan * 100) : undefined,
|
||||
actual_coin_consumed_m: usesActualCheckout(result) ? coinM ?? undefined : undefined,
|
||||
actual_coin_consumed_m: usesActualCheckout(result) ? (coinM ?? undefined) : undefined,
|
||||
actual_consumable_amount_cent: usesActualCheckout(result)
|
||||
? Math.round((consumableYuan ?? 0) * 100)
|
||||
: undefined,
|
||||
@@ -285,6 +294,7 @@ export function AdminDisputeDetailDrawer({
|
||||
message.success('仲裁结果已保存')
|
||||
setModalOpen(false)
|
||||
onUpdated(updated)
|
||||
void invalidateAdminDashboard(queryClient)
|
||||
} catch (error) {
|
||||
const text =
|
||||
error && typeof error === 'object' && 'response' in error
|
||||
@@ -299,7 +309,8 @@ export function AdminDisputeDetailDrawer({
|
||||
}
|
||||
}
|
||||
|
||||
const canHandle = dispute != null && ['open', 'processing'].includes(dispute.status) && canArbitrate
|
||||
const canHandle =
|
||||
dispute != null && ['open', 'processing'].includes(dispute.status) && canArbitrate
|
||||
const showAmount = allowsAmount(result)
|
||||
const showActual = usesActualCheckout(result)
|
||||
|
||||
@@ -312,8 +323,7 @@ export function AdminDisputeDetailDrawer({
|
||||
subtitle={
|
||||
dispute ? (
|
||||
<span>
|
||||
{dispute.order_no} · {disputeTypeLabel(dispute.type)} ·{' '}
|
||||
{initiatorLabel(dispute)}
|
||||
{dispute.order_no} · {disputeTypeLabel(dispute.type)} · {initiatorLabel(dispute)}
|
||||
</span>
|
||||
) : null
|
||||
}
|
||||
@@ -331,11 +341,7 @@ export function AdminDisputeDetailDrawer({
|
||||
dispute ? (
|
||||
<Space wrap>
|
||||
{canHandle ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={openArbitration}
|
||||
>
|
||||
<Button type="primary" icon={<CheckCircleOutlined />} onClick={openArbitration}>
|
||||
提交仲裁
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -360,7 +366,8 @@ export function AdminDisputeDetailDrawer({
|
||||
>
|
||||
{dispute ? (
|
||||
<Typography.Paragraph type="secondary">
|
||||
订单 {dispute.order_no},申诉 #{dispute.id}。裁决会立即影响订单、钱包与商品状态,请核对后提交。
|
||||
订单 {dispute.order_no},申诉 #{dispute.id}
|
||||
。裁决会立即影响订单、钱包与商品状态,请核对后提交。
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
|
||||
@@ -395,9 +402,7 @@ export function AdminDisputeDetailDrawer({
|
||||
precision={2}
|
||||
value={consumableYuan}
|
||||
addonBefore="¥"
|
||||
onChange={value =>
|
||||
setConsumableYuan(typeof value === 'number' ? value : null)
|
||||
}
|
||||
onChange={value => setConsumableYuan(typeof value === 'number' ? value : null)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,16 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
App,
|
||||
Button,
|
||||
Image,
|
||||
Input,
|
||||
Modal,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
} from 'antd'
|
||||
import { App, Button, Image, Input, Modal, Space, Tag, Typography } from 'antd'
|
||||
import { CopyOutlined, DownOutlined, StopOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
|
||||
import {
|
||||
@@ -20,6 +11,7 @@ import {
|
||||
offlineAdminListing,
|
||||
rejectAdminListing,
|
||||
} from '@/features/admin/api'
|
||||
import { invalidateAdminDashboard } from '@/features/admin/cache'
|
||||
import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import { handoffModeMap, readStatusMeta, settlementModeMap } from '@/features/admin/labels'
|
||||
@@ -78,8 +70,16 @@ function buildSections(listing: AdminListing): DetailSection[] {
|
||||
key: 'overview',
|
||||
title: '商品概览',
|
||||
fields: [
|
||||
{ key: 'listing_no', label: '商品编号', value: <span className="admin-mono">{listing.listing_no}</span> },
|
||||
{ key: 'status', label: '商品状态', value: <StatusTag meta={listingStatusMeta(listing.status)} /> },
|
||||
{
|
||||
key: 'listing_no',
|
||||
label: '商品编号',
|
||||
value: <span className="admin-mono">{listing.listing_no}</span>,
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '商品状态',
|
||||
value: <StatusTag meta={listingStatusMeta(listing.status)} />,
|
||||
},
|
||||
{
|
||||
key: 'review_status',
|
||||
label: '审核状态',
|
||||
@@ -185,9 +185,7 @@ function buildSections(listing: AdminListing): DetailSection[] {
|
||||
{
|
||||
key: 'reason',
|
||||
label: '审核意见',
|
||||
value: (
|
||||
<div className="admin-detail__pre">{listing.review_reason || '暂无审核意见'}</div>
|
||||
),
|
||||
value: <div className="admin-detail__pre">{listing.review_reason || '暂无审核意见'}</div>,
|
||||
span: 2,
|
||||
},
|
||||
{
|
||||
@@ -212,7 +210,8 @@ function buildSections(listing: AdminListing): DetailSection[] {
|
||||
}
|
||||
|
||||
if (listing.screenshot_urls.length > 0 || listing.cover_url) {
|
||||
const images = listing.screenshot_urls.length > 0 ? listing.screenshot_urls : [listing.cover_url]
|
||||
const images =
|
||||
listing.screenshot_urls.length > 0 ? listing.screenshot_urls : [listing.cover_url]
|
||||
sections.push({
|
||||
key: 'screenshots',
|
||||
title: '商品截图',
|
||||
@@ -273,6 +272,7 @@ export function AdminListingDetailDrawer({
|
||||
await query.refetch()
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-listings'] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-pending-listings'] })
|
||||
await invalidateAdminDashboard(queryClient)
|
||||
onChanged?.()
|
||||
}
|
||||
|
||||
@@ -345,7 +345,9 @@ export function AdminListingDetailDrawer({
|
||||
!['offline', 'completed', 'sealed'].includes(listing.status) &&
|
||||
can('listing:offline')
|
||||
const canMarkAbnormal =
|
||||
listing != null && !['abnormal', 'completed', 'sealed'].includes(listing.status) && can('listing:offline')
|
||||
listing != null &&
|
||||
!['abnormal', 'completed', 'sealed'].includes(listing.status) &&
|
||||
can('listing:offline')
|
||||
|
||||
const actionCopyItem = reasonAction ? actionCopy[reasonAction] : null
|
||||
|
||||
@@ -398,7 +400,11 @@ export function AdminListingDetailDrawer({
|
||||
标记异常
|
||||
</Button>
|
||||
) : null}
|
||||
<Button icon={<CopyOutlined />} onClick={() => void copyListingNo()} disabled={!listing}>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={() => void copyListingNo()}
|
||||
disabled={!listing}
|
||||
>
|
||||
复制编号
|
||||
</Button>
|
||||
<Button onClick={onClose}>关闭</Button>
|
||||
|
||||
@@ -1,18 +1,29 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Alert, App, Button, Modal, Radio, Space, Tag, Typography, type TableColumnsType } from 'antd'
|
||||
import {
|
||||
Alert,
|
||||
App,
|
||||
Button,
|
||||
Modal,
|
||||
Radio,
|
||||
Space,
|
||||
Tag,
|
||||
Typography,
|
||||
type TableColumnsType,
|
||||
} from 'antd'
|
||||
import { EyeOutlined, ReloadOutlined } from '@ant-design/icons'
|
||||
|
||||
import { approveAdminRefund, fetchPendingRefundOrders, rejectAdminRefund } from '@/features/admin/api'
|
||||
import {
|
||||
approveAdminRefund,
|
||||
fetchPendingRefundOrders,
|
||||
rejectAdminRefund,
|
||||
} from '@/features/admin/api'
|
||||
import { AdminOrderDetailDrawer } from '@/features/admin/AdminOrderDetailDrawer'
|
||||
import { invalidateAdminDashboard } from '@/features/admin/cache'
|
||||
import { AdminPageHeader } from '@/features/admin/components/AdminPageHeader'
|
||||
import { DataTable } from '@/features/admin/components/DataTable'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import {
|
||||
handoffStatusMeta,
|
||||
orderStatusMap,
|
||||
readStatusMeta,
|
||||
} from '@/features/admin/labels'
|
||||
import { handoffStatusMeta, orderStatusMap, readStatusMeta } from '@/features/admin/labels'
|
||||
import type { AdminOrderDetail } from '@/features/admin/types'
|
||||
import { useAdminSessionStore } from '@/stores/adminSession'
|
||||
import { formatDateTime, formatMoney, formatText } from '@/shared/utils/format'
|
||||
@@ -137,7 +148,7 @@ export function AdminRefundReviewPage() {
|
||||
const refreshAll = async () => {
|
||||
await query.refetch()
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-orders'] })
|
||||
await queryClient.invalidateQueries({ queryKey: ['admin-dashboard'] })
|
||||
await invalidateAdminDashboard(queryClient)
|
||||
}
|
||||
|
||||
const openDetail = (row: AdminOrderDetail) => {
|
||||
@@ -174,7 +185,9 @@ export function AdminRefundReviewPage() {
|
||||
setSubmitting(true)
|
||||
try {
|
||||
await rejectAdminRefund(rejectOrder.id, rejectMode)
|
||||
message.success(rejectMode === 'restore' ? '退款已驳回,订单恢复待交接' : '退款已驳回,订单已关闭')
|
||||
message.success(
|
||||
rejectMode === 'restore' ? '退款已驳回,订单恢复待交接' : '退款已驳回,订单已关闭'
|
||||
)
|
||||
setRejectOrder(null)
|
||||
await refreshAll()
|
||||
} catch (error) {
|
||||
@@ -185,7 +198,8 @@ export function AdminRefundReviewPage() {
|
||||
}
|
||||
|
||||
const columns = buildColumns(openDetail, approve, openReject, canClose, canView)
|
||||
const errorText = query.error instanceof Error ? query.error.message : '请稍后重试或重新登录后台。'
|
||||
const errorText =
|
||||
query.error instanceof Error ? query.error.message : '请稍后重试或重新登录后台。'
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
@@ -200,7 +214,11 @@ export function AdminRefundReviewPage() {
|
||||
</Space>
|
||||
}
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()} loading={query.isFetching}>
|
||||
<Button
|
||||
icon={<ReloadOutlined />}
|
||||
onClick={() => void query.refetch()}
|
||||
loading={query.isFetching}
|
||||
>
|
||||
刷新队列
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { App, Button, Image, Input, Modal, Space, Tag, Typography } from 'antd'
|
||||
import { CheckOutlined, CloseOutlined, WalletOutlined } from '@ant-design/icons'
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
fetchAdminWithdrawal,
|
||||
reviewAdminWithdrawal,
|
||||
} from '@/features/admin/api'
|
||||
import { invalidateAdminDashboard } from '@/features/admin/cache'
|
||||
import { DetailDrawer, type DetailSection } from '@/features/admin/components/DetailDrawer'
|
||||
import { StatusTag } from '@/features/admin/components/StatusTag'
|
||||
import { readStatusMeta, withdrawalStatusMap } from '@/features/admin/labels'
|
||||
@@ -51,7 +52,11 @@ function buildSections(item: AdminWithdrawal): DetailSection[] {
|
||||
label: '提现状态',
|
||||
value: <StatusTag meta={readStatusMeta(withdrawalStatusMap, item.status)} />,
|
||||
},
|
||||
{ key: 'user', label: '用户', value: `${item.user_nickname || '未设置昵称'} · ${item.user_phone}` },
|
||||
{
|
||||
key: 'user',
|
||||
label: '用户',
|
||||
value: `${item.user_nickname || '未设置昵称'} · ${item.user_phone}`,
|
||||
},
|
||||
{ key: 'user_id', label: '用户 ID', value: `UID ${item.user_id}` },
|
||||
{
|
||||
key: 'site',
|
||||
@@ -103,7 +108,12 @@ function buildSections(item: AdminWithdrawal): DetailSection[] {
|
||||
{ key: 'review_remark', label: '审核备注', value: formatText(item.review_remark), span: 2 },
|
||||
{ key: 'paid_by', label: '打款人', value: formatText(item.paid_by_name) },
|
||||
{ key: 'paid_at', label: '打款时间', value: formatDateTime(item.paid_at) },
|
||||
{ key: 'payment_remark', label: '打款备注', value: formatText(item.payment_remark), span: 2 },
|
||||
{
|
||||
key: 'payment_remark',
|
||||
label: '打款备注',
|
||||
value: formatText(item.payment_remark),
|
||||
span: 2,
|
||||
},
|
||||
{
|
||||
key: 'proof',
|
||||
label: '打款凭证',
|
||||
@@ -167,6 +177,7 @@ export function AdminWithdrawalDetailDrawer({
|
||||
onChanged,
|
||||
}: AdminWithdrawalDetailDrawerProps) {
|
||||
const { message } = App.useApp()
|
||||
const queryClient = useQueryClient()
|
||||
const permissions = useAdminSessionStore(state => state.admin?.permissions)
|
||||
const can = (code: string) => Boolean(permissions?.includes('*') || permissions?.includes(code))
|
||||
const [action, setAction] = useState<WithdrawalAction | null>(null)
|
||||
@@ -216,6 +227,7 @@ export function AdminWithdrawalDetailDrawer({
|
||||
}
|
||||
setAction(null)
|
||||
await query.refetch()
|
||||
await invalidateAdminDashboard(queryClient)
|
||||
onChanged?.()
|
||||
} catch (error) {
|
||||
const text =
|
||||
@@ -278,11 +290,7 @@ export function AdminWithdrawalDetailDrawer({
|
||||
</>
|
||||
) : null}
|
||||
{canPay ? (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<WalletOutlined />}
|
||||
onClick={() => openAction('pay')}
|
||||
>
|
||||
<Button type="primary" icon={<WalletOutlined />} onClick={() => openAction('pay')}>
|
||||
确认已打款
|
||||
</Button>
|
||||
) : null}
|
||||
@@ -308,8 +316,8 @@ export function AdminWithdrawalDetailDrawer({
|
||||
{item ? (
|
||||
<Typography.Paragraph>
|
||||
提现单 {item.withdraw_no},申请 {formatMoney(item.amount_cent)},到账{' '}
|
||||
{formatMoney(item.actual_amount_cent)},收款人 {item.account_name}(
|
||||
{item.account_no})。
|
||||
{formatMoney(item.actual_amount_cent)},收款人 {item.account_name}({item.account_no}
|
||||
)。
|
||||
</Typography.Paragraph>
|
||||
) : null}
|
||||
<Input.TextArea
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import type { QueryClient } from '@tanstack/react-query'
|
||||
|
||||
/**
|
||||
* 后台写操作统一失效工作台缓存。
|
||||
* 商品审核、订单处理、退款、提现、申诉等操作都会改变看板数字,写成功后调用。
|
||||
*/
|
||||
export function invalidateAdminDashboard(queryClient: QueryClient) {
|
||||
return queryClient.invalidateQueries({ queryKey: ['admin-dashboard'] })
|
||||
}
|
||||
Reference in New Issue
Block a user