山东大学软件学院创新实训--“智愈医院自助服务系统“-(5)-文档管理子系统设计
一、背景与需求分析
1.1 业务背景
在智能医疗诊断系统中,诊断书文档是核心数据载体。用户上传一张诊断书图片或 PDF 后,系统需要完成文件存储、AI 视觉识别、结构化数据提取、报告生成等一系列操作。这个过程涉及多个环节:
- 文件上传:支持图片(jpg、png、jpeg)和 PDF 格式,大小限制 10MB
- 文件存储:上传后的文件需要安全、持久化保存,并能生成临时访问链接供 AI 模型读取
- AI 分析:调用通义千问 VL 多模态大模型对图片内容进行识别,提取结构化诊断信息
- 结果持久化:将分析结果存入数据库,支持历史追溯
- 报告导出:将分析结果生成 PDF 诊断报告,可供下载和打印
这一系列流程若采用同步阻塞方式实现,会带来三个突出问题:
- 用户体验差:大文件或高并发场景下,请求耗时可能超过 30 秒,前端需要长时间等待
- 系统脆弱:HTTP 线程被长时间占用,一旦中间环节失败整个请求失败,无重试机制
- 不可追踪:分析中的状态、失败原因均无记录,用户无法查看历史分析进度
因此,设计一个文档全生命周期管理系统,引入异步任务队列和状态跟踪机制,是提升系统健壮性和用户体验的关键。
1.2 核心需求
| 模块 | 需求 | 优先级 |
|---|---|---|
| 文件存储 | 支持诊断书文件上传、安全存储、临时访问、过期清理 | P0 |
| 任务队列 | 异步处理分析任务,支持排队、重试、超时控制 | P0 |
| 状态跟踪 | 跟踪每份文档的分析进度(待处理/分析中/成功/失败) | P0 |
| API 接口 | 提供标准的 RESTful API 用于触发分析、查询结果、下载文档 | P0 |
| 文件管理 | 用户文件列表、单条/批量删除、存储统计 | P1 |
二、系统架构设计
2.1 整体架构分层
整个文档管理子系统采用 四层架构,自顶向下依次为:
┌─────────────────────────────────────────────────────┐
│ 表现层 (UI Layer) │
│ medical-scan.html / analysis-history.html │
│ (上传页面、历史记录页面、结果展示页面) │
├─────────────────────────────────────────────────────┤
│ API 层 (Controller) │
│ MedicalScanController │
│ └─ /api/medical-record/analyze 触发分析任务 │
│ └─ /api/analysis-history/list 查询历史列表 │
│ └─ /api/analysis-history/detail 查询任务详情 │
│ └─ /api/medical-record/export-pdf 导出PDF报告 │
├─────────────────────────────────────────────────────┤
│ 业务层 (Service) │
│ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ TaskService │ │ AnalysisHistoryService│ │
│ │ 任务调度、队列管理│ │ 历史记录CRUD │ │
│ └────────┬────────┘ └──────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ AsyncTaskRunner │ 异步任务执行器 │
│ │ (线程池 + 队列) │ │
│ └────────┬────────┘ │
│ │ │
│ ┌────────▼────────┐ ┌──────────────────────┐ │
│ │ MedicalRecord │ │ PdfExportUtil │ │
│ │ AnalysisService │ │ PDF报告生成 │ │
│ │ AI模型调用 │ │ │ │
│ └─────────────────┘ └──────────────────────┘ │
├─────────────────────────────────────────────────────┤
│ 数据层 (DAO) │
│ AnalysisHistoryDao (MyBatis-Plus) │
│ ┌──────────────────────────────────────────────┐ │
│ │ 阿里云 OSS (文件存储) │ │
│ └──────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
各层职责说明:
| 层级 | 核心组件 | 职责 |
|---|---|---|
| 表现层 | Thymeleaf 模板 | 用户交互界面,上传入口、进度展示、历史查询 |
| API 层 | MedicalScanController | RESTful 接口,参数校验,响应封装 |
| 业务层 | 异步任务引擎 | 任务调度、队列管理、状态变更、AI 调用编排 |
| 数据层 | MyBatis-Plus + OSS | 结构化数据持久化、文件对象存储 |
2.2 核心业务流程
一个诊断书从上传到生成报告的完整生命周期:
用户上传 ──> 文件存储OSS ──> 创建分析任务 ──> 进入队列
│
┌──────▼──────┐
│ 任务调度器 │
│ (线程池消费) │
└──────┬──────┘
│
┌──────▼──────┐
│ AI 模型分析 │
│ (通义千问VL) │
└──────┬──────┘
│
┌────────────┼────────────┐
▼ ▼ ▼
分析成功 部分识别 完全失败
│ │ │
▼ ▼ ▼
保存结构化结果 保存部分结果 记录错误信息
│ │ │
▼ ▼ ▼
状态: SUCCESS 状态: WARNING 状态: FAILED
三、数据模型设计
3.1 数据库表设计
核心表 analysis_history(分析历史记录表)设计如下:
CREATE TABLE `analysis_history` (
`id` INT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` INT NOT NULL COMMENT '用户ID',
`file_name` VARCHAR(255) NOT NULL COMMENT '原始文件名',
`file_type` VARCHAR(50) NOT NULL COMMENT '文件类型(image/pdf)',
`file_size` BIGINT DEFAULT NULL COMMENT '文件大小(字节)',
`image_url` VARCHAR(1024) DEFAULT NULL COMMENT 'OSS存储URL',
`status` VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '任务状态:PENDING/PROCESSING/SUCCESS/FAILED',
`progress` INT NOT NULL DEFAULT 0 COMMENT '分析进度(0-100)',
`error_message` VARCHAR(1000) DEFAULT NULL COMMENT '失败原因',
`analysis_result` JSON DEFAULT NULL COMMENT '分析结果(JSON)',
`patient_name` VARCHAR(100) DEFAULT NULL COMMENT '患者姓名',
`primary_diagnosis` VARCHAR(500) DEFAULT NULL COMMENT '主要诊断',
`confidence` DOUBLE DEFAULT NULL COMMENT 'AI置信度',
`create_time` DATETIME NOT NULL COMMENT '创建时间',
`update_time` DATETIME NOT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='诊断书分析历史记录表';
关键设计说明:
status字段:任务状态枚举,驱动业务流程流转progress字段:分析进度百分比,支持前端进度条展示error_message字段:失败原因持久化,便于排查问题analysis_result字段:JSON 类型存储完整分析结果,灵活扩展
3.2 状态机设计
分析任务的状态转换遵循有限状态机模型:
┌──────────┐
│ PENDING │ (待处理)
└────┬─────┘
│ 线程池调度
┌────▼─────┐
┌──────│PROCESSING│ (处理中)
│ └────┬─────┘
│ │
┌──────▼──┐ ┌─────▼────┐ ┌────────┐
│ SUCCESS │ │ WARNING │ │ FAILED │
│ (成功) │ │ (部分识别)│ │ (失败) │
└─────────┘ └──────────┘ └────────┘
状态转换规则:
| 当前状态 | 触发条件 | 目标状态 |
|---|---|---|
| PENDING | 线程池获取任务开始分析 | PROCESSING |
| PROCESSING | AI 返回完整结构化结果 | SUCCESS |
| PROCESSING | AI 返回部分结果(置信度低) | WARNING |
| PROCESSING | 分析异常/超时/API 调用失败 | FAILED |
| SUCCESS / WARNING / FAILED | 用户重新分析 | PENDING |
WARNING 状态是一个增强设计:当 AI 模型返回内容但结构化提取不完全时,标记为 WARNING 而非直接 FAILED,保留部分提取结果供用户参考。
四、文件存储策略设计
4.1 存储层级
文件存储采用 两层架构:
诊断书文件
│
├── 热存储 (阿里云 OSS)
│ └── bucket: smart-medicine-docs
│ └── /medical_records/{userId}/{uuid}.{ext}
│
└── 冷存储 (OSS Lifecycle 自动沉降)
└── 30天后自动转为归档存储
4.2 文件命名规范
{业务前缀}/{用户ID}/{唯一标识符}.{扩展名}
示例:
medical_records/42/a1b2c3d4e5f6.jpg
- 业务前缀:
medical_records/区分不同业务模块 - 用户 ID:按用户隔离,便于权限管理和清理
- UUID:使用 Hutool
IdUtil.simpleUUID()生成,避免文件名冲突和中文乱码
4.3 访问策略
- 上传:服务端直传到 OSS(通过服务端签名或 SDK 直传)
- 读取:生成 Presigned URL(签名 URL),有效期 30 分钟
- AI 分析服务使用签名 URL 读取图片
- 前端展示使用签名 URL 预览
- 过期清理:OSS Lifecycle 规则,30 天转为归档存储,60 天自动删除
4.4 安全性设计
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 用户上传 │ ──> │ 后端校验 │ ──> │ OSS存储 │
│ │ │ 文件类型 │ │ 私有可能 │
│ │ │ 文件大小 │ │ 签名访问 │
└──────────┘ └──────────┘ └──────────┘
- 服务端校验文件类型(白名单:jpg/jpeg/png/pdf)
- 服务端校验文件大小(上限 10MB)
- OSS Bucket 设置为私有,所有访问通过签名 URL 进行
- 签名 URL 设置有效期,避免 URL 泄露后长期可访问
五、异步任务队列架构设计
5.1 设计目标
引入异步任务队列要解决三个核心问题:
- 解耦:上传请求与 AI 分析分离,用户上传后立即获得响应
- 削峰:请求突增时,任务队列起到缓冲作用,防止 AI 服务被打满
- 可靠:失败任务支持重试,避免一次失败丢失全部数据
5.2 架构方案
考虑到当前系统规模(单体 Spring Boot 应用),采用 内存队列 + 线程池 的方案,不引入外部 MQ 依赖,降低架构复杂度:
┌─────────────┐
│ Controller │ (接收上传请求)
└──────┬──────┘
│ ① 创建任务记录(状态=PENDING)
┌──────▼──────┐
│ Analysis │
│ History │ (写入数据库)
│ Service │
└──────┬──────┘
│ ② 提交到线程池
┌──────▼──────┐
│ ThreadPool │ (LinkedBlockingQueue)
│ Executor │
└──────┬──────┘
│ ③ 消费任务
┌──────▼──────┐
│ AsyncTask │ (状态 → PROCESSING)
│ Runner │
└──────┬──────┘
│ ④ 调用AI模型
┌──────▼──────┐
│ MedicalRecord│
│ Analysis │ (通义千问VL)
│ Service │
└──────┬──────┘
│ ⑤ 更新结果
┌──────▼──────┐
│ Analysis │ (状态 → SUCCESS/FAILED)
│ History │
│ Service │
└─────────────┘
5.3 线程池配置
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean("analysisExecutor")
public ThreadPoolTaskExecutor analysisExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 核心线程数
executor.setMaxPoolSize(4); // 最大线程数
executor.setQueueCapacity(50); // 队列容量
executor.setThreadNamePrefix("analysis-");
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy() // 满队列时由主线程执行
);
executor.initialize();
return executor;
}
}
参数设计依据:
| 参数 | 值 | 理由 |
|---|---|---|
| corePoolSize | 2 | AI 分析为 IO 密集型,保留线程切换开销 |
| maxPoolSize | 4 | 实测单次分析约 5-15s,4 线程并发吞吐足够 |
| queueCapacity | 50 | 等待队列长度,防止内存溢出 |
| CallerRunsPolicy | — | 满队列时主线程同步执行,降级而非丢弃 |
5.4 重试机制
失败 → 重试(3次) → 均失败 → 状态=FAILED,记录错误信息
↑
指数退避: 10s → 30s → 60s
- 每次任务最多重试 3 次
- 重试间隔采用指数退避(10 秒、30 秒、60 秒)
- 三次均失败则将状态更新为
FAILED,记录完整错误信息
六、RESTful API 接口规范
6.1 接口列表
POST /api/medical-record/analyze # 上传诊断书并触发分析
GET /api/analysis-history/list # 查询历史分析记录
GET /api/analysis-history/detail # 查询单条分析详情
GET /api/analysis-history/status # 查询指定任务状态
POST /api/analysis-history/retry # 重新分析失败的任务
POST /api/analysis-history/delete # 删除历史记录
POST /api/analysis-history/clear # 清空所有历史记录
POST /api/medical-record/export-pdf # 导出诊断报告 PDF
6.2 接口设计示例
上传并触发分析
POST /api/medical-record/analyze
Content-Type: multipart/form-data
参数:
- file: 诊断书文件 (MultipartFile)
响应:
{
"code": "SUCCESS",
"message": "分析任务已提交",
"data": {
"taskId": 10042,
"status": "PENDING",
"estimatedWait": "约 10-30 秒完成"
}
}
查询任务状态
GET /api/analysis-history/status?id=10042
响应:
{
"code": "SUCCESS",
"data": {
"id": 10042,
"status": "PROCESSING",
"progress": 60,
"createTime": "2026-05-24 10:00:00",
"updateTime": "2026-05-24 10:00:15"
}
}
查询分析详情
GET /api/analysis-history/detail?id=10042
响应:
{
"code": "SUCCESS",
"data": {
"id": 10042,
"fileName": "诊断报告_20260524.jpg",
"patientName": "张三",
"primaryDiagnosis": "急性上呼吸道感染",
"status": "SUCCESS",
"analyisResult": { ... },
"createTime": "2026-05-24 10:00:00"
}
}
6.3 接口设计规范
| 规范 | 约定 |
|---|---|
| 响应格式 | 统一使用 { code, message, data } 结构 |
| HTTP 方法 | POST 用于写操作,GET 用于读操作 |
| 参数传递 | GET 用 URL 参数,POST 用 JSON body 或 multipart |
| 状态码 | 业务状态用 code 字段区分,HTTP 统一 200 |
| 分页 | 列表接口统一使用 pageNum/pageSize 参数 |
七、技术选型对比
7.1 任务队列方案对比
| 方案 | 复杂度 | 可靠性 | 部署依赖 | 推荐场景 |
|---|---|---|---|---|
| 内存队列+线程池 | ★☆☆ | ★★★ | 无 | 单体应用、初期阶段 |
| Redis List + 定时任务 | ★★☆ | ★★★ | Redis | 中等规模、需要持久化 |
| RabbitMQ | ★★★ | ★★★★ | RabbitMQ | 分布式、高可靠场景 |
| RocketMQ | ★★★★ | ★★★★★ | RocketMQ | 大规模、事务消息 |
选型结论:当前系统为单体 Spring Boot 应用,且并发量可控,采用内存队列+线程池方案,性价比最高。
7.2 文件存储方案对比
| 方案 | 成本 | 扩展性 | 运维 | 推荐场景 |
|---|---|---|---|---|
| 本地磁盘 | 低 | 差 | 低 | 开发测试、小规模 |
| 阿里云 OSS | 中 | 好 | 无 | 生产环境、云部署 |
| MinIO(自建 S3) | 中 | 好 | 中 | 私有化部署 |
| 阿里云 OSS + CDN | 高 | 最好 | 无 | 高并发访问场景 |
选型结论:项目已使用阿里云部署,OSS 是最优选择,零运维成本,支持生命周期管理。
八、总结与展望
本文从文档全生命周期管理的业务需求出发,完成了以下设计工作:
- 四层架构:UI → API → Service → DAO,职责清晰,层次分明
- 数据模型:扩展
analysis_history表,引入状态枚举和进度跟踪 - 文件存储:基于阿里云 OSS,设计了存储层级、命名规范和访问策略
- 异步任务:采用线程池 + 内存队列,实现异步解耦和削峰填谷
- API 规范:统一响应格式,设计了 8 个标准 RESTful 接口
下一篇博客:将进入具体的编码实现环节,包括数据库迁移脚本、线程池配置、异步任务执行器、状态机逻辑,以及完整的 API 控制器代码实现。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)