SpringBoot+Vue实战:多模态疾病初筛与护理建议系统(含泳道图+时序图+完整后端代码)

0. 项目简介

本项目是一个"多模态疾病初筛与护理建议系统"后端,核心目标是:

  1. 用户上传图片 + 文字症状描述;
  2. 服务端调用通义千问多模态模型做初筛;
  3. 返回结构化结果(风险等级、护理建议、下一步建议、免责声明);
  4. 记录问诊历史,可分页检索;
  5. 支持导出 txt/pdf 报告;
  6. 管理员查看系统运营看板(趋势、风险分布、活跃用户、Top 用户)。

1. 技术栈与选型

1.1 后端技术栈

  • Spring Boot 3.3.5
  • MyBatis 3.0.4
  • MySQL 8.x
  • RestTemplate(调用千问接口)
  • iTextPDF(生成 PDF 报告)
  • Jackson(JSON 解析/序列化)

1.2 选型原因

  • Spring Boot:快速搭建 REST API,生态成熟。
  • MyBatis:SQL 可控,统计类查询更直观。
  • MySQL:结构化数据(用户/会话/问诊记录)存储稳定。
  • iTextPDF:可做医院风格报告模板,支持中文字体嵌入。
  • 千问多模态:支持图片+文本联合理解,适配初筛场景。

1.3 页面截图放置说明

本章不强制放图,建议在后文"页面截图占位"章节集中放置。


2. 系统架构设计

2.1 总体架构说明

系统分为 5 层:

  1. 表现层(Controller):接收请求、鉴权、返回统一响应。
  2. 业务层(Service):核心业务编排(上传、AI 分析、落库、报表)。
  3. 数据访问层(Mapper + XML):执行 SQL、聚合统计。
  4. 数据层(MySQL + 本地上传目录):结构化数据 + 图片文件。
  5. 外部能力层(Qwen API):多模态初筛推理。

2.2 架构图(Mermaid)

Vue 前端
SpringBoot Controller
AuthService
ConsultationService
AdminDashboardService
ImageStorageService
QwenAiService
user_account
user_session
consultation_record
DashScope/Qwen API
uploads 目录

3. 需求分析与角色用例

3.1 角色定义

  • 普通用户(USER)
  • 管理员(ADMIN)
  • 外部 AI 服务(Qwen API)

3.2 功能性需求

  1. 注册、登录、获取个人信息、退出登录。
  2. 提交问诊(图片+文本),获取结构化初筛结果。
  3. 问诊记录分页、详情、检索、统计。
  4. 导出报告(文本与 PDF)。
  5. 管理员运营看板(用户量、问诊量、趋势、风险分布、Top 用户)。

3.3 非功能性需求

  1. 接口响应统一格式。
  2. 鉴权失败、业务异常、系统异常可区分。
  3. 图片文件安全落盘,避免路径穿越。
  4. AI 返回不稳定时有兜底结构化结果。
  5. 报告支持中文字体,跨平台可读。

3.4 用例图(Mermaid 表达版)

普通用户
注册
登录
提交问诊
查看问诊列表
查看问诊详情
导出TXT报告
导出PDF报告
查看个人统计
退出登录
管理员
登录
查看运营看板
Qwen API

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:用户 ID
  • token:登录 token(唯一)
  • expire_at:过期时间
  • created_at:创建时间
4.1.3 问诊记录表 consultation_record
  • id:主键
  • user_id:用户 ID
  • nickname:本次问诊昵称
  • 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)

has
owns
USER_ACCOUNT
BIGINT
id
PK
VARCHAR
username
VARCHAR
password_hash
VARCHAR
nickname
VARCHAR
role
DATETIME
created_at
USER_SESSION
BIGINT
id
PK
BIGINT
user_id
FK
VARCHAR
token
DATETIME
expire_at
DATETIME
created_at
CONSULTATION_RECORD
BIGINT
id
PK
BIGINT
user_id
FK
VARCHAR
nickname
TEXT
question_text
VARCHAR
image_url
TEXT
preliminary_assessment
VARCHAR
risk_level
TEXT
nursing_advice
TEXT
next_step
TEXT
disclaimer
LONGTEXT
raw_answer
DATETIME
created_at

5. 核心业务流程(重点)


5.1 流程一:注册/登录/会话鉴权

5.1.1 关键逻辑
  1. 用户输入账号密码。
  2. 后端校验格式(用户名 4-20 位字母数字下划线,密码 6-32 位)。
  3. 使用 SHA-256 + salt 生成密码摘要。
  4. 登录成功后生成超长 token(两个 UUID 去横杠拼接)。
  5. token 与过期时间写入 user_session
  6. 每次鉴权时先清理过期会话,再校验 token 与角色。
