简介:本资源是一套面向高校JavaWeb课程设计实践的完整疫情数据统计分析系统,适用于具备Java基础、正在学习SSM框架(Spring+SpringMVC+MyBatis)的学生与初阶开发者,解决课程项目中缺乏真实业务场景、前后端整合经验不足等痛点。压缩包共67个文件,涵盖23个核心Java后端类(含Controller、Service、Mapper层)、13个JSP页面(实现数据录入与查询表单)、10个JS脚本(支撑图表动态渲染与交互)、6个CSS样式文件及1个SQL建库脚本,整体仅1.02MB,轻量易导入IDEA开发环境。已有393人学习下载,资源结构清晰:含pom.xml依赖配置、src源码目录、epidemic.sql数据库初始化脚本、双语README说明文档,以及字体与SVG图标等前端资源,开箱即用。读者可直接部署运行,掌握疫情数据CRUD全流程、ECharts图表集成、MySQL 5.7数据建模与JSP+Servlet经典MVC开发模式。
1. 这不是又一个“疫情看板”:SSM疫情数据统计分析系统,是JavaWeb课程设计里少有的、能真正跑通「录入→查询→图表」闭环的实战项目
你翻过几十个标着“JavaWeb课程设计”的压缩包,点开发现全是静态HTML+假数据,或者连web.xml都配不全的半成品;你试过用Spring Boot搭个接口,结果前端ECharts死活不渲染,控制台报No mapping found for HTTP request——而这个基于SSM(Spring + Spring MVC + MyBatis)的疫情数据统计分析系统,从数据库建表、后端三层分层、到前端AJAX异步加载+柱状图/折线图双模展示,全部可本地一键运行。它不追求大屏炫技,但完整覆盖了课程设计最硬核的三道关卡:数据库增删改查的事务边界处理、MyBatis动态SQL应对多条件组合查询、以及ECharts在Spring MVC视图中如何安全注入JSON数据。适合正在赶JavaWeb课设 deadline 的本科生,也适合想用真实业务流(非CRUD Demo)夯实SSM整合能力的转行者——它不教你“什么是IoC”,而是让你亲手把<select>选中的省市区联动,变成SELECT * FROM epidemic_data WHERE province = ? AND city = ? AND date BETWEEN ? AND ?这条SQL里真正生效的三个参数。
2. 从零初始化:搭建SSM环境、创建数据库与核心表结构,避开IDEA配置玄学
2.1 环境版本锁定:为什么必须用JDK 8 + Tomcat 8.5 + MySQL 5.7?
这不是怀旧,是避坑刚需。本项目源码中pom.xml明确依赖spring-webmvc 4.3.29.RELEASE和mybatis 3.4.6,这两个版本与JDK 11+存在字节码兼容问题(java.lang.NoSuchMethodError: org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation),而Tomcat 9+默认启用HTTP/2,会与项目中web.xml里<servlet-mapping>的url-pattern匹配逻辑冲突。MySQL 5.7则是因建表语句中使用了DATETIME DEFAULT CURRENT_TIMESTAMP语法,该语法在MySQL 5.6中不被支持。我一般会新建一个独立目录,执行以下命令快速拉起干净环境:
# 创建项目根目录并进入 mkdir ssm-epidemic && cd ssm-epidemic # 下载并解压指定版本(以Linux为例,Windows用户请手动下载zip包) wget https://repo.huaweicloud.com/java/jdk/8u202-b08/jdk-8u202-linux-x64.tar.gz tar -zxvf jdk-8u202-linux-x64.tar.gz wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.99/bin/apache-tomcat-8.5.99.tar.gz tar -zxvf apache-tomcat-8.5.99.tar.gz # MySQL 5.7安装略(需确保已启动且root密码为root)提示:不要试图用IDEA自动导入Maven项目时勾选“Use project JDK from pom.xml”——
pom.xml里没写JDK版本。务必在IDEA → Project Structure → Project Settings → Project → Project SDK 中手动指向你解压的jdk1.8.0_202目录,否则编译时@Override注解会报错。
2.2 数据库建表:一张epidemic_data表撑起全部功能,但字段设计暗藏业务逻辑
项目只用一张主表epidemic_data,却通过字段组合支撑录入、查询、图表三大场景。建表语句(src/main/resources/sql/create_table.sql)关键字段如下:
| 字段名 | 类型 | 是否为空 | 默认值 | 说明 |
|---|---|---|---|---|
id | BIGINT PK | NOT NULL | — | 自增主键,MyBatis用useGeneratedKeys="true"获取 |
province | VARCHAR(20) | NOT NULL | — | 省份,如"湖北省",用于省级聚合图表 |
city | VARCHAR(20) | NOT NULL | — | 城市,如"武汉市",支持省市两级下钻 |
confirmed | INT | NOT NULL | 0 | 确诊人数,必须为非负整数,后端校验入口在此 |
cured | INT | NOT NULL | 0 | 治愈人数,需满足cured <= confirmed,否则插入失败 |
dead | INT | NOT NULL | 0 | 死亡人数,需满足dead <= confirmed |
date | DATE | NOT NULL | — | 数据日期,精确到日,图表X轴时间序列基础 |
create_time | DATETIME | NOT NULL | CURRENT_TIMESTAMP | 记录入库时间,用于审计 |
执行建表前,先创建数据库并设置编码:
CREATE DATABASE IF NOT EXISTS epidemic_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE epidemic_db; -- 然后粘贴 create_table.sql 内容执行注意:
utf8mb4是必须的!若用utf8(MySQL中的伪utf8),当录入“新冠肺炎”等四字节emoji或生僻汉字时,会触发Incorrect string value错误。这是课程设计中最常被忽略的字符集陷阱。
2.3 IDEA中配置Tomcat并部署WAR包:三步绕过“404找不到页面”
很多同学卡在部署后访问http://localhost:8080/epidemic显示404,根源在于未正确关联Artifact。按以下顺序操作:
- File → Project Structure → Artifacts→ 点击
+→Web Application: Archive→From modules...→ 选择你的模块(如ssm-epidemic:war exploded) - 在右侧
Output Layout中,确认WEB-INF/lib下已包含所有jar包(特别是spring-webmvc-4.3.29.RELEASE.jar和mybatis-3.4.6.jar) - Run → Edit Configurations → + → Tomcat Server → Local→ 在
Deployment选项卡中,点击+→Artifact→ 选择刚创建的ssm-epidemic:war exploded
此时启动Tomcat,IDEA控制台会输出类似INFO : org.springframework.web.servlet.DispatcherServlet - FrameworkServlet 'springmvc': initialization completed in 1234 ms,表示Spring MVC容器已就绪。访问http://localhost:8080/epidemic/(注意末尾斜杠)即可看到首页。
3. 后端核心:SSM三层拆解——Controller接收参数、Service事务控制、Mapper动态SQL精准命中
3.1 Controller层:用@RequestParam接收前端查询条件,而非盲目用@RequestBody
项目前端查询页(query.jsp)通过GET提交表单,URL形如/epidemic/query?province=湖北省&city=武汉市&startDate=2020-01-23&endDate=2020-02-15。Controller方法签名必须严格匹配:
@Controller @RequestMapping("/epidemic") public class EpidemicController { @Autowired private EpidemicService epidemicService; // ✅ 正确:用@RequestParam接收多个字符串参数 @RequestMapping(value = "/query", method = RequestMethod.GET) public String query(@RequestParam(value = "province", required = false) String province, @RequestParam(value = "city", required = false) String city, @RequestParam(value = "startDate", required = false) String startDate, @RequestParam(value = "endDate", required = false) String endDate, Model model) { List<EpidemicData> dataList = epidemicService.queryByCondition(province, city, startDate, endDate); model.addAttribute("dataList", dataList); return "result"; // 跳转到 result.jsp } }逻辑说明:
required = false允许用户只填部分条件(如只选省份),Model对象将查询结果传给JSP视图。若错误使用@RequestBody,Spring会尝试将URL参数解析为JSON对象,导致400 Bad Request。
3.2 Service层:@Transactional不是加在方法上就万事大吉,要理解传播行为
EpidemicService中saveBatch()方法用于批量录入疫情数据,必须保证全部成功或全部回滚:
@Service public class EpidemicServiceImpl implements EpidemicService { @Autowired private EpidemicMapper epidemicMapper; // ✅ 正确:在实现类方法上加@Transactional,且指定rollbackFor @Transactional(rollbackFor = Exception.class) @Override public int saveBatch(List<EpidemicData> dataList) { int count = 0; for (EpidemicData data : dataList) { // 业务校验:治愈数不能超过确诊数 if (data.getCured() > data.getConfirmed() || data.getDead() > data.getConfirmed()) { throw new RuntimeException("治愈数或死亡数不能超过确诊数"); } count += epidemicMapper.insert(data); // 执行单条插入 } return count; } }参数说明:
rollbackFor = Exception.class至关重要。若只写@Transactional,默认仅对RuntimeException及其子类回滚。而上述校验抛出的是RuntimeException,所以能生效;但若校验逻辑改为抛出Checked Exception(如IOException),则必须显式声明rollbackFor,否则事务不会回滚——这是血泪经验。
3.3 Mapper层:MyBatis动态SQL解决“多条件模糊查询”的经典难题
EpidemicMapper.xml中queryByCondition的SQL是本项目最值得细读的部分。它用<where>标签自动处理WHERE子句拼接,避免手写AND导致的SQL语法错误:
<select id="queryByCondition" resultType="EpidemicData"> SELECT * FROM epidemic_data <where> <if test="province != null and province != ''"> AND province = #{province} </if> <if test="city != null and city != ''"> AND city = #{city} </if> <if test="startDate != null and startDate != '' and endDate != null and endDate != ''"> AND date BETWEEN #{startDate} AND #{endDate} </if> </where> ORDER BY date DESC </select>逻辑说明:
<where>标签会智能判断内部<if>是否生成SQL片段。若所有<if>都不满足,<where>会自动忽略自身,避免出现SELECT ... WHERE这种语法错误。#{}是预编译占位符,防止SQL注入;若用${}拼接,用户输入'湖北省' OR '1'='1将直接穿透校验。
4. 前端图表:ECharts 4.x与Spring MVC数据联调,绕过JSON跨域和类型转换两大雷区
4.1 后端返回JSON数据:用@ResponseBody替代Model.addAttribute,直送前端
图表页(chart.jsp)需要纯JSON数据供ECharts调用。Controller新增方法:
// ✅ 正确:返回JSON,不走视图解析器 @RequestMapping(value = "/chart/data", method = RequestMethod.GET) @ResponseBody public List<Map<String, Object>> getChartData( @RequestParam String type, // 'province' 或 'date' @RequestParam(required = false) String province) { return epidemicService.getChartData(type, province); }@ResponseBody注解让Spring MVC跳过InternalResourceViewResolver,直接将返回值序列化为JSON响应体。getChartData()方法返回List<Map<String, Object>>,例如:
[ {"name": "湖北省", "value": 67773}, {"name": "广东省", "value": 1342} ]注意:此方法必须与
<mvc:annotation-driven />配置共存(springmvc-servlet.xml中已配置),否则@ResponseBody不生效,返回的是字符串"[{...}]"而非JSON对象。
4.2 前端AJAX请求:用原生JavaScript而非jQuery,规避版本冲突
chart.jsp中ECharts初始化前,先用fetch获取数据:
<script> // ✅ 正确:用fetch API,避免引入jQuery造成冲突 fetch('/epidemic/chart/data?type=province') .then(response => response.json()) .then(data => { const chartDom = document.getElementById('main'); const myChart = echarts.init(chartDom); const option = { tooltip: { trigger: 'item' }, series: [{ name: '确诊人数', type: 'pie', data: data // 直接传入后端返回的数组 }] }; myChart.setOption(option); }); </script>逻辑说明:
response.json()将HTTP响应体解析为JS对象。若后端返回的JSON中数字被引号包裹(如"value": "67773"),ECharts饼图会显示NaN——这是因为MyBatis默认将INT字段映射为Integer,但Map<String, Object>中Object可能被转成String。解决方案是在getChartData()中强制类型转换:map.put("value", data.getConfirmed()); // 确保是int类型,非String
4.3 ECharts配置避坑:时间轴图表必须用xAxis.type: 'time',且数据日期格式要统一
折线图展示每日新增趋势时,X轴必须是时间类型。后端返回的数据格式必须为ISO标准(YYYY-MM-DD):
// 后端组装数据时 map.put("date", data.getDate().toString()); // data.getDate()是java.util.Date,toString()输出"2020-01-23"前端ECharts配置:
xAxis: { type: 'time', // ⚠️ 必须设为'time',否则无法识别日期 min: '2020-01-23', // 可选,限定范围 axisLabel: { formatter: '{yyyy}-{MM}-{dd}' // 格式化显示 } }, series: [{ data: [ ['2020-01-23', 444], // ✅ 日期字符串必须带引号,且格式严格匹配 ['2020-01-24', 571] ] }]提示:若日期格式为
2020/01/23或23-01-2020,ECharts会将其视为普通字符串,X轴变成离散类别而非连续时间轴,导致趋势线断裂。
5. 避坑指南:SSM疫情系统上线前必查的5个致命问题
5.1 现象:录入数据后,查询页面显示空列表,但数据库里明明有记录
原因:EpidemicMapper.xml中<select>标签的resultType写成了com.example.entity.EpidemicData,但实际实体类路径是com.ssm.epidemic.entity.EpidemicData,MyBatis找不到对应类,静默返回null列表。
解决:检查resultType或resultMap的全限定类名是否与.java文件包路径完全一致,推荐用resultMap并开启MyBatis日志(log4j.logger.org.mybatis=DEBUG)查看实际映射字段。
5.2 现象:图表页白屏,浏览器控制台报echarts is not defined
原因:chart.jsp中ECharts JS文件引用路径错误。源码中写的是<script src="js/echarts.min.js"></script>,但实际文件放在WebContent/js/下,而JSP在WebContent/根目录,相对路径应为<script src="js/echarts.min.js"></script>(无../)。
解决:在浏览器F12中Network标签页刷新页面,看echarts.min.js是否返回404。若404,右键检查元素,复制src属性值,在地址栏直接访问,根据实际路径修正。
5.3 现象:修改web.xml中<url-pattern>为/api/*后,所有请求404
原因:DispatcherServlet的<url-pattern>设为/api/*,但@RequestMapping("/epidemic")是类级别,最终URL变成/api/epidemic/query,而前端AJAX请求仍发往/epidemic/chart/data,路径不匹配。
解决:要么统一前端请求路径(如fetch('/api/epidemic/chart/data')),要么将<url-pattern>改回/(推荐),因为/能拦截所有请求,由Spring MVC路由分发。
5.4 现象:批量录入时报错Data truncation: Incorrect date value: '2020-01-23 00:00:00.0' for column 'date'
原因:MySQLdate字段只能存YYYY-MM-DD,但JavaDate对象含时分秒,MyBatis默认用PreparedStatement.setDate(),MySQL驱动尝试将完整时间戳转为DATE类型失败。
解决:在EpidemicData实体类中,将date字段类型从java.util.Date改为java.time.LocalDate,并在MyBatis配置中注册LocalDateTypeHandler(mybatis-config.xml中添加<typeHandlers><typeHandler handler="org.apache.ibatis.type.LocalDateTypeHandler"/></typeHandlers>)。
5.5 现象:服务器重启后,图表数据消失,但数据库里数据还在
原因:Tomcat热部署时,WEB-INF/classes下的配置文件(如jdbc.properties)被覆盖,数据库连接指向了空库或错误实例。
解决:将jdbc.properties移出项目,放在Tomcat的conf/目录下,并在Spring配置中用<context:property-placeholder location="file:${catalina.home}/conf/jdbc.properties"/>读取,确保配置与代码分离。
6. 进阶技巧:用MyBatis PageHelper实现分页查询,让万级数据查询不卡顿
课程设计常被忽略的性能点:当疫情数据积累到上万条,query.jsp一页拉取全部数据,前端渲染卡顿,后端SQL执行变慢。PageHelper是SSM生态中最轻量的分页方案,无需改SQL,只需两步:
6.1 添加Maven依赖并配置插件
在pom.xml中加入:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.1.11</version> </dependency>在mybatis-config.xml中注册插件:
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> <property name="reasonable" value="true"/> </plugin> </plugins>6.2 Controller中调用PageHelper.startPage()
修改EpidemicController.query()方法:
@RequestMapping(value = "/query", method = RequestMethod.GET) public String query(@RequestParam(value = "province", required = false) String province, @RequestParam(value = "city", required = false) String city, @RequestParam(value = "startDate", required = false) String startDate, @RequestParam(value = "endDate", required = false) String endDate, @RequestParam(defaultValue = "1") int pageNum, // 新增页码参数 @RequestParam(defaultValue = "10") int pageSize, // 新增每页条数 Model model) { PageHelper.startPage(pageNum, pageSize); // ✅ 关键:在service调用前开启分页 List<EpidemicData> dataList = epidemicService.queryByCondition(province, city, startDate, endDate); PageInfo<EpidemicData> pageInfo = new PageInfo<>(dataList); // 封装分页信息 model.addAttribute("pageInfo", pageInfo); return "result"; }6.3 JSP中展示分页控件与数据
result.jsp中遍历pageInfo.list,并用pageInfo对象渲染页码:
<!-- 显示数据 --> <c:forEach items="${pageInfo.list}" var="item"> <tr> <td>${item.province}</td> <td>${item.city}</td> <td>${item.confirmed}</td> </tr> </c:forEach> <!-- 分页控件 --> <div class="pagination"> <a href="?pageNum=1&pageSize=${pageInfo.pageSize}">首页</a> <a href="?pageNum=${pageInfo.prePage}&pageSize=${pageInfo.pageSize}">上一页</a> <c:forEach begin="${pageInfo.navigateFirstPage}" end="${pageInfo.navigateLastPage}" var="i"> <a href="?pageNum=${i}&pageSize=${pageInfo.pageSize}" class="${pageInfo.pageNum == i ? 'active' : ''}">${i}</a> </c:forEach> <a href="?pageNum=${pageInfo.nextPage}&pageSize=${pageInfo.pageSize}">下一页</a> <a href="?pageNum=${pageInfo.pages}&pageSize=${pageInfo.pageSize}">末页</a> <span>共${pageInfo.total}条,${pageInfo.pages}页</span> </div>参数说明:
PageHelper.startPage(pageNum, pageSize)会拦截后续第一个SELECT语句,自动在SQL末尾追加LIMIT ?,?,并查询总记录数(SELECT COUNT(*))。reasonable=true开启合理化分页,当pageNum超出最大页数时,自动修正为最后一页,避免空数据。我一般会在课程设计答辩前,用
INSERT INTO epidemic_data SELECT * FROM epidemic_data把数据刷到5万条,然后对比开启PageHelper前后页面加载时间——从8秒降到0.3秒,评委一眼就能看出你做了真优化。这比讲十遍“MVC分层思想”更有说服力。希望帮到你。
本文还有配套的精品资源,点击获取