# 质控任务自动化改造需求与实现方案 ## 一、需求背景 ### 1.1 业务需求 当前质控任务只支持手动执行,需要增加周期性自动执行功能: - **自动任务**:通过定时任务周期执行(如每周一执行一次),使用Quartz调度 - **手动任务**:保持现有功能,手动选择数据后执行 ### 1.2 核心差异 | 对比项 | 自动任务 | 手动任务 | |--------|----------|----------| | **执行方式** | 定时自动执行 | 手动触发执行 | | **数据选择** | 按规则自动抽取 | 手动选择具体检查数据 | | **数据范围** | 指定数量+抽取方式 | 指定时间范围+手动选择 | | **结果查看** | 查看多次执行的历史记录 | 查看单次执行结果 | | **技术实现** | Quartz定时调度 | 即时执行 | ## 二、现状分析 ### 2.1 现有表结构 ``` 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 - ...其他质控字段 ``` ### 2.2 现有页面 - **QcTask.vue**: 质控任务列表页 - **QcResult.vue**: 质控结果列表页 - **QcResultDetail.vue**: 质控结果详情页 ## 三、功能需求 ### 3.1 任务创建表单差异化 #### 3.1.1 共有字段 ``` - 任务名称(必填,支持手动修改,自动生成默认值:qc+日期+4位字母+4位数字) - 任务类型(单选:自动/手动) ``` #### 3.1.2 自动任务专属字段 ``` - 机构(必选,下拉选择) - 时间周期(必填,Cron表达式生成器) - 快捷选项:每天/每周/每月 - 高级配置:Cron表达式编辑器 - 预览下次执行时间 - 数据数量(必填,数字输入,1-10000) - 数据抽取方式(单选:随机抽取/按时间顺序) ``` #### 3.1.3 手动任务专属字段 ``` - 机构(必选,下拉选择) - 时间范围(必填,日期范围选择器) - 开始日期 - 结束日期 - 检查数据(必选,弹出选择器) - 显示已选择数量 - 支持查看已选数据列表 - 支持清空选择 - 数据数量(自动计算=已选数量,只读) ``` ### 3.2 任务列表展示 #### 3.2.1 列表字段 ``` 序号 | 任务名称 | 任务类型 | 机构名称 | 计划数量 | 执行统计 | 最近执行 | 下次执行 | 状态 | 操作 ``` #### 3.2.2 字段展示规则 **任务类型列**: - 自动任务:蓝色tag "自动" - 手动任务:橙色tag "手动" **执行统计列**: - 自动任务: ``` 已执行12次 | 平均通过率85% ``` - 手动任务: ``` 一次性 | 已完成 ``` **最近执行列**: - 自动任务:显示最近一次执行时间和状态 ``` 02-03 10:00 成功 ``` - 手动任务:显示执行时间 ``` 02-03 15:30 ``` **下次执行列**: - 自动任务:显示下次执行时间 ``` 02-10 10:00 ``` - 手动任务:显示 "-" **操作列差异**: - 自动任务: - 查看历史 - 立即执行(手动触发一次) - 编辑 - 启用/禁用 - 删除 - 手动任务: - 开始执行(待执行状态) - 查看进度(执行中状态) - 查看结果(已完成状态) - 编辑 - 删除 ### 3.3 自动任务执行历史 #### 3.3.1 执行历史列表页 **入口**:点击自动任务的"查看历史"按钮 **页面结构**: ``` ┌──────────────────────────────────────┐ │ 返回 执行历史 - [任务名称] │ ├──────────────────────────────────────┤ │ 统计卡片(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 |- |[查看错误]│ └──────────────────────────────────────┘ ``` **统计指标**: - 总执行次数 - 成功率(成功次数/总次数) - 平均检查数量(所有执行的总数/次数) - 平均通过率 **表格列**: - 执行编号:第N次 - 执行时间:yyyy-MM-dd HH:mm - 状态:成功(✓)/失败(✗)/执行中/待执行 - 检查数量:实际检查的数量 - 通过率:合格检查数/总数 - 操作:查看结果、查看错误、重试 ### 3.4 质控结果查看 #### 3.4.1 自动任务的质控结果 **入口**:执行历史页 → 点击某次执行的"查看结果" **展示内容**: - 与现有QcResultDetail.vue一致 - 标题显示:"执行历史详情 - 第N次执行" - 查询参数基于execution_id #### 3.4.2 手动任务的质控结果 **入口**:任务列表 → 点击"查看结果" **展示内容**: - 保持现有功能不变 ## 四、技术方案 ### 4.1 技术架构 ``` ┌─────────────────────────────────────────┐ │ 前端(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(质控结果) │ └─────────────────────────────────────────┘ ``` ### 4.2 技术选型 #### 4.2.1 后端技术栈 - **Spring Boot 2.7.18**(现有) - **Quartz 2.3.x**(定时调度) - **MyBatis-Plus**(现有ORM) - **MySQL**(现有数据库) #### 4.2.2 前端技术栈 - **Vue 3 + Composition API**(现有) - **Element Plus**(现有UI库) - **vue-cron-generator**(Cron表达式生成器组件) ## 五、数据库设计 ### 5.1 新增表:qc_task_execution(任务执行记录表) ```sql 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='质控任务执行记录表'; ``` ### 5.2 修改表:qc_task(任务表) ```sql 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 } ``` ### 5.3 修改表:qc_task_exam(任务检查关联表) ```sql ALTER TABLE `qc_task_exam` ADD COLUMN `execution_id` bigint DEFAULT NULL COMMENT '执行记录ID(自动任务必填,手动任务为空)' AFTER `task_id`, ADD INDEX `idx_execution_id` (`execution_id`); ``` ### 5.4 修改表:qc_task_image_result(质控结果表) ```sql 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`); ``` ## 六、接口设计 ### 6.1 任务管理接口 #### 6.1.1 创建/更新任务 ``` 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 } } ``` #### 6.1.2 任务列表 ``` 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 } } ``` ### 6.2 执行历史接口 #### 6.2.1 执行历史列表 ``` 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 } } ``` #### 6.2.2 执行统计汇总 ``` 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次 } } ``` ### 6.3 执行操作接口 #### 6.3.1 手动触发执行(自动任务) ``` POST /api/qc/task/trigger-manual/{taskId} 响应: { "code": 200, "message": "已加入执行队列", "data": { "executionId": 456 } } ``` #### 6.3.2 启用/禁用任务 ``` PUT /api/qc/task/toggle/{taskId} 参数: { "isEnabled": 0/1 } 响应:标准响应 ``` #### 6.3.3 失败重试 ``` POST /api/qc/task/retry/{executionId} 响应: { "code": 200, "message": "重新执行成功", "data": { "executionId": 457 } } ``` ### 6.4 质控结果接口(改造) #### 6.4.1 获取执行记录的质控结果 ``` GET /api/qc/execution/result/{executionId} # 注意:复用现有的质控结果接口逻辑 # 区别:查询条件基于 execution_id 而非 task_id ``` #### 6.4.2 获取执行记录的检查列表 ``` 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 } ] } } ``` ## 七、前端实现方案 ### 7.1 Cron表达式生成器 #### 7.1.1 组件选择 推荐使用 `vue-cron-generator` 或自建简化版: **安装**: ```bash npm install vue-cron-generator-quartz ``` **使用示例**: ```vue ``` **快捷方案**(自建简化版): ```vue 每天 每周 每月 周一 周二 周三 周四 周五 周六 周日 ``` ### 7.2 页面结构 #### 7.2.1 QcTask.vue 改造 **创建表单(条件渲染)**: ```vue 自动任务 手动任务 ``` **任务列表(差异展示)**: ```vue ``` #### 7.2.2 ExecutionHistory.vue(新建) **页面结构**: ```vue ``` #### 7.2.3 QcResultDetail.vue 改造 **路由适配**: ```typescript // router/index.ts 新增路由 { path: '/qc/execution/result/:executionId', name: 'ExecutionResultDetail', component: () => import('@/views/QcResultDetail.vue'), meta: { title: '质控结果详情', requiresAuth: true } } ``` **数据加载适配**: ```typescript // 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 } } ``` ### 7.3 路由设计 ```typescript // 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 } } ] ``` ## 八、后端实现方案 ### 8.1 Quartz集成 #### 8.1.1 Maven依赖 ```xml org.quartz-scheduler quartz 2.3.2 org.quartz-scheduler quartz-jobs 2.3.2 ``` #### 8.1.2 Quartz配置类 ```java @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; } } ``` #### 8.1.3 Quartz表初始化 Quartz需要创建以下表(在数据库中执行): - QRTZ_JOB_DETAILS - QRTZ_TRIGGERS - QRTZ_SIMPLE_TRIGGERS - QRTZ_CRON_TRIGGERS - QRTZ_BLOB_TRIGGERS - QRTZ_CALENDARS - QRTZ_FIRED_TRIGGERS - QRTZ_PAUSED_TRIGGER_GRPS - QRTZ_SCHEDULER_STATE - QRTZ_LOCKS - QRTZ_SIMPROP_TRIGGERS SQL脚本位置:`quartz-2.3.2/docs/dbTables/` ### 8.2 Job实现 #### 8.2.1 自动质控Job ```java @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 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); } } } ``` #### 8.2.2 Job管理服务 ```java @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("触发任务失败"); } } } ``` ### 8.3 服务层改造 #### 8.3.1 QcTaskService 改造 ```java @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 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); } } ``` #### 8.3.2 QcExecutionService(新增) ```java @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 extractExams(Long taskId, Long executionId) { QcTask task = qcTaskMapper.selectById(taskId); JSONObject config = JSON.parseObject(task.getScheduleConfig()); // 1. 根据配置查询符合条件的检查数据 LambdaQueryWrapper 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 allStudies = studyInfoMapper.selectList(query); // 2. 根据抽取方式选择数据 int sampleCount = config.getIntValue("sampleCount"); String sampleType = config.getString("sampleType"); List 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 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 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() .eq(QcTaskImageResult::getExecutionId, executionId) ); long pass = imageResultMapper.selectCount( new LambdaQueryWrapper() .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()); } } ``` ## 九、实现步骤 ### 9.1 第一阶段:数据库和基础架构(1-2天) 1. **数据库表创建** - 执行Quartz表初始化脚本 - 创建qc_task_execution表 - 修改qc_task、qc_task_exam、qc_task_image_result表 2. **Quartz集成** - 添加Maven依赖 - 创建QuartzConfig配置类 - 创建Quartz表 ### 9.2 第二阶段:后端服务层(2-3天) 1. **实体类和Mapper** - QcTaskExecution实体类 - QcTaskExecutionMapper 2. **服务层实现** - QcJobService:Job管理(创建、暂停、恢复、删除、触发) - QcExecutionService:执行记录管理 - AutoQcJob:定时任务Job - QcTaskService改造:支持自动/手动任务区分 3. **Controller层** - QcTaskController增加执行历史相关接口 - 响应DTO改造 ### 9.3 第三阶段:前端页面(2-3天) 1. **任务创建表单改造** - 条件渲染(自动/手动) - Cron表达式生成器集成 - 表单验证规则调整 2. **任务列表改造** - 差异化展示字段 - 操作按钮差异化 3. **执行历史页面新建** - ExecutionHistory.vue - 统计卡片 - 执行历史表格 4. **路由和API** - 路由配置 - API函数封装 ### 9.4 第四阶段:联调测试(1-2天) 1. **功能测试** - 创建自动任务 - 等待定时执行 - 手动触发执行 - 查看执行历史 - 查看质控结果 2. **异常测试** - Quartz停止后重启 - 执行失败重试 - 并发执行 ### 9.5 第五阶段:优化上线(1天) 1. **性能优化** - 异步执行优化 - 数据查询优化 2. **用户体验优化** - 加载状态 - 错误提示 - 操作反馈 ## 十、注意事项和风险点 ### 10.1 技术风险 1. **Quartz持久化** - 确保数据库连接池配置正确 - Quartz表锁机制可能导致死锁 - 集群环境需要配置集群节点 2. **数据一致性** - 执行记录快照避免历史数据被修改 - 并发执行时的数据隔离 - 事务边界控制 3. **性能问题** - 定时任务执行时间可能较长,需要异步处理 - 大量数据的抽取和质控可能影响系统性能 - 需要合理的超时和重试机制 ### 10.2 业务风险 1. **任务冲突** - 同一任务的多次执行可能重叠 - 需要配置`@DisallowConcurrentExecution` 2. **数据量控制** - 自动任务抽取数量需要合理限制 - 避免一次性处理过多数据 3. **失败处理** - 定时任务执行失败需要告警 - 提供重试机制 - 记录详细错误日志 ### 10.3 兼容性 1. **现有功能不受影响** - 手动任务功能保持不变 - 现有质控结果页面兼容executionId参数 2. **数据迁移** - 现有质控结果数据execution_id为空 - 新增逻辑要兼容空值情况 ## 十一、成功标准 ### 11.1 功能完整性 - ✅ 自动任务可以创建、编辑、删除 - ✅ 自动任务按配置周期执行 - ✅ 可以查看执行历史记录 - ✅ 可以查看每次执行的质控结果 - ✅ 手动任务功能不受影响 ### 11.2 性能指标 - 定时任务执行延迟 < 5秒 - 单次质控执行时间 < 30分钟(100条数据) - 执行历史列表查询 < 1秒 ### 11.3 稳定性 - Quartz任务调度成功率 > 99% - 自动任务执行失败率 < 1% - 系统支持至少100个并发自动任务 --- ## 附录 ### 附录A:Cron表达式示例 ``` # 每天上午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 * * ? ``` ### 附录B:关键文件清单 **后端文件**: ``` 新增: - 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 ``` ### 附录C:测试用例 1. **自动任务创建** - 输入:任务名称、机构、每周一10点、100条、随机抽取 - 预期:任务创建成功,Quartz Job创建成功 2. **定时执行验证** - 操作:等待下次执行时间 - 预期:任务自动执行,生成执行记录,生成质控结果 3. **手动触发** - 操作:点击"立即执行" - 预期:立即生成执行记录并执行质控 4. **执行历史查看** - 操作:点击"查看历史" - 预期:显示所有执行记录和统计信息 5. **失败重试** - 操作:执行失败的记录点击"重试" - 预期:重新执行该任务 --- **文档版本**:v1.0 **创建日期**:2026-02-03 **最后更新**:2026-02-03 **文档状态**:待评审