feat: 累积功能变更 — 聊天集成、租户管理、小程序更新、ETL 增强、迁移脚本
包含多个会话的累积代码变更: - backend: AI 聊天服务、触发器调度、认证增强、WebSocket、调度器最小间隔 - admin-web: ETL 状态页、任务管理、调度配置、登录优化 - miniprogram: 看板页面、聊天集成、UI 组件、导航更新 - etl: DWS 新任务(finance_area_daily/board_cache)、连接器增强 - tenant-admin: 项目初始化 - db: 19 个迁移脚本(etl_feiqiu 11 + zqyy_app 8) - packages/shared: 枚举和工具函数更新 - tools: 数据库工具、报表生成、健康检查 - docs: PRD/架构/部署/合约文档更新 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
apps/admin-web/src/pages/TriggerJobs.tsx
Normal file
206
apps/admin-web/src/pages/TriggerJobs.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 定时任务管理页面。
|
||||
*
|
||||
* 展示 biz.trigger_jobs 表中所有定时任务,支持手动执行。
|
||||
*/
|
||||
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { Table, Tag, Button, message, Modal, Typography, Card, Space, Popconfirm, Tooltip } from 'antd';
|
||||
import {
|
||||
ReloadOutlined,
|
||||
ClockCircleOutlined,
|
||||
PlayCircleOutlined,
|
||||
CheckCircleOutlined,
|
||||
ExclamationCircleOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { fetchTriggerJobs, runTriggerJob, clearAllTasks, type TriggerJob } from '../api/triggerJobs';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
const TRIGGER_LABEL: Record<string, string> = {
|
||||
cron: '定时(Cron)',
|
||||
interval: '间隔',
|
||||
event: '事件触发',
|
||||
};
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = {
|
||||
enabled: 'green',
|
||||
disabled: 'default',
|
||||
};
|
||||
|
||||
function formatTime(raw: string | null): string {
|
||||
if (!raw) return '—';
|
||||
const d = new Date(raw);
|
||||
return Number.isNaN(d.getTime()) ? raw : d.toLocaleString('zh-CN');
|
||||
}
|
||||
|
||||
function formatTriggerConfig(job: TriggerJob): string {
|
||||
const cfg = job.trigger_config;
|
||||
if (!cfg) return '—';
|
||||
if (job.trigger_condition === 'cron') return cfg.cron_expression as string || '—';
|
||||
if (job.trigger_condition === 'interval') {
|
||||
const sec = cfg.interval_seconds as number;
|
||||
if (sec >= 3600) return `每 ${sec / 3600} 小时`;
|
||||
if (sec >= 60) return `每 ${sec / 60} 分钟`;
|
||||
return `每 ${sec} 秒`;
|
||||
}
|
||||
if (job.trigger_condition === 'event') return `事件: ${cfg.event_name || '—'}`;
|
||||
return JSON.stringify(cfg);
|
||||
}
|
||||
|
||||
const TriggerJobs: React.FC = () => {
|
||||
const [jobs, setJobs] = useState<TriggerJob[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [runningId, setRunningId] = useState<number | null>(null);
|
||||
const [clearing, setClearing] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const data = await fetchTriggerJobs();
|
||||
setJobs(data);
|
||||
} catch {
|
||||
message.error('加载定时任务失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => { load(); }, [load]);
|
||||
|
||||
const handleRun = async (jobId: number) => {
|
||||
setRunningId(jobId);
|
||||
try {
|
||||
const result = await runTriggerJob(jobId);
|
||||
if (result.success) {
|
||||
message.success(result.message);
|
||||
} else {
|
||||
message.error(result.message);
|
||||
}
|
||||
await load();
|
||||
} catch {
|
||||
message.error('执行失败');
|
||||
} finally {
|
||||
setRunningId(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearAllTasks = async () => {
|
||||
setClearing(true);
|
||||
try {
|
||||
const result = await clearAllTasks();
|
||||
if (result.success) {
|
||||
Modal.success({
|
||||
title: '清空完成',
|
||||
content: result.message,
|
||||
});
|
||||
await load();
|
||||
} else {
|
||||
message.error(result.message);
|
||||
}
|
||||
} catch {
|
||||
message.error('清空任务失败');
|
||||
} finally {
|
||||
setClearing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<TriggerJob> = [
|
||||
{
|
||||
title: '任务名称', dataIndex: 'job_name', key: 'job_name', width: 180,
|
||||
render: (name: string, record) => (
|
||||
<Tooltip title={record.description || name}>
|
||||
<Text strong>{record.description || name}</Text>
|
||||
<br />
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>{name}</Text>
|
||||
</Tooltip>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '触发方式', dataIndex: 'trigger_condition', key: 'trigger_condition', width: 120,
|
||||
render: (v: string) => <Tag>{TRIGGER_LABEL[v] || v}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '触发配置', key: 'trigger_config', width: 150,
|
||||
render: (_: unknown, record) => <code style={{ fontSize: 12 }}>{formatTriggerConfig(record)}</code>,
|
||||
},
|
||||
{
|
||||
title: '状态', dataIndex: 'status', key: 'status', width: 80,
|
||||
render: (v: string) => <Tag color={STATUS_COLOR[v] || 'default'}>{v === 'enabled' ? '启用' : '禁用'}</Tag>,
|
||||
},
|
||||
{
|
||||
title: '上次执行', dataIndex: 'last_run_at', key: 'last_run_at', width: 170,
|
||||
render: (v: string | null) => formatTime(v),
|
||||
},
|
||||
{
|
||||
title: '下次执行', dataIndex: 'next_run_at', key: 'next_run_at', width: 170,
|
||||
render: (v: string | null) => formatTime(v),
|
||||
},
|
||||
{
|
||||
title: '最近错误', dataIndex: 'last_error', key: 'last_error', width: 200,
|
||||
render: (v: string | null) => v
|
||||
? <Tooltip title={v}><Text type="danger" ellipsis style={{ maxWidth: 180 }}><ExclamationCircleOutlined /> {v}</Text></Tooltip>
|
||||
: <Text type="success"><CheckCircleOutlined /> 正常</Text>,
|
||||
},
|
||||
{
|
||||
title: '操作', key: 'action', width: 100, fixed: 'right',
|
||||
render: (_: unknown, record) => (
|
||||
<Popconfirm
|
||||
title={`确认手动执行「${record.description || record.job_name}」?`}
|
||||
onConfirm={() => handleRun(record.id)}
|
||||
okText="执行"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={runningId === record.id}
|
||||
disabled={record.status !== 'enabled'}
|
||||
>
|
||||
执行
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Title level={4} style={{ margin: 0 }}>
|
||||
<ClockCircleOutlined style={{ marginRight: 8 }} />
|
||||
定时任务管理
|
||||
</Title>
|
||||
<Space>
|
||||
<Popconfirm
|
||||
title="确认清空所有助教任务?"
|
||||
description="将删除 coach_tasks 和 coach_task_history 中的全部数据,此操作不可撤销。"
|
||||
onConfirm={handleClearAllTasks}
|
||||
okText="确认清空"
|
||||
cancelText="取消"
|
||||
okButtonProps={{ danger: true }}
|
||||
>
|
||||
<Button danger loading={clearing}>🧹 清空所有任务</Button>
|
||||
</Popconfirm>
|
||||
<Button icon={<ReloadOutlined />} onClick={load} loading={loading}>刷新</Button>
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
<Card size="small">
|
||||
<Table<TriggerJob>
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={jobs}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
size="small"
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TriggerJobs;
|
||||
Reference in New Issue
Block a user