在医疗健康与AI深度融合的浪潮下,如何利用多模态大模型快速搭建一套疾病初筛与护理建议系统?本文基于SpringBoot与Vue,结合通义千问多模态能力,从架构设计、数据库建模到核心代码实现,完整拆解这一全栈项目的落地路径,为前端与后端开发者提供可复用的工程方案。

一、项目定位与技术选型

本系统的核心链路非常清晰:用户上传症状图片并辅以文字描述,服务端调用多模态AI模型完成初筛推理,最终返回结构化的风险等级、护理建议、下一步行动指引及免责声明。同时,系统还需支撑问诊历史分页检索、TXT/PDF报告导出,以及管理员运营看板等功能。

在技术选型上,后端采用SpringBoot快速构建REST API,配合MyBatis实现SQL精细控制,MySQL负责结构化数据存储。报告生成选用iTextPDF,支持中文字体嵌入与医院风格模板。AI能力层接入通义千问多模态模型,实现图片与文本的联合理解。

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

对于前端开发者而言,虽然本项目以Vue为前端框架,但其设计思想同样适用于React或Angular。理解后端接口契约与数据流,是UI开发与前端工具链高效协作的基础。

[AFFILIATE_SLOT_1]

二、系统架构与角色用例

系统整体分为五层:表现层(Controller,负责请求接收与鉴权)、业务层(Service,核心业务编排)、数据访问层(Mapper+XML,SQL执行与聚合统计)、数据层(MySQL+本地文件目录)、外部能力层(Qwen API多模态推理)。

角色方面,系统定义了普通用户、管理员与外部AI服务三类。普通用户可完成注册登录、提交问诊、查看记录、导出报告及个人统计;管理员则聚焦运营看板,查看用户量、问诊量、趋势与风险分布。

非功能性需求同样关键:接口响应统一格式、异常可区分、图片安全落盘防路径穿越、AI返回不稳定时有兜底结构化结果、报告支持中文字体跨平台可读。这些约束直接影响了后续的代码实现策略。

三、数据库设计与核心表结构 ✅

数据库设计围绕三张核心表展开:用户表(user_account)、会话表(user_session)与问诊记录表(consultation_record)。

  • user_account
  • id:主键
  • username:登录名(唯一)
  • password_hash:密码哈希
  • nickname:昵称
  • role:角色(USER/ADMIN)
  • created_at:创建时间
  • user_session
  • id:主键
  • user_id:用户ID
  • token:登录token(唯一)
  • expire_at:过期时间
  • created_at:创建时间
  • 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:创建时间

ER关系上,用户与会话为一对多,用户与问诊记录同样为一对多。这种设计既保证了数据隔离,又便于后续统计聚合。

四、核心业务流程拆解(泳道图+时序图)

系统包含四条核心流程,每条流程都涉及前端、后端、数据库与外部服务的协作。

流程一:注册/登录/会话鉴权。用户输入账号密码后,后端校验格式(用户名4-20位字母数字下划线,密码6-32位),使用SHA-256 + salt生成密码摘要。登录成功后生成超长token(两个UUID去横杠拼接),写入user_session。每次鉴权先清理过期会话,再校验token与角色。

在这里插入图片描述