5.1.2 泳道图

数据库
后端
前端
用户
填写账号密码
携带Authorization访问接口
调用登录接口
保存token
后续请求附带token
校验参数
密码加盐哈希比对
创建session并返回token
清理过期session
校验token并加载用户
user_account
user_session

5.1.3 页面截图占位(登录/注册)

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

5.2.1 关键逻辑
  1. 接收 multipart 表单:question + image (+ nickname)
  2. 校验问题描述长度(<=500),校验图片类型为 image/*
  3. 图片落盘 uploads,生成:
    • publicUrl(给前端展示)
    • dataUrl(给 AI 接口传图)
  4. 构造多模态提示词和请求体,调用 Qwen。
  5. 解析 AI 返回:
    • 如果是 JSON,提取结构化字段;
    • 如果解析失败,走 fallback(默认中风险+默认护理建议)。
  6. 将完整记录落库(包含 raw_answer 便于审计)。
  7. 返回详情对象。
5.2.2 泳道图

存储
外部
后端
前端
用户
上传图片并输入症状
POST /api/consultations
鉴权 requireUser
校验参数
保存图片
调用Qwen多模态
解析结构化结果
写入consultation_record
返回详情
DashScope Qwen
MySQL
uploads

5.2.3 时序图

MySQL QwenAiService ImageStorageService ConsultationService AuthService ConsultationController Vue前端 用户 MySQL QwenAiService ImageStorageService ConsultationService AuthService ConsultationController Vue前端 用户 选择图片+输入问题 POST /api/consultations (multipart) requireUser(token) 查询session + user 用户信息 currentUser createConsultation(...) store(image) publicUrl + dataUrl analyze(question,dataUrl) structuredResult insert consultation_record new id selectById(id,userId) consultationDetail detail ApiResponse.success(detail)


5.3 流程三:导出报告(TXT + PDF)

5.3.1 核心点
  1. consultationId + userId 查记录,防止越权。
  2. TXT:拼接结构化文本。
  3. PDF:
    • 封面页 + 内容页;
    • 读取问诊图片嵌入报告;
    • 头部脚部(报告编号、分页、生成时间);
    • 中文字体嵌入(TTF/OTF/Fallback)。
5.3.2 泳道图

存储
后端
前端
用户
点击导出报告
GET /report 或 /report/pdf
下载文件
鉴权+查记录
生成TXT内容
生成PDF字节
Base64返回
consultation_record
uploads

5.3.3 页面截图占位(报告导出)

5.4 流程四:管理员看板统计

5.4.1 指标口径
  • totalUsers:总用户数
  • totalConsultations:总问诊数
  • recent7DaysConsultations:近7天问诊量
  • activeUsers:近30天活跃用户(有问诊记录)
  • riskDistribution:风险等级分布
  • dailyTrends:按日趋势(空白天补 0)
  • topUsers:问诊量 TOP 用户
5.4.2 泳道图

数据库
后端
管理员
访问看板
requireAdmin
归一化days 7~30
聚合查询多项统计
补齐每日缺失数据
返回DashboardDTO
user_account
consultation_record

5.4.3 页面截图占位(管理后台)



6. 关键代码实现拆解

以下代码均来自本项目后端,按模块截取关键片段。

6.1 密码加盐哈希

java 复制代码
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)

java 复制代码
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 图片存储与安全处理

java 复制代码
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 调用与提示词

java 复制代码
String prompt = "你是医学初筛与护理建议助手。请结合图片和问题做初步分析,不要做确诊。"
        + "请严格返回 JSON,字段如下:"
        + "{\"preliminaryAssessment\":\"\",\"riskLevel\":\"LOW|MEDIUM|HIGH\",\"nursingAdvice\":[\"\"],\"nextStep\":\"\",\"disclaimer\":\"\"}"
        + "。nursingAdvice 至少给 3 条,语言用简体中文。";

说明:强约束 JSON 格式,便于后端结构化入库。

6.5 AI 返回解析与兜底

java 复制代码
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 创建问诊主流程

java 复制代码
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(分页检索 + 风险分布 + 趋势)

xml 复制代码
<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>
xml 复制代码
<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 全局异常统一返回

java 复制代码
@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 统一返回格式

json 复制代码
{
  "code": 0,
  "message": "ok",
  "data": {},
  "timestamp": "2026-02-19T11:00:00"
}

7.2 鉴权接口

7.2.1 注册
  • POST /api/auth/register
  • Body:
json 复制代码
{
  "username": "test_user",
  "password": "Test123456",
  "nickname": "测试用户"
}
7.2.2 登录
  • POST /api/auth/login
  • Body:
json 复制代码
{
  "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/statistics
  • GET /api/consultations/{id}/report
  • GET /api/consultations/{id}/report/pdf

7.4 首页与管理端接口

  • GET /api/home/overview
  • GET /api/home/highlights
  • GET /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 环境准备

  1. JDK 17
  2. MySQL 8.x
  3. Maven 3.8+

9.2 初始化数据库

执行:

sql 复制代码
source src/main/resources/db/schema.sql;

9.3 配置建议

建议使用环境变量,不要把真实密钥写入仓库:

bash 复制代码
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 启动方式

bash 复制代码
mvn clean package -DskipTests
java -jar target/screening-backend-1.0.0.jar

9.5 部署图(Mermaid)

Vue 前端\nNginx/本地Vite
SpringBoot Jar
MySQL
uploads目录
DashScope Qwen API

9.6 部署截图占位


10. 安全设计与可优化点

10.1 已实现的安全措施

  1. 密码不明文存储(加盐哈希)。
  2. token 会话过期机制。
  3. 接口按 USER/ADMIN 角色控制。
  4. 上传文件仅允许 image/*
  5. 读取图片时做路径规范化和存在性校验。
  6. 异常统一处理,避免堆栈直接泄露给前端。

10.2 建议继续优化

  1. 将 token 会话迁移到 Redis(支持多实例横向扩展)。
  2. 引入 JWT + 刷新令牌机制。
  3. 上传文件增加内容签名校验和病毒扫描。
  4. 接口增加限流(如 Bucket4j)。
  5. 关键审计日志落库(登录失败、导出报告、管理员访问)。
  6. 补充单元测试/集成测试(当前项目暂无自动化测试类)。
  7. AI 调用增加重试、超时降级与熔断。
  8. application.yml 中敏感信息进行彻底脱敏。

11. 项目亮点总结

  1. 完整走通了"多模态输入 -> AI结构化 -> 可追溯存储 -> 报告导出"链路。
  2. 业务与统计并重,既有 C 端能力也有 B 端运营看板。
  3. AI 返回不稳定时有 fallback,保证系统可用性。
  4. PDF 报告做了中文字体兼容与医院风格排版。

13. 附:更多图表模板(可选加分)

13.1 会话状态图

登录成功
超过expire_at
主动logout
未登录
已登录
已过期
已退出

13.2 异常处理流程图



UnauthorizedException
ForbiddenException
BusinessException
ValidationException
Other
Controller收到请求
是否抛异常
ApiResponse.success
异常类型
code=4010
code=4030
code=4001
code=4000
code=5000

13.3 说明

本章图表已经是 Mermaid,可直接保留,不需要再补截图。


14. 结语

这个项目的核心不是"把 AI 接上去"这么简单,而是把它工程化:
鉴权、存储、解析兜底、权限边界、报告产出、运营统计 都要形成闭环。

如果你在做毕业设计/课程设计/医疗方向实战,这套结构可以直接复用并继续扩展。

相关推荐
玄〤1 小时前
个人博客网站搭建day1-Spring Security 核心配置详解:CSRF、会话管理、授权与异常处理(漫画解析)
java·后端·spring·spring security·csrf
老迟聊架构1 小时前
你的程序应该启动多少线程?
后端·架构
李昊哲小课1 小时前
Spring Cloud微服务课程设计 第二章:Eureka注册中心
spring boot·spring cloud·微服务·eureka
golang学习记1 小时前
Fiber v3 新特性全解析:更快、更强大、更优雅的 Go Web 框架
后端·go
心之语歌1 小时前
flutter 父子组件互相更新
后端·flutter
无心水1 小时前
【任务调度:数据库锁 + 线程池实战】2、MySQL 8.0+ vs PostgreSQL:SKIP LOCKED 终极对决,谁才是分布式调度的王者?
java·人工智能·后端·面试·架构
百锦再1 小时前
HashMap、Hashtable、TreeMap异同深度详解
jvm·spring boot·struts·spring cloud·缓存·kafka·tomcat
HelloReader1 小时前
Tauri 用“系统 WebView + 原生能力”构建更小更快的跨平台应用
前端·javascript·后端
滕青山1 小时前
JSON转TypeScript接口核心JS实现
前端·javascript·vue.js