项目名称:医学影像智能质控系统(QConline) 项目定位:演示型质控系统,支持真实质控和预制结果模拟 技术栈:
POST /api/pacs/study内置质控因子示例:
| 因子编码 | 因子名称 | 分类 | 数据类型 | 规则示例 |
|---|---|---|---|---|
| DATA_001 | 检查范围 | 数据质控 | string | {"operator":"notEmpty"} |
| IMAGE_001 | 体位 | 影像质控 | string | {"operator":"in","value":["正位","侧位","斜位"]} |
| IMAGE_002 | 图像伪影 | 影像质控 | string | {"operator":"equals","value":"无"} |
| IMAGE_003 | 中心线 | 影像质控 | string | {"operator":"equals","value":"居中"} |
| IMAGE_004 | 图像等级 | 影像质控 | number | {"operator":">=","value":3} |
| IMAGE_005 | 图像数量 | 影像质控 | number | {"operator":">=","value":10} |
| DATA_002 | 患者姓名完整性 | 数据质控 | string | {"operator":"notEmpty"} |
| DATA_003 | 检查日期 | 数据质控 | date | {"operator":"notNull"} |
| REPORT_001 | 报告完整性 | 报告质控 | boolean | {"operator":"equals","value":true} |
| REPORT_002 | 报告字数 | 报告质控 | number | {"operator":">=","value":50} |
| REPORT_003 | 诊断结论 | 报告质控 | string | {"operator":"notEmpty"} |
| REPORT_004 | 报告时效性 | 报告质控 | number | {"operator":"<=","value":24,"unit":"小时"} |
标准配置示例:
标准名称:CT检查质控标准
包含因子:
- 检查范围(权重10%,必检)
- 体位(权重15%,必检)
- 图像伪影(权重20%,必检)
- 中心线(权重15%)
- 图像等级(权重20%,阈值≥4)
- 图像数量(权重20%,阈值≥15)
合格分数线:80分
[ ] 预制结果模板管理
示例:
图像伪影不合格:10条(40%)
中心线不合格:8条(32%)
图像等级不合格:7条(28%)
编辑/删除预制模板
预制结果生成逻辑:
1. 获取数据范围内的检查列表(如100条)
2. 按照预制模板设定:
- 前75条标记为合格(随机生成高分:85-100分)
- 后25条标记为不合格(随机生成低分:50-79分)
3. 不合格的25条按因子分布随机分配不合格因子
4. 保存到质控结果表(与真实结果表结构一致)
5. 更新任务状态为已完成
┌─────────────┐
│ 用户登录 │
└──────┬──────┘
│
▼
┌─────────────────────┐
│ 输入用户名/密码 │
│ 后端验证(Spring │
│ Security + JWT) │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ 验证成功 │
│ 1. 生成JWT Token │
│ 2. 查询用户菜单权限 │
│ 3. 查询用户机构权限 │
│ 4. 存入Redis缓存 │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ 返回前端 │
│ - Token │
│ - 用户信息 │
│ - 菜单列表 │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ 前端动态加载路由 │
│ 根据菜单权限生成 │
│ 侧边栏菜单 │
└─────────────────────┘
┌─────────────┐
│ PACS系统 │
│ 解析DICOM │
└──────┬──────┘
│
▼
┌─────────────────────┐
│ HTTP POST接口 │
│ /api/pacs/study │
│ 推送JSON数据: │
│ { │
│ patientId, │
│ patientName, │
│ studyId, │
│ studyDate, │
│ modality, │
│ imageCount, │
│ ... │
│ } │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ 质控系统后端 │
│ 1. 数据校验 │
│ 2. 检查是否重复 │
│ (根据studyId) │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ 保存/更新数据库 │
│ - patient_info │
│ - study_info │
└──────┬──────────────┘
│
▼
┌─────────────────────┐
│ 返回成功响应 │
│ PACS系统记录推送日志│
└─────────────────────┘
┌─────────────────┐
│ 用户进入质控 │
│ 标准管理页面 │
└────────┬────────┘
│
▼
┌─────────────────┐
│ 创建新标准 │
│ - 填写基本信息 │
│ - 选择分类 │
└────────┬────────┘
│
▼
┌───────────────────────┐
│ 进入标准配置页面 │
│ 左侧:质控因子库 │
│ (按分类展示) │
│ 右侧:已选因子列表 │
└────────┬──────────────┘
│
▼
┌───────────────────────┐
│ 从因子库拖拽/选择因子 │
│ 添加到右侧列表 │
└────────┬──────────────┘
│
▼
┌───────────────────────┐
│ 配置每个因子 │
│ - 权重(自动计算总和) │
│ - 是否必检 │
│ - 阈值(可选) │
└────────┬──────────────┘
│
▼
┌───────────────────────┐
│ 实时校验 │
│ - 总权重是否=100% │
│ - 必检因子至少1个 │
└────────┬──────────────┘
│
▼
┌───────────────────────┐
│ 保存标准 │
│ 后端保存: │
│ - qc_standard 表 │
│ - qc_standard_factor表│
└───────────────────────┘
┌─────────────────┐
│ 用户创建质控任务│
└────────┬────────┘
│
▼
┌─────────────────────┐
│ 步骤1:基本信息 │
│ - 任务名称 │
│ - 选择质控标准 │
│ - 选择机构 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 步骤2:数据范围 │
│ 选择方式: │
│ ○ 时间范围 │
│ ○ 患者列表 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 预览检查数据 │
│ 显示将要质控的 │
│ 检查数量(≤100) │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 步骤3:确认提交 │
│ 显示任务摘要 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 后端创建任务记录 │
│ 状态:待执行 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 异步执行任务 │
│ (@Async线程池) │
└────────┬────────────┘
│
▼
┌─────────────────────────┐
│ 执行引擎流程: │
│ 1. 更新任务状态为执行中 │
│ 2. 查询数据范围内的检查 │
│ 3. 获取质控标准配置 │
└────────┬────────────────┘
│
▼
┌─────────────────────────┐
│ 遍历每条检查数据 │
│ FOR EACH study: │
└────────┬────────────────┘
│
▼
┌───────────────────────────┐
│ 应用质控标准中的所有因子 │
│ FOR EACH factor: │
│ 1. 获取检查数据中的字段 │
│ 2. 应用规则引擎判断 │
│ 3. 记录是否合格 │
│ 4. 计算得分(权重) │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 计算总分 │
│ score = Σ(因子得分×权重) │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 判断合格/不合格 │
│ isPass = (score >= 80) │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 保存质控结果 │
│ - qc_result表 │
│ - 记录不合格因子详情 │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 更新任务进度 │
│ - 已检数量+1 │
│ - 合格数/不合格数 │
│ - 进度写入Redis │
│ (每10条更新一次) │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 循环结束 │
│ 所有检查都已质控完成 │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 更新任务状态为已完成 │
│ - complete_time │
│ - total_count │
│ - pass_count │
│ - fail_count │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 前端轮询获取最新进度 │
│ (每2秒查询一次任务状态) │
└───────────────────────────┘
┌─────────────────┐
│ 用户创建质控任务│
└────────┬────────┘
│
▼
┌─────────────────────┐
│ 步骤1:基本信息 │
│ - 任务名称 │
│ - 选择质控标准 │
│ - 选择机构 │
│ ☑ 使用预制结果 │ ← 勾选此选项
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 选择预制结果模板 │
│ 下拉选择: │
│ "CT质控演示模板" │
│ (总数100,合格75) │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 步骤2:数据范围 │
│ (同真实质控) │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 预览预制结果统计 │
│ - 总数:100 │
│ - 合格:75 (75%) │
│ - 不合格:25 (25%) │
│ - 不合格因子分布 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 提交任务 │
│ 标记为预制任务 │
└────────┬────────────┘
│
▼
┌─────────────────────────┐
│ 后端模拟执行流程 │
│ 1. 创建任务,状态=执行中│
│ 2. 查询数据范围内的检查 │
│ 3. 延迟3-5秒(模拟耗时)│
└────────┬────────────────┘
│
▼
┌─────────────────────────┐
│ 根据预制模板生成结果 │
│ 1. 随机抽取100条检查 │
│ 2. 前75条生成合格结果 │
│ - 随机分数85-100 │
│ - 所有因子都合格 │
│ 3. 后25条生成不合格结果 │
│ - 随机分数50-79 │
│ - 按模板分配不合格因子│
└────────┬────────────────┘
│
▼
┌─────────────────────────┐
│ 保存到qc_result表 │
│ (与真实结果表结构一致) │
└────────┬────────────────┘
│
▼
┌─────────────────────────┐
│ 更新任务状态为已完成 │
│ 前端轮询获取完成状态 │
└─────────────────────────┘
┌─────────────────┐
│ 用户进入质控 │
│ 结果列表页面 │
└────────┬────────┘
│
▼
┌─────────────────────┐
│ 筛选条件 │
│ - 任务名称 │
│ - 患者姓名/ID │
│ - 合格状态 │
│ - 日期范围 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 结果列表(分页) │
│ 显示: │
│ - 患者信息 │
│ - 检查信息 │
│ - 质控分数 │
│ - 合格状态 │
│ - 操作按钮 │
└────────┬────────────┘
│
▼
┌─────────────────────┐
│ 点击"查看详情" │
└────────┬────────────┘
│
▼
┌───────────────────────────┐
│ 结果详情页面 │
│ ┌─────────────────────┐ │
│ │ 基本信息区 │ │
│ │ - 患者、检查、任务 │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 评分卡片 │ │
│ │ - 总分/合格分/实际分 │ │
│ │ - 合格状态(标签) │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ 因子检查结果表格 │ │
│ │ 不合格项红色高亮 │ │
│ └─────────────────────┘ │
│ ┌─────────────────────┐ │
│ │ [查看影像] 按钮 │ │
│ └─────────────────────┘ │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 点击"查看影像" │
│ 打开弹窗/新页面 │
└────────┬──────────────────┘
│
▼
┌───────────────────────────┐
│ 内嵌阅片器(iframe) │
│ URL构建: │
│ baseUrl + studyId + 其他参数│
│ baseUrl从配置文件读取 │
└───────────────────────────┘
┌─────────────────┐
│ 用户登录 │
│ is_admin=0 │ (普通用户)
└────────┬────────┘
│
▼
┌─────────────────────────┐
│ 查询用户关联的机构列表 │
│ SELECT institution_id │
│ FROM sys_user_institution│
│ WHERE user_id = ? │
│ 结果:[101, 102, 103] │
└────────┬────────────────┘
│
▼
┌─────────────────────────┐
│ 存入ThreadLocal │
│ institutionIds = [...] │
└────────┬────────────────┘
│
▼
┌─────────────────────────┐
│ 执行业务查询 │
│ 如:查询患者列表 │
└────────┬────────────────┘
│
▼
┌─────────────────────────────┐
│ MyBatis拦截器自动拼接条件 │
│ SELECT * FROM patient_info │
│ WHERE ... │
│ AND institution_id IN │
│ (101, 102, 103) │
└─────────────────────────────┘
方案:MyBatis-Plus 数据权限插件 + ThreadLocal
// 拦截器配置
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new DataPermissionInterceptor());
return interceptor;
}
// 数据权限处理器
public class DataPermissionHandler implements DataPermissionHandler {
@Override
public Expression getSqlSegment(Expression where, String mappedStatementId) {
// 从ThreadLocal获取用户机构列表
List<String> institutionIds = DataScopeContext.getInstitutionIds();
if (管理员) {
return where; // 不过滤
}
// 拼接 AND institution_id IN (...)
}
}
规则格式(JSON):
{
"operator": ">=", // 运算符:>= <= == != in notEmpty notNull contains
"value": 10, // 期望值
"dataType": "number", // 数据类型:string/number/date/boolean
"unit": "小时" // 单位(可选,用于时效性)
}
规则引擎核心代码:
public class QcRuleEngine {
public QcCheckResult check(String factorCode, Object actualValue, String ruleJson) {
QcRule rule = JSON.parseObject(ruleJson, QcRule.class);
switch (rule.getOperator()) {
case ">=":
return compareNumber(actualValue, rule.getValue(), ">=");
case "notEmpty":
return checkNotEmpty(actualValue);
case "in":
return checkIn(actualValue, rule.getValue());
// ... 其他运算符
}
}
}
@Service
public class QcExecuteService {
@Async("qcTaskExecutor")
public void executeTask(String taskId) {
// 1. 更新任务状态为执行中
// 2. 查询数据范围内的检查列表
// 3. 获取质控标准配置
// 4. 遍历执行质控
for (Study study : studyList) {
QcResult result = checkStudy(study, standard);
qcResultMapper.insert(result);
// 每10条更新一次进度
if (count % 10 == 0) {
updateProgress(taskId, count, totalCount);
}
}
// 5. 更新任务状态为已完成
}
}
线程池配置:
@Configuration
public class AsyncConfig implements AsyncConfigurer {
@Bean("qcTaskExecutor")
public Executor qcTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(5);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("qc-task-");
return executor;
}
}
// 任务详情页面轮询
const pollTaskStatus = () => {
const timer = setInterval(async () => {
const res = await getTaskDetail(taskId)
if (res.data.status === 2) { // 已完成
clearInterval(timer)
ElMessage.success('质控任务执行完成!')
loadResults() // 加载结果
} else if (res.data.status === 3) { // 失败
clearInterval(timer)
ElMessage.error('任务执行失败')
} else {
// 更新进度条
progress.value = res.data.progress
}
}, 2000) // 每2秒轮询一次
}
前端配置文件(.env.production):
VITE_VIEWER_BASE_URL=https://ppacsview.pacsonline.cn/#/pc
VITE_VIEWER_STUDY_URL=https%3A%2F%2Fquery.pacsonline.cn%2Fquery%2F%3Faddress%3D
阅片器组件(ViewerDialog.vue):
<template>
<el-dialog
v-model="visible"
title="影像查看"
width="90%"
fullscreen
>
<iframe
:src="viewerUrl"
style="width:100%; height:80vh; border:none;"
/>
</el-dialog>
</template>
<script setup lang="ts">
const viewerUrl = computed(() => {
const baseUrl = import.meta.env.VITE_VIEWER_BASE_URL
const studyUrl = import.meta.env.VITE_VIEWER_STUDY_URL
return `${baseUrl}?studyurl=${studyUrl}&study_id=${props.studyId}&node_type=1&version=V1.2.0.0`
})
</script>
public void generatePresetResults(QcTask task, PresetTemplate template) {
List<Study> studies = getStudiesByRange(task);
// 洗牌随机化
Collections.shuffle(studies);
int totalCount = template.getTotalCount();
int passCount = template.getPassCount();
// 生成合格结果
for (int i = 0; i < passCount; i++) {
QcResult result = new QcResult();
result.setStudyId(studies.get(i).getStudyId());
result.setActualScore(RandomUtils.nextInt(85, 101)); // 85-100分
result.setIsPass(1);
result.setFailFactors("[]"); // 空数组
qcResultMapper.insert(result);
}
// 生成不合格结果
List<FactorDistribution> distributions = template.getDistributions();
for (int i = passCount; i < totalCount; i++) {
// 随机选择不合格因子
FactorDistribution factor = distributions.get(i % distributions.size());
QcResult result = new QcResult();
result.setStudyId(studies.get(i).getStudyId());
result.setActualScore(RandomUtils.nextInt(50, 80)); // 50-79分
result.setIsPass(0);
// 构造不合格因子JSON
List<FailFactor> failFactors = Arrays.asList(
new FailFactor(factor.getFactorId(), factor.getFactorName(),
"实际值", "期望值")
);
result.setFailFactors(JSON.toJSONString(failFactors));
qcResultMapper.insert(result);
}
}
QConline/
├── doc/
│ ├── sql/
│ │ ├── init.sql # 初始化建表
│ │ ├── menu_data.sql # 菜单初始数据
│ │ └── qc_factor_data.sql # 质控因子初始数据
│ ├── 开发大纲.md
│ └── API文档.md
├── src/main/java/com/zskk/qconline/
│ ├── modules/
│ │ ├── system/ # 系统管理
│ │ ├── patient/ # 患者管理
│ │ ├── qc/ # 质控管理
│ │ └── pacs/ # PACS接口
│ ├── security/ # 认证授权
│ ├── config/ # 配置类
│ ├── component/ # 通用组件
│ └── utils/ # 工具类
├── src/main/resources/
│ ├── mapper/ # MyBatis XML
│ ├── application.yml
│ └── logback-spring.xml
├── Dockerfile
└── pom.xml
qconline-web/
├── public/
├── src/
│ ├── api/ # API接口
│ ├── assets/ # 静态资源
│ ├── components/ # 全局组件
│ │ └── ViewerDialog/ # 阅片器组件
│ ├── layout/ # 布局组件
│ ├── router/ # 路由配置
│ ├── stores/ # Pinia状态管理
│ ├── utils/ # 工具类
│ │ ├── request.ts # axios封装
│ │ └── auth.ts # token处理
│ ├── views/ # 页面组件
│ │ ├── login/
│ │ ├── dashboard/
│ │ ├── system/
│ │ ├── patient/
│ │ └── qc/
│ ├── App.vue
│ └── main.ts
├── .env.development # 开发环境配置
├── .env.production # 生产环境配置
├── Dockerfile
├── nginx.conf # Nginx配置
├── package.json
└── vite.config.ts
┌────────────────────────────────┐
│ 服务器(单机部署) │
│ │
│ ┌──────────────────────────┐ │
│ │ Nginx (前端静态资源) │ │
│ │ 端口:80 │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼───────────────┐ │
│ │ Spring Boot (后端) │ │
│ │ 端口:8080 │ │
│ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼───────────────┐ │
│ │ MySQL │ │
│ │ 端口:3306 │ │
│ └──────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ │
│ │ Redis │ │
│ │ 端口:6379 │ │
│ └──────────────────────────┘ │
└────────────────────────────────┘
docker-compose.yml:
version: '3.8'
services:
# MySQL数据库
mysql:
image: mysql:8.0
container_name: qconline-mysql
environment:
MYSQL_ROOT_PASSWORD: your_password
MYSQL_DATABASE: qconline
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
- ./doc/sql:/docker-entrypoint-initdb.d
networks:
- qconline-net
# Redis缓存
redis:
image: redis:7-alpine
container_name: qconline-redis
ports:
- "6379:6379"
networks:
- qconline-net
# 后端服务
backend:
build:
context: ./QConline
dockerfile: Dockerfile
container_name: qconline-backend
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
MYSQL_HOST: mysql
REDIS_HOST: redis
depends_on:
- mysql
- redis
networks:
- qconline-net
# 前端服务
frontend:
build:
context: ./qconline-web
dockerfile: Dockerfile
container_name: qconline-frontend
ports:
- "80:80"
depends_on:
- backend
networks:
- qconline-net
networks:
qconline-net:
driver: bridge
QConline/Dockerfile:
FROM openjdk:11-jre-slim
WORKDIR /app
COPY target/qconline-*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
qconline-web/Dockerfile:
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 运行阶段
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
nginx.conf:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://qconline-backend:8080/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
后端:
前端:
总计:18天
INSERT INTO sys_user (username, password, realname, is_admin, status)
VALUES ('admin', '{加密后的密码}', '系统管理员', 1, 1);
系统管理
- 用户管理
- 机构管理
- 菜单权限
患者管理
- 患者列表
- 检查列表
质控管理
- 质控因子
- 质控标准
- 质控任务
- 质控结果
统计分析
- 质控概览
(参考前文因子列表,共12个内置因子)
数据库命名规范:
qconline ?阅片器参数:
质控因子数据来源:
预制结果:
AI质控:
请您确认以上开发大纲,我将根据您的反馈开始开发!