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 密码加盐哈希

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/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 初始化数据库

执行:

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)

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 接上去”这么简单,而是把它工程化:
鉴权、存储、解析兜底、权限边界、报告产出、运营统计都要形成闭环。
如果你在做毕业设计/课程设计/医疗方向实战,这套结构可以直接复用并继续扩展。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