fix(admin): 订单列表默认近7天并按支付时间筛选

避免无日期过滤时展示全库,关键词搜索不受日期限制;SSE 插入裁剪并尊重筛选,新单入库优先使用平台支付时间。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-15 13:41:29 +08:00
co-authored by Cursor
parent b7d246e5b9
commit 40c51e1d8e
7 changed files with 279 additions and 58 deletions
+104 -11
View File
@@ -53,6 +53,7 @@ import {
} from '@/utils/admin-auth'
import { formatAdminDateTime } from '@/utils/admin-time'
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
import dayjs from '@/lib/dayjs'
const SIDEBAR_COLLAPSED_KEY = 'react-admin-sidebar-collapsed'
const NOTIFICATION_SOUND_KEY = 'admin-notification-sound-enabled'
@@ -493,35 +494,82 @@ function patchAdminOrderCaches(
queryClient: ReturnType<typeof useQueryClient>,
event: RealtimeEvent,
) {
const item = event.data as { orderId?: unknown; platformOrderId?: unknown; payStatus?: unknown }
const item = event.data as {
orderId?: unknown
platformOrderId?: unknown
payStatus?: unknown
paidAt?: unknown
createdAt?: unknown
provider?: unknown
shopName?: unknown
itemTitle?: unknown
itemSummary?: unknown
}
for (const [queryKey, current] of queryClient.getQueriesData({ queryKey: ['admin-orders'] })) {
const params = isRecord(queryKey[1]) ? queryKey[1] : {}
const page = Number(params.page || 1)
const matches =
(!String(params.platformOrderId || '').trim() ||
String(item.platformOrderId || '') === String(params.platformOrderId || '').trim()) &&
const pageSize = Math.max(1, Number(params.pageSize || 20) || 20)
const keyword = String(params.keyword || '').trim()
const matchesFilters =
matchesAdminOrderKeyword(item, keyword) &&
matchesAdminOrderProvider(item, String(params.provider || '').trim()) &&
(!String(params.payStatus || '').trim() ||
String(item.payStatus || '') === String(params.payStatus || '').trim())
const shouldInsert = event.operation === 'upsert' && page === 1 && matches && !params.skuCode
const operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert'
String(item.payStatus || '') === String(params.payStatus || '').trim()) &&
// 有关键词时不限日期;否则实时插入也要落在当前日期筛选内。
(keyword ? true : matchesAdminOrderDateRange(item, params))
const shouldInsert =
event.operation === 'upsert' && page === 1 && matchesFilters && !params.skuCode
const operation = event.operation === 'remove' || !matchesFilters ? 'remove' : 'upsert'
queryClient.setQueryData(
queryKey,
patchAdminOrderListEnvelope(current, { ...event, operation }, shouldInsert),
patchAdminOrderListEnvelope(current, { ...event, operation }, shouldInsert, pageSize),
)
}
}
function patchAdminOrderListEnvelope(current: unknown, event: RealtimeEvent, insert = false) {
function patchAdminOrderListEnvelope(
current: unknown,
event: RealtimeEvent,
insert = false,
pageSize = 20,
) {
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
return current
}
const id = Number((event.data as { orderId?: unknown }).orderId || event.entityId)
const items = current.data.items as unknown[]
const nextItems =
const existed = items.some((entry) => Number((entry as { orderId?: unknown })?.orderId) === id)
let nextItems =
event.operation === 'remove'
? items.filter((entry) => Number((entry as { orderId?: unknown })?.orderId) !== id)
: upsertById(items, event.data, 'orderId', insert)
return { ...current, data: { ...current.data, items: nextItems } }
if (insert && nextItems.length > pageSize) {
nextItems = nextItems.slice(0, pageSize)
}
const pagination = isRecord(current.data.pagination) ? current.data.pagination : null
let nextPagination = pagination
if (pagination && insert && !existed) {
nextPagination = {
...pagination,
total: Number(pagination.total || 0) + 1,
}
} else if (pagination && event.operation === 'remove' && existed) {
nextPagination = {
...pagination,
total: Math.max(0, Number(pagination.total || 0) - 1),
}
}
return {
...current,
data: {
...current.data,
items: nextItems,
...(nextPagination ? { pagination: nextPagination } : {}),
},
}
}
function patchWorkOrderListEnvelope(current: unknown, event: RealtimeEvent, insert = false) {
@@ -560,6 +608,51 @@ function upsertById(items: unknown[], nextItem: unknown, idKey: string, insert =
return items.map((item, itemIndex) => (itemIndex === index ? nextItem : item))
}
function matchesAdminOrderKeyword(
item: {
platformOrderId?: unknown
shopName?: unknown
itemTitle?: unknown
itemSummary?: unknown
},
keyword: string,
) {
const normalizedKeyword = keyword.trim().toLowerCase()
if (!normalizedKeyword) return true
return [item.platformOrderId, item.shopName, item.itemTitle, item.itemSummary]
.map((value) => String(value || '').toLowerCase())
.some((value) => value.includes(normalizedKeyword))
}
function matchesAdminOrderProvider(item: { provider?: unknown }, provider: string) {
const normalized = provider.trim().toLowerCase()
if (!normalized) return true
const value = String(item.provider || '').toLowerCase()
if (normalized === 'pdd') return value === 'pdd'
if (normalized === '91' || normalized === '91kaquan' || normalized === 'open_91') {
return ['91kaquan', 'open_91', '91'].includes(value)
}
if (normalized === 'kuaishou' || normalized === 'kuaishou-industry') {
return value.startsWith('kuaishou')
}
return value === normalized
}
function matchesAdminOrderDateRange(
item: { paidAt?: unknown; createdAt?: unknown },
params: Record<string, unknown>,
) {
const dateFrom = String(params.dateFrom || '').trim().slice(0, 10)
const dateTo = String(params.dateTo || '').trim().slice(0, 10)
if (!dateFrom && !dateTo) return true
const raw = String(item.paidAt || item.createdAt || '').trim()
if (!raw) return false
const day = dayjs(raw).format('YYYY-MM-DD')
if (dateFrom && day < dateFrom) return false
if (dateTo && day > dateTo) return false
return true
}
function matchesWorkOrderKeyword(
item: { productName?: unknown; platformOrderId?: unknown },
keyword: string,
@@ -13,6 +13,7 @@ import { fetchAdminOrders } from '@/services/admin'
import type { AdminOrderListItem } from '@/types/admin'
import {
adminOrderExecutorOptions,
adminOrderProviderOptions,
adminPayStatusOptions,
adminTaskStatusOptions,
} from '@/utils/admin-options'
@@ -25,20 +26,46 @@ import { formatAdminDateTime } from '@/utils/admin-time'
type OrderFilterForm = {
keyword?: string
provider?: string
executorKey?: string
payStatus?: string
taskStatus?: string
dateRange?: unknown
}
/** 默认展示近 7 天(含今天)。 */
function resolveDefaultOrderDateRange() {
const today = dayjs()
return {
dateFrom: today.subtract(6, 'day').format('YYYY-MM-DD'),
dateTo: today.format('YYYY-MM-DD'),
}
}
export default function AdminOrdersPage() {
const navigate = useNavigate()
const [form] = Form.useForm<OrderFilterForm>()
const [searchParams, setSearchParams] = useSearchParams()
const page = Number(searchParams.get('page') || 1) || 1
const pageSize = resolveAdminPageSize(searchParams.get('pageSize'), ADMIN_DEFAULT_PAGE_SIZE)
// 首次进入(仅有分页参数)时写入默认近 7 天,避免一把拉出全库。
useEffect(() => {
const meaningfulKeys = [...searchParams.keys()].filter(
(key) => key !== 'page' && key !== 'pageSize',
)
if (meaningfulKeys.length > 0) {
return
}
const { dateFrom, dateTo } = resolveDefaultOrderDateRange()
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }), { replace: true })
// 仅首屏引导默认日期,避免与后续筛选互相打架。
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])
const filters = {
keyword: searchParams.get('keyword') || '',
provider: searchParams.get('provider') || '',
executorKey: searchParams.get('executorKey') || '',
payStatus: searchParams.get('payStatus') || '',
taskStatus: searchParams.get('taskStatus') || '',
@@ -54,20 +81,31 @@ export default function AdminOrdersPage() {
filters.executorKey,
filters.keyword,
filters.payStatus,
filters.provider,
filters.taskStatus,
page,
pageSize,
],
)
// 等待默认日期写入 URL 后再请求,避免首屏无日期打出全库。
const hasBootstrappedFilters =
Boolean(filters.dateFrom && filters.dateTo) ||
Boolean(filters.keyword) ||
searchParams.get('allTime') === '1' ||
Boolean(filters.provider || filters.executorKey || filters.payStatus || filters.taskStatus)
const query = useQuery({
queryKey: ['admin-orders', queryParams],
queryFn: () => fetchAdminOrders(queryParams),
enabled: hasBootstrappedFilters,
})
const data = query.data?.data
useEffect(() => {
form.setFieldsValue({
keyword: filters.keyword || undefined,
provider: filters.provider || undefined,
executorKey: filters.executorKey || undefined,
payStatus: filters.payStatus || undefined,
taskStatus: filters.taskStatus || undefined,
@@ -82,6 +120,7 @@ export default function AdminOrdersPage() {
filters.executorKey,
filters.keyword,
filters.payStatus,
filters.provider,
filters.taskStatus,
form,
])
@@ -185,14 +224,14 @@ export default function AdminOrdersPage() {
),
},
{
title: '金额 / 时间',
title: '金额 / 支付时间',
minWidth: 180,
render: (_, row) => (
<div className="cell-stack">
<strong>
{row.totalAmount || '0.00'} {row.currency}
</strong>
<span className="muted">{formatAdminDateTime(row.createdAt)}</span>
<span className="muted">{formatAdminDateTime(row.paidAt || row.createdAt)}</span>
</div>
),
},
@@ -200,29 +239,50 @@ export default function AdminOrdersPage() {
function applyFilters(values: OrderFilterForm) {
const { dateRange, ...restValues } = values
const [dateFrom, dateTo] = Array.isArray(dateRange)
const keyword = String(restValues.keyword || '').trim()
const [rawFrom, rawTo] = Array.isArray(dateRange)
? dateRange.map((item: { format?: (pattern: string) => string }) =>
item?.format ? item.format('YYYY-MM-DD') : '',
)
: ['', '']
setSearchParams(cleanParams({ ...restValues, dateFrom, dateTo, page: 1, pageSize }))
// 有关键词时不强制日期(后端也会忽略日期);清空日期且无关键词则查全部时间。
const dateFrom = rawFrom || ''
const dateTo = rawTo || ''
const allTime = !dateFrom && !dateTo && !keyword ? '1' : ''
setSearchParams(
cleanParams({
...restValues,
keyword,
dateFrom,
dateTo,
allTime,
page: 1,
pageSize,
}),
)
}
function resetFilters() {
const { dateFrom, dateTo } = resolveDefaultOrderDateRange()
form.setFieldsValue({
keyword: undefined,
provider: undefined,
executorKey: undefined,
payStatus: undefined,
taskStatus: undefined,
dateRange: undefined,
dateRange: [dayjs(dateFrom), dayjs(dateTo)],
})
setSearchParams(cleanParams({ page: 1, pageSize }))
setSearchParams(cleanParams({ page: 1, pageSize, dateFrom, dateTo }))
}
return (
<section className="page-stack">
<PageHeader title="订单" description="查看订单入库、商品匹配和任务拆分情况。" />
<PageHeader
title="订单"
description="默认近 7 天(按支付/成团时间)。搜单号或商品时不限日期。"
/>
<Card>
<Form<OrderFilterForm>
@@ -234,8 +294,16 @@ export default function AdminOrdersPage() {
<Form.Item name="keyword">
<Input
allowClear
placeholder="订单号 / 商品标题 / SKU 标题 / 买家 / 店铺"
style={{ width: 320 }}
placeholder="订单号 / 商品标题 / SKU / 买家 / 店铺"
style={{ width: 300 }}
/>
</Form.Item>
<Form.Item name="provider">
<Select
allowClear
placeholder="来源渠道"
options={adminOrderProviderOptions}
style={{ minWidth: 130 }}
/>
</Form.Item>
<Form.Item name="executorKey">
@@ -243,15 +311,15 @@ export default function AdminOrdersPage() {
allowClear
placeholder="履约渠道"
options={adminOrderExecutorOptions}
style={{ minWidth: 150 }}
style={{ minWidth: 140 }}
/>
</Form.Item>
<Form.Item name="payStatus">
<Select
allowClear
placeholder="支付状态"
options={adminPayStatusOptions}
style={{ minWidth: 140 }}
options={adminPayStatusOptions.filter((item) => item.value)}
style={{ minWidth: 130 }}
/>
</Form.Item>
<Form.Item name="taskStatus">
@@ -260,12 +328,12 @@ export default function AdminOrdersPage() {
showSearch
optionFilterProp="label"
placeholder="任务状态"
options={adminTaskStatusOptions}
style={{ minWidth: 170 }}
options={adminTaskStatusOptions.filter((item) => item.value)}
style={{ minWidth: 150 }}
/>
</Form.Item>
<Form.Item name="dateRange">
<AdminRangePicker />
<AdminRangePicker allowClear placeholder={['支付开始', '支付结束']} />
</Form.Item>
<Form.Item>
<Space>
@@ -283,7 +351,7 @@ export default function AdminOrdersPage() {
<Card>
<Table<AdminOrderListItem>
rowKey="orderId"
loading={query.isLoading || query.isFetching}
loading={query.isLoading || query.isFetching || !hasBootstrappedFilters}
columns={columns}
dataSource={data?.items || []}
scroll={{ x: 900 }}
@@ -292,7 +360,14 @@ export default function AdminOrdersPage() {
pageSize: data?.pagination.pageSize || pageSize,
total: data?.pagination.total || 0,
onChange: (nextPage, nextPageSize) => {
setSearchParams(cleanParams({ ...filters, page: nextPage, pageSize: nextPageSize }))
setSearchParams(
cleanParams({
...filters,
allTime: searchParams.get('allTime') || '',
page: nextPage,
pageSize: nextPageSize,
}),
)
},
})}
/>
+6
View File
@@ -6,6 +6,12 @@ export const adminPayStatusOptions = [
{ label: '已退款', value: 'refunded' },
]
export const adminOrderProviderOptions = [
{ label: '拼多多', value: 'pdd' },
{ label: '91卡券', value: '91kaquan' },
{ label: '快手', value: 'kuaishou' },
]
export const adminOrderExecutorOptions = [
{ label: '联盟发货', value: 'affiliate_dash' },
{ label: '行业凭证', value: 'kuaishou-industry' },