feat(scheduler): 财务管道自动注册、水位记录与后台状态页
- 老配置读取时自动补齐六个管道任务,升级后无需手工添加 - 每个管道运行后写水位与 last_success_at,失败写 last_error - 新增 GET /admin/fulfillment/pipeline/status 与单步重跑 - 后台「履约渠道 → 管道状态」展示启停、水位、最近执行与错误 - 定时任务管理接口不再丢弃管道自己的 config 字段
This commit is contained in:
@@ -34,7 +34,9 @@ import {
|
||||
fetchChannelExceptions,
|
||||
fetchChannelRules,
|
||||
fetchPendingChannelGroups,
|
||||
fetchPipelineStatus,
|
||||
runFinancePipeline,
|
||||
type FinancePipelineStatus,
|
||||
saveChannelRule,
|
||||
type ChannelRule,
|
||||
type PendingChannelGroup,
|
||||
@@ -72,6 +74,7 @@ export default function AdminFulfillmentChannelPage() {
|
||||
{ key: 'pending', label: '待分类队列', children: <PendingPanel /> },
|
||||
{ key: 'rules', label: '渠道规则', children: <RulesPanel /> },
|
||||
{ key: 'exceptions', label: '问题队列', children: <ExceptionsPanel /> },
|
||||
{ key: 'pipelines', label: '管道状态', children: <PipelinesPanel /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
@@ -406,6 +409,106 @@ function ExceptionsPanel() {
|
||||
)
|
||||
}
|
||||
|
||||
function PipelinesPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const query = useQuery({
|
||||
queryKey: ['fulfillment-pipelines'],
|
||||
queryFn: fetchPipelineStatus,
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
const runMutation = useMutation({
|
||||
mutationFn: (pipeline: string) => runFinancePipeline({ pipeline }),
|
||||
onSuccess: () => {
|
||||
message.success('管道已执行')
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pipelines'] })
|
||||
void queryClient.invalidateQueries({ queryKey: ['fulfillment-pending'] })
|
||||
},
|
||||
onError: (error) => message.error(error instanceof Error ? error.message : '执行管道失败'),
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ title: '管道', dataIndex: 'label', width: 130 },
|
||||
{
|
||||
title: '定时任务',
|
||||
width: 150,
|
||||
render: (_: unknown, row: FinancePipelineStatus) =>
|
||||
row.scheduler ? (
|
||||
row.scheduler.enabled ? (
|
||||
<Tag color="green">已启用</Tag>
|
||||
) : (
|
||||
<Tag>已停用</Tag>
|
||||
)
|
||||
) : (
|
||||
<Tag color="orange">未注册</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '上次成功',
|
||||
dataIndex: 'lastSuccessAt',
|
||||
width: 170,
|
||||
render: (value: string | null) => (value ? formatDateTime(value) : '从未成功'),
|
||||
},
|
||||
{
|
||||
title: '水位',
|
||||
width: 200,
|
||||
render: (_: unknown, row: FinancePipelineStatus) =>
|
||||
row.lastStatDate
|
||||
? `日期 ${row.lastStatDate}`
|
||||
: row.lastItemId > 0
|
||||
? `订单项 ${row.lastItemId}`
|
||||
: '—',
|
||||
},
|
||||
{
|
||||
title: '最近一次执行',
|
||||
width: 260,
|
||||
render: (_: unknown, row: FinancePipelineStatus) => row.scheduler?.lastMessage || '—',
|
||||
},
|
||||
{
|
||||
title: '错误',
|
||||
render: (_: unknown, row: FinancePipelineStatus) =>
|
||||
row.lastError ? <Typography.Text type="danger">{row.lastError}</Typography.Text> : '—',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 110,
|
||||
render: (_: unknown, row: FinancePipelineStatus) => (
|
||||
<Button
|
||||
size="small"
|
||||
loading={runMutation.isPending && runMutation.variables === row.pipeline}
|
||||
onClick={() => runMutation.mutate(row.pipeline)}
|
||||
>
|
||||
立即执行
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<Card
|
||||
title="管道状态"
|
||||
extra={
|
||||
<Button icon={<ReloadOutlined />} onClick={() => void query.refetch()}>
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Typography.Paragraph type="secondary">
|
||||
六个管道由定时任务自动执行,无需人工干预;这里只用于查看进度、
|
||||
排障和补跑。默认间隔:渠道判定/载体 60s,商品/成本 300s,事实/总账 600s。
|
||||
</Typography.Paragraph>
|
||||
<PercentTable
|
||||
rowKey="pipeline"
|
||||
size="small"
|
||||
loading={query.isFetching}
|
||||
dataSource={query.data?.data || []}
|
||||
columns={columns}
|
||||
pagination={false}
|
||||
/>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
function rowKey(row: PendingChannelGroup) {
|
||||
return `${row.provider}|${row.shop_id}|${row.sku_code}`
|
||||
}
|
||||
|
||||
@@ -51,6 +51,25 @@ export type ChannelExceptionListResult = {
|
||||
|
||||
export type PipelineRunResult = Record<string, unknown>
|
||||
|
||||
export type FinancePipelineStatus = {
|
||||
pipeline: string
|
||||
label: string
|
||||
lastItemId: number
|
||||
lastStatDate: string | null
|
||||
lastSuccessAt: string | null
|
||||
lastError: string
|
||||
jobId: string
|
||||
scheduler: {
|
||||
running: boolean
|
||||
enabled: boolean
|
||||
lastRunAt: string
|
||||
lastFinishedAt: string
|
||||
nextRunAt: string
|
||||
lastStatus: string
|
||||
lastMessage: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export function fetchPendingChannelGroups(
|
||||
params: {
|
||||
page?: number
|
||||
@@ -112,7 +131,11 @@ export function deleteChannelRule(ruleId: number) {
|
||||
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/fulfillment/rules/${ruleId}`)
|
||||
}
|
||||
|
||||
export function runFinancePipeline(payload: { days?: number } = {}) {
|
||||
export function fetchPipelineStatus() {
|
||||
return apiGet<FinancePipelineStatus[]>('/api/v1/admin/fulfillment/pipeline/status')
|
||||
}
|
||||
|
||||
export function runFinancePipeline(payload: { days?: number; pipeline?: string } = {}) {
|
||||
return apiPost<PipelineRunResult>('/api/v1/admin/fulfillment/pipeline/run', payload, {
|
||||
timeout: 300_000,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user