匹配日志支持结果状态关键词与日期筛选及服务端分页

This commit is contained in:
yml2213
2026-08-23 10:30:47 +08:00
parent b4918769f1
commit fdf83a6745
4 changed files with 160 additions and 19 deletions
@@ -231,16 +231,65 @@ export async function createWorkProductMatchLog(input: {
return result.rows[0] || null return result.rows[0] || null
} }
export async function listWorkProductMatchLogs(limit = 100): Promise<WorkProductMatchLogRow[]> { /** 匹配日志分页查询:表本身全量保留,可按结果状态、关键词(商品/SKU/店铺)与日期范围筛选。 */
export async function listWorkProductMatchLogs({
page = 1,
pageSize = 20,
matchStatus = '',
keyword = '',
createdFrom = '',
createdTo = '',
}: {
page?: number
pageSize?: number
matchStatus?: string
keyword?: string
createdFrom?: string
createdTo?: string
} = {}): Promise<{ items: WorkProductMatchLogRow[]; total: number }> {
const conditions: string[] = []
const params: unknown[] = []
const status = String(matchStatus || '').trim()
if (status) {
params.push(status)
conditions.push(`wpml.match_status = $${params.length}`)
}
const normalizedKeyword = String(keyword || '').trim()
if (normalizedKeyword) {
params.push(`%${normalizedKeyword}%`)
conditions.push(
`(wpml.item_title ILIKE $${params.length} OR wpml.rel_item_id ILIKE $${params.length} OR wpml.rel_sku_id ILIKE $${params.length} OR wpml.sku_nick ILIKE $${params.length} OR wpml.seller_id ILIKE $${params.length})`,
)
}
if (createdFrom) {
params.push(createdFrom)
conditions.push(
`wpml.created_at >= ($${params.length}::date)::timestamp AT TIME ZONE 'Asia/Shanghai'`,
)
}
if (createdTo) {
params.push(createdTo)
conditions.push(
`wpml.created_at < (($${params.length}::date + 1)::timestamp AT TIME ZONE 'Asia/Shanghai')`,
)
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
const totalResult = await query<{ total: number }>(
`SELECT COUNT(*)::int AS total FROM work_product_match_logs wpml ${whereClause}`,
params,
)
params.push(pageSize, (page - 1) * pageSize)
const result = await query<WorkProductMatchLogRow>( const result = await query<WorkProductMatchLogRow>(
` `
SELECT wpml.*, wpr.rule_key, wpr.product_name SELECT wpml.*, wpr.rule_key, wpr.product_name
FROM work_product_match_logs wpml FROM work_product_match_logs wpml
LEFT JOIN work_product_rules wpr ON wpr.id = wpml.rule_id LEFT JOIN work_product_rules wpr ON wpr.id = wpml.rule_id
${whereClause}
ORDER BY wpml.created_at DESC, wpml.id DESC ORDER BY wpml.created_at DESC, wpml.id DESC
LIMIT $1 LIMIT $${params.length - 1} OFFSET $${params.length}
`, `,
[Math.min(500, Math.max(1, Math.floor(Number(limit) || 100)))], params,
) )
return result.rows return { items: result.rows, total: Number(totalResult.rows[0]?.total || 0) }
} }
@@ -15,7 +15,12 @@ import type { JsonObject } from '../../types/json.js'
import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js' import type { OrderItemRow, OrderRow } from '../../types/repository/rows.js'
import { createHttpError } from '../../utils/http.js' import { createHttpError } from '../../utils/http.js'
import { nowIso } from '../../utils/time.js' import { nowIso } from '../../utils/time.js'
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js' import {
normalizeDateString,
normalizePage,
normalizePageSize,
safeParseJson,
} from '../admin/admin-query-utils.js'
import { import {
mapWorkProductRule, mapWorkProductRule,
normalizeBoolean, normalizeBoolean,
@@ -262,8 +267,17 @@ export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
} }
export async function listAdminWorkProductMatchLogs(payload: JsonObject = {}) { export async function listAdminWorkProductMatchLogs(payload: JsonObject = {}) {
const limit = Math.min(500, normalizePositiveInteger(payload.limit, 100)) const page = normalizePage(payload.page)
return { items: (await listWorkProductMatchLogs(limit)).map(mapWorkProductMatchLog) } const pageSize = Math.min(200, normalizePageSize(payload.pageSize))
const { items, total } = await listWorkProductMatchLogs({
page,
pageSize,
matchStatus: String(payload.matchStatus || payload.status || '').trim(),
keyword: String(payload.keyword || '').trim(),
createdFrom: normalizeDateString(payload.createdFrom),
createdTo: normalizeDateString(payload.createdTo),
})
return { items: items.map(mapWorkProductMatchLog), pagination: { page, pageSize, total } }
} }
function mapWorkProductRuleMapping(mapping: WorkProductRuleMappingRow) { function mapWorkProductRuleMapping(mapping: WorkProductRuleMappingRow) {
@@ -1,4 +1,4 @@
import { PlayCircleOutlined, ReloadOutlined } from '@ant-design/icons' import { PlayCircleOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { import {
App, App,
@@ -20,6 +20,8 @@ import { useState } from 'react'
import { useNavigate } from 'react-router' import { useNavigate } from 'react-router'
import JsonPreview from '@/components/admin/JsonPreview' import JsonPreview from '@/components/admin/JsonPreview'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
import { import {
fetchAdminKuaishouIndustryShops, fetchAdminKuaishouIndustryShops,
fetchAdminKuaishouMatchSources, fetchAdminKuaishouMatchSources,
@@ -31,6 +33,7 @@ import {
import type { KuaishouMatchSource, WorkProductMatchLog } from '@/types/worker-platform' import type { KuaishouMatchSource, WorkProductMatchLog } from '@/types/worker-platform'
import type { AdminKuaishouIndustryShopOption } from '@/types/admin' import type { AdminKuaishouIndustryShopOption } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time' import { formatAdminDateTime } from '@/utils/admin-time'
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
export default function ProductMatchPanel() { export default function ProductMatchPanel() {
const { message } = App.useApp() const { message } = App.useApp()
@@ -42,6 +45,11 @@ export default function ProductMatchPanel() {
>(null) >(null)
const [selectedLog, setSelectedLog] = useState<WorkProductMatchLog | null>(null) const [selectedLog, setSelectedLog] = useState<WorkProductMatchLog | null>(null)
const [activeTab, setActiveTab] = useState('sources') const [activeTab, setActiveTab] = useState('sources')
const [logStatus, setLogStatus] = useState('')
const [logKeywordInput, setLogKeywordInput] = useState('')
const [logKeyword, setLogKeyword] = useState('')
const [logCreatedRange, setLogCreatedRange] = useState<[string, string] | null>(null)
const [logPage, setLogPage] = useState(1)
const shopsQuery = useQuery({ const shopsQuery = useQuery({
queryKey: ['admin-kuaishou-industry-shops', 'product-match'], queryKey: ['admin-kuaishou-industry-shops', 'product-match'],
@@ -52,8 +60,22 @@ export default function ProductMatchPanel() {
queryFn: () => fetchAdminKuaishouMatchSources(200), queryFn: () => fetchAdminKuaishouMatchSources(200),
}) })
const logsQuery = useQuery({ const logsQuery = useQuery({
queryKey: ['admin-worker-platform-product-match-logs'], queryKey: [
queryFn: () => fetchAdminWorkProductMatchLogs(100), 'admin-worker-platform-product-match-logs',
logStatus,
logKeyword,
logCreatedRange,
logPage,
],
queryFn: () =>
fetchAdminWorkProductMatchLogs({
matchStatus: logStatus || undefined,
keyword: logKeyword || undefined,
createdFrom: logCreatedRange?.[0],
createdTo: logCreatedRange?.[1],
page: logPage,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
}),
}) })
const matchConfigQuery = useQuery({ const matchConfigQuery = useQuery({
queryKey: ['admin-worker-platform-product-match-config'], queryKey: ['admin-worker-platform-product-match-config'],
@@ -62,6 +84,7 @@ export default function ProductMatchPanel() {
const sources = sourcesQuery.data?.data.items || [] const sources = sourcesQuery.data?.data.items || []
const logs = logsQuery.data?.data.items || [] const logs = logsQuery.data?.data.items || []
const logsPagination = logsQuery.data?.data.pagination
const shops = shopsQuery.data?.data.shops || [] const shops = shopsQuery.data?.data.shops || []
const matchConfig = matchConfigQuery.data?.data const matchConfig = matchConfigQuery.data?.data
const shopNameBySellerId = createShopNameBySellerId(shops) const shopNameBySellerId = createShopNameBySellerId(shops)
@@ -305,10 +328,62 @@ export default function ProductMatchPanel() {
}, },
{ {
key: 'logs', key: 'logs',
label: `匹配日志 (${logs.length})`, label: `匹配日志 (${logsPagination?.total ?? 0})`,
children: ( children: (
<div className="page-stack"> <div className="page-stack">
<Space> <Space wrap>
<Select
allowClear
value={logStatus || undefined}
style={{ width: 130 }}
placeholder="全部结果"
options={[
{ value: 'matched', label: '命中' },
{ value: 'unmatched', label: '未命中' },
{ value: 'ambiguous', label: '冲突' },
]}
onChange={(value) => {
setLogStatus(value || '')
setLogPage(1)
}}
/>
<Input
allowClear
value={logKeywordInput}
style={{ width: 240 }}
placeholder="商品名 / 商品ID / SKU / 店铺ID"
onChange={(event) => setLogKeywordInput(event.target.value)}
onPressEnter={() => {
setLogKeyword(logKeywordInput.trim())
setLogPage(1)
}}
/>
<AdminRangePicker
value={
logCreatedRange
? [dayjs(logCreatedRange[0]), dayjs(logCreatedRange[1])]
: null
}
onChange={(dates) => {
const [start, end] = dates || []
setLogCreatedRange(
start && end
? [start.format(ADMIN_DATE_FORMAT), end.format(ADMIN_DATE_FORMAT)]
: null,
)
setLogPage(1)
}}
/>
<Button
type="primary"
icon={<SearchOutlined />}
onClick={() => {
setLogKeyword(logKeywordInput.trim())
setLogPage(1)
}}
>
</Button>
<Button <Button
icon={<ReloadOutlined />} icon={<ReloadOutlined />}
loading={logsQuery.isFetching} loading={logsQuery.isFetching}
@@ -317,7 +392,7 @@ export default function ProductMatchPanel() {
</Button> </Button>
<Typography.Text type="secondary"> <Typography.Text type="secondary">
</Typography.Text> </Typography.Text>
</Space> </Space>
<Table<WorkProductMatchLog> <Table<WorkProductMatchLog>
@@ -325,7 +400,12 @@ export default function ProductMatchPanel() {
loading={logsQuery.isLoading} loading={logsQuery.isLoading}
columns={logColumns} columns={logColumns}
dataSource={logs} dataSource={logs}
pagination={{ pageSize: 20, showSizeChanger: false }} pagination={buildAdminTablePagination({
page: logPage,
pageSize: ADMIN_DEFAULT_PAGE_SIZE,
total: logsPagination?.total || 0,
onChange: setLogPage,
})}
size="small" size="small"
/> />
</div> </div>
@@ -332,12 +332,10 @@ export function testAdminKuaishouProductMatch(rawPayload: string) {
}>('/api/v1/admin/worker-platform/product-match-test', { rawPayload }) }>('/api/v1/admin/worker-platform/product-match-test', { rawPayload })
} }
export function fetchAdminWorkProductMatchLogs(limit = 100) { export function fetchAdminWorkProductMatchLogs(params?: Record<string, unknown>) {
return apiGet<{ items: WorkProductMatchLog[] }>( return apiGet<WorkerListResponse<WorkProductMatchLog>>(
'/api/v1/admin/worker-platform/product-match-logs', '/api/v1/admin/worker-platform/product-match-logs',
{ params,
limit,
},
) )
} }