简介:针对JavaWeb方向学习与毕业设计人群,这份运动与健康管理系统完整源代码与数据库资源,可用于快速掌握用户管理、运动记录、健康监测、数据分析等典型模块的开发流程,也可作为课程设计的起始模板。压缩包共707个文件,约9.59MB,整体以jsp页面与java类承载后端业务逻辑,js与css负责前端交互与样式,gif、jpg提供界面演示素材,sql为数据库初始化脚本,jar为依赖库,另有html、xml等辅助配置,目录结构清晰,便于按模块对照学习。已有2019人学习查看,适用于需要参考完整项目实现、理解表结构设计与Servlet/JSP开发思路的读者。通过源码可看到用户表、运动记录表、健康指标表的关联方式,同时附带的项目配置与静态资源,能帮助还原运行环境,是JavaWeb综合项目实践的一份实用参考资料。
1. JavaWeb 运动与健康管理系统:为什么值得下载并且跑通它
如果你搜过「javawe 运动与健康管理系统 源代码」——没错,后缀少个 b 是常见拼写——大概率看到一大排压缩包链接。我拆过好几套之后有个反直觉的结论:这类系统真正值钱的不是「能登录、能打卡」的 CRUD 壳子,而是数据库表结构和后端分层写法能不能经得起你往里面加需求。这套基于 JSP + Servlet + MySQL 的经典 JavaWeb 架构,几乎覆盖了课设和毕业设计里所有要求:用户端运动打卡、个人健康档案、管理员后台、数据统计预览,外加完整数据库脚本。适合正在做 JavaWeb 课设的学生、想练手 Servlet 全流程的开发者,也想用现成源码二次改造成内部健康管理小工具的人。下面我按拆包顺序讲:先立住数据模型,再讲功能实现,然后把部署时最容易翻车的三个坎铺开说。
2. 从 ER 图到建表脚本:运动打卡数据怎么存才不返工
2.1 四张核心表:用户、运动记录、健康指标、管理员
拆开数据库脚本(一般是一个.sql文件,项目解压后通常放在db/或sql/目录),第一件事不是双击导入,而是先打开看建表结构。这套系统的核心表有四个:用户表、运动记录表、健康指标表、管理员表。健康指标表是很多人做设计时候遗漏的一张表——如果没有它,「个人健康档案」页面里的体重、心率、睡眠时长根本没有独立的数据载体,最后会塞进用户表变成一堆冗余字段。
四张表的核心字段如下:
| 表名 | 核心字段 | 关键设计 |
|---|---|---|
t_user | id、username、password、age、gender、height、weight、create_time | 密码建议 MD5 处理;身高体重冗余存储,方便首页直接读 |
t_exercise_record | id、user_id、exercise_type、duration_minutes、calories、record_date、create_time | 日期用 DATE 类型;热量允许小数 |
t_health_metric | id、user_id、weight、heart_rate、sleep_hours、metric_date、create_time | 同一天可有多次记录,但预警统计时取最新一条 |
t_admin | id、username、password、real_name、role | 与业务表完全解耦,登录入口独立 |
为什么运动记录表要单独拆出来而不是合并进用户表?因为「运动打卡」在业务上天然是一对多的:一个用户一天可以打卡多次,一次跑步一次拉伸是两条记录。如果把这些塞进用户表,后面写统计 SQL 时每查一次运动时长都要做字符串切割,那才是真正的返工地狱。健康指标表同理,体重和心率是会变化的量,必须按日期累积,不然趋势图没数据可画。
2.2 建表脚本:主键、时间戳、唯一索引缺一不可
不要直接在 Navicat 里手动画表,把 SQL 文件拖进命令行或客户端执行一遍更稳妥。这套系统的核心建表语句大概是这样的结构:
CREATE TABLE t_exercise_record ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT '主键', user_id INT NOT NULL COMMENT '关联 t_user.id', exercise_type VARCHAR(32) NOT NULL COMMENT '运动类型:跑步/游泳/骑行/拉伸', duration_minutes INT NOT NULL COMMENT '运动时长,单位分钟', calories DECIMAL(8,2) DEFAULT 0 COMMENT '预估消耗热量,单位千卡', record_date DATE NOT NULL COMMENT '打卡日期', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间', UNIQUE KEY uk_user_date (user_id, record_date, exercise_type) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='运动打卡记录表';这段脚本里值得留意三个细节。第一,AUTO_INCREMENT主键是常规选择,删除记录后主键不会复用,统计口径不会被过往数据污染。第二,UNIQUE KEY uk_user_date是我拆这套系统时最想夸的地方——它防止了「同一天同一种运动重复打卡」,这个约束放在应用层做要多写一堆查询,放在数据库层一行就堵死了。第三,DECIMAL(8,2)而不是FLOAT,热量计算要的是精确两位小数,浮点数在累计求和时会抖出 0.0000001 的误差,虽然肉眼看不出来,但报表对不上时会让人怀疑人生。
2.3 数据类型选型:为什么热量和体重都用 DECIMAL
建表脚本里另一个值得说的是类型选型逻辑。duration_minutes用 INT,因为运动时长是整数分钟;calories用 DECIMAL(8,2),因为热量计算涉及运动系数,结果可能是小数;gmt_create这类时间字段统一 DATETIME,不要用 TIMESTAMP,因为 TIMESTAMP 的范围只到 2038 年,而且受时区影响,DATETIME 更省心。
这里有个真实发生过的教训:某开发者在二次开发时把体重字段改成了DOUBLE,理由是「JSP 页面上显示体重,后端接个字符串就转了,无所谓」。后面做月度体重曲线图时发现AVG(weight)出来一串 60.00000000001,前端图表点尾数全是 1。从那以后我对建表脚本的原则就是:凡是展示给用户看、参与计算或者聚合的数值,一律 DECIMAL,FLOAT/DOUBLE只允许出现在科学计算这种不追求精确的场景。
3. 用户端四个功能模块:登录、打卡、报表、个人中心的后端实现
3.1 从 JSP 到 Servlet 的请求流转:一次打卡请求怎么走完整条链路
这套系统是典型的 MVC 结构:JSP 负责渲染,Servlet 负责接请求调业务,DAO 负责和数据库打交道。很多新手把 JSP 直接写上 Java 代码,页面里塞一堆<%,那不是不能跑,但后面改需求时会变得没法看。这套系统的分层拆得比较干净——虽然代码不是最新式框架,但「JSP 只负责收参数、Servlet 只负责转方向、DAO 只负责数据库」这条线是清楚的。
登录请求的流转大概是这样的:用户在login.jsp输入用户名密码,表单method="post"提交到LoginServlet。Servlet 调用UserDao.findByUsernameAndPassword(),查到了就把user对象放进 Session,跳转index.jsp;查不到就request.setAttribute("error", "用户名或密码错误"),转发回登录页。核心代码是这样:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/index.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }这里有个关键参数容易被忽略:request.setCharacterEncoding("UTF-8")必须放在getParameter()调用之前。如果这一行漏了或放后面,中文用户名和密码会乱码,查库直接失败,下次登录会显示「用户名或密码错误」,而实际上密码是对的——这个坑后面避坑章还会详细说。getParameter拿到的全是字符串,DAO 层做密码校验后返回User对象或null,这只是项目里最常见的写法,不是唯一写法。
3.2 打卡核心:DAO 层插入前先做「重复判断」
运动打卡是用户端最核心的操作。前端表单收集运动类型、时长、日期,提交到AddExerciseServlet,Servlet 调ExerciseRecordDao.insert(). 真正写代码时要处理的业务逻辑是:同一天同一种运动不能重复打卡。数据库那层UNIQUE KEY是兜底,但在应用层最好也先查一次,因为数据库报DuplicateKeyException时异常不够友好,页面直接白屏。
public boolean insert(ExerciseRecord record) { String checkSql = "SELECT id FROM t_exercise_record WHERE user_id = ? AND record_date = ? AND exercise_type = ?"; String insertSql = "INSERT INTO t_exercise_record (user_id, exercise_type, duration_minutes, calories, record_date) VALUES (?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement checkPs = conn.prepareStatement(checkSql); PreparedStatement insertPs = conn.prepareStatement(insertSql)) { checkPs.setInt(1, record.getUserId()); checkPs.setDate(2, new java.sql.Date(record.getRecordDate().getTime())); checkPs.setString(3, record.getExerciseType()); ResultSet rs = checkPs.executeQuery(); if (rs.next()) { return false; // 当天同类型已存在,返回 false 由 Servlet 提示用户 } insertPs.setInt(1, record.getUserId()); insertPs.setString(2, record.getExerciseType()); insertPs.setInt(3, record.getDurationMinutes()); insertPs.setBigDecimal(4, record.getCalories()); insertPs.setDate(5, new java.sql.Date(record.getRecordDate().getTime())); return insertPs.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }这段代码用了三重嵌套的try-with-resources,连接和语句都会自动关闭,这在 JDK 7 之后的支持很好,课设代码里常见老式finally手动关连接,但那样一旦中间某个语句异常,连接就泄漏了。PreparedStatement用占位符?传参,避免拼接 SQL 导致注入——这是这类项目必须做扎实的点,因为运动健康系统涉及用户隐私数据,登录接口如果用拼接字符串,被注入一次就全表沦陷。
3.3 个人中心:把体重、心率、睡眠聚合成一张数据卡
个人中心页面展示用户最近一次体重、心率,以及近 7 天运动汇总。页面组装的 SQL 逻辑比 Java 循环聚合方便得多。近 7 天汇总的典型写法是在 SQL 层按日期分组:
SELECT record_date, SUM(duration_minutes) AS total_minutes, SUM(calories) AS total_calories FROM t_exercise_record WHERE user_id = ? AND record_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY record_date ORDER BY record_date ASC;这段 SQL 里DATE_SUB(CURDATE(), INTERVAL 7 DAY)是计算「7 天前」的日期,CURDATE()只取当前日期不取时间,避免当天凌晨打卡被日期边界问题干扰。用GROUP BY record_date按天聚合,返回的结果直接就是前端图表想要的数据结构,Java 层只需要while(rs.next())循环塞进 List,不需要在内存里做二次求和。
这里不要用SELECT *再在 Java 里算总和——数据库做聚合比 Java 循环快得多,而且代码量少一半。这套系统里所有涉及「按时间统计」的地方基本都是这个套路,理解了这一条,后面管理后台的统计图表你基本也能看懂。
4. 管理后台与统计视图:让运动数据变成健康画像
4.1 用户管理:不靠「删除」,靠「状态位」
管理后台的用户管理模块,常见的课设实现是「点删除直接 DELETE FROM t_user」,这套系统用的是软删除:t_user表里有一个status字段,0正常、1禁用。管理员点击「禁用」时执行的 SQL 不是删除,而是更新状态位:
UPDATE t_user SET status = 1 WHERE id = ?为什么这样做?因为用户的运动记录表t_exercise_record.user_id关联了用户表主键,如果直接物理删除用户,记录表里的历史数据就成了孤行——统计报表会出现「查不到用户名只有一串数字」的脏数据。软删除之后,登录校验会自动加一个status = 0的条件,被禁用的用户虽然还在表里,但登录不进去,历史数据仍然可以被后台统计。
这里有两点补充。第一,删运动记录和删用户是两回事:如果只是为了腾空间,删的是记录表数据;如果是封号,动的是用户表状态位。第二,如果你在二次开发时要同时干净清除某个用户的所有数据,那要先把t_exercise_record、t_health_metric里的关联记录删掉,最后再删t_user,顺序反过来外键会报错。
4.2 健康预警:一条 SQL 揪出「久坐高危用户」
这套系统后台有个健康预警模块,逻辑很简单:最近 7 天运动总时长低于 30 分钟的用户会被标为「运动不足」。这个模块的价值不在算法多高级,而在 SQL 怎么写——只用一条关联子查询就能扫出全表高危用户。
SELECT u.id, u.username, IFNULL(SUM(e.duration_minutes), 0) AS total_minutes FROM t_user u LEFT JOIN t_exercise_record e ON e.user_id = u.id AND e.record_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) WHERE u.status = 0 GROUP BY u.id HAVING total_minutes < 30;注意这里用的是LEFT JOIN而不是INNER JOIN,因为一个用户如果在过去 7 天压根没运动,他不会有任何记录行,用 INNER JOIN 会把他漏掉;LEFT JOIN保证每个用户都出现在结果集里,IFNULL(SUM(...), 0)把没有记录的用户的汇总值填补成 0。最后HAVING total_minutes < 30是在分组后过滤,WHERE做不到这一点——这是新手写统计 SQL 最常见的翻车点:WHERE在分组前执行,它不认识聚合列;加上HAVING才能对分组后的聚合结果过滤。
4.3 统计图表的数据接口:聚合查询怎么写才不难看
管理后台的首页通常有「运动类型占比」「每日活跃用户数」等图表。这套系统的图表数据走的是 JSON 接口,由一个StatisticServlet返回。典型的查询是统计每种运动类型的累计时长 TOP5:
SELECT exercise_type, SUM(duration_minutes) AS total_minutes FROM t_exercise_record WHERE record_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY exercise_type ORDER BY total_minutes DESC LIMIT 5;执行完这个查询后,后台把每行结果转成 JSON 数组,格式大概是[["跑步", 320], ["骑行", 180]],前端图表库直接拿数组渲染饼图或柱状图。这个数据接口有两个常见问题:一是LIMIT 5会截断数据,饼图的「其他」类别需要 Java 层手动把第 6 名以后的数据合并成一行「其他」;二是日期范围 30 天是常量,如果想让管理员在页面上自定义日期范围,把这个常量替换成参数即可——StatisticServlet里加一个receiveParam("days"),默认 30,接口就变通用了。
图表联动这块,这套系统的实现不算复杂,但架构是通的:前端定时请求/statistic/recent,拿到上次时间戳之后的新增打卡记录,直接更新图表数据点。如果你要把它改成实时大屏,追加推送通道就行,不用动后端查询接口。
5. 部署避坑实录:JDK 版本、字符集、连接池这几个坎最常翻车
5.1 JDK 版本与 MySQL 驱动不匹配:ClassNotFoundException 不是驱动没放
现象:Tomcat 启动后,访问登录页一切正常,一提交表单直接白屏,控制台抛ClassNotFoundException: com.mysql.cj.jdbc.Driver。很多人第一反应是「缺 jar」,把驱动包反复拷进WEB-INF/lib还是报错。
原因:这个项目如果数据库脚本是用 MySQL 5.x 写的,而本机装的是 MySQL 8.x,旧版驱动com.mysql.jdbc.Driver在 MySQL 8 连接串下已经不适用了。更隐蔽的是,如果本机用的是 JDK 8 但驱动包是新版,也会因为编译版本问题抛UnsupportedClassVersionError。
解决:先确认 MySQL 版本。MySQL 5.x 用com.mysql.jdbc.Driver,MySQL 8.x 用com.mysql.cj.jdbc.Driver,同时要把连接串加参数serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8。改完之后重启 Tomcat,如果还抛Public Key Retrieval is not allowed,再在连接串加allowPublicKeyRetrieval=true。这三个参数是这类项目里最常见的「差一个就死」配置。
5.2 中文乱码:三个地方字符集不一致,表单提交就变问号
现象:登录页中文用户名输入「张三」,点击登录后提示「用户名或密码错误」;打开数据库,查到username存的值是????。页面显示正常,一写库就乱。
原因:字符集有三层,任何一层不一致都会翻车——JSP 页面编码、Servlet 接收请求的编码、MySQL 表结构的编码。很多时候建表脚本用的utf8(实际是 MySQL 的 utf8mb3),而 JDBC 连接串没带characterEncoding=UTF-8,数据库层就把 Java 传来的字符串按默认字符集转换,中文全部变成问号。
解决:逐层核对。把建表脚本的DEFAULT CHARSET=utf8mb4统一写死;JDBC 连接串带useUnicode=true&characterEncoding=UTF-8;每个 Servlet 的doPost第一行加request.setCharacterEncoding("UTF-8");如果页面是动态渲染,还要检查 JSP 顶部contentType="text/html; charset=UTF-8"是否漏了。这一套走完,中文乱码基本根治。
5.3 数据库连不上:本地能跑,换台机器就报 Connection refused
现象:源码在自己电脑上跑得好好的,发给某同事或换了一台电脑,启动后访问列表页直接报Communications link failure或者Access denied for user。
原因:这类项目的数据库配置通常写在src/jdbc.properties(或db.properties)里,常见写法是jdbc.url=jdbc:mysql://localhost:3306/health。换机器后如果目标机器的 MySQL 没建同名数据库、用户名密码不一致,或者本机 MySQL 端口不是 3306,直接连不上是必然的。更隐蔽的是配置里用了绝对路径加载配置文件,导致别人那边资源加载失败。
解决:拆包后第一件事就是打开jdbc.properties(或DBUtil.java里的连接常量),确认库名、用户名、密码和当前环境一致。导入数据库前先确认:库是否建好、脚本是否完整跑完、navicat 里能否用同样的账号密码直接连上。如果jdbc.properties路径有问题,建议把配置文件直接放到src根目录,用类路径加载,避免FileNotFoundException。
5.4 Tomcat 9 下 EL 表达式不生效:页面输出 ${user.username}
现象:JSP 页面里写了${user.username},出来不是值,而是原样显示字符串“${user.username}”。页面不上数据也不报错,数据在 Servlet 端明明查到了。
原因:Tomcat 对 JSP 的 EL 支持默认分版本:Servlet 3.0 / JSP 2.2 以上才默认开启 EL,但项目如果用了老的 web.xml 头声明(比如web-app版本是 2.3 或 2.4),EL 会被当成普通文本。很多课设项目的 web.xml 是多年前从网上抄的,头声明版本过低,就等于关闭了 EL 求值。
解决:把web.xml头改成 Servlet 3.1(Tomcat 8/9 都支持),或者干脆把<web-app>根元素版本声明升级到 3.0 以上。如果不想动 web.xml,更省事的做法是把${user.username}改成<%= ((User)request.getSession().getAttribute("loginUser")).getUsername() %>,先跑通功能再回头收拾版本号。
6. 把课设升级成真实项目的第一步:把 JDBC 直连接成 Druid 连接池
6.1 为什么第一步要换连接池:高并发只是顺带的,稳定才是刚需
课设代码里的DBUtil.getConnection()常见写法是DriverManager.getConnection(url, user, password),每次请求都新建物理连接。单机演示没问题,一旦页面里多个图表接口同时请求,MySQL 的连接数会被打满,报Too many connections。Druid 连接池的核心价值不是「变快」,而是「连接复用 + 自动回收」,让应用负载上来时不会瞬间把数据库压垮。这个改造的复杂度并不高,因为它只动DBUtil一个类。
6.2 改造步骤:DruidUtil 替换 DriverManager
在src目录加一个druid.properties:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/health?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=5 maxActive=20 maxWait=10000 testWhileIdle=true validationQuery=SELECT 1然后新建一个DruidUtil.java:
public class DruidUtil { private static DataSource dataSource; static { try (InputStream in = DruidUtil.class.getClassLoader() .getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }改完之后,原来调DBUtil.getConnection()的地方只需要把类名换成DruidUtil,DAO 代码一行都不用动。initialSize=5是启动时预先创建 5 个连接,maxActive=20是连接池上限,超过这个数新请求会进入maxWait等待队列;testWhileIdle=true会在连接闲置时用SELECT 1探活,避免拿到已被数据库掐断的死连接。这几个参数里,validationQuery=SELECT 1是这套系统最值得折腾的一行——MySQL 8 默认连接超时是 8 小时,如果晚上挂服务器第二天早上访问报连接断,十有八九是没加探活语句。
6.3 验证改造有没有生效
改完后用如下方式自查:正常启动项目,多刷新几个图表页,然后打开 MySQL 命令行执行SHOW STATUS LIKE 'Threads_connected';。如果连接池生效,这个值应该稳定在个位数而不是随着访问量增长;连续刷新 50 次页面后再看,连接数不会猛增。如果报Too many connections,八成是maxActive配得太高而 MySQL 的max_connections默认才 151,把maxActive调到 20 以下基本不会再碰到。
从那以后我每次拿到新源码,不管是课设还是毕设,第一件事就是先打开数据库配置文件和连接工具类,配合当前环境把连接池、字符集、驱动版本一次配齐,再启动项目。这套运动健康系统断断续续解剖了几天,数据库脚本从头到尾过了一遍,该藏的坑基本都翻出来了。希望帮到你。
本文还有配套的精品资源,点击获取