
简介这是一套基于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 的中医体质辨识系统”这个标题上。中医体质辨识的核心不是花哨页面而是九种体质的判定算法平和质、气虚质、阳虚质、阴虚质、痰湿质、湿热质、血瘀质、气郁质、特禀质每条目按 15 分计再按量表公式转成转化分最后依据阈值给出“是 / 倾向是 / 否”的结论。这套逻辑用 Java 实现非常顺枚举定义体质类型Map 维护维度映射一个纯函数做分数计算前端用 HTML 表单采集答案Servlet 或 Spring Boot 接收请求并回传 JSON。代码量不大但涉及 HTTP 请求、参数校验、中文编码、数据库存储正好把 Java Web 的常见知识点串起来。这篇就按“领域建模 → 系统搭建 → 环境排错 → 二次开发”的顺序把能直接复用的方案写清楚。2. 中医体质辨识系统的领域建模与辨识规则2.1 体质判定标准转化分公式与阈值划分中医体质辨识目前通行的做法是参考《中医体质分类与判定》标准里给出的量表思路每个体质维度由若干条目组成受试者对每个条目选择 15 分1 没有、2 很少、3 有时、4 经常、5 总是。不同维度的条目数并不相同所以不能直接拿原始分横向比较必须先把原始分换算成转化分公式是转化分 (原始分 - 条目数) / (条目数 × 4) × 100举个例子气虚质维度有 8 个条目受试者打分总和是 32 分那么转化分就是(32 - 8) / (8 × 4) × 100 75分。转化分的意义是把不同条目数的维度拉到同一个 0100 的标尺上这样九个维度之间才可以互相比大小。判定规则一般分两层体质类型转化分条件判定结果平和质转化分 ≥ 60且其余八种偏颇体质转化分均 30是平和质转化分 ≥ 60且其余八种偏颇体质存在 3039 分基本是有偏颇倾向偏颇体质转化分 ≥ 40是偏颇体质转化分 3039倾向是偏颇体质转化分 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 MapInteger, ConstitutionType DIMENSION_MAP new HashMap(); static { DIMENSION_MAP.put(1, ConstitutionType.QI_XU); DIMENSION_MAP.put(2, ConstitutionType.QI_XU); // 实际项目里这里从数据库或JSON加载不用硬编码 } // 记录每个维度下题目数量 private static final MapConstitutionType, Integer ITEM_COUNT new HashMap(); public static CalculationResult calculate(MapInteger, Integer answers) { MapConstitutionType, Integer rawScoreMap new EnumMap(ConstitutionType.class); for (Map.EntryInteger, Integer entry : answers.entrySet()) { ConstitutionType type DIMENSION_MAP.get(entry.getKey()); if (type null) { continue; // 跳过未配置的题目ID } rawScoreMap.merge(type, entry.getValue(), Integer::sum); } MapConstitutionType, 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(MapConstitutionType, Double convertedMap) { // 找出转化分最高的偏颇体质 ConstitutionType mainType null; double maxScore 0; ListConstitutionType 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 包扔进 TomcatSpring 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT辨识结果表;answer_data使用 JSON 类型存储答案快照省去为每人每天建一条明细表的麻烦detail_json则把九个维度的转化分整体存下来后续做健康档案追溯时不用重新计算。如果你拿到的免费源码用的是“每个答案一行记录”的表结构也无需重构只是查询时会多几张关联表。3.3 HTML 答题页与 Java 后端的交互3.3.1 用 HTML 表单采集答案Servlet 接收参数传统的 JSP 做法是后端用 JSTL 循环渲染题目前端用 form 提交。为了减少对标签库的依赖很多免费源码会直接用静态 HTML 配合 JavaScript 动态生成题目下面这个例子是符合 HTML 表单规范的最小实现form idquizForm div classquestion>protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { req.setCharacterEncoding(UTF-8); MapInteger, Integer answers new HashMap(); for (EnumerationString 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;charsetUTF-8); resp.getWriter().write(new Gson().toJson(result)); }关键点是第一行setCharacterEncoding必须放在第一次读取参数之前否则中文用户名会乱码getParameterNames遍历结合startsWith(q_)做前缀过滤这样即使以后在表单里加了手机号、年龄等字段后端代码也不需要改动。3.3.2 用 Ajax 保存答题进度并回显结果上面用fetch提交属于一次性作答。更完整的系统会要求“答到一半刷新不丢题”常见做法是把答案实时写入 localStorage提交时再整体发送。在change事件里持久化document.querySelectorAll(.question input[typeradio]).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.xml或web.xml再决定装什么环境而不是拿着最新版 JDK 硬跑。源码特征JDK 版本Web 容器MySQL 驱动JSP Servletjavax.servlet包8 或 11Tomcat 8.5 / 9mysql-connector-java 5.1.49 或 8.0.xSpring Boot 2.x 依赖8 或 11内置 Tomcat 98.0.xSpring 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 contentTypetext/html;charsetUTF-8 languagejava %再检查数据库连接串是否带编码参数String url jdbc:mysql://localhost:3306/tz_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai;serverTimezone缺失会导致 JDBC 8.x 连接时报时区错误characterEncodingutf8mb4比utf8更保险因为 MySQL 的utf8字符集并不完整支持四字节字符。如果是老项目用了useSSLtrue而本机 MySQL 未配置证书还需要在后面加上useSSLfalse否则驱动启动时会刷出大量警告严重时直接拒绝连接。静态资源 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 里出现eval加document.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_MAP和ITEM_COUNT。注意反转分条目量表中某些题目是反向计分如“您容易失眠吗”对判断阴虚质是正向计分但放在“平和质”维度下就要反转在 JSON 里加一个reverse: true字段加载时判断score (6 - rawScore)。如果不处理反转分转化分计算结果和临床结论会明显偏离这是二次开发时最容易忽略的业务细节。5.2 生成 CSV 体质报告并验证算法边界免费的 HTML 页面大多只展示屏幕上的结果导出报告是最容易出彩的增量功能。不引入 POI 这类重依赖直接输出 CSV 就能满足“保存存档”需求response.setContentType(text/csv;charsetUTF-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() { MapInteger, 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每次调整阈值或修改题目后跑一遍即可。本文还有配套的精品资源点击获取