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)
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 功能性需求
- 注册、登录、获取个人信息、退出登录。
- 提交问诊(图片+文本),获取结构化初筛结果。
- 问诊记录分页、详情、检索、统计。
- 导出报告(文本与 PDF)。
- 管理员运营看板(用户量、问诊量、趋势、风险分布、Top 用户)。
3.3 非功能性需求
- 接口响应统一格式。
- 鉴权失败、业务异常、系统异常可区分。
- 图片文件安全落盘,避免路径穿越。
- AI 返回不稳定时有兜底结构化结果。
- 报告支持中文字体,跨平台可读。
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:用户 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)
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 关键逻辑
- 用户输入账号密码。
- 后端校验格式(用户名 4-20 位字母数字下划线,密码 6-32 位)。
- 使用
SHA-256 + salt生成密码摘要。 - 登录成功后生成超长 token(两个 UUID 去横杠拼接)。
- token 与过期时间写入
user_session。 - 每次鉴权时先清理过期会话,再校验 token 与角色。
5.1.2 泳道图
数据库
后端
前端
用户
填写账号密码
携带Authorization访问接口
调用登录接口
保存token
后续请求附带token
校验参数
密码加盐哈希比对
创建session并返回token
清理过期session
校验token并加载用户
user_account
user_session
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 泳道图
存储
外部
后端
前端
用户
上传图片并输入症状
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 核心点
- 按
consultationId + userId查记录,防止越权。 - TXT:拼接结构化文本。
- 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/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 初始化数据库
执行:
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 已实现的安全措施
- 密码不明文存储(加盐哈希)。
- token 会话过期机制。
- 接口按 USER/ADMIN 角色控制。
- 上传文件仅允许
image/*。 - 读取图片时做路径规范化和存在性校验。
- 异常统一处理,避免堆栈直接泄露给前端。
10.2 建议继续优化
- 将 token 会话迁移到 Redis(支持多实例横向扩展)。
- 引入 JWT + 刷新令牌机制。
- 上传文件增加内容签名校验和病毒扫描。
- 接口增加限流(如 Bucket4j)。
- 关键审计日志落库(登录失败、导出报告、管理员访问)。
- 补充单元测试/集成测试(当前项目暂无自动化测试类)。
- AI 调用增加重试、超时降级与熔断。
- 对
application.yml中敏感信息进行彻底脱敏。
11. 项目亮点总结
- 完整走通了"多模态输入 -> AI结构化 -> 可追溯存储 -> 报告导出"链路。
- 业务与统计并重,既有 C 端能力也有 B 端运营看板。
- AI 返回不稳定时有 fallback,保证系统可用性。
- 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 接上去"这么简单,而是把它工程化:
鉴权、存储、解析兜底、权限边界、报告产出、运营统计 都要形成闭环。
如果你在做毕业设计/课程设计/医疗方向实战,这套结构可以直接复用并继续扩展。