feat(scheduler): 财务管道自动注册、水位记录与后台状态页

- 老配置读取时自动补齐六个管道任务,升级后无需手工添加
- 每个管道运行后写水位与 last_success_at,失败写 last_error
- 新增 GET /admin/fulfillment/pipeline/status 与单步重跑
- 后台「履约渠道 → 管道状态」展示启停、水位、最近执行与错误
- 定时任务管理接口不再丢弃管道自己的 config 字段
This commit is contained in:
yml2213
2026-10-31 19:13:38 +08:00
parent 2ba2656d58
commit 808949f3b8
7 changed files with 336 additions and 2 deletions
@@ -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,
})