基于Java Web的中医体质辨识系统设计与实现
2026/9/16 6:29:34 网站建设 项目流程

简介:这是一套基于Java与HTML研发的中医体质辨识系统完整源码,面向中医信息化学习者、Java桌面客户端与Web界面整合的开发者,以及需要课程设计或毕业设计素材的高校学生。项目共66个文件,压缩包约17.7MB,核心包括33个Java源文件,覆盖用户登录注册、问卷答题、数据库交互与结果展示等业务模块;另有6个JAR包集成MySQL驱动、JFreeChart图表与界面美化库,4个XML配置辅助工程设置,PNG/JPG图片和HTML文件提供界面素材与入口页面,同时附带演示视频和readme说明。目录按系统功能划分,便于快速定位前后端逻辑。已有436人学习下载。基于可运行代码,开发者可直观看到传统医学知识与现代编程结合的实现路径,适合在此基础上做二次开发或功能扩展。

1. 中医体质辨识系统:从九种体质到 Java Web 交付

一个社区卫生院想给居民做体质测评,一个毕业设计要交可运行的 Web 系统,一个刚学完 Java 基础的人想找一个能讲清楚“表单提交 → 业务判定 → 结果回显”的完整案例——这三类需求最后几乎都会落到“基于 Java 和 HTML 的中医体质辨识系统”这个标题上。中医体质辨识的核心不是花哨页面,而是九种体质的判定算法:平和质、气虚质、阳虚质、阴虚质、痰湿质、湿热质、血瘀质、气郁质、特禀质,每条目按 1~5 分计,再按量表公式转成转化分,最后依据阈值给出“是 / 倾向是 / 否”的结论。这套逻辑用 Java 实现非常顺:枚举定义体质类型,Map 维护维度映射,一个纯函数做分数计算,前端用 HTML 表单采集答案,Servlet 或 Spring Boot 接收请求并回传 JSON。代码量不大,但涉及 HTTP 请求、参数校验、中文编码、数据库存储,正好把 Java Web 的常见知识点串起来。这篇就按“领域建模 → 系统搭建 → 环境排错 → 二次开发”的顺序,把能直接复用的方案写清楚。

2. 中医体质辨识系统的领域建模与辨识规则

2.1 体质判定标准:转化分公式与阈值划分

中医体质辨识目前通行的做法是参考《中医体质分类与判定》标准里给出的量表思路:每个体质维度由若干条目组成,受试者对每个条目选择 1~5 分(1 没有、2 很少、3 有时、4 经常、5 总是)。不同维度的条目数并不相同,所以不能直接拿原始分横向比较,必须先把原始分换算成转化分,公式是:

转化分 = (原始分 - 条目数) / (条目数 × 4) × 100

举个例子:气虚质维度有 8 个条目,受试者打分总和是 32 分,那么转化分就是(32 - 8) / (8 × 4) × 100 = 75分。转化分的意义是把不同条目数的维度拉到同一个 0~100 的标尺上,这样九个维度之间才可以互相比大小。

判定规则一般分两层:

体质类型转化分条件判定结果
平和质转化分 ≥ 60,且其余八种偏颇体质转化分均 < 30
平和质转化分 ≥ 60,且其余八种偏颇体质存在 30~39 分基本是,有偏颇倾向
偏颇体质转化分 ≥ 40
偏颇体质转化分 30~39倾向是
偏颇体质转化分 < 30

提示:不同教材和实现版本里条目数和阈值会有微调。做系统时不要把阈值写死在页面或散落在 Service 的各个 if 里,建议做成配置文件或数据库字典,方便中医师后期调整。

2.2 用 Java 枚举和 Map 实现九种体质的分数聚合

刚学 Java 的人很容易写出“九个 if 各算各的”这样的代码,表面上没问题,但后续加一个“特禀质条目调整”就要改九处。更好的做法是把体质类型定义成枚举,把“哪个题目属于哪个维度”放进一个静态 Map,再用一个循环完成聚合。

