SpringBoot+Vue实战:多模态疾病初筛与护理建议系统(含泳道图+时序图+完整后端代码)
本文完整复盘我做的“多模态疾病初筛与护理建议系统”后端实现。技术栈采用 SpringBoot + MyBatis + MySQL + 通义千问多模态接口,详细讲解注册登录鉴权、图片上传、AI结构化解析、问诊记录落库、TXT/PDF报告导出、管理员看板统计等关键逻辑。文中提供可直接复用的 Mermaid 用例图/泳道图/时序图/ER图,以及 S01~S20 的页面截图一一对应清单,适合毕业设计、课程
·
0. 项目简介
本项目是一个“多模态疾病初筛与护理建议系统”后端,核心目标是:
- 用户上传图片 + 文字症状描述;
- 服务端调用通义千问多模态模型做初筛;
- 返回结构化结果(风险等级、护理建议、下一步建议、免责声明);
- 记录问诊历史,可分页检索;
- 支持导出 txt/pdf 报告;
- 管理员查看系统运营看板(趋势、风险分布、活跃用户、Top 用户)。
1. 技术栈与选型
1.1 后端技术栈
Spring Boot 3.3.5MyBatis 3.0.4MySQL 8.xRestTemplate(调用千问接口)iTextPDF(生成 PDF 报告)Jackson(JSON 解析/序列化)
1.2 选型原因
- Spring Boot:快速搭建 REST API,生态成熟。
- MyBatis:SQL 可控,统计类查询更直观。
- MySQL:结构化数据(用户/会话/问诊记录)存储稳定。
- iTextPDF:可做医院风格报告模板,支持中文字体嵌入。
- 千问多模态:支持图片+文本联合理解,适配初筛场景。
1.3 页面截图放置说明
本章不强制放图,建议在后文“页面截图占位”章节集中放置。
2. 系统架构设计
2.1 总体架构说明
系统分为 5 层:
- 表现层(Controller):接收请求、鉴权、返回统一响应。
- 业务层(Service):核心业务编排(上传、AI 分析、落库、报表)。
- 数据访问层(Mapper + XML):执行 SQL、聚合统计。
- 数据层(MySQL + 本地上传目录):结构化数据 + 图片文件。
- 外部能力层(Qwen API):多模态初筛推理。
2.2 架构图(Mermaid)
3. 需求分析与角色用例
3.1 角色定义
- 普通用户(USER)
- 管理员(ADMIN)
- 外部 AI 服务(Qwen API)
3.2 功能性需求
- 注册、登录、获取个人信息、退出登录。
- 提交问诊(图片+文本),获取结构化初筛结果。
- 问诊记录分页、详情、检索、统计。
- 导出报告(文本与 PDF)。
- 管理员运营看板(用户量、问诊量、趋势、风险分布、Top 用户)。
3.3 非功能性需求
- 接口响应统一格式。
- 鉴权失败、业务异常、系统异常可区分。
- 图片文件安全落盘,避免路径穿越。
- AI 返回不稳定时有兜底结构化结果。
- 报告支持中文字体,跨平台可读。
3.4 用例图(Mermaid 表达版)
4. 数据库设计
4.1 核心表结构
4.1.1 用户表 user_account
id:主键username:登录名(唯一)password_hash:密码哈希nickname:昵称role:角色(USER/ADMIN)created_at:创建时间
4.1.2 会话表 user_session
id:主键user_id:用户 IDtoken:登录 token(唯一)expire_at:过期时间created_at:创建时间
4.1.3 问诊记录表 consultation_record
id:主键user_id:用户 IDnickname:本次问诊昵称question_text:问题描述image_url:图片访问路径preliminary_assessment:初筛结论risk_level:风险等级(LOW/MEDIUM/HIGH)nursing_advice:护理建议(JSON 数组字符串)next_step:下一步建议disclaimer:免责声明raw_answer:AI 原始响应文本created_at:创建时间
4.2 ER 图(Mermaid)
5. 核心业务流程(重点)
5.1 流程一:注册/登录/会话鉴权
5.1.1 关键逻辑
- 用户输入账号密码。
- 后端校验格式(用户名 4-20 位字母数字下划线,密码 6-32 位)。
- 使用
SHA-256 + salt生成密码摘要。 - 登录成功后生成超长 token(两个 UUID 去横杠拼接)。
- token 与过期时间写入
user_session。 - 每次鉴权时先清理过期会话,再校验 token 与角色。
5.1.2 泳道图
5.1.3 页面截图占位(登录/注册)

