匹配日志支持结果状态关键词与日期筛选及服务端分页
This commit is contained in:
@@ -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,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user