做毕业设计或者课程设计的时候,最怕碰到哪种题目?不是那种框架一搭就结束的管理系统,也不是纯算法能蒙混过关的“数据挖掘类”。最怕的是那种“看着像管理系统,但仔细一想里面还藏着一条业务逻辑线”的题目。“基于Java Web的动物疾病诊断系统设计与实现”就属于这一类。
这个系统表面上是把动物疾病信息做成增删改查,但实际上核心价值在“诊断推理”那部分——用户选完症状,系统要能给出可能性最高的一批疾病,还要附带预防措施和治疗建议。做得好,它就是一个靠谱的兽医辅助问诊工具;做得糙,它就是一个带搜索框的疾病词条库。区别就在设计思路上。
这篇文章我从头到尾把这个项目的设计、建库、算法实现、页面交互、常见坑都过一遍,适合正在搞Java Web课程设计、毕业设计,或者想自己练手做一个“规则判断型”业务系统的同学参考。不管你是刚学完Servlet和JSP,还是已经用过Spring Boot,这篇内容都能给你一些可以直接落地的思路。
1. 系统定位与核心需求拆解
1.1 这个系统到底在解决什么问题
先说痛点。宠物主人遇到动物生病,第一反应是上网搜症状,“狗狗呕吐”“猫咪精神不振”这种关键词搜出来,结果五花八门,甚至能给你带到“犬瘟”“猫瘟”这种吓人的方向上,越搜越焦虑。
系统的定位就是把“症状—疾病”之间的对应关系结构化,让用户按照实际情况选择症状,系统基于预设的知识库去匹配可能的疾病,再给出概率排序。这里的“诊断”不是替代兽医,而是做初步筛查和科普,减少信息不对等。
所以整个系统要服务两类人:
- 普通用户:选症状,看结果,了解疾病知识。
- 管理员:维护疾病库、症状库、疾病与症状的关联关系、病例记录。
清楚了这两类角色,功能边界就出来了。普通用户不需要看到管理后台,管理员也不需要天天走诊断流程。登录、角色权限、功能菜单的划分,全部围绕这个来。
1.2 核心功能模块怎么划分
功能模块按角色拆,不要混在一起。我当时画功能结构图的时候是这么分的:
用户端:
- 用户注册与登录
- 症状选择(按动物类型分组,如犬、猫、家禽、反刍动物)
- 诊断结果显示(匹配疾病列表,按匹配度排序)
- 疾病详情浏览(病因、症状、治疗建议、预防措施)
- 诊断历史记录(登录用户可查历史诊断记录)
管理端:
- 管理员登录
- 疾病信息管理(增删改查)
- 症状信息管理
- 疾病—症状关联关系配置
- 病例记录管理(可按疾病、动物类型筛选)
- 用户管理(可选,简单系统可跳过)
这里特别提醒一点:很多人拿到题目就直接建五张表开始写代码,结果做到一半发现“疾病症状关联”这个关系没有单独建模,只能靠字符串拼接存储,诊断逻辑根本写不下去。这个模块是系统的数据核心,必须单独设计一张关联表。
1.3 用户流程和业务闭环
用户的完整操作流程应该是:
- 登录系统(游客可以只浏览疾病库,但不能用诊断功能,避免无脑刷接口)
- 选择动物类型(犬、猫、鸟类、兔等)
- 勾选当前出现的症状(支持多选,建议限制在1~10个)
- 提交诊断,系统返回匹配疾病列表
- 点击疾病名称查看详情,包括治疗建议和预防措施
- 如果用户已登录,这次诊断记录自动存入历史,用户可以在“我的诊断记录”里回头看
把这个流程理顺了,再去做页面跳转设计和接口设计就很简单。页面之间的参数传递也不容易乱。
2. 技术选型与方案取舍
2.1 Java Web技术路线怎么选
这个题目没有限定框架,所以选型空间比较大。不同的选型对应不同的开发成本和学习收益,可以根据自己的基础来定。
| 方案 | 技术栈 | 适合场景 | 难度 |
|---|---|---|---|
| 传统JSP+Servlet | Servlet + JSP + JDBC + MySQL + Tomcat | 学校要求用纯JSP做课设,考察基础 | 中 |
| SSM整合 | Spring + SpringMVC + MyBatis | 经典企业级组合,适合巩固框架知识 | 中高 |
| Spring Boot + Thymeleaf | Spring Boot + MyBatis/JPA + Thymeleaf + Bootstrap | 快速开发,适合毕设和实操项目 | 中 |
我个人推荐Spring Boot + Thymeleaf这套组合。为什么?
第一,Spring Boot内嵌Tomcat,开发环境不用单独配置服务器,省事。第二,Thymeleaf是服务端渲染模板,写页面可以沿用传统思路,不需要前后端分离,对课设类项目来说心智负担最低。第三,简历上写Spring Boot比写Servlet好看,而且这套技术的资料最多,卡住了搜得到解决方案。
如果学校明确要求必须用JSP,那也别慌,下面的表结构设计和诊断逻辑完全可以复用,只是Controller层换成Servlet,ORM换成JDBC或者DbUtils就行。核心业务逻辑不受框架影响。
2.2 诊断推理怎么实现
很多人一看到“疾病诊断”四个字,就想去搞人工智能,想着用神经网络训练症状与疾病的映射模型,或者是引入贝叶斯网络、决策树之类的东西。这属于过度设计。
课设和毕设场景,用“规则匹配 + 加权评分”就够了。思路跟学习通上做试卷一样:
- 每一种疾病对应若干条“典型症状”
- 用户选择一系列症状
- 遍历疾病库,计算“选中症状中该疾病典型症状的数量”
- 用匹配度和覆盖率两个指标排序
- 得出诊断结果
这种方案的好处很直接:不需要训练集,不需要标注数据,逻辑完全可解释。演示答辩的时候,老师问你“这个结果怎么算出来的”,你能把评分公式写在黑板上,每一步都说得清楚,比丢个黑盒模型强多了。
2.3 数据库MySQL版本和字符集
数据库首选MySQL,课设和毕设基本默认。需要注意的是两个点:
一是版本,5.7和8.0都行。建议直接用8.0,连接驱动用8.x对应的com.mysql.cj.jdbc.Driver,URL要带上serverTimezone=Asia/Shanghai和useSSL=false,否则连数据库容易报时区错误和SSL警告。
二是字符集,建库的时候统一用utf8mb4。不要用utf8,因为MySQL的utf8是阉割版,不支持四字节的emoji和特殊符号。虽然这个系统不太可能存emoji,但数据导入导出的时候字符集问题很折磨人,用utf8mb4一劳永逸。
CREATE DATABASE animal_diag DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;3. 数据库设计:这个系统的地基
3.1 五张核心表怎么建模
这个系统最少需要五张表:用户表、疾病表、症状表、疾病症状关联表、诊断记录表。表结构我直接给出设计,你可以根据自己的需求做增减。
用户表(t_user):
CREATE TABLE `t_user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码,MD5加密存储', `real_name` varchar(50) DEFAULT NULL COMMENT '姓名', `phone` varchar(20) DEFAULT NULL COMMENT '联系电话', `role` tinyint(1) NOT NULL DEFAULT '1' COMMENT '角色:1普通用户 2管理员', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;疾病表(t_disease):
CREATE TABLE `t_disease` ( `id` int(11) NOT NULL AUTO_INCREMENT, `disease_name` varchar(50) NOT NULL COMMENT '疾病名称', `animal_type` varchar(20) NOT NULL COMMENT '动物类型:犬、猫、鸟等', `pathogen` varchar(255) DEFAULT NULL COMMENT '病原体/病因', `symptom_desc` text COMMENT '典型症状描述', `treatment` text COMMENT '治疗方案', `prevention` text COMMENT '预防措施', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;症状表(t_symptom):
CREATE TABLE `t_symptom` ( `id` int(11) NOT NULL AUTO_INCREMENT, `symptom_name` varchar(50) NOT NULL COMMENT '症状名称', `animal_type` varchar(20) NOT NULL COMMENT '适用动物类型', `sort_order` int(11) DEFAULT '0' COMMENT '展示排序,小值在前', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;疾病症状关联表(t_disease_symptom):
CREATE TABLE `t_disease_symptom` ( `id` int(11) NOT NULL AUTO_INCREMENT, `disease_id` int(11) NOT NULL, `symptom_id` int(11) NOT NULL, `weight` int(11) DEFAULT '1' COMMENT '症状权重,1一般 2典型 3高特异性', PRIMARY KEY (`id`), UNIQUE KEY `uk_disease_symptom` (`disease_id`, `symptom_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;诊断记录表(t_diag_record):
CREATE TABLE `t_diag_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '诊断用户ID', `animal_type` varchar(20) DEFAULT NULL, `selected_symptoms` varchar(500) NOT NULL COMMENT '选择的症状ID集合,逗号分隔', `result_content` text COMMENT '诊断结果,JSON格式保存', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里重点聊一下t_disease_symptom这张表。它是整个诊断系统的知识库核心。疾病与症状是多对多的关系,一条病可能有十个典型症状,一个症状也可能出现在五种疾病里。如果不建这张中间表,只在t_disease表里放一个symptoms字段存字符串“1,2,3,4”,那你做匹配的时候就只能用FIND_IN_SET和LIKE这种土办法,写出来的SQL又丑又慢,而且扩展性极差。多建一张表,前期看着多写几行代码,后期诊断算法写起来能省一半力气。
3.2 初始化数据怎么准备
数据是这类系统的灵魂。很多学生做完系统,里面有表但没数据,演示效果很差。建议至少为常见动物疾病准备10条以上的数据,例如犬瘟热、细小病毒病、犬传染性肝炎、猫瘟、猫鼻支、犬肠胃炎、皮肤真菌感染等。
每一条疾病数据都要认真填写症状关联。比如“犬瘟热”的典型症状是:发热、咳嗽、眼鼻分泌物增多、呕吐、腹泻、抽搐、食欲下降。其中“抽搐”神经症状特异性很高,权重可以设为3,“发热”是常见症状,权重设为1。这样诊断结果才有区分度。
初始化SQL我一般是写成独立脚本,比如data.sql,和建表脚本分开存放。这样如果调试时数据乱了,重新执行一遍脚本就行,不用手动一条条敲。
4. 诊断算法的实现逻辑
4.1 规则匹配和评分公式
前面说了,我们不用机器学习,用加权评分。这个公式要提前设计好,再写代码实现。
假设用户选中的症状集合为 S,某个疾病 D 对应的典型症状集合为 C(D),那么:
- 匹配数:M = |S ∩ C(D)|,即用户选中且属于该疾病的症状数量
- 待匹配数:N = |C(D)|,即该疾病在知识库中登记的症状总数
- 匹配度 = M / N,表示该疾病的症状被用户覆盖的比例,值越大越像
- 命中数 = M,值越大说明用户症状和该疾病重合越多
排序的时候以“命中数”为主要指标,以“匹配度”为次要指标。为什么两个都要?比如用户选了5个症状,“犬瘟热”命中4个但疾病症状共10条,匹配度只有0.4;“急性肠胃炎”命中3个但疾病症状共4条,匹配度0.75。前者更可能是方向,后者更精准。所以输出的时候两个指标都要展示,排序逻辑建议先比命中数再比匹配度,不然会出现疾病A有20条症状被命中9条却排到有5条症状全部命中的疾病B后面,这不符合直觉。
另外可以引入“特异性”。“抽搐”出现在犬瘟热里,其他病很少见,一旦用户选了它,犬瘟热的排名应该大幅提前。实现方式就是在t_disease_symptom表里加一个weight字段,计算时对命中集合内的症状权重求和。
评分公式调整为:
score = ∑(命中症状的weight) + 匹配度 × 2也就是“加权命中权重和”叠加一个“覆盖率奖励”。代码里把两部分算清楚就可以,这种可解释性强的算法,答辩的时候老师随便问怎么计算,都能答得清清楚楚。
4.2 Java代码实现
诊断逻辑写在Service层,我给出一个核心伪代码示例:
public List<DiagnosisResult> matchDisease(Integer animalType, List<Integer> symptomIds) { // 1. 查询该动物类型下的所有疾病 List<Disease> diseaseList = diseaseMapper.selectByAnimalType(animalType); // 2. 查询用户选中症状对应的症状信息 List<Symptom> selectedSymptoms = symptomMapper.selectBatchIds(symptomIds); // 3. 查询所有疾病-症状关联关系,构建Map结构 Map<Integer, List<DiseaseSymptom>> dsMap = diseaseSymptomMapper.selectAll() .stream() .collect(Collectors.groupingBy(DiseaseSymptom::getDiseaseId)); // 4. 逐疾病计算匹配度 List<DiagnosisResult> results = new ArrayList<>(); for (Disease disease : diseaseList) { List<DiseaseSymptom> diseaseSymptoms = dsMap.getOrDefault(disease.getId(), Collections.emptyList()); // 计算该疾病的症状总数 int totalSymptomCount = diseaseSymptoms.size(); // 计算命中数量及权重和 int hitCount = 0; int score = 0; for (DiseaseSymptom ds : diseaseSymptoms) { if (symptomIds.contains(ds.getSymptomId())) { hitCount++; score += ds.getWeight(); } } if (hitCount == 0) { continue; // 一个症状都没命中,直接丢弃 } // 匹配度:命中数 / 疾病总症状数 double matchRate = (double) hitCount / totalSymptomCount; // 综合评分:加权命中权重和 + 覆盖率奖励 double finalScore = score + matchRate * 2; results.add(new DiagnosisResult(disease, hitCount, matchRate, finalScore)); } // 排序:综合评分降序,命中数降序 results.sort((r1, r2) -> { int cmp = Double.compare(r2.getFinalScore(), r1.getFinalScore()); return cmp != 0 ? cmp : Integer.compare(r2.getHitCount(), r1.getHitCount()); }); // 5. 截取前10个返回 return results.stream().limit(10).collect(Collectors.toList()); }这段逻辑的核心就是把“知识”和“推理”分离。疾病、症状、权重都是数据,推理算法只做遍历和评分。将来想换更高级的算法,比如贝叶斯打分,只需要改这一段,数据表不用动。
4.3 一个推理实例看效果
举个例子。用户选了“犬”类型,症状勾选了“发热”“咳嗽”“眼鼻分泌物增多”“抽搐”四项。
“犬瘟热”在知识库中登记了9个症状:发热、咳嗽、眼鼻分泌物增多、抽搐、呕吐、腹泻、食欲下降、精神沉郁、脚垫增厚。命中4个,权重分别是1、1、2、3,合计7分,匹配度4/9≈0.44。综合评分7+0.88=7.88。
“犬窝咳”登记4个症状:咳嗽、流鼻涕、发热、食欲下降。命中3个(咳嗽、发热),但注意“眼鼻分泌物增多”和“抽搐”不在里面。命中2个,权重1+1=2,匹配度2/4=0.5。综合评分2+1.0=3.0。
很明显犬瘟热排最前面,而且因为“抽搐”这个高特异权重症状,评分拉得很高。这里就是权重设计的价值所在。
// 犬瘟热的症状关联示例 INSERT INTO t_disease_symptom(disease_id, symptom_id, weight) VALUES (1, 1, 1), -- 发热 -> 权重1 (1, 2, 1), -- 咳嗽 -> 权重1 (1, 3, 2), -- 眼鼻分泌物增多 -> 权重2 (1, 4, 3), -- 抽搐 -> 权重3,强特异性 (1, 5, 1), (1, 6, 1), (1, 7, 1), (1, 8, 1), (1, 9, 2);5. 前后端实现与页面设计
5.1 页面结构和路由设计
页面不用搞得太复杂,一个清爽的四层结构就够用了:
- 首页
- 用户登录注册页
- 症状选择与诊断页
- 疾病列表与详情页
- 我的诊断记录页
- 管理后台(疾病管理、症状管理、关联关系管理)
由于用的是Thymeleaf服务端渲染,路由设计就是Controller直接返回视图名称,没有前后端分离的跨域问题。目录结构建议这样:
src/main/java/com/example/animaldiag/ ├── controller/ # Controller层 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis Mapper接口 ├── entity/ # 实体类 └── config/ # 配置类(拦截器、登录校验等) src/main/resources/ ├── mapper/ # MyBatis XML文件 ├── static/ # CSS、JS、图片 └── templates/ # Thymeleaf模板5.2 症状选择页的多选交互
症状选择页是整个系统的门面,用户体验好不好就看这一页。
建议按动物类型先用下拉框分类,比如用户选“犬”,下方展示犬类常见症状列表。每个症状做成一个可点击的卡片样式,点击高亮选中,再次点击取消。卡片上用文字描述症状,不用图片,加载快。
多选状态用原生JavaScript控制即可,不用引入Vue。核心代码很简单:
function toggleSymptom(el, symptomId) { el.classList.toggle('active'); var hiddenInput = document.getElementById('selectedSymptoms'); var ids = hiddenInput.value ? hiddenInput.value.split(',') : []; var index = ids.indexOf(String(symptomId)); if (index >= 0) { ids.splice(index, 1); } else { ids.push(String(symptomId)); } hiddenInput.value = ids.join(','); // 实时更新已选标签数量 document.getElementById('countText').innerText = '已选' + ids.length + '项'; }这里有一个细节坑:表单用隐藏域存症状ID集合时,一定要在提交前处理好字符串格式,去掉首尾逗号。如果用户一个症状都没选就提交,后端要友好提示“请至少选择一项症状”,不要直接报空指针。
5.3 诊断结果页的数据展示
诊断结果页直接展示匹配列表,按综合评分从高到低排列。每一行显示:
- 疾病名称
- 命中症状数,例如“命中 4 / 9”
- 匹配度进度条
- 简要的病因描述
- “查看详情”按钮
匹配度用进度条可视化会很直观,用Bootstrap的progress组件就够了,不需要写图表库。
<div class="progress"> <div class="progress-bar" th:style="'width:' + ${item.matchRatePercent} + '%'"> <span th:text="${item.matchRatePercent} + '%'"></span> </div> </div>疾病详情页再展示完整的治疗建议和预防措施。注意治疗建议的文案一定要加免责声明,提示“如症状严重请及时就医,本系统结果仅供参考”。这个不光是负责任,也是答辩时老师会注意到的细节。
5.4 管理后台的关联配置
管理后台最容易被忽略的是“疾病症状关联管理”。很多同学做成疾病管理、症状管理两块就完事了,结果发现疾病和症状之间建不了关系,那诊断功能就没有数据可用。
正确的做法是做一个独立页面:选择一个疾病,然后显示当前已关联的症状列表,同时列出未关联症状,通过勾选和设置权重来更新关联关系。
@PostMapping("/admin/disease/{diseaseId}/symptoms") public String updateRelation(@PathVariable Integer diseaseId, @RequestParam(required = false) List<Integer> symptomIds, @RequestParam(required = false) List<Integer> weights) { // 先删除该疾病的全部关联,再重新插入,简单粗暴但有效 diseaseSymptomMapper.deleteByDiseaseId(diseaseId); if (symptomIds != null) { for (int i = 0; i < symptomIds.size(); i++) { DiseaseSymptom ds = new DiseaseSymptom(); ds.setDiseaseId(diseaseId); ds.setSymptomId(symptomIds.get(i)); ds.setWeight(weights.get(i)); diseaseSymptomMapper.insert(ds); } } return "redirect:/admin/disease/detail/" + diseaseId; }这种“先删后插”的方式在管理配置类功能里很常用,数据量不大时性能完全没问题,而且逻辑简单不会出现残留数据。
6. 环境搭建与项目落地的关键步骤
6.1 开发环境清单
做这个项目前,先把环境准备齐全,省得写代码时被工具折腾:
- JDK 1.8或更高版本(推荐JDK 8,稳定,资料多)
- Maven 3.6+
- MySQL 5.7/8.0
- IntelliJ IDEA或Eclipse
- Spring Boot 2.x(不要追新到3.x,Spring Boot 3要求JDK17,有些学校电脑没装)
- Navicat或MySQL Workbench做数据库可视化
如果是用Spring Boot 2.x,pom.xml里依赖用spring-boot-starter-web、mybatis-spring-boot-starter、thymeleaf和mysql-connector-java即可。
6.2 项目初始化步骤
第一步,创建Spring Boot项目,可以用IDEA自带的Spring Initializr,也可以去网站生成。注意Group和Artifact名称取简单的,比如com.example.animaldiag。
第二步,配置文件application.yml:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/animal_diag?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.animaldiag.entity第三步,编写数据库脚本并用Navicat执行。第四步,写实体类、Mapper接口、XML文件、Service、Controller,一步步来。
6.3 登录校验和会话管理
系统既然区分普通用户和管理员,那登录校验不能只靠前端隐藏按钮。后端必须做统一拦截。
用拦截器方案:
@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login?redirect=" + request.getRequestURI()); return false; } return true; } }然后在配置类里注册拦截规则:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/loginSubmit", "/register", "/registerSubmit", "/css/**", "/js/**", "/images/**", "/disease/list", "/disease/detail/**"); } }管理员接口单独校验user.getRole()==2,用另一个拦截器或直接在Controller里判断。不要在每一个Controller方法里手写判断,代码会重复到让你想哭,而且一旦漏判就会被绕过。
6.4 密码加密不能偷懒
用户表里的密码不能存明文。至少要加一层MD5,有条件就加盐。虽然这个系统没有别的高价值数据,但毕业设计答辩时老师看到明文密码,印象分会大打折扣。用Spring自带的DigestUtils.md5DigestAsHex就够:
String encodePwd = DigestUtils.md5DigestAsHex(password.getBytes(StandardCharsets.UTF_8));登录时同样把用户输入的密码做MD5再比对。如果觉得MD5不安全,换成BCrypt也行,spring-security-crypto包里自带BCryptPasswordEncoder,使用时注意每次加密结果不同,比对要用matches()方法而不是直接equals。
7. 测试场景设计与常见问题排查
7.1 测试用例怎么设计
功能做完要跑一遍完整的测试流程。至少覆盖这几个场景:
- 新用户注册,用户名重复时要有错误提示
- 普通用户登录,看到用户端菜单,不能进入管理后台
- 管理员登录,能正常管理疾病和症状关联
- 不选任何症状直接提交,系统友好报错
- 选择某一个疾病的所有典型症状,该疾病排名第一
- 删除一个疾病后,关联关系表中的数据级联删除,诊断结果中不再出现该病
- 连续两次诊断,历史记录能正确显示
这些用例不用写成自动化测试,手工过一遍就行。但每一条都要记录结果,发现问题立刻修。
7.2 高频报错速查表
以下是我实际开发中统计到的高频问题,每个都踩过坑,直接列给大家保平安。
| 现象 | 原因 | 解决办法 |
|---|---|---|
| Tomcat启动失败,报端口被占用 | 8080端口被其他进程占用 | 改配置端口为8081,或用netstat -ano查占用进程结束掉 |
| 连接MySQL报Communications link failure | 数据库服务没启动,或者URL时区参数缺失 | 确认MySQL服务启动,URL加serverTimezone=Asia/Shanghai |
| 中文乱码,存进去是问号 | 连接URL没有characterEncoding,或库不是utf8mb4 | URL加characterEncoding=utf8,重建库用utf8mb4 |
| 页面加载不出CSS样式 | 静态资源被拦截器挡掉,或Thymeleaf路径写错 | 拦截器放行/css/**/js/**,模板里用th:href="@{/css/style.css}" |
MyBatis报Invalid bound statement | Mapper接口和XML文件的namespace不对,或XML没编译到classes目录 | 检查namespace和接口全限定名一致,pom里<resources>把mapper目录包含进去 |
| 宝塔/Navicat导入SQL数据时报错 | SQL语句中用了utf8mb4_0900_ai_ci排序规则,MySQL5.7不支持 | 把排序规则改成utf8mb4_general_ci |
| Session里取不到用户 | 登录时没有存Session,或跳转是重定向导致Session变化 | 确认request.getSession().setAttribute存在,拦截器里再set一次 |
| 诊断结果为空 | 疾病数据没有关联症状记录,或者动物类型没对上 | 查t_disease_symptom表,确认关联数据里disease_id对应正确 |
7.3 诊断结果不准怎么调
系统交付后,如果发现某些病完全识别不出来,八成不是代码问题,而是知识库数据不完善。
比如“猫鼻支”,如果只在关联表里登记了“打喷嚏”“流泪”,而用户选的是“发热”“食欲下降”,那就匹配不上。这时候需要回数据脚本,把疾病的典型症状补全。建议每种疾病至少关联6个症状,覆盖常见表现,不要只关联两三条。
权重上也要有意识的区分。共性的症状(发热、食欲下降)权重给1,高鉴别度的症状(抽搐、皮肤溃烂、眼部分泌物)权重给2~3,这样诊断结果的区分度自然就上来了。
8. 项目扩展方向和实操心得
8.1 可以继续加的功能
如果这个项目的时间充足,或者你想让它在简历上更好看,可以从这几个方向扩展。
第一,增加图片上传功能。疾病详情页允许管理员上传病理图片,用户诊断后看到的不只是文字,还能有视觉参考。这个用Spring Boot的MultipartFile就能实现,保存到本地目录再映射成静态资源即可。
第二,诊断记录导出为PDF。用户历史记录支持导出检查报告,方便带去宠物医院给医生看。用iText或POI可以做到,但注意字体问题,中文渲染要引入中文字体文件。
第三,增加搜索关键词联想。在症状选择框里支持输入文字模糊搜索,用前端JS实现本地过滤就行,不需要Elasticsearch。大数据量才有必要上检索框架,这个规模的数据用索引内存匹配就够了。
第四,接入疫苗提醒功能。用户给自己宠物建档后,记录疫苗接种日期,系统提前推送提醒。这个就涉及定时任务和通知,可以学习Quartz或Spring自带的@Scheduled。
8.2 实际开发中容易踩的隐形坑
有些坑不是一次踩完就记住的,我吃了亏之后才总结出几条经验,这里分享给大家。
Thymeleaf模板中如果某个字段为null,渲染会直接报错。比如疾病库里有条数据的prevention字段是空的,你写th:text="${disease.prevention}",页面直接500。解决办法是在实体类给默认空字符串,或者用th:text="${disease.prevention ?: '暂无'}"。
MyBatis的XML中写SQL时,<和>要用转义,否则XML解析直接报错。SQL里凡是出现小于号的地方,都要写成<。这个报错信息看着像SQL语法错,其实是XML没解析,排查思路完全不同。
管理员“先删后插”更新关联时,如果前端忘记传权重,后端可能存0,诊断逻辑里权重为0相当于没加分。所以前端表单里权重默认选1,后端也要判空处理。
8.3 答辩和演示时的加分建议
如果这个项目是用来答辩或者项目评审的,我有几个亲测有效的演示技巧。
准备一份初始化数据脚本,里面包含10种以上疾病和40条以上症状,演示时现场执行导入,展示数据真实感。不要只在本地库看得到数据,换台电脑就崩了。
演示诊断功能时,选一个知识库里记录比较完整的疾病,故意勾选它的3个高权重典型症状,让结果第一就是这个病。这样老师一看就能理解“诊断”不是瞎猜,是基于权重规则匹配的。
讲解的时候重点说清楚两张图:一张是功能结构图,一张是诊断算法流程图。用文字把“选症状→查关联→算权重→排序→展示”这条链路讲透,比堆代码强得多。老师最怕听到“这个模块就是增删改查”,最想听到的是“这里的核心难点在于关联关系的引入和权重评分的量化设计”。
做这个项目给我最大的体会是:看似是管理系统,但真正拉开差距的地方是知识库的建模和推理逻辑的设计。很多同学把大量时间花在堆页面上,结果诊断功能只是一个简单的数据库模糊查询,失去了“诊断系统”的灵魂。先把疾病-症状-权重这套数据模型想清楚,系统的品质自然就有保障了。