这段时间陆续有几个人来问我同一个题目——基于JSP的校园宿舍电费缴纳系统,项目编号后面还带着SpringBoot和132这种后缀,一看就是课设、毕设题库里的常见货。说实话,现在听到JSP这个词,第一反应确实有点穿越,但冷静下来想想,这个题目一点都不落伍:它是非常典型的Web信息管理系统,业务闭环清晰,前台学生查费缴费,后台管理员抄表定价,数据关系简单又完整,拿来练手或者应付毕业设计都再合适不过了。
这篇文章就基于我实际做过的一个版本,把技术选型、数据库设计、核心模块实现和踩过的坑完整过一遍。我自己在做的时候,一边骂JSP老,一边又不得不承认它确实省事——不用搭前后端分离那套工程结构,一个Tomcat跑起来就能用,给不懂技术的宿管老师演示也方便。如果你是刚准备动手做这个题目,或者正在为Spring Boot版本和JSP集成的问题发愁,那这篇文章应该能帮你少走不少弯路。
1. 项目整体设计与技术选型思路
1.1 为什么这种“老搭配”还值得做
我先说下这套系统的定位。校园宿舍电费缴纳系统本质上就是一个信息管理系统,数据流向很清晰:管理员维护宿舍楼、房间、电价标准,每月录入电表底数;系统根据用电量算出应缴费用;学生登录后看到自己的欠费情况,在线缴纳;管理员随时查看缴费记录和统计报表。
技术栈方面,我最终采用的是Spring Boot 2.7.18 + JSP + MyBatis-Plus + MySQL。这套组合放在今天看确实不够“新潮”,但它有两个非常现实的优势。第一,Spring Boot负责把Spring MVC那一套繁琐配置全部自动搞定,开发者只需要关注业务代码;第二,JSP作为服务端模板,天然适合这种以表单交互和列表展示为主的管理系统,页面渲染逻辑在服务端直接完成,不用额外搭前端工程,部署上也省心。
很多同学纠结为什么不用Vue做前后端分离。我的看法是:如果题目明确写了JSP,那就不要给自己加戏。前后端分离意味着你要维护两套工程、处理跨域、做Token鉴权,工作量翻倍不说,评委老师还不一定认可。反过来,JSP方案在答辩时更好讲清楚:“用户请求由Controller接收,ModelAndView渲染JSP页面返回浏览器”,这张请求链路图一画,整个系统的设计思路就非常直观。
1.2 Spring Boot版本选型,不要被新版带偏
这里必须单独说一个非常关键的问题:Spring Boot版本不要选太高。现在Maven中央仓库默认拉取的是3.x版本,JDK要求17起步,但JSP在Spring Boot 3.x里适配很麻烦——Servlet API换成了Jakarta命名空间,JSTL和JSP支持也不如2.x时期成熟,很多人从这里开始就掉坑了。
这个项目我选的是Spring Boot 2.7.18,这是2.x系列的最后一个维护版本,稳定性和兼容性都经过了充分验证。它默认的Tomcat是9.0,内置了对JSP的良好支持,配合JDK 1.8和MySQL 5.7,整条链路都是经典组合,跑起来几乎没有兼容性问题。
提示:如果你用的IDEA新建Spring Boot项目,在Spring Initializr页面要手动把版本切换成2.7.18,Server URL选默认的start.spring.io即可。创建完成后再去pom.xml里确认一下Spring Boot父 parent 版本号。
另外,还有个关于Spring Boot版本的坑要提前说:2.7.x版本的自动装配原理、starter机制和3.x基本一致,但具体依赖坐标会有差异。比如JSP需要引入tomcat-embed-jasper,这个依赖在3.x里也能用,但JSTL坐标从javax.servlet变成了jakarta.servlet,如果不注意就是一路报错。所以选型直接锁定2.7.x,能省去至少半天排查依赖的时间。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与业务模型拆解
2.1 表结构设计:从宿舍楼到缴费记录
数据库设计是整个系统的基础,我建议不要一上来就写代码,先把表关系理清楚。这个系统核心涉及四类对象:宿舍楼、宿舍房间、用户(学生和管理员)、电费业务数据。我把表拆成了7张,这样既能满足功能需求,又不会让结构过于复杂。
| 表名 | 作用 | 关键字段 |
|---|---|---|
| building | 宿舍楼信息 | id, name, room_count |
| room | 宿舍房间 | id, building_id, room_no, student_id, meter_no |
| student | 学生信息 | id, student_no, name, password, room_id, phone |
| admin | 管理员信息 | id, username, password, name |
| meter_record | 电表抄表记录 | id, room_id, record_month, last_read, current_read, amount, status |
| charge_rule | 电费单价规则 | id, price, effective_date, description |
| pay_record | 缴费记录 | id, student_id, room_id, amount, pay_time, order_no, status |
这里我特别说明几个设计时的考虑点。第一,room表里放一个student_id字段表示当前住宿学生,但其实一个宿舍可以住多人,为了严谨可以在表设计里做成room_student关联表。如果项目要控制工期,可以先用student表里加room_id的方式,表示一个学生对应一个宿舍,一人一表一缴费,逻辑简化但演示效果不差。
第二,meter_record表里我加了status字段,0表示待缴费,1表示已缴费,2表示异常。这个状态字段非常有用,后面做统计报表、筛选欠费宿舍都靠它。
第三,charge_rule表里存的是电价规则,我做的是按不同生效日期来区分不同时间段的价格,比如某学校实行阶梯电价,就可以在规则里配置多个档位。虽然大部分课设只需要一个统一单价,但把表设计成可扩展的,答辩的时候能多讲一个设计亮点。
建表SQL这里不全部贴出来了,只贴最核心的缴费和抄表记录这两张,其余的照着上面的字段设计去建就行:
sql复制CREATE TABLE `meter_record` (
`id` int NOT NULL AUTO_INCREMENT,
`room_id` int NOT NULL COMMENT '房间ID',
`record_month` varchar(7) NOT NULL COMMENT '账期月份,格式:2025-06',
`last_read` decimal(10,2) DEFAULT '0.00' COMMENT '上月读数',
`current_read` decimal(10,2) DEFAULT '0.00' COMMENT '本月读数',
`amount` decimal(10,2) DEFAULT '0.00' COMMENT '本月用电量',
`status` tinyint DEFAULT '0' COMMENT '0待缴费,1已缴费,2异常',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_room_month` (`room_id`, `record_month`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='电表抄表和计费记录';
注意record_month字段我用了varchar(7),配合room_id做联合索引,这样查询某房间某个月记录非常快。联合索引的字段顺序很重要,等值查询条件room_id要放在前面,这是MySQL索引最基本的使用原则。
2.2 电费计算逻辑与金额精度问题
这个系统核心业务逻辑就是电费计算:用电量 = 本月读数 - 上月读数,应缴金额 = 用电量 × 单价。看着简单,但不同宿舍的抄表记录之间是有关联的——上一条记录的“本月读数”就是下一条的“上月读数”,所以录入抄表数据时,系统要自动带出上一期的数据。
我在实现时是这样设计的:管理员选择宿舍和账期后,后端先生成一个MeterRecord对象,先把last_read设置为该宿舍上一个账期的current_read,然后管理员只需填入current_read,系统自动算出差值和费用。这一段逻辑放在MeterRecordService里,用了MyBatis-Plus的LambdaQueryWrapper来查询上一条记录:
java复制// 查询该房间上一个账期的记录
MeterRecord lastRecord = meterRecordService.getOne(
new LambdaQueryWrapper<MeterRecord>()
.eq(MeterRecord::getRoomId, roomId)
.eq(MeterRecord::getStatus, 1)
.orderByDesc(MeterRecord::getRecordMonth)
.last("limit 1")
);
关于金额精度,这里必须提醒大家:电费金额和用电量千万不要用float或double,Java里这两个类型计算小数会有精度丢失问题,比如0.1 + 0.2的实际结果是0.30000000000000004,放到财务报表里会出大问题。数据库字段我用的是decimal(10,2),实体类对应的是BigDecimal,计算时通过BigDecimal的subtract和multiply方法来完成金额运算。这个细节写进代码注释里,答辩时老师问起来也有得说。
电费单价假设是0.52元/度,这个数作为常量配置在charge_rule表中。如果系统要做扩展,可以在这里加电费阶梯、线损率、基础管理费等规则,但课设场景下不用过度设计,统一单价已经足够演示完整流程。
3. 核心功能模块实现过程
3.1 登录、会话与权限控制
整个系统有前台和后台两块,前台是学生登录,后台是管理员登录。学生登录后可以查看自己宿舍的电费账单、在线缴费、查询历史记录;管理员登录后可以管理宿舍楼、房间、录入抄表、查看缴费统计。
登录这块我用了经典的Session方案,没有引入Spring Security或者JWT。原因很简单:JSP项目都是服务端渲染,Session天然合适,引入Security会在配置和角色权限定义上增加很多不必要的工作量。我自定义了一个拦截器实现权限控制:
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object user = session.getAttribute("loginUser");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/login");
return false;
}
return true;
}
}
然后注册拦截器,并设置放行路径。注意/login页面、/static/**下的静态资源(CSS、JS、图片)都要放行,否则会出现页面样式丢失或者无限重定向的问题。
密码安全方面,我在这个项目里用的是MD5加盐存储。诚然,BCrypt是更安全的方案,我也推荐你在有条件时使用Spring Security自带的BCryptPasswordEncoder。不过考虑到课设项目迭代成本和JSP场景的实际需求,MD5加盐已经能避免“明文存储”这个最大隐患了。这里要专门提醒:千万不能把密码明文存到数据库,哪怕题目要求很简单,这也是开发习惯的红线。
登录成功之后,我把用户对象放进Session,同时把用户角色(student/admin)也放到Session里。这样JSP页面通过${sessionScope.loginUser.name}就能直接显示当前用户信息,非常方便。
3.2 学生端:账单查询与缴费闭环
学生端最重要的页面是“电费查询与缴纳”。学生登录后,系统查到他所在的房间ID,然后从meter_record表查出当前未缴费的记录,展示应缴金额。缴费动作本质就是一次数据库事务:把meter_record的状态改为已缴费,并往pay_record表插入一条缴费记录。
这里我用@Transactional注解保证事务一致性。可能有的同学会问:一个系统课设为什么要强调事务?因为如果没有事务,万一缴费成功后插入流水失败,就出现“钱交了但记录丢了”的情况。写代码时把这两步操作放到一个方法里,加上事务注解,数据库就能保证要么全部成功要么全部回滚:
java复制@Transactional(rollbackFor = Exception.class)
public void pay(PayRecord payRecord) {
// 1. 更新电费记录状态
MeterRecord meterRecord = meterRecordService.getById(payRecord.getMeterRecordId());
meterRecord.setStatus(1);
meterRecordService.updateById(meterRecord);
// 2. 插入缴费记录
payRecord.setPayTime(new Date());
payRecord.setStatus(1);
this.save(payRecord);
}
前端页面我用了Layui来做表格和表单的渲染,它比Bootstrap更贴合国内后台管理系统的习惯。学生端首页我会展示:当前欠费金额、最近三个月缴费记录、宿舍用电趋势。查询历史用MyBatis-Plus自带的分页插件,前端通过Layui的表格接口对接后端返回的分页JSON格式,这样既不用写一行JS拼接HTML的代码,又能保证分页交互流畅。
3.3 管理员端:抄表录入与统计展示
管理员端是整个系统里功能最重的一块。除了宿舍和房间的增删改查,核心的抄表录入流程是这样的:管理员选择房间→系统自动带出上一期读数→填写本期读数→点击保存。保存时后端自动计算用电量和金额。
为了让演示更有说服力,我还加了两个统计维度。一个是按宿舍楼的用电量排行,用MySQL的GROUP BY查询每个宿舍楼的总电量,在页面用柱状图展示;另一个是缴费率统计,即已缴费记录数除以总账单数,反映整个宿舍区的缴费完成情况。统计页面用ECharts绘制图表,后端返回JSON数据,前端Ajax请求后渲染,这个操作在答辩时非常加分,因为它体现了从数据库到图表展示的完整数据处理能力。
图表展示这里我不推荐用复杂的实时大屏,课设项目做到“有图表、能筛选、数据正确”就已经达标了。重点是接口返回的数据结构要清晰:{code: 200, data: [...]},前端拿到之后直接映射到ECharts的series里就行。
4. JSP页面细节与开发注意事项
4.1 JSP在Spring Boot里的配置与目录结构
JSP这玩意在Spring Boot里配置是有讲究的。它不像Thymeleaf那样是Spring Boot的原生支持模块,需要额外引入tomcat-embed-jasper依赖,并且要在application.yml里手动指定视图解析器的前后缀:
yaml复制spring:
mvc:
view:
prefix: /WEB-INF/jsp/
suffix: .jsp
对应的目录结构是src/main/webapp/WEB-INF/jsp/。这里有个关键知识点:放在WEB-INF目录下的JSP页面无法通过浏览器URL直接访问,只能通过Controller渲染跳转。这是一种安全设计,防止用户绕过权限控制直接访问页面。很多新手不知道这一点,把JSP直接放到webapp根目录下,结果登录拦截器形同虚设。
前端静态资源(CSS、JS、图片)则放在src/main/resources/static/目录下,Spring Boot会自动映射。注意JSP页面里引用静态资源时要写${pageContext.request.contextPath}/static/css/style.css这种完整路径,不然直接写相对路径的话,在二级路径跳转时经常会404。
4.2 JSP脚本片段的风险与规范写法
很多同学写JSP时习惯在页面里直接塞<% Java代码 %>,这个“脚本片段”方式虽然能跑,但后患无穷。第一,页面里出现Java代码会导致前端开发和调试极度困难,改了页面还要重新编译;第二,如果Java代码里包含SQL拼接参数而不做转义处理,很容易被SQL注入攻击;第三,页面和业务逻辑耦合后,代码复用基本为零。
正确的做法是:JSP页面里只使用EL表达式和JSTL标签来取数据。比如展示学生姓名,用${sessionScope.loginUser.name},遍历缴费列表用<c:forEach>,判断状态用<c:if>。所有数据都在Controller里提前处理成完整的对象集合,JSP只负责“显示”,不负责“计算”。
这里再补充一个容易忽略的安全细节:当JSP输出用户输入的文本内容时,比如宿舍楼名称、报修备注等,要使用JSTL的<c:out>标签而不是直接${} 输出,因为<c:out>默认会对HTML标签进行转义,可以防止存储型XSS攻击。比如学生提交的备注如果写成<script>alert('xss')</script>,直接输出会执行脚本,用<c:out>则会把尖括号转义成<和>,浏览器只会把它当作文本显示。
4.3 页面编码与个人信息展示的小坑
JSP页面的中文乱码是一个经典问题。我建议在所有JSP文件头部统一加上这一行:
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
同时在application.yml里配置CharacterEncodingFilter,确保请求和响应都是UTF-8。否则就会出现数据库里的中文正常,但页面显示一堆问号的灵异现象。
关于“个人信息展示页面”,热点里提到这个关键词我估计是很多人卡在用户头像上传或资料回显上。这个系统里学生个人信息在student表里,页面展示时直接用JSP的EL表达式即可。如果要支持修改资料,注意表单提交时要在Controller层做参数校验,比如手机号正则、邮箱格式,校验失败用BindingResult把错误信息带回页面,而不是简单返回一个错误页。这样用户体验会好很多,也符合一个正式系统的交互标准。
5. 常见问题排查与避坑实录
5.1 页面404与JSP无法访问
这是做Spring Boot + JSP项目遇到最多的问题。运行项目后访问Controller返回的视图路径,结果404。根据经验,先检查三件事:
pom.xml里有没有引入tomcat-embed-jasper依赖,这是Spring Boot内嵌Tomcat编译JSP的核心依赖。application.yml里视图解析器的前缀后缀有没有配置正确,关键是prefix一定要以/结尾。- 使用IDEA运行时,观察target目录下有没有JSP文件被拷贝过去。Spring Boot的Maven插件默认不会把
src/main/webapp下的JSP文件打进可执行JAR包,如果发现target里没有,需要在pom.xml里配置maven-resources-plugin,或者在IDEA的Project Structure里把webapp目录标记为资源目录。
这里我还要强调一个实际问题:如果你最终用java -jar的方式打包运行,JSP页面在可执行JAR包内是没法正常访问的,Spring Boot官方也明确说JSP不能用于可执行JAR包。所以这个项目的部署方式请使用war包:将pom.xml里的<packaging>war</packaging>,然后在启动类继承SpringBootServletInitializer并重写configure方法,最后部署到外部的Tomcat容器。
5.2 IDEA里yml文件不提示、Spring Boot版本太高怎么办
使用IDEA时还有一个常见小问题:新建项目的application.yml文件,在配置spring.datasource.url等属性时不弹提示。这通常是因为IDEA的Spring插件没有正确识别到项目里的Spring Boot依赖。我遇到过这个问题,解决办法是确认IDEA版本比较新,然后在Project Structure的Facets里重新添加Spring组件,或者把pom.xml重新导入一次让IDEA刷新Maven依赖。
如果你的机器上只有Spring Boot 3.x的开发环境,又非要跑JSP项目,可以先尝试调整JSTL依赖用jakarta.servlet开头的新坐标,但这套适配很折腾,我的建议是直接装一个JDK 8 + Spring Boot 2.7.x的环境,一条路走到黑,别在版本兼容上耗费时间。还有一个细节:Spring Boot项目的banner.txt可以自定义启动时输出的Logo,网上有在线的Banner生成器,复制一段字符画过去,启动时逼格拉满,这个小操作不涉及任何代码逻辑,纯粹是锦上添花,答辩时能活跃一下气氛。
5.3 金额计算、事务失效与文件导出心得
最后分享三个我在实际开发中踩过的具体坑。第一,BigDecimal除以整数或比较大小要调用对应方法,别把Java的==用在BigDecimal上——两个数值相等的对象用==判断结果可能是false,因为BigDecimal是引用类型。正确做法是用compareTo方法。第二,@Transactional注解默认只在运行时异常下回滚,如果你在事务方法里手动try-catch住了异常又没有继续抛出,事务是不会回滚的。这是我见过最多的“事务失效”场景:异常被吞掉了。如果业务上必须捕获异常,请在catch块里手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),或者把异常重新抛出去。
第三,有同学问过如何在浏览器里下载缴费记录Excel文件。这里要注意:浏览器出于安全限制,JavaScript是无法获取服务器端文件保存路径的,所以不能指望前端Ajax直接下载文件。正确的做法是后端的Controller返回一个ResponseEntity<byte[]>,设置响应头Content-Disposition: attachment; filename=xxx.xlsx,前端用window.location.href去请求这个下载地址,浏览器就会自动弹出保存文件的对话框。我在这个系统里用的是EasyExcel工具库,把缴费列表按导出模板直接写入响应流,效果很好,代码量也不大。
我做了不少Spring Boot项目之后回头看,这个JSP版校园宿舍电费缴纳系统虽然技术栈不前沿,但业务闭环完整,从数据库建模到服务端渲染再到权限控制,覆盖面非常广,用来练手或者做毕业设计,性价比其实相当高。如果你选了这个题目,我建议别急着写代码,花半天时间把表结构和页面流转理顺,先画一张简单的功能结构清单,比如“学生端三个页面:首页、缴费、历史记录;管理员端五个页面:登录、房间管理、抄表录入、缴费统计、流水查询”,理清之后再动手,你会发现后面每一步都顺很多。