2.2.1 用枚举定义体质维度与默认阈值
public enum ConstitutionType { PING_HE("平 和 质", 60, 30), QI_XU("气 虚 质", 40, 30), YANG_XU("阳 虚 质", 40, 30), YIN_XU("阴 虚 质", 40, 30), TAN_SHI("痰 湿 质", 40, 30), SHI_RE("湿 热 质", 40, 30), XUE_YU("血 瘀 质", 40, 30), QI_YU("气 郁 质", 40, 30), TE_BING("特禀质", 40, 30); private final String displayName; private final int yesThreshold; private final int tendencyThreshold; ConstitutionType(String displayName, int yesThreshold, int tendencyThreshold) { this.displayName = displayName; this.yesThreshold = yesThreshold; this.tendencyThreshold = tendencyThreshold; } public String getDisplayName() { return displayName; } public int getYesThreshold() { return yesThreshold; } public int getTendencyThreshold() { return tendencyThreshold; } }

枚举里把“显示名”和“判定阈值”绑定在一起,比散落的常量清晰。平和质的yesThreshold字段在这里不用于自身判定,而是配合“其余体质均小于 30”这个条件,所以代码里要单独处理平和质逻辑。

2.2.2 转化分计算与主倾向判定
public class ConstitutionCalculator { // 题目ID -> 所属体质维度 private static final Map<Integer, ConstitutionType> DIMENSION_MAP = new HashMap<>(); static { DIMENSION_MAP.put(1, ConstitutionType.QI_XU); DIMENSION_MAP.put(2, ConstitutionType.QI_XU); // 实际项目里这里从数据库或JSON加载,不用硬编码 } // 记录每个维度下题目数量 private static final Map<ConstitutionType, Integer> ITEM_COUNT = new HashMap<>(); public static CalculationResult calculate(Map<Integer, Integer> answers) { Map<ConstitutionType, Integer> rawScoreMap = new EnumMap<>(ConstitutionType.class); for (Map.Entry<Integer, Integer> entry : answers.entrySet()) { ConstitutionType type = DIMENSION_MAP.get(entry.getKey()); if (type == null) { continue; // 跳过未配置的题目ID } rawScoreMap.merge(type, entry.getValue(), Integer::sum); } Map<ConstitutionType, Double> convertedMap = new EnumMap<>(ConstitutionType.class); for (ConstitutionType type : ConstitutionType.values()) { int rawScore = rawScoreMap.getOrDefault(type, 0); int itemCount = ITEM_COUNT.getOrDefault(type, 0); if (itemCount == 0) { convertedMap.put(type, 0.0); continue; } double converted = (rawScore - itemCount) * 100.0 / (itemCount * 4); convertedMap.put(type, converted); } return judge(convertedMap); } }

说明几个关键点:EnumMap在枚举数量固定且较少时性能好,遍历顺序稳定;merge方法避免了自己写“先 get 再 put”的分支;条目数为 0 的维度单独判断,防止除零异常。实际项目里DIMENSION_MAP直接从数据库加载,静态块只是演示用的兜底写法。

2.2.3 判定平和质与偏颇倾向
public static CalculationResult judge(Map<ConstitutionType, Double> convertedMap) { // 找出转化分最高的偏颇体质 ConstitutionType mainType = null; double maxScore = 0; List<ConstitutionType> tendencies = new ArrayList<>(); for (ConstitutionType type : ConstitutionType.values()) { if (type == ConstitutionType.PING_HE) continue; double score = convertedMap.getOrDefault(type, 0.0); if (score >= type.getYesThreshold() && score > maxScore) { maxScore = score; mainType = type; } if (score >= type.getTendencyThreshold() && score < type.getYesThreshold()) { tendencies.add(type); } } double pingHeScore = convertedMap.getOrDefault(ConstitutionType.PING_HE, 0.0); boolean hasStrongTendency = convertedMap.entrySet().stream() .filter(e -> e.getKey() != ConstitutionType.PING_HE) .anyMatch(e -> e.getValue() >= ConstitutionType.PING_HE.getTendencyThreshold()); if (mainType == null) { // 没有任何偏颇体质达到"是" if (pingHeScore >= 60 && hasStrongTendency) { return CalculationResult.basicPingHe(pingHeScore, tendencies); } if (pingHeScore >= 60) { return CalculationResult.purePingHe(pingHeScore, tendencies); } return CalculationResult.unclear(pingHeScore, tendencies); } return CalculationResult.withMainType(mainType, maxScore, convertedMap, tendencies); }

这段代码体现了两个业务细节:一是主体质只从“达到是阈值”的偏颇体质里挑最高分,而不是全维度最高分——不然平和质患者可能因为某个低分维度被误判;二是平和质的“基本是”状态依赖其他体质是否存在 30 分以上的倾向,这要求两层判断嵌套。stream().anyMatch在这里比 for 循环更直观,适合表达“是否存在任意一个满足条件”的语义。

提示:将主判定和倾向判定分开返回,前端就能展示“您的体质为阳虚质,兼有气虚倾向”这种复合结论,这也是临床上比单一判定更常见的结果形态。

3. 用 Java 和 HTML 搭建可运行的辨识系统

3.1 技术选型:Servlet/JSP 还是 Spring Boot

拿到免费源码后先看它是什么架构,再决定怎么跑起来。常见的形态有三种:

架构形态典型技术栈适合场景部署要点
传统 JSP 单体Servlet + JSP + MySQL毕业设计、小型内网系统打 WAR 包扔进 Tomcat
Spring Boot 单体Spring Boot + Thymeleaf/HTML + MyBatis正式项目、后续要扩展接口打可执行 JAR,内置 Tomcat
前后端分离Spring Boot API + 静态 HTML需要对接小程序、AppAPI 与静态页分开部署,注意 CORS

就“中医体质辨识系统”这个体量来说,Servlet/JSP 版本维护成本低,面试时也更容易讲清楚请求流转过程;Spring Boot 版本则省掉大量 XML 配置,适合想要快速出效果的人。判断方法很简单:源码里有web.xml.jsp文件就是传统 Servlet 项目,有pom.xml且依赖spring-boot-starter-web就是 Spring Boot。

3.2 数据库表结构与初始化 SQL

不管哪种架构,核心数据表就三张:题目表、用户答题记录表、结果表。题目表用来存维度归属和选项分值,这样调整题目不用改代码。

CREATE TABLE tz_question ( question_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '题目ID', dimension_code VARCHAR(20) NOT NULL COMMENT '体质维度编码,如 QI_XU', sort_no INT DEFAULT 0 COMMENT '显示顺序', content VARCHAR(500) NOT NULL COMMENT '题目内容', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_dimension (dimension_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='中医体质辨识题目表'; CREATE TABLE tz_answer_record ( record_id BIGINT PRIMARY KEY AUTO_INCREMENT, user_name VARCHAR(50) COMMENT '受试者姓名', answer_data JSON COMMENT '题目答案快照,如 {"1":5,"2":3}', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='答题记录表'; CREATE TABLE tz_result ( result_id BIGINT PRIMARY KEY AUTO_INCREMENT, record_id BIGINT NOT NULL, main_type VARCHAR(20) COMMENT '主判定体质', main_score DECIMAL(5,1) COMMENT '主判定转化分', detail_json JSON COMMENT '九种体质转化分完整快照', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_record (record_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='辨识结果表';

answer_data使用 JSON 类型存储答案快照,省去为每人每天建一条明细表的麻烦;detail_json则把九个维度的转化分整体存下来,后续做健康档案追溯时不用重新计算。如果你拿到的免费源码用的是“每个答案一行记录”的表结构,也无需重构,只是查询时会多几张关联表。

3.3 HTML 答题页与 Java 后端的交互

3.3.1 用 HTML 表单采集答案,Servlet 接收参数

传统的 JSP 做法是后端用 JSTL 循环渲染题目,前端用 form 提交。为了减少对标签库的依赖,很多免费源码会直接用静态 HTML 配合 JavaScript 动态生成题目,下面这个例子是符合 HTML 表单规范的最小实现:

<form id="quizForm"> <div class="question">protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding("UTF-8"); Map<Integer, Integer> answers = new HashMap<>(); for (Enumeration<String> paramNames = req.getParameterNames(); paramNames.hasMoreElements(); ) { String name = paramNames.nextElement(); if (name.startsWith("q_")) { int qid = Integer.parseInt(name.substring(2)); int score = Integer.parseInt(req.getParameter(name)); answers.put(qid, score); } } CalculationResult result = ConstitutionCalculator.calculate(answers); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(new Gson().toJson(result)); }

关键点是第一行setCharacterEncoding必须放在第一次读取参数之前,否则中文用户名会乱码;getParameterNames遍历结合startsWith("q_")做前缀过滤,这样即使以后在表单里加了手机号、年龄等字段,后端代码也不需要改动。

3.3.2 用 Ajax 保存答题进度并回显结果

上面用fetch提交属于一次性作答。更完整的系统会要求“答到一半刷新不丢题”,常见做法是把答案实时写入 localStorage,提交时再整体发送。在change事件里持久化:

document.querySelectorAll('.question input[type=radio]').forEach(radio => { radio.addEventListener('change', () => { const qid = radio.name.replace('q_', ''); const saved = JSON.parse(localStorage.getItem('tz_answers') || '{}'); saved[qid] = radio.value; localStorage.setItem('tz_answers', JSON.stringify(saved)); }); });

回显时遍历localStorage中的值,把对应 radio 置为checked。这个方案不依赖任何第三方库,HTML 页面本身也不需要变成 JSP,纯静态文件就能完成进度记录,符合“HTML 网页制作”里的标准做法,也降低了免费源码部署时对服务端渲染的要求。

4. 源码免费下载后的环境搭建与排错要点

4.1 JDK、Tomcat 与 MySQL 的版本匹配

免费下载的源码时间跨度很大,版本不匹配是最常见的跑不起来原因。先看pom.xmlweb.xml再决定装什么环境,而不是拿着最新版 JDK 硬跑。

源码特征JDK 版本Web 容器MySQL 驱动
JSP + Servlet,javax.servlet8 或 11Tomcat 8.5 / 9mysql-connector-java 5.1.49 或 8.0.x
Spring Boot 2.x 依赖8 或 11内置 Tomcat 98.0.x
Spring Boot 3.x 依赖17+内置 Tomcat 108.1.x 或新版com.mysql.cj.jdbc.Driver

判断老项目最直接的方式是看代码里有没有import javax.servlet.*。如果在 Tomcat 10 上运行,会直接抛ClassNotFoundException: javax.servlet.http.HttpServlet,因为 Tomcat 10 起 Jakarta EE 9 把包名从javax改成了jakarta。遇到这种情况,要么换回 Tomcat 9,要么用 IDE 的全局替换把javax.servlet改成jakarta.servlet,但后者会牵涉很多三方库,不如直接降 Tomcat 版本省事。

4.2 中文乱码与静态资源路径的典型坑

JSP 页面中文乱码通常涉及三个位置:JSP 文件本身编码、服务器响应编码、数据库连接编码。先检查 JSP 头部:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

再检查数据库连接串是否带编码参数:

String url = "jdbc:mysql://localhost:3306/tz_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai";

serverTimezone缺失会导致 JDBC 8.x 连接时报时区错误;characterEncoding=utf8mb4utf8更保险,因为 MySQL 的utf8字符集并不完整支持四字节字符。如果是老项目用了useSSL=true而本机 MySQL 未配置证书,还需要在后面加上useSSL=false,否则驱动启动时会刷出大量警告,严重时直接拒绝连接。

静态资源 404 也是高频问题:JSP 页面里写死了/css/style.css,项目部署到 Tomcat 的webapps/auto路径下后,浏览器请求的是/css/style.css,并不包含上下文路径。在 JSP 里统一使用<c:url value="/css/style.css"/>${pageContext.request.contextPath}/css/style.css,不要写绝对根路径。

4.3 下载源码后的安全检查

免费下载的源码不能直接放进生产环境,先做一次粗暴的安全审查。重点关注几个高风险特征:

grep -rn "Runtime.getRuntime().exec\|ProcessBuilder" src/ grep -rn "base64_decode\|eval(\|document.write" src/main/webapp/ grep -rn "Class.forName\|URLClassLoader" src/main/java/ grep -rn "\b(http|https)://[0-9]\{1,3\}\.[0-9]\{1,3\}\." src/

第一条命令查的是命令执行和后门基础;第二条查前端恶意脚本和加密混淆代码,尤其是 JS 里出现evaldocument.write属于高危组合;第三条查动态加载外部类;第四条查硬编码外网 IP。如果看到这些特征,先删除对应文件再跑。此外还要改数据库密码、改application.yml里的账号密码,不要沿用源码自带的root/123456

提示:安全审查和版本匹配这两步做完,再开始改代码。否则改了业务逻辑后发现问题,很难判断是环境问题还是改坏了。

5. 从免费源码到可用系统:题库动态化与结果导出

5.1 把题目与判定规则拆成 JSON 配置

很多免费源码的题目是“写死”在 JSP 里的,中医师想改一道题的措辞就得翻页面文件。我一般会把它拆成一个独立的questions.json,放在src/main/resources下,用一个加载器读取:

[ { "id": 1, "dimension": "QI_XU", "content": "您容易疲乏吗?", "scoreMode": "direct" }, { "id": 2, "dimension": "YANG_XU", "content": "您手脚发凉吗?", "scoreMode": "direct" } ]

Java 端用 Gson 或 Jackson 加载后重建成DIMENSION_MAPITEM_COUNT。注意反转分条目:量表中某些题目是反向计分(如“您容易失眠吗”对判断阴虚质是正向计分,但放在“平和质”维度下就要反转),在 JSON 里加一个"reverse": true字段,加载时判断:score = (6 - rawScore)。如果不处理反转分,转化分计算结果和临床结论会明显偏离,这是二次开发时最容易忽略的业务细节。

5.2 生成 CSV 体质报告并验证算法边界

免费的 HTML 页面大多只展示屏幕上的结果,导出报告是最容易出彩的增量功能。不引入 POI 这类重依赖,直接输出 CSV 就能满足“保存存档”需求:

response.setContentType("text/csv;charset=UTF-8"); String fileName = URLEncoder.encode("体质辨识结果_" + userName, "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + fileName + ".csv"); Writer writer = response.getWriter(); writer.write("体质类型,转化分,判定结果\n"); for (ConstitutionType type : ConstitutionType.values()) { writer.write(type.getDisplayName() + "," + scoreMap.get(type) + "," + judgeText + "\n"); } writer.flush();

filename*=UTF-8''是处理中文文件名最稳妥的方案,直接写在filename里在部分浏览器会乱码。CSV 文件建议用 Excel 打开验证一下是否存在列错位,如果出现中文乱码,把输出的文件用记事本另存为带 BOM 的 UTF-8 再测。

最后做一个自测类来保障判定结果可回归:

@Test void testAllHighScore() { Map<Integer, Integer> answers = new HashMap<>(); for (int qid = 1; qid <= totalCount; qid++) { answers.put(qid, 5); } CalculationResult result = ConstitutionCalculator.calculate(answers); // 全高分情况下偏颇体质必然是主判定,平和质不可能为真 assertNotNull(result.getMainType()); assertNotEquals("PING_HE", result.getMainType()); }

用全 5 分、全 1 分、每维都打 3 分的样本分别跑一遍,确认边界条件没有异常。把这个自测类放进src/test/java,每次调整阈值或修改题目后跑一遍即可。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询