5.2 流程二:提交问诊(图片+文本)并生成初筛结果

5.2.1 关键逻辑
- 接收 multipart 表单:
question + image (+ nickname)。 - 校验问题描述长度(<=500),校验图片类型为
image/*。 - 图片落盘
uploads,生成:publicUrl(给前端展示)dataUrl(给 AI 接口传图)
- 构造多模态提示词和请求体,调用 Qwen。
- 解析 AI 返回:
- 如果是 JSON,提取结构化字段;
- 如果解析失败,走 fallback(默认中风险+默认护理建议)。
- 将完整记录落库(包含 raw_answer 便于审计)。
- 返回详情对象。
5.2.2 泳道图
5.2.3 时序图
5.3 流程三:导出报告(TXT + PDF)
5.3.1 核心点
- 按
consultationId + userId查记录,防止越权。 - TXT:拼接结构化文本。
- PDF:
- 封面页 + 内容页;
- 读取问诊图片嵌入报告;
- 头部脚部(报告编号、分页、生成时间);
- 中文字体嵌入(TTF/OTF/Fallback)。
5.3.2 泳道图
5.3.3 页面截图占位(报告导出)

5.4 流程四:管理员看板统计
5.4.1 指标口径
totalUsers:总用户数totalConsultations:总问诊数recent7DaysConsultations:近7天问诊量activeUsers:近30天活跃用户(有问诊记录)riskDistribution:风险等级分布dailyTrends:按日趋势(空白天补 0)topUsers:问诊量 TOP 用户
5.4.2 泳道图
5.4.3 页面截图占位(管理后台)


6. 关键代码实现拆解
以下代码均来自本项目后端,按模块截取关键片段。
6.1 密码加盐哈希
public String hash(String password) {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
String raw = password + "#" + appProperties.getAuth().getPasswordSalt();
byte[] bytes = digest.digest(raw.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(bytes);
}
说明:不保存明文密码,比较时比较 hash 值。
6.2 登录鉴权核心(token + session)
public UserAccount requireUser(String authorization) {
String token = extractToken(authorization);
LocalDateTime now = LocalDateTime.now();
userSessionMapper.deleteExpired(now);
UserSession session = userSessionMapper.selectByToken(token);
if (session == null || session.getExpireAt() == null || !session.getExpireAt().isAfter(now)) {
throw new UnauthorizedException("登录状态已失效,请重新登录");
}
UserAccount userAccount = userAccountMapper.selectById(session.getUserId());
if (userAccount == null) {
throw new UnauthorizedException("用户不存在,请重新登录");
}
return userAccount;
}
说明:每次鉴权会先做过期会话清理,减轻脏数据积累。
6.3 图片存储与安全处理
public StoredImage store(MultipartFile imageFile) {
String contentType = imageFile.getContentType() == null ? "" : imageFile.getContentType().toLowerCase(Locale.ROOT);
if (!contentType.startsWith("image/")) {
throw new BusinessException("仅支持图片文件");
}
byte[] imageBytes = imageFile.getBytes();
String filename = FORMATTER.format(LocalDateTime.now()) + "-" + UUID.randomUUID().toString().replace("-", "") + extension;
Path target = uploadPath.resolve(filename);
Files.write(target, imageBytes, StandardOpenOption.CREATE_NEW);
String dataUrl = "data:" + contentType + ";base64," + Base64.getEncoder().encodeToString(imageBytes);
String publicUrl = "/uploads/" + filename;
return new StoredImage(publicUrl, dataUrl);
}
说明:同一份图片输出两个地址,publicUrl 给前端展示,dataUrl 供 AI 调用。
6.4 多模态 AI 调用与提示词
String prompt = "你是医学初筛与护理建议助手。请结合图片和问题做初步分析,不要做确诊。"
+ "请严格返回 JSON,字段如下:"
+ "{\"preliminaryAssessment\":\"\",\"riskLevel\":\"LOW|MEDIUM|HIGH\",\"nursingAdvice\":[\"\"],\"nextStep\":\"\",\"disclaimer\":\"\"}"
+ "。nursingAdvice 至少给 3 条,语言用简体中文。";
说明:强约束 JSON 格式,便于后端结构化入库。
6.5 AI 返回解析与兜底
private AiStructuredResult buildStructuredResult(String contentText) {
String cleaned = stripCodeFence(contentText);
String jsonSegment = extractJsonSegment(cleaned);
if (!StringUtils.hasText(jsonSegment)) {
return buildFallback(contentText);
}
try {
JsonNode jsonNode = objectMapper.readTree(jsonSegment);
// ...读取字段并标准化 riskLevel
} catch (Exception ex) {
return buildFallback(contentText);
}
}
说明:即使 AI 输出偏离预期,也能返回可用结果而不是直接失败。
6.6 创建问诊主流程
public ConsultationDetailResponse createConsultation(Long userId, String nickname, String question, MultipartFile image) {
ImageStorageService.StoredImage storedImage = imageStorageService.store(image);
AiStructuredResult aiResult = qwenAiService.analyze(question.trim(), storedImage.getDataUrl());
ConsultationRecord record = new ConsultationRecord();
record.setUserId(userId);
record.setQuestionText(question.trim());
record.setImageUrl(storedImage.getPublicUrl());
record.setPreliminaryAssessment(aiResult.getPreliminaryAssessment());
record.setRiskLevel(aiResult.getRiskLevel());
record.setNursingAdvice(toJson(aiResult.getNursingAdvice()));
record.setRawAnswer(aiResult.getRawText());
consultationRecordMapper.insert(record);
ConsultationRecord saved = consultationRecordMapper.selectById(record.getId(), userId);
return toResponse(saved);
}
6.7 统计 SQL(分页检索 + 风险分布 + 趋势)
<select id="selectPage" resultMap="consultationRecordMap">
SELECT id, user_id, nickname, question_text, image_url, preliminary_assessment,
risk_level, nursing_advice, next_step, disclaimer, raw_answer, created_at
FROM consultation_record
<where>
user_id = #{userId}
<if test="keyword != null and keyword != ''">
AND (
question_text LIKE CONCAT('%', #{keyword}, '%')
OR preliminary_assessment LIKE CONCAT('%', #{keyword}, '%')
)
</if>
</where>
ORDER BY created_at DESC
LIMIT #{size} OFFSET #{offset}
</select>
<select id="dailyTrend" resultType="com.medical.screening.dto.DailyTrendItem">
SELECT DATE_FORMAT(created_at, '%Y-%m-%d') AS day, COUNT(1) AS count
FROM consultation_record
WHERE created_at >= CONCAT(#{startDay}, ' 00:00:00')
GROUP BY DATE_FORMAT(created_at, '%Y-%m-%d')
ORDER BY day ASC
</select>
6.8 全局异常统一返回
@ExceptionHandler(UnauthorizedException.class)
public ApiResponse<Void> handleUnauthorizedException(UnauthorizedException ex) {
return ApiResponse.fail(4010, ex.getMessage());
}
@ExceptionHandler(BusinessException.class)
public ApiResponse<Void> handleBusinessException(BusinessException ex) {
return ApiResponse.fail(4001, ex.getMessage());
}
说明:前端只需要按 code 做分支处理即可。
7. API 设计与示例
7.1 统一返回格式
{
"code": 0,
"message": "ok",
"data": {},
"timestamp": "2026-02-19T11:00:00"
}
7.2 鉴权接口
7.2.1 注册
POST /api/auth/register- Body:
{
"username": "test_user",
"password": "Test123456",
"nickname": "测试用户"
}
7.2.2 登录
POST /api/auth/login- Body:
{
"username": "test_user",
"password": "Test123456"
}
7.2.3 获取当前用户
GET /api/auth/me- Header:
Authorization: Bearer <token>
7.2.4 退出登录
POST /api/auth/logout- Header:
Authorization: Bearer <token>
7.3 问诊接口
7.3.1 创建问诊
POST /api/consultations- Content-Type:
multipart/form-data - 参数:
question:必填image:必填nickname:选填
7.3.2 问诊分页
GET /api/consultations?page=1&size=10&keyword=咳嗽
7.3.3 详情/统计/报告
GET /api/consultations/{id}GET /api/consultations/statisticsGET /api/consultations/{id}/reportGET /api/consultations/{id}/report/pdf
7.4 首页与管理端接口
GET /api/home/overviewGET /api/home/highlightsGET /api/admin/dashboard?days=14(需 ADMIN)
7.5 错误码约定
4000:参数错误4001:业务异常4010:未登录/登录失效4030:无权限5000:系统异常
7.7 前端页面截图
7.8 截图一一对应替换表(发布前必看)
| 编号 | 章节位置 | 页面实际名称(建议) | 建议文件名 |
|---|---|---|---|
| S01 | 5.1.3 | 用户端-登录页 | S01-用户端-登录页.png |
| S02 | 5.1.3 | 用户端-注册页 | S02-用户端-注册页.png |
| S03 | 5.2.4 | 用户端-智能问诊页-症状输入 | S03-用户端-智能问诊页-症状输入.png |
| S04 | 5.2.4 | 用户端-智能问诊页-图片上传预览 | S04-用户端-智能问诊页-图片上传预览.png |
| S05 | 5.2.4 | 用户端-问诊结果页-风险与护理建议 | S05-用户端-问诊结果页-风险与护理建议.png |
| S06 | 5.3.3 | 用户端-问诊详情页-导出入口 | S06-用户端-问诊详情页-导出入口.png |
| S07 | 5.3.3 | 用户端-PDF报告预览页 | S07-用户端-PDF报告预览页.png |
| S08 | 5.4.3 | 管理端-运营看板页-核心指标卡片 | S08-管理端-运营看板页-核心指标卡片.png |
| S09 | 5.4.3 | 管理端-运营看板页-趋势与风险分布 | S09-管理端-运营看板页-趋势与风险分布.png |
| S10 | 7.7 | 用户端-首页概览页 | S10-用户端-首页概览页.png |
| S11 | 7.7 | 用户端-问诊记录列表页 | S11-用户端-问诊记录列表页.png |
| S12 | 7.7 | 用户端-问诊记录详情页 | S12-用户端-问诊记录详情页.png |
| S13 | 7.7 | 用户端-个人中心页 | S13-用户端-个人中心页.png |
| S14 | 7.7 | 用户端-个人统计页-风险分布图 | S14-用户端-个人统计页-风险分布图.png |
| S15 | 7.6 | 接口调试页-注册登录与问诊接口 | S15-接口调试页-注册登录与问诊接口.png |
| S16 | 7.6 | 接口返回示例页-问诊详情JSON | S16-接口返回示例页-问诊详情JSON.png |
| S17 | 8.5 | 测试验证页-Postman集合 | S17-测试验证页-Postman集合.png |
| S18 | 8.5 | 测试验证页-MySQL数据校验 | S18-测试验证页-MySQL数据校验.png |
| S19 | 9.6 | 部署架构页-前后端与MySQL | S19-部署架构页-前后端与MySQL.png |
| S20 | 9.6 | 运行验证页-后端服务日志 | S20-运行验证页-后端服务日志.png |
8. 典型测试用例设计
8.1 鉴权用例
| 用例ID | 场景 | 输入 | 预期 |
|---|---|---|---|
| AUTH-01 | 注册成功 | 合法用户名密码 | 返回 token + 用户信息 |
| AUTH-02 | 重复用户名 | 同一 username 二次注册 | code=4001 |
| AUTH-03 | 密码太短 | 5位密码 | code=4001 |
| AUTH-04 | 未登录访问 | 无 Authorization | code=4010 |
| AUTH-05 | 普通用户访问管理员接口 | USER token 调用 dashboard | code=4030 |
8.2 问诊用例
| 用例ID | 场景 | 输入 | 预期 |
|---|---|---|---|
| CON-01 | 正常提交 | 合法图片+问题 | 创建成功并落库 |
| CON-02 | 非图片文件 | txt 文件 | code=4001 |
| CON-03 | 问题过长 | >500 字 | code=4001 |
| CON-04 | AI返回异常结构 | 模拟无JSON输出 | fallback 返回中风险建议 |
| CON-05 | 越权访问记录 | A用户访问B记录id | code=4001/记录不存在 |
8.3 报告导出用例
| 用例ID | 场景 | 输入 | 预期 |
|---|---|---|---|
| REP-01 | TXT导出 | 合法id | 返回 fileName + content |
| REP-02 | PDF导出 | 合法id | 返回 fileName + base64 |
| REP-03 | 图片丢失 | image_url 不存在 | PDF 文字正常,图片提示跳过 |
8.4 管理端用例
| 用例ID | 场景 | 输入 | 预期 |
|---|---|---|---|
| ADM-01 | days 下限 | days=1 | 实际按7天 |
| ADM-02 | days 上限 | days=100 | 实际按30天 |
| ADM-03 | 趋势补零 | 某些天无记录 | dailyTrends 仍连续 |
9. 部署与运行
9.1 环境准备
- JDK 17
- MySQL 8.x
- Maven 3.8+
9.2 初始化数据库
执行:
source src/main/resources/db/schema.sql;
9.3 配置建议
建议使用环境变量,不要把真实密钥写入仓库:
export DASHSCOPE_API_KEY=your_real_key
export MYSQL_HOST=localhost
export MYSQL_PORT=3306
export MYSQL_DB=medical_screening
export MYSQL_USER=root
export MYSQL_PASSWORD=xxxxxx
9.4 启动方式
mvn clean package -DskipTests
java -jar target/screening-backend-1.0.0.jar
9.5 部署图(Mermaid)
9.6 部署截图占位
10. 安全设计与可优化点
10.1 已实现的安全措施
- 密码不明文存储(加盐哈希)。
- token 会话过期机制。
- 接口按 USER/ADMIN 角色控制。
- 上传文件仅允许
image/*。 - 读取图片时做路径规范化和存在性校验。
- 异常统一处理,避免堆栈直接泄露给前端。
10.2 建议继续优化
- 将 token 会话迁移到 Redis(支持多实例横向扩展)。
- 引入 JWT + 刷新令牌机制。
- 上传文件增加内容签名校验和病毒扫描。
- 接口增加限流(如 Bucket4j)。
- 关键审计日志落库(登录失败、导出报告、管理员访问)。
- 补充单元测试/集成测试(当前项目暂无自动化测试类)。
- AI 调用增加重试、超时降级与熔断。
- 对
application.yml中敏感信息进行彻底脱敏。
11. 项目亮点总结
- 完整走通了“多模态输入 -> AI结构化 -> 可追溯存储 -> 报告导出”链路。
- 业务与统计并重,既有 C 端能力也有 B 端运营看板。
- AI 返回不稳定时有 fallback,保证系统可用性。
- PDF 报告做了中文字体兼容与医院风格排版。
13. 附:更多图表模板(可选加分)
13.1 会话状态图
13.2 异常处理流程图
13.3 说明
本章图表已经是 Mermaid,可直接保留,不需要再补截图。
14. 结语
这个项目的核心不是“把 AI 接上去”这么简单,而是把它工程化:
鉴权、存储、解析兜底、权限边界、报告产出、运营统计都要形成闭环。
如果你在做毕业设计/课程设计/医疗方向实战,这套结构可以直接复用并继续扩展。
更多推荐



所有评论(0)