FEATURED · 精选文章

JSP校园卡一卡通管理系统:数据库设计、Servlet事务与部署排错全解析

发布时间 / 2026/9/15 2:53:18
来源 / 创域科博编辑部
栏目 / 资讯中心
JSP校园卡一卡通管理系统:数据库设计、Servlet事务与部署排错全解析 简介这是一份基于JSP、Java与MySQL的校园卡一卡通管理系统完整项目资源既适合Java Web初学者理解真实业务场景也可作为计算机专业课程设计或毕业设计的参考材料。资源整体5.6MB共219个文件包含28个Java源文件、26个JSP页面、29个Class编译文件、16个CSS样式、9个JS脚本、13个JAR依赖库以及45个GIF演示动图和24张JPG界面截图同时附带SQL数据库脚本目录按源码、数据库、截图分层整理便于开发时快速定位和学习。目前已有276人学习浏览。从内容预览可见项目包含消费账单、用户管理、充值申请、管理员后台等核心业务类覆盖了主要业务流程。结合源码、数据库表结构设计和页面交互可以直观学习MVC分层、JDBC数据库操作、会话管理、权限校验等知识点也可将数据库脚本导入MySQL并部署到Tomcat运行适合反复阅读和二次改造是掌握校园信息化系统开发流程的不错练手资源。1. JSP校园卡一卡通管理系统课程设计交付物背后的三层架构该怎么撑起来拿到一个 JSPJAVAMYSQL 的校园卡一卡通管理系统压缩包很多人的第一反应是「又是一套 Java Web 课设」但拆开看会发现这套系统麻雀虽小五脏俱全卡片开户、充值、消费、挂失、流水查询每一个功能都落在 JSP 页面、Servlet 控制器和 MySQL 事务这三层上。它适合两类人研究一类是要用这份工程完成课程设计交付的学生另一类是要接手或改造老旧 JSP 项目的运维工程师。前者需要知道表结构和业务代码怎么对得上后者需要知道连接池、编码和事务边界在哪里改。下面按「建表 → 业务 → 页面 → 部署」的顺序把这套系统的完整骨架讲清楚重点放在那些参数和坑上。2. 数据库先行校园卡一卡通的账户表、卡片表和流水表怎么拆2.1 一卡通的核心是「卡账户分离」还是「卡户一体」校园卡系统的本质不是管卡而是管「卡对应的钱」。设计数据库时首先要想清楚一个问题卡片信息和账户信息是放一张表还是拆两张表。常见做法是拆两张表一张 card 表存物理卡片信息卡号、芯片编号、状态、绑定学生 ID一张 account 表存资金账户信息余额、累计充值、累计消费、账户状态。这么拆的理由有两个一是卡可能补办补办时换卡不换账户资金记录不断档二是未来如果支持手机二维码刷码二维码凭证可以直接复用 account 表而不必依赖物理卡。-- 卡片信息表 CREATE TABLE card ( card_id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL COMMENT 卡面号例如20240001, chip_no VARCHAR(32) COMMENT 芯片序列号可为空, student_id VARCHAR(20) NOT NULL COMMENT 学号, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 2挂失 3注销, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_card_no (card_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 资金账户表 CREATE TABLE account ( account_id INT PRIMARY KEY AUTO_INCREMENT, card_id INT NOT NULL, balance DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 当前余额单位元, total_recharge DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 累计充值, total_consume DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 累计消费, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, FOREIGN KEY (card_id) REFERENCES card(card_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段建表 SQL 里有几个值得注意的参数DECIMAL(10,2) 表示余额最大 99999999.99 元对校园卡场景绰绰有余而且 DECIMAL 是定点数不会像 FLOAT、DOUBLE 那样出现 0.10.2 的二进制精度误差card_no 上建唯一索引是为了防止并发下同号开卡version 字段是后面做乐观锁用的先留好。2.1.1 交易流水表一天几十万条查询怎么不卡除了卡片和账户还必须有一张流水表。充值、消费、退款、挂失补卡每一次资金变动都要落流水否则余额对不上账时无从查起。CREATE TABLE transaction_log ( trans_id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL, trans_type TINYINT NOT NULL COMMENT 1充值 2消费 3退款 4补卡结转, amount DECIMAL(10,2) NOT NULL, balance_after DECIMAL(10,2) NOT NULL COMMENT 交易后余额快照, op_channel VARCHAR(10) DEFAULT window COMMENT window/pos/app, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_card_time (card_no, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个初学者常犯的错误流水表主键用 INT。校园卡系统一天的流水量可能超过十万条流水表是只增不改的几年累计下来 INT 会逼近上限直接上 BIGINT 一劳永逸。联合索引 idx_card_time 支撑「查某张卡某段时间的交易记录」这类高频查询单独按 card_no 查也能命中最左前缀。2.2 一卡通业务的几个关键字段和状态机卡片状态用 TINYINT 而不是字符串查询快、存储省但代码可读性要靠常量类来保证。常规状态设计如下状态值卡片状态账户状态可执行操作1正常正常充值、消费、挂失2挂失冻结解挂、补卡3注销销户仅退款4已补卡正常(新卡)消费、充值状态字段的变更必须走服务层统一方法不要在每个 JSP 页面里直接 UPDATE。常见做法是写一个 CardStatusService所有状态变更都在这里做合法性校验比如「挂失状态不能再次消费」这类规则集中在一个文件里后面要加「挂失 3 天后自动冻结」之类的规则也只改一处。2.3 MySQL 事务余额扣减为什么不能先查后改校园卡系统里最容易出事故的代码就是消费扣款。新手写法是「先 SELECT 余额判断够不够再 UPDATE 扣款」这在单用户测试时没问题一旦两个窗口同时消费就会超扣。MySQL 的 InnoDB 默认隔离级别是可重复读REPEATABLE READ两个连接同时读到余额 50 元各自判断够用各自扣 20结果账户变成 30 而不是 10。解决办法是把扣款做成原子操作UPDATE account SET balance balance - 20, version version 1, total_consume total_consume 20 WHERE card_id 123 AND balance 20 AND version 5;这条 UPDATE 单条语句原子执行WHERE 里的 balance 20 是余额保护version 5 是乐观锁比对。如果影响行数为 0代码就判断这次扣款失败提示余额不足或数据已变更。相比 SELECT FOR UPDATE 的悲观锁这种写法在低冲突场景下性能更好也不会因为事务没提交而锁住整行。3. JSPServlet 处理一卡通核心业务充值、消费、挂失的请求流转3.1 从 index.jsp 表单到 MySQL 的完整调用链JSP 项目的经典调用链是JSP 页面收集参数 → form 提交到 Servlet → Servlet 调 Service 层 → Service 里写 JDBC 或 MyBatis → 数据库返回 → Servlet 转发或重定向回页面。以充值功能为例JSP 里通常是这样的form action${pageContext.request.contextPath}/recharge methodpost input typehidden namecardNo value${sessionScope.cardNo} / 金额input typenumber nameamount min1 max500 step0.01 required / button typesubmit确认充值/button /form对应的 Servlet 在 doPost 里先收参数、做校验再调 ServiceWebServlet(/recharge) public class RechargeServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String cardNo req.getParameter(cardNo); BigDecimal amount new BigDecimal(req.getParameter(amount)); // 业务校验金额必须大于0且不超过单笔上限 if (amount.compareTo(BigDecimal.ZERO) 0 || amount.compareTo(new BigDecimal(500)) 0) { req.setAttribute(error, 充值金额必须在0.01到500元之间); req.getRequestDispatcher(/recharge.jsp).forward(req, resp); return; } boolean ok CardService.getInstance().recharge(cardNo, amount); if (ok) { resp.sendRedirect(req.getContextPath() /balance.jsp?cardNo cardNo); } else { req.setAttribute(error, 充值失败请检查卡号或卡片状态); req.getRequestDispatcher(/recharge.jsp).forward(req, resp); } } }这个 Servlet 里有三个细节值得说明req.setCharacterEncoding(UTF-8) 必须在读取第一个参数之前调用否则中文参数会乱码金额从 String 转用 new BigDecimal 而不是 Double.valueOf避免浮点精度问题充值成功后用 sendRedirect 重定向防止用户按 F5 重复提交表单导致二次入账。3.2 Service 层事务边界和连接管理Service 层是 JSP 项目里最容易写坏的一层。常见误用是每个 DAO 方法各自开一个 Connection充值事务里先插入流水、再更新余额两个连接各干各的中间一旦崩溃钱扣了流水没记。正确做法是事务边界放在 Service 方法上单个 Connection 贯穿整个业务public boolean recharge(String cardNo, BigDecimal amount) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关掉自动提交开启事务 // 1. 更新余额 int rows accountDao.recharge(conn, cardNo, amount); if (rows 0) { conn.rollback(); return false; } // 2. 写流水 transactionDao.insert(conn, cardNo, 1, amount); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } log.error(recharge failed, e); return false; } finally { DBUtil.close(conn); } }Connection 由 DBUtil 统一管理每次从连接池取用完归还。这里强调一点rollback 和 close 必须分开处理rollback 抛出的异常要吞掉不能让回滚失败掩盖原始业务异常。事务里的更新余额和插入流水必须同时成功或同时失败这是一卡通系统对账的基本保障。3.3 挂失与解挂状态校验放在哪一层挂失的核心逻辑是「把卡状态从正常改成挂失并同步冻结账户」这个操作也必须走事务因为一旦改了卡状态而账户没冻结这张卡在状态切换的间隙可能已经被消费。状态变更的 SQL 建议写成带条件更新UPDATE card SET status 2 WHERE card_no 20240001 AND status 1; UPDATE account SET status 2 WHERE card_id (SELECT card_id FROM card WHERE card_no 20240001);第一句更新影响行数为 0说明卡不在「正常」状态可能是已挂失或已注销此时应当直接返回「该卡当前状态不可挂失」而不是继续执行账户冻结。把状态前置条件写进 WHERE比先查再判省一次往返也避免了两条语句之间的并发缝隙。解挂时要注意补卡后旧卡状态变成 3注销新卡状态是 1新卡复用原账户余额。这一步在代码里要跟结转流水一起做转账金额等于旧卡余额trans_type 写 4补卡结转保证对账记录齐全。4. 页面层JSP 个人信息展示页面、EL 表达式与分页查询的参数设计4.1 用 EL JSTL 渲染一卡通用户信息避免在 JSP 里写 Java 代码JSP 页面的最佳实践是页面里不写% %Java 脚本全部用 EL 表达式和 JSTL 标签。以个人信息展示页面为例Servlet 往 request 里塞一个 userInfo 对象后页面这样渲染% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % div classcard-info p学号${userInfo.studentId}/p p姓名${userInfo.studentName}/p p卡号${userInfo.cardNo}/p p余额fmt:formatNumber value${userInfo.balance} typecurrency currencySymbol//p p卡片状态 c:choose c:when test${userInfo.cardStatus 1}正常/c:when c:when test${userInfo.cardStatus 2}挂失/c:when c:otherwise注销/c:otherwise /c:choose /p /divuserInfo 是 Servlet 用 request.setAttribute(userInfo, vo) 写入的EL 表达式 ${userInfo.studentId} 会自动调用 getter。fmt:formatNumber 处理金额格式化避免在 Java 里拼字符串。c:choose 做状态值到文案的映射比在 Servlet 里预先转好字符串更直白日后加状态只要改页面一处。4.2 交易记录分页pageNo 和 pageSize 的取值与 SQL 写法一卡通系统的流水查询页是典型的分页场景。分页参数由前端传入 pageNo页码从 1 开始和 pageSize每页条数服务端要做两件事查总数算总页数、查当页数据。分页查询 SQL 用 LIMIT 语句SELECT trans_id, card_no, trans_type, amount, balance_after, create_time FROM transaction_log WHERE card_no 20240001 ORDER BY trans_id DESC LIMIT ?, ?;两个占位符分别对应 offset 和 sizeoffset (pageNo - 1) * pageSize。参数校验很关键pageSize 不能超过 100否则有人传 pageNo1、pageSize9999999 就能一次拉走全表pageNo 小于 1 时强制改成 1。查总数要用 SELECT COUNT(*) 单独执行不要用 SQL_CALC_FOUND_ROWS这个语法在 MySQL 8.0 已废弃。一个最常见的坑是 ORDER BY 用 create_time而 create_time 默认精度是秒同一秒内的多条记录排序不稳定翻页时会出现记录重复或遗漏。解决办法是加主键作为次级排序ORDER BY create_time DESC, trans_id DESC。主键自增天然反映插入顺序与时间排序方向一致稳定且能用到索引。4.3 屏蔽 JSP 离开页面提示onbeforeunload 的正确打开方式「屏蔽 JSP 离开页面提示」说的是浏览器 onbeforeunload 事件。这个事件本意是防止表单内容没保存就离开但在 JSP 课设里常被误用成「页面跳转时弹窗确认」反而在提交成功跳转时也弹窗非常影响体验。正确的做法是只在「有未保存修改」时绑定该事件提交成功后移除let formDirty false; document.querySelector(#rechargeForm).addEventListener(input, () { formDirty true; }); window.addEventListener(beforeunload, (e) { if (!formDirty) return; // 没有改动直接放行不弹提示 e.preventDefault(); e.returnValue ; // 兼容旧版浏览器 }); // 表单提交成功后置回 false跳转时不再提示 document.querySelector(#rechargeForm).addEventListener(submit, () { formDirty false; });关键点是 e.preventDefault() 加 returnValue 双保险Chrome 71 之后只调用 preventDefault 不设 returnValue弹窗不会出现。很多 JSP 模板把 onbeforeunload 直接写进 body 标签导致所有页面跳转都弹提示这种写法要避免。4.4 结合 JSONArray 的常见需求后台返回 JSON 给页面局部刷新现代 JSP 项目里页面局部刷新是标配。后台返回 JSONJSP 页面用 JavaScript 解析渲染避免整页刷新。国内项目里用 fastjson 较多引入和使用的写法是import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; JSONArray arr new JSONArray(); JSONObject item new JSONObject(); item.put(cardNo, 20240001); item.put(balance, 123.45); arr.add(item); resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write(arr.toJSONString());前端拿到 JSON 后用 fetch 或 axios 请求接口渲染到表格中。注意响应头 charsetUTF-8 必须写否则浏览器默认按 ISO-8859-1 解析中文会乱码。若项目内没引入 fastjson 的 jar用 JDK 自带的 org.json 或 Jackson 也可以核心是响应头别漏。5. 部署排错与发布从 war 包到 jsp 编译 class 的排查路径5.1 Tomcat 下 JSP 编译后的 class 文件保存在哪里接手 JSP 项目遇到 500 错误时最该看的就是 JSP 翻译后的 Servlet 源码。JSP 首次被访问时由 Tomcat 的 Jasper 引擎编译成 Java 源文件再编译成 class存放在work/Catalina/localhost/项目名/org/apache/jsp/比如 index.jsp 对应 index_jsp.java 和 index_jsp.class。JSP 改了不生效时删掉 work 目录下项目文件夹再重启 Tomcat 即可强制重新编译。5.2 rar 工程部署到 Tomcat 的三个必查配置拿到 rar 压缩包解压后先查下面三项能省掉大部分排错时间。编码问题最隐蔽MySQL 连接串的 characterEncodingutf8 只管传输层JSP 的 pageEncoding 和表的 charset 三者必须一致否则中文全变问号命令行启动 Tomcat 报找不到 javac 时检查 JAVA_HOME 和 PATHJDK 版本切换要两个变量一起改。检查项位置典型错误MySQL 连接串src/jdbc.properties 或 DBUtil.javalocalhost:3306 与实际端口不符Java 编译版本项目构建配置JDK8 编译的 class 跑在 JDK17 上报 UnsupportedClassVersionError文件编码JSP 的 pageEncodingGBK 工程配 UTF-8 连接串中文乱码5.3 定时备份mysql 自动备份 bat 脚本与验证技巧一卡通系统最怕数据库宕机丢数据。Windows 下给 MySQL 做定时备份批处理脚本配合任务计划程序是最简单的方案echo off set BACKUP_DIRD:\backup\mysql set MYSQL_HOMEC:\Program Files\MySQL\MySQL Server 8.0\bin set DB_USERroot set DB_PASS123456 set DB_NAMEcampus_card if not exist %BACKUP_DIR% mkdir %BACKUP_DIR% set FILENAME%DB_NAME%_%date:~0,4%%date:~5,2%%date:~8,2%_%time:~0,2%%time:~3,2%.sql set FILENAME%FILENAME: 0% %MYSQL_HOME%\mysqldump.exe -u%DB_USER% -p%DB_PASS% --default-character-setutf8mb4 %DB_NAME% %BACKUP_DIR%\%FILENAME% forfiles /p %BACKUP_DIR% /d -30 /c cmd /c del path 2nul%date% 取到的小时可能带前导空格比如「 9」不替换掉文件名就带空格forfiles 清理时路径解析会失败mysqldump 加 --default-character-setutf8mb4 防止备份文件中文乱码。部署完成后用mysqldump -u root -p --no-data campus_card验证所有表能正常导出再刷新个人信息展示页面结合 work 目录 class 文件的时间戳判断 JSP 是否被重新编译五分钟内能定位大多数部署故障。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