流程二:提交问诊并生成初筛结果。接收multipart表单(question + image (+ nickname)),校验问题描述长度≤500、图片类型为image/*。图片落盘uploads,生成publicUrl(前端展示)与dataUrl(AI传图)。构造多模态提示词调用Qwen,解析返回JSON;若解析失败则走fallback(默认中风险+默认护理建议),完整记录落库并返回详情。

在这里插入图片描述

流程三:导出报告(TXT+PDF)。按consultationId + userId查记录防止越权。TXT拼接结构化文本;PDF包含封面页、内容页、问诊图片嵌入、头脚部(报告编号、分页、生成时间)及中文字体嵌入。

在这里插入图片描述

流程四:管理员看板统计。指标口径包括:totalUsers(总用户数)、totalConsultations(总问诊数)、recent7DaysConsultations(近7天问诊量)、activeUsers(近30天活跃用户)、riskDistribution(风险等级分布)、dailyTrends(按日趋势,空白天补0)、topUsers(问诊量TOP用户)。

在这里插入图片描述

在这里插入图片描述

五、关键代码实现与API设计

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

在密码安全方面,系统不保存明文密码,比较时比较hash值:

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);
}

登录鉴权核心逻辑中,每次鉴权会先做过期会话清理,减轻脏数据积累:

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;
}

图片存储与安全处理方面,同一份图片输出两个地址,publicUrl给前端展示,dataUrl供AI调用:

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);
}

多模态AI调用时,提示词强约束JSON格式,便于后端结构化入库:

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

AI返回解析与兜底逻辑确保即使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);
}
}

创建问诊主流程代码如下:

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);
}

统计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>

全局异常统一返回,前端只需按code做分支处理:

@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());
    }

API设计上,统一返回格式如下:

{
"code": 0,
"message": "ok",
"data": {},
"timestamp": "2026-02-19T11:00:00"
}

鉴权接口包括注册、登录、获取当前用户与退出登录:

POST /api/auth/register

{
"username": "test_user",
"password": "Test123456",
"nickname": "测试用户"
}

POST /api/auth/login

{
"username": "test_user",
"password": "Test123456"
}

GET /api/auth/me

Authorization: Bearer <token>

POST /api/auth/logout

Authorization: Bearer <token>

问诊接口涵盖创建、分页、详情、统计与报告导出:

POST /api/consultations

multipart/form-data

question:必填

image:必填

nickname:选填

GET /api/consultations?page=1&size=10&keyword=咳嗽

GET /api/consultations/{id}

GET /api/consultations/statistics

GET /api/consultations/{id}/report

GET /api/consultations/{id}/report/pdf

首页与管理端接口:

GET /api/home/overview

GET /api/home/highlights

GET /api/admin/dashboard?days=14(需ADMIN)

错误码约定:4000(参数错误)、4001(业务异常)、4010(未登录/登录失效)、4030(无权限)、5000(系统异常)。

前端页面截图与替换对应表:

编号章节位置页面实际名称(建议)建议文件名
S015.1.3用户端-登录页S01-用户端-登录页.png
S025.1.3用户端-注册页S02-用户端-注册页.png
S035.2.4用户端-智能问诊页-症状输入S03-用户端-智能问诊页-症状输入.png
S045.2.4用户端-智能问诊页-图片上传预览S04-用户端-智能问诊页-图片上传预览.png
S055.2.4用户端-问诊结果页-风险与护理建议S05-用户端-问诊结果页-风险与护理建议.png
S065.3.3用户端-问诊详情页-导出入口S06-用户端-问诊详情页-导出入口.png
S075.3.3用户端-PDF报告预览页S07-用户端-PDF报告预览页.png
S085.4.3管理端-运营看板页-核心指标卡片S08-管理端-运营看板页-核心指标卡片.png
S095.4.3管理端-运营看板页-趋势与风险分布S09-管理端-运营看板页-趋势与风险分布.png
S107.7用户端-首页概览页S10-用户端-首页概览页.png
S117.7用户端-问诊记录列表页S11-用户端-问诊记录列表页.png
S127.7用户端-问诊记录详情页S12-用户端-问诊记录详情页.png
S137.7用户端-个人中心页S13-用户端-个人中心页.png
S147.7用户端-个人统计页-风险分布图S14-用户端-个人统计页-风险分布图.png
S157.6接口调试页-注册登录与问诊接口S15-接口调试页-注册登录与问诊接口.png
S167.6接口返回示例页-问诊详情JSONS16-接口返回示例页-问诊详情JSON.png
S178.5测试验证页-Postman集合S17-测试验证页-Postman集合.png
S188.5测试验证页-MySQL数据校验S18-测试验证页-MySQL数据校验.png
S199.6部署架构页-前后端与MySQLS19-部署架构页-前后端与MySQL.png
S209.6运行验证页-后端服务日志S20-运行验证页-后端服务日志.png

[AFFILIATE_SLOT_2]

六、测试、部署与安全优化

测试用例设计覆盖鉴权、问诊、报告导出与管理端四大场景:

用例ID场景输入预期
AUTH-01注册成功合法用户名密码返回 token + 用户信息
AUTH-02重复用户名同一 username 二次注册code=4001
AUTH-03密码太短5位密码code=4001
AUTH-04未登录访问无 Authorizationcode=4010
AUTH-05普通用户访问管理员接口USER token 调用 dashboardcode=4030

用例ID场景输入预期
CON-01正常提交合法图片+问题创建成功并落库
CON-02非图片文件txt 文件code=4001
CON-03问题过长>500 字code=4001
CON-04AI返回异常结构模拟无JSON输出fallback 返回中风险建议
CON-05越权访问记录A用户访问B记录idcode=4001/记录不存在

用例ID场景输入预期
REP-01TXT导出合法id返回 fileName + content
REP-02PDF导出合法id返回 fileName + base64
REP-03图片丢失image_url 不存在PDF 文字正常,图片提示跳过

用例ID场景输入预期
ADM-01days 下限days=1实际按7天
ADM-02days 上限days=100实际按30天
ADM-03趋势补零某些天无记录dailyTrends 仍连续

部署环境需JDK 17、MySQL 8.x、Maven 3.8+。初始化数据库执行:

source src/main/resources/db/schema.sql;

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

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

启动方式:

mvn clean package -DskipTests
java -jar target/screening-backend-1.0.0.jar

安全方面,已实现密码加盐哈希、token会话过期、USER/ADMIN角色控制、上传文件仅允许image/*、读取图片路径规范化与存在性校验、异常统一处理避免堆栈泄露。

建议继续优化:将token会话迁移至Redis支持多实例、引入JWT+刷新令牌、上传文件增加内容签名与病毒扫描、接口限流(如Bucket4j)、关键审计日志落库、补充自动化测试、AI调用增加重试超时降级与熔断、对application.yml中敏感信息彻底脱敏。

总结:本文完整拆解了基于SpringBoot+Vue的多模态疾病初筛系统,从技术选型、架构分层、数据库设计到四条核心流程与关键代码,覆盖了AI集成、文件安全、报告生成与运营统计等工程要点。对于前端开发者而言,理解后端契约与数据流是高效UI开发的前提;对于全栈团队,这套方案可直接复用并持续优化。