当前质控任务只支持手动执行,需要增加周期性自动执行功能:
| 对比项 | 自动任务 | 手动任务 |
|---|---|---|
| 执行方式 | 定时自动执行 | 手动触发执行 |
| 数据选择 | 按规则自动抽取 | 手动选择具体检查数据 |
| 数据范围 | 指定数量+抽取方式 | 指定时间范围+手动选择 |
| 结果查看 | 查看多次执行的历史记录 | 查看单次执行结果 |
| 技术实现 | Quartz定时调度 | 即时执行 |
qc_task: 质控任务表
- id: 任务ID
- task_name: 任务名称
- task_type: 任务类型
- institution_id: 机构ID
- standard_id: 质控标准ID
- plan_count: 计划数量
- sample_type: 抽样类型
- data_range_type: 数据范围类型(1时间/2患者)
- start_date/ end_date: 时间范围
- patient_ids: 患者ID列表
- exam_ids: 检查ID列表(逗号分隔)
- status: 任务状态(0待执行/1执行中/2已完成/3失败)
- create_time/ update_time
qc_task_exam: 任务检查关联表
- id
- task_id: 任务ID
- study_id: 检查ID
- create_time
qc_task_image_result: 图像质控结果表
- id
- task_id
- study_id
- image_index
- sop_instance_uid
- is_qualified
- total_score
- ...其他质控字段
- 任务名称(必填,支持手动修改,自动生成默认值:qc+日期+4位字母+4位数字)
- 任务类型(单选:自动/手动)
- 机构(必选,下拉选择)
- 时间周期(必填,Cron表达式生成器)
- 快捷选项:每天/每周/每月
- 高级配置:Cron表达式编辑器
- 预览下次执行时间
- 数据数量(必填,数字输入,1-10000)
- 数据抽取方式(单选:随机抽取/按时间顺序)
- 机构(必选,下拉选择)
- 时间范围(必填,日期范围选择器)
- 开始日期
- 结束日期
- 检查数据(必选,弹出选择器)
- 显示已选择数量
- 支持查看已选数据列表
- 支持清空选择
- 数据数量(自动计算=已选数量,只读)
序号 | 任务名称 | 任务类型 | 机构名称 | 计划数量 | 执行统计 | 最近执行 | 下次执行 | 状态 | 操作
任务类型列:
执行统计列:
自动任务:
已执行12次 | 平均通过率85%
手动任务:
一次性 | 已完成
最近执行列:
自动任务:显示最近一次执行时间和状态
02-03 10:00 成功
手动任务:显示执行时间
02-03 15:30
下次执行列:
自动任务:显示下次执行时间
02-10 10:00
手动任务:显示 "-"
操作列差异:
入口:点击自动任务的"查看历史"按钮
页面结构:
┌──────────────────────────────────────┐
│ 返回 执行历史 - [任务名称] │
├──────────────────────────────────────┤
│ 统计卡片(4个) │
│ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │
│ │总数 │ │成功率│ │平均数│ │通过率│ │
│ │ 12 │ │91.7%│ │ 98 │ │86.5%│ │
│ └─────┘ └─────┘ └─────┘ └─────┘ │
├──────────────────────────────────────┤
│ 执行历史表格 │
│ 序号|执行时间|状态|数量|通过率|操作 │
│ 12 |02-03 10:00|✓|100|85%|[查看结果]│
│ 11 |01-27 10:00|✓|98 |88%|[查看结果]│
│ 10 |01-20 10:00|✗|95 |- |[查看错误]│
└──────────────────────────────────────┘
统计指标:
表格列:
入口:执行历史页 → 点击某次执行的"查看结果"
展示内容:
入口:任务列表 → 点击"查看结果"
展示内容:
┌─────────────────────────────────────────┐
│ 前端(Vue3) │
│ - QcTask.vue(任务列表) │
│ - ExecutionHistory.vue(执行历史) │
│ - QcResultDetail.vue(结果详情-复用) │
└──────────────┬──────────────────────────┘
│ HTTP API
┌──────────────▼──────────────────────────┐
│ 后端 │
│ - QcTaskController │
│ - QcTaskService │
│ - QuartzScheduler(定时调度) │
│ - QcExecutionService(执行服务) │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ Quartz │
│ - Job: AutoQcJob │
│ - Trigger: CronTrigger │
│ - Scheduler: StdScheduler │
└──────────────┬──────────────────────────┘
│
┌──────────────▼──────────────────────────┐
│ 数据库层 │
│ - qc_task(任务模板) │
│ - qc_task_execution(执行记录) │
│ - qc_task_exam(检查关联) │
│ - qc_task_image_result(质控结果) │
└─────────────────────────────────────────┘
CREATE TABLE `qc_task_execution` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '执行记录ID',
`task_id` bigint NOT NULL COMMENT '关联任务ID',
`task_name` varchar(255) NOT NULL COMMENT '任务名称(快照)',
`execute_no` int NOT NULL COMMENT '执行编号(第N次执行)',
`plan_execute_time` datetime NOT NULL COMMENT '计划执行时间',
`actual_execute_time` datetime DEFAULT NULL COMMENT '实际执行时间',
`status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0待执行/1执行中/2已完成/3失败',
-- 执行结果统计
`plan_count` int DEFAULT 0 COMMENT '计划数量',
`total_count` int DEFAULT 0 COMMENT '实际检查数量',
`completed_count` int DEFAULT 0 COMMENT '已完成数量',
`pass_count` int DEFAULT 0 COMMENT '通过数量',
`fail_count` int DEFAULT 0 COMMENT '不通过数量',
`pass_rate` decimal(5,2) DEFAULT 0.00 COMMENT '通过率(%)',
-- 时间信息
`start_time` datetime DEFAULT NULL COMMENT '开始执行时间',
`end_time` datetime DEFAULT NULL COMMENT '结束执行时间',
`duration` int DEFAULT NULL COMMENT '执行耗时(秒)',
-- 错误信息
`error_message` text COMMENT '失败原因',
`stack_trace` text COMMENT '错误堆栈',
-- 基础字段
`creator` varchar(64) DEFAULT NULL COMMENT '创建人',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL COMMENT '修改时间',
`is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_task_id` (`task_id`),
KEY `idx_execute_time` (`plan_execute_time`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='质控任务执行记录表';
ALTER TABLE `qc_task`
ADD COLUMN `schedule_config` json DEFAULT NULL COMMENT '定时配置' AFTER `task_type`,
ADD COLUMN `is_enabled` tinyint NOT NULL DEFAULT 1 COMMENT '是否启用:0禁用/1启用' AFTER `schedule_config`;
-- schedule_config JSON结构示例
{
"schedule_type": "weekly", // daily/weekly/monthly/custom
"cron_expression": "0 0 10 ? * MON", // Cron表达式
"next_execute_time": "2026-02-10 10:00:00",
"last_execute_time": "2026-02-03 10:00:00",
"sample_type": "random", // random/sequential
"sample_count": 100
}
ALTER TABLE `qc_task_exam`
ADD COLUMN `execution_id` bigint DEFAULT NULL COMMENT '执行记录ID(自动任务必填,手动任务为空)' AFTER `task_id`,
ADD INDEX `idx_execution_id` (`execution_id`);
ALTER TABLE `qc_task_image_result`
ADD COLUMN `execution_id` bigint DEFAULT NULL COMMENT '执行记录ID(自动任务必填)' AFTER `task_id`,
ADD INDEX `idx_execution_id` (`execution_id`);
POST /api/qc/task/create
PUT /api/qc/task/update
请求体:
{
"id": "编辑时必填",
"taskName": "qc20260203ABCD1234",
"taskType": "auto", // auto/manual
"institutionId": "机构ID",
// 自动任务字段
"scheduleConfig": {
"scheduleType": "weekly",
"cronExpression": "0 0 10 ? * MON",
"sampleCount": 100,
"sampleType": "random" // random/sequential
},
// 手动任务字段
"dataRangeType": 1,
"startDate": "2026-01-01",
"endDate": "2026-01-31",
"examIds": "studyId1,studyId2,studyId3", // 手动选择的检查ID
"planCount": 3 // 自动=已选数量
}
响应:
{
"code": 200,
"message": "创建成功",
"data": {
"taskId": 123
}
}
GET /api/qc/task/list?pageNum=1&pageSize=10&taskName=&status=
响应:
{
"code": 200,
"data": {
"records": [
{
"id": 123,
"taskName": "qc20260203ABCD1234",
"taskType": "auto",
"institutionName": "某某医院",
"planCount": 100,
"status": 1, // 0待执行/1执行中/2已完成/3失败
"isEnabled": 1,
"createTime": "2026-01-01 10:00:00",
// 执行统计(自动任务)
"executionStats": {
"totalCount": 12,
"successCount": 11,
"failCount": 1,
"avgPassRate": 85.5
},
// 最近执行(自动任务)
"lastExecution": {
"executeNo": 12,
"executeTime": "2026-02-03 10:00:00",
"status": 2,
"passRate": 85.0
},
// 下次执行(自动任务)
"nextExecuteTime": "2026-02-10 10:00:00"
}
],
"total": 100
}
}
GET /api/qc/task/execution-history?taskId=123&pageNum=1&pageSize=20
响应:
{
"code": 200,
"data": {
"records": [
{
"id": 456,
"taskId": 123,
"taskName": "qc20260203ABCD1234",
"executeNo": 12,
"planExecuteTime": "2026-02-03 10:00:00",
"actualExecuteTime": "2026-02-03 10:00:05",
"status": 2, // 0待执行/1执行中/2已完成/3失败
"planCount": 100,
"totalCount": 98,
"completedCount": 98,
"passCount": 83,
"failCount": 15,
"passRate": 85.0,
"duration": 125, // 秒
"errorMessage": null,
"createTime": "2026-02-03 10:00:00"
}
],
"total": 12
}
}
GET /api/qc/task/execution-stats?taskId=123
响应:
{
"code": 200,
"data": {
"totalCount": 12,
"successCount": 11,
"failCount": 1,
"avgPlanCount": 100,
"avgTotalCount": 98,
"avgPassRate": 85.5,
"recentExecutions": [...] // 最近5次
}
}
POST /api/qc/task/trigger-manual/{taskId}
响应:
{
"code": 200,
"message": "已加入执行队列",
"data": {
"executionId": 456
}
}
PUT /api/qc/task/toggle/{taskId}
参数: { "isEnabled": 0/1 }
响应:标准响应
POST /api/qc/task/retry/{executionId}
响应:
{
"code": 200,
"message": "重新执行成功",
"data": {
"executionId": 457
}
}
GET /api/qc/execution/result/{executionId}
# 注意:复用现有的质控结果接口逻辑
# 区别:查询条件基于 execution_id 而非 task_id
GET /api/qc/execution/exams/{executionId}
响应:
{
"code": 200,
"data": {
"executionInfo": {
"id": 456,
"taskName": "qc20260203ABCD1234",
"executeNo": 12,
"status": 2,
"totalCount": 98,
"passCount": 83
},
"exams": [
{
"studyId": "study-001",
"patientName": "**",
"modality": "CT",
"studyDate": "2026-02-03",
"examItemName": "CT胸部平扫",
"isQualified": 1,
"totalScore": 85.5
}
]
}
}
推荐使用 vue-cron-generator 或自建简化版:
安装:
npm install vue-cron-generator-quartz
使用示例:
<template>
<el-form-item label="执行周期" required>
<cron-selector-quartz
v-model="form.scheduleConfig.cronExpression"
:locale="locale"
@change="handleCronChange"
/>
<div class="cron-preview">
<el-tag type="info">下次执行: {{ nextExecuteTime }}</el-tag>
</div>
</el-form-item>
</template>
快捷方案(自建简化版):
<el-form-item label="执行周期">
<el-radio-group v-model="scheduleType">
<el-radio value="daily">每天</el-radio>
<el-radio value="weekly">每周</el-radio>
<el-radio value="monthly">每月</el-radio>
</el-radio-group>
<!-- 每周选择 -->
<el-checkbox-group v-if="scheduleType === 'weekly'" v-model="weekdays">
<el-checkbox value="MON">周一</el-checkbox>
<el-checkbox value="TUE">周二</el-checkbox>
<el-checkbox value="WED">周三</el-checkbox>
<el-checkbox value="THU">周四</el-checkbox>
<el-checkbox value="FRI">周五</el-checkbox>
<el-checkbox value="SAT">周六</el-checkbox>
<el-checkbox value="SUN">周日</el-checkbox>
</el-checkbox-group>
<!-- 时间选择 -->
<el-time-picker v-model="executeTime" format="HH:mm" />
</el-form-item>
创建表单(条件渲染):
<el-form ref="formRef" :model="form" :rules="rules">
<!-- 共有字段 -->
<el-form-item label="任务名称" prop="taskName" required>
<el-input v-model="form.taskName" />
</el-form-item>
<el-form-item label="任务类型" prop="taskType">
<el-radio-group v-model="form.taskType" @change="handleTaskTypeChange">
<el-radio value="auto">自动任务</el-radio>
<el-radio value="manual">手动任务</el-radio>
</el-radio-group>
</el-form-item>
<!-- 自动任务字段 -->
<template v-if="form.taskType === 'auto'">
<el-form-item label="质控机构" prop="institutionId" required>
<el-select v-model="form.institutionId">
<!-- 机构选项 -->
</el-select>
</el-form-item>
<el-form-item label="执行周期" prop="scheduleConfig" required>
<!-- Cron生成器 -->
</el-form-item>
<el-form-item label="数据数量" prop="planCount" required>
<el-input-number v-model="form.planCount" :min="1" :max="10000" />
</el-form-item>
<el-form-item label="抽取方式" prop="sampleType" required>
<el-radio-group v-model="form.sampleType">
<el-radio value="random">随机抽取</el-radio>
<el-radio value="sequential">按时间顺序</el-radio>
</el-radio-group>
</el-form-item>
</template>
<!-- 手动任务字段 -->
<template v-else>
<el-form-item label="质控机构" prop="institutionId" required>
<el-select v-model="form.institutionId">
<!-- 机构选项 -->
</el-select>
</el-form-item>
<el-form-item label="时间范围" prop="dateRange" required>
<el-date-picker
v-model="dateRange"
type="daterange"
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="检查数据" prop="examIds" required>
<el-button @click="showExamSelector">选择检查数据</el-button>
<el-tag>已选择 {{ selectedExams.length }} 条</el-tag>
</el-form-item>
<el-form-item label="数据数量">
<el-input v-model="selectedExams.length" disabled />
</el-form-item>
</template>
</el-form>
任务列表(差异展示):
<el-table-column label="执行统计">
<template #default="{ row }">
<span v-if="row.taskType === 'manual'">
一次性
</span>
<span v-else>
已执行{{ row.executionStats.totalCount }}次 |
平均通过率{{ row.executionStats.avgPassRate }}%
</span>
</template>
</el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<!-- 自动任务操作 -->
<template v-if="row.taskType === 'auto'">
<el-button @click="viewHistory(row)">查看历史</el-button>
<el-button @click="triggerManual(row)">立即执行</el-button>
<el-button @click="toggleTask(row)">
{{ row.isEnabled ? '禁用' : '启用' }}
</el-button>
</template>
<!-- 手动任务操作 -->
<template v-else>
<el-button v-if="row.status === 0" @click="execute(row)">开始执行</el-button>
<el-button v-if="row.status === 2" @click="viewResult(row)">查看结果</el-button>
</template>
<el-button @click="handleEdit(row)">编辑</el-button>
<el-button @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
页面结构:
<template>
<div class="execution-history">
<!-- 页面头部 -->
<el-page-header @back="goBack">
<template #content>
执行历史 - {{ taskInfo.taskName }}
</template>
</el-page-header>
<!-- 统计卡片 -->
<el-row :gutter="16" class="stats-cards">
<el-col :span="6">
<el-card>
<el-statistic title="总执行次数" :value="stats.totalCount" />
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<el-statistic
title="成功率"
:value="stats.successRate"
suffix="%"
:value-style="{ color: stats.successRate >= 80 ? '#67C23A' : '#F56C6C' }"
/>
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<el-statistic title="平均检查数" :value="stats.avgCount" />
</el-card>
</el-col>
<el-col :span="6">
<el-card>
<el-statistic
title="平均通过率"
:value="stats.avgPassRate"
suffix="%"
/>
</el-card>
</el-col>
</el-row>
<!-- 执行历史表格 -->
<el-table :data="executionList" v-loading="loading">
<el-table-column type="index" label="序号" width="60" />
<el-table-column prop="executeNo" label="执行编号" width="100">
<template #default="{ row }">
第{{ row.executeNo }}次
</template>
</el-table-column>
<el-table-column prop="planExecuteTime" label="计划执行时间" width="180" />
<el-table-column prop="actualExecuteTime" label="实际执行时间" width="180" />
<el-table-column label="状态" width="100" align="center">
<template #default="{ row }">
<el-tag v-if="row.status === 2" type="success">成功</el-tag>
<el-tag v-else-if="row.status === 3" type="danger">失败</el-tag>
<el-tag v-else-if="row.status === 1" type="warning">执行中</el-tag>
<el-tag v-else type="info">待执行</el-tag>
</template>
</el-table-column>
<el-table-column prop="totalCount" label="检查数量" width="100" align="center" />
<el-table-column prop="passRate" label="通过率" width="100" align="center">
<template #default="{ row }">
{{ row.passRate }}%
</template>
</el-table-column>
<el-table-column label="操作" width="150" align="center">
<template #default="{ row }">
<el-button
v-if="row.status === 2"
type="primary"
link
@click="viewResult(row)"
>
查看结果
</el-button>
<el-button
v-if="row.status === 3"
type="primary"
link
@click="viewError(row)"
>
查看错误
</el-button>
<el-button
v-if="row.status === 3"
type="warning"
link
@click="retry(row)"
>
重试
</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { getExecutionHistory, getExecutionStats } from '@/api/qc'
const route = useRoute()
const router = useRouter()
const taskId = route.params.taskId
const taskInfo = ref({})
const stats = ref({})
const executionList = ref([])
const loading = ref(false)
const loadData = async () => {
loading.value = true
try {
// 加载统计
const statsRes = await getExecutionStats(taskId)
stats.value = statsRes.data
// 加载历史列表
const listRes = await getExecutionHistory({
taskId,
pageNum: 1,
pageSize: 100
})
executionList.value = listRes.data.records
} finally {
loading.value = false
}
}
const viewResult = (row) => {
// 跳转到质控结果页,传递 execution_id
router.push(`/qc/execution/result/${row.id}`)
}
onMounted(() => {
loadData()
})
</script>
路由适配:
// router/index.ts 新增路由
{
path: '/qc/execution/result/:executionId',
name: 'ExecutionResultDetail',
component: () => import('@/views/QcResultDetail.vue'),
meta: { title: '质控结果详情', requiresAuth: true }
}
数据加载适配:
// QcResultDetail.vue
const executionId = route.params.executionId
const taskId = route.query.taskId // 兼容手动任务
const loadDetail = async () => {
if (executionId) {
// 自动任务:加载执行记录的质控结果
const res = await getExecutionResult(executionId)
detailData.value = res.data
} else if (taskId) {
// 手动任务:加载任务的质控结果
const res = await getTaskResult(taskId)
detailData.value = res.data
}
}
// router/index.ts
const routes: RouteRecordRaw[] = [
// ... 现有路由
// 新增:执行历史
{
path: '/qc/task/execution-history/:taskId',
name: 'ExecutionHistory',
component: () => import('@/views/ExecutionHistory.vue'),
meta: { title: '执行历史', requiresAuth: true }
},
// 改造:质控结果详情(兼容executionId)
{
path: '/qc/result',
name: 'QcResult',
component: () => import('@/views/QcResult.vue'),
meta: { title: '质控结果', requiresAuth: true }
},
{
path: '/qc/execution/result/:executionId',
name: 'ExecutionResultDetail',
component: () => import('@/views/QcResultDetail.vue'),
meta: { title: '质控结果详情', requiresAuth: true }
}
]
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.3.2</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jobs</artifactId>
<version>2.3.2</version>
</dependency>
@Configuration
@ComponentScan(basePackages = "com.zskk.qcns.modules.qc.quartz")
public class QuartzConfig {
@Bean
public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) {
SchedulerFactoryBean factory = new SchedulerFactoryBean();
factory.setDataSource(dataSource);
factory.setJobFactory(springBeanJobFactory);
factory.setApplicationContextSchedulerContextKey("applicationContext");
// 持久化配置
factory.setQuartzProperties(quartzProperties());
return factory;
}
@Bean
public SpringBeanJobFactory springBeanJobFactory(ApplicationContext applicationContext) {
AutowiringSpringBeanJobFactory jobFactory = new AutowiringSpringBeanJobFactory();
jobFactory.setApplicationContext(applicationContext);
return jobFactory;
}
private Properties quartzProperties() {
Properties prop = new Properties();
prop.put("org.quartz.scheduler.instanceName", "QcScheduler");
prop.put("org.quartz.scheduler.instanceId", "AUTO");
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX");
prop.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_");
prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool");
prop.put("org.quartz.threadPool.threadCount", "5");
return prop;
}
}
Quartz需要创建以下表(在数据库中执行):
SQL脚本位置:quartz-2.3.2/docs/dbTables/
@Component
@DisallowConcurrentExecution
public class AutoQcJob extends QuartzJobBean {
@Resource
private QcExecutionService qcExecutionService;
@Resource
private QcTaskService qcTaskService;
@Override
protected void execute(JobExecutionContext context) throws JobExecutionException {
Long taskId = context.getJobDetail().getJobDataMap().getLong("taskId");
log.info("自动质控任务开始执行,taskId={}", taskId);
try {
// 1. 创建执行记录
QcTaskExecution execution = qcExecutionService.createExecution(taskId);
// 2. 根据配置抽取检查数据
List<QcTaskExam> exams = qcExecutionService.extractExams(taskId, execution.getId());
// 3. 执行质控
qcExecutionService.executeQc(execution.getId(), exams);
// 4. 更新执行记录状态
qcExecutionService.completeExecution(execution.getId());
log.info("自动质控任务执行完成,executionId={}", execution.getId());
} catch (Exception e) {
log.error("自动质控任务执行失败,taskId={}", taskId, e);
throw new JobExecutionException(e);
}
}
}
@Service
public class QcJobService {
@Resource
private Scheduler scheduler;
/**
* 创建定时任务
*/
public void scheduleJob(Long taskId, QcTask task) {
try {
// 1. 创建JobDetail
JobDetail jobDetail = JobBuilder.newJob(AutoQcJob.class)
.withIdentity("JOB_" + taskId, "QC_TASK_GROUP")
.usingJobData("taskId", taskId)
.build();
// 2. 创建Trigger(Cron表达式)
CronTrigger trigger = TriggerBuilder.newTrigger()
.withIdentity("TRIGGER_" + taskId, "QC_TASK_GROUP")
.withSchedule(CronScheduleBuilder.cronSchedule(task.getCronExpression()))
.build();
// 3. 调度任务
scheduler.scheduleJob(jobDetail, trigger);
scheduler.start();
log.info("定时任务创建成功,taskId={}", taskId);
} catch (Exception e) {
log.error("定时任务创建失败,taskId={}", taskId, e);
throw new ServiceException("定时任务创建失败");
}
}
/**
* 暂停任务
*/
public void pauseJob(Long taskId) {
try {
JobKey jobKey = JobKey.jobKey("JOB_" + taskId, "QC_TASK_GROUP");
scheduler.pauseJob(jobKey);
} catch (SchedulerException e) {
throw new ServiceException("暂停任务失败");
}
}
/**
* 恢复任务
*/
public void resumeJob(Long taskId) {
try {
JobKey jobKey = JobKey.jobKey("JOB_" + taskId, "QC_TASK_GROUP");
scheduler.resumeJob(jobKey);
} catch (SchedulerException e) {
throw new ServiceException("恢复任务失败");
}
}
/**
* 删除任务
*/
public void deleteJob(Long taskId) {
try {
JobKey jobKey = JobKey.jobKey("JOB_" + taskId, "QC_TASK_GROUP");
scheduler.deleteJob(jobKey);
} catch (SchedulerException e) {
throw new ServiceException("删除任务失败");
}
}
/**
* 手动触发执行
*/
public Long triggerJob(Long taskId) {
try {
// 1. 创建执行记录
QcTaskExecution execution = qcExecutionService.createExecution(taskId);
// 2. 手动触发Job
JobKey jobKey = JobKey.jobKey("JOB_" + taskId, "QC_TASK_GROUP");
scheduler.triggerJob(jobKey);
return execution.getId();
} catch (SchedulerException e) {
throw new ServiceException("触发任务失败");
}
}
}
@Service
public class QcTaskServiceImpl implements QcTaskService {
@Resource
private QcJobService qcJobService;
/**
* 创建任务(区分自动/手动)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long createTask(QcTaskVO taskVO) {
// 1. 保存任务基本信息
QcTask task = new QcTask();
// ... 设置字段
task.setTaskType(taskVO.getTaskType());
task.setIsEnabled(1);
taskMapper.insert(task);
// 2. 自动任务:创建定时任务
if ("auto".equals(taskVO.getTaskType())) {
// 保存定时配置
task.setScheduleConfig(JSON.toJSONString(taskVO.getScheduleConfig()));
taskMapper.updateById(task);
// 创建Quartz Job
qcJobService.scheduleJob(task.getId(), task);
}
// 3. 手动任务:保存检查数据关联
else {
List<QcTaskExam> exams = buildTaskExams(task.getId(), taskVO.getExamIds());
qcTaskExamMapper.saveBatch(exams);
}
return task.getId();
}
/**
* 启用/禁用任务
*/
@Override
public void toggleTask(Long taskId, Boolean isEnabled) {
QcTask task = taskMapper.selectById(taskId);
task.setIsEnabled(isEnabled ? 1 : 0);
taskMapper.updateById(task);
// 自动任务:控制Quartz Job
if ("auto".equals(task.getTaskType())) {
if (isEnabled) {
qcJobService.resumeJob(taskId);
} else {
qcJobService.pauseJob(taskId);
}
}
}
/**
* 删除任务
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteTask(Long taskId) {
QcTask task = taskMapper.selectById(taskId);
// 自动任务:删除Quartz Job
if ("auto".equals(task.getTaskType())) {
qcJobService.deleteJob(taskId);
}
// 删除任务数据
taskMapper.deleteById(taskId);
}
}
@Service
public class QcExecutionServiceImpl implements QcExecutionService {
@Resource
private QcTaskExecutionMapper executionMapper;
@Resource
private StudyInfoMapper studyInfoMapper;
@Resource
private QcTaskImageResultMapper imageResultMapper;
/**
* 创建执行记录
*/
@Override
@Transactional(rollbackFor = Exception.class)
public QcTaskExecution createExecution(Long taskId) {
QcTask task = qcTaskMapper.selectById(taskId);
// 1. 获取下次执行时间(从Quartz Trigger)
Date nextExecuteTime = getNextExecuteTime(taskId);
// 2. 创建执行记录
QcTaskExecution execution = new QcTaskExecution();
execution.setTaskId(taskId);
execution.setTaskName(task.getTaskName());
execution.setPlanExecuteTime(nextExecuteTime);
execution.setActualExecuteTime(new Date());
execution.setStatus(1); // 执行中
execution.setPlanCount(task.getPlanCount());
executionMapper.insert(execution);
return execution;
}
/**
* 抽取检查数据(自动任务)
*/
@Override
public List<QcTaskExam> extractExams(Long taskId, Long executionId) {
QcTask task = qcTaskMapper.selectById(taskId);
JSONObject config = JSON.parseObject(task.getScheduleConfig());
// 1. 根据配置查询符合条件的检查数据
LambdaQueryWrapper<StudyInfo> query = new LambdaQueryWrapper<>();
query.eq(StudyInfo::getInstitutionId, task.getInstitutionId());
query.eq(StudyInfo::getIsDeleted, 0);
// 时间范围:使用上次执行时间到当前时间
query.ge(StudyInfo::getUploadTime, config.getDate("lastExecuteTime"));
query.le(StudyInfo::getUploadTime, new Date());
List<StudyInfo> allStudies = studyInfoMapper.selectList(query);
// 2. 根据抽取方式选择数据
int sampleCount = config.getIntValue("sampleCount");
String sampleType = config.getString("sampleType");
List<StudyInfo> selectedStudies;
if ("random".equals(sampleType)) {
// 随机抽取
Collections.shuffle(allStudies);
selectedStudies = allStudies.subList(0, Math.min(sampleCount, allStudies.size()));
} else {
// 按时间顺序
allStudies.sort(Comparator.comparing(StudyInfo::getUploadTime).reversed());
selectedStudies = allStudies.subList(0, Math.min(sampleCount, allStudies.size()));
}
// 3. 保存到qc_task_exam表
List<QcTaskExam> exams = selectedStudies.stream().map(study -> {
QcTaskExam exam = new QcTaskExam();
exam.setTaskId(taskId);
exam.setExecutionId(executionId);
exam.setStudyId(study.getStudyId());
return exam;
}).collect(Collectors.toList());
qcTaskExamMapper.saveBatch(exams);
return exams;
}
/**
* 执行质控
*/
@Override
@Async
public void executeQc(Long executionId, List<QcTaskExam> exams) {
try {
// 执行质控逻辑(复用现有代码)
for (QcTaskExam exam : exams) {
// 执行图像质控
executeImageQc(executionId, exam.getStudyId());
}
} catch (Exception e) {
log.error("质控执行失败,executionId={}", executionId, e);
throw e;
}
}
/**
* 完成执行
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void completeExecution(Long executionId) {
QcTaskExecution execution = executionMapper.selectById(executionId);
// 统计执行结果
int total = imageResultMapper.selectCount(
new LambdaQueryWrapper<QcTaskImageResult>()
.eq(QcTaskImageResult::getExecutionId, executionId)
);
long pass = imageResultMapper.selectCount(
new LambdaQueryWrapper<QcTaskImageResult>()
.eq(QcTaskImageResult::getExecutionId, executionId)
.eq(QcTaskImageResult::getIsQualified, 1)
);
// 更新执行记录
execution.setStatus(2); // 已完成
execution.setTotalCount(total);
execution.setCompletedCount(total);
execution.setPassCount(pass.intValue());
execution.setFailCount(total - pass.intValue());
execution.setPassRate(total > 0 ? (pass * 100.0 / total) : 0);
execution.setEndTime(new Date());
execution.setDuration((int)((execution.getEndTime().getTime() - execution.getStartTime().getTime()) / 1000));
executionMapper.updateById(execution);
// 更新任务的last_execute_time和next_execute_time
updateTaskScheduleInfo(execution.getTaskId());
}
}
数据库表创建
Quartz集成
实体类和Mapper
服务层实现
Controller层
任务创建表单改造
任务列表改造
执行历史页面新建
路由和API
功能测试
异常测试
性能优化
用户体验优化
Quartz持久化
数据一致性
性能问题
任务冲突
@DisallowConcurrentExecution数据量控制
失败处理
现有功能不受影响
数据迁移
# 每天上午10点执行
0 0 10 * * ?
# 每周一上午10点执行
0 0 10 ? * MON
# 每月1号上午10点执行
0 0 10 1 * ?
# 每周一、三、五上午10点执行
0 0 10 ? * MON,WED,FRI
# 每2小时执行一次
0 0 */2 * * ?
后端文件:
新增:
- QcTaskExecution.java (实体)
- QcTaskExecutionMapper.java
- QcTaskExecutionService.java
- QcJobService.java
- AutoQcJob.java
- QuartzConfig.java
修改:
- QcTaskService.java
- QcTaskController.java
- QcTaskExam.java
- QcTaskImageResult.java
前端文件:
新增:
- ExecutionHistory.vue
- CronSelector.vue (Cron生成器组件)
修改:
- QcTask.vue
- QcResultDetail.vue
- router/index.ts
- api/qc.ts
自动任务创建
定时执行验证
手动触发
执行历史查看
失败重试
文档版本:v1.0 创建日期:2026-02-03 最后更新:2026-02-03 文档状态:待评审