
简介一份面向 Java Web 初学者的电影院售票管理系统完整源码基于 Java Servlet JSP JDBC MySQL 构建采用 B/S 架构覆盖电影信息管理、场次与座位管理、在线购票、订单生成、用户及管理员后台等模块适合作为毕业设计或期末大作业参考。压缩包共 103 个文件约 2.31MB包含 23 个 Java 源文件及对应 class 文件、10 个 XML 配置、8 个 SQL 脚本另有 17 个 zbak 备份、项目文档与界面截图目录结构清晰便于直接导入与调试。源码带有较详细注释个人手打 98 分项目界面简洁、操作直观附带的数据库脚本和文档能帮助快速搭建运行环境梳理购票流程与管理逻辑。目前已有 86 人学习适合需要完整案例来理解 Servlet/JSP/JDBC 整合、用户权限控制及基础数据表设计的读者参考。1. 电影院售票管理系统为什么这套JavaWeb组合值得拆一遍如果你拿到的毕业设计题目是“电影院售票管理系统”打开源码包那一刻先别急着跑大概率会遇到Tomcat启动失败、MySQL密码不匹配、页面中文乱码三连击。这套JavaServletJSPJDBCMySQL的组合看起来不算新但它把一个请求从浏览器到Servlet、再到DAO、再到数据库、最后返回JSP页面的完整路径全暴露在代码里没有框架替你藏住任何细节每一层都能在答辩台上讲清楚。这篇文章拆三件事为什么选这套技术栈、五张表怎么设计、核心代码怎么组织中间穿插我部署和调试时踩过的坑。准备拿这个题目做课程设计或毕业设计的同学建议跟着把链路一步步跑通那比直接下源码跑起来有价值得多。2. 架构与数据库设计五张表如何支撑整条购票链路2.1 技术选型ServletJSP这套老组合为什么还够用先说结论毕业设计看重的不是技术多新而是你能否把“为什么这么选”讲清楚。Spring Boot已经把太多细节封装掉了——你写一个RestController加Autowired就完事但老师追问“请求从浏览器到服务端中间经历了什么”你可能答不上来。用ServletJSPJDBC手写你可以在黑板上画出完整链路浏览器发HTTP请求 → Tomcat解析请求并匹配WebServlet注册的映射 → 对应Servlet的doGet/doPost被调用 → 读取request参数 → 通过JDBC从MySQL取数据 → 把结果放进request或session作用域 →forward到JSP → JSP模板渲染HTML → 响应写回浏览器。这条链路画完Web开发的基本功就全在台面上了。Spring Boot帮你省掉的东西恰好也是答辩评委最爱追问的东西。用这套组合你反而能把底层说透。另外要理解JSP的本质一个index.jsp在第一次被访问时Tomcat会把它编译成一个继承自HttpJspBase的Java类而HttpJspBase再往上追还是HttpServlet。也就是说JSP不是独立技术它就是Servlet的一种模板化写法页面里的HTML和标签会在编译阶段转成Java代码来输出。这不是玄学你打开Tomcat的work目录就能看到编译产物。各层的职责划分大概是这样Servlet接请求、解析参数、做简单校验、调DAO、决定页面跳转JSP只做展示配合EL表达式和JSTL标签输出数据DAO封装SQL执行返回实体对象或ListMySQL存储用户、电影、场次、座位、订单五类数据这个复杂度不需要一个单独的Service层。购票这种涉及多步更新的逻辑直接写在Servlet里用事务包住反而更直观。2.2 项目目录结构代码按职责分开摆放一个规范的源码包目录结构一般是这样的cinema/ ├── src/com/cinema/ │ ├── entity/ │ │ ├── User.java │ │ ├── Movie.java │ │ ├── Schedule.java │ │ ├── Seat.java │ │ └── Order.java │ ├── dao/ │ │ ├── UserDao.java │ │ ├── MovieDao.java │ │ ├── ScheduleDao.java │ │ └── OrderDao.java │ ├── util/ │ │ ├── DBUtil.java │ │ └── OrderNoGenerator.java │ └── servlet/ │ ├── LoginServlet.java │ ├── RegisterServlet.java │ ├── MovieListServlet.java │ ├── MovieDetailServlet.java │ └── OrderServlet.java ├── WebContent/ │ ├── jsp/ │ │ ├── login.jsp │ │ ├── register.jsp │ │ ├── movieList.jsp │ │ ├── movieDetail.jsp │ │ └── orderResult.jsp │ ├── css/ │ ├── js/ │ └── WEB-INF/ │ └── web.xml ├── lib/ │ ├── mysql-connector-java-8.0.x.jar │ └── jstl-1.2.jar └── sql/ └── cinema.sql在这个结构里改一个字段的路径是固定的实体类加属性、DAO改SQL、JSP加展示逻辑上不交叉。很多同学拿到源码先找web.xml我反而建议先看entity目录把几个实体类字段过一遍再回头看SQL建表语句整个系统的数据模型在脑子里就有了轮廓。实体类和数据库表一一对应但有一个小习惯值得注意。比如Movie实体里release_time字段我建议直接声明成String在DAO层把Date转好再放进去。这样JSP页面直接用${movie.releaseTime}输出不用在页面上做日期格式化省掉一批麻烦。2.3 数据库设计用户、电影、场次、座位、订单五张表电影院售票系统最核心的数据模型就是五张表。先看整体对应关系表名业务含义核心字段user注册用户username, password, phone, realnamemovie电影信息name, duration, release_date, statusschedule场次安排movie_id, hall, show_time, priceseat座位的实时状态schedule_id, row_no, col_no, statusorders购票订单order_no, user_id, schedule_id, seat_ids, total_price建表语句里最先要注意的是字符集。MySQL 8.0下直接建库最好用utf8mb4它兼容全量Unicode字符也比utf8在排序时更稳。看下面这段CREATE DATABASE cinema DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cinema; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, realname VARCHAR(50), phone VARCHAR(20), created_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;user是MySQL里的保留字建表必须用反引号包住或者干脆改名t_user否则导入sql文件时会报语法错误。这个细节看起来小但坑过不少人——sql文件导入失败整个项目就卡在第一步。场次、座位和订单三张表的关联关系是重点CREATE TABLE schedule ( id INT AUTO_INCREMENT PRIMARY KEY, movie_id INT NOT NULL, hall VARCHAR(20), show_time DATETIME, price DECIMAL(8,2), INDEX idx_show_time (show_time), FOREIGN KEY (movie_id) REFERENCES movie(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE seat ( id INT AUTO_INCREMENT PRIMARY KEY, schedule_id INT NOT NULL, row_no VARCHAR(5), col_no INT, status TINYINT DEFAULT 0 COMMENT 0-空闲 1-锁定 2-已售出, UNIQUE KEY uk_schedule_seat (schedule_id, row_no, col_no), FOREIGN KEY (schedule_id) REFERENCES schedule(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, schedule_id INT NOT NULL, seat_ids VARCHAR(255) COMMENT 冗余座位ID逗号分隔, total_price DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT 0-待支付 1-已支付 2-已取消, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (schedule_id) REFERENCES schedule(id), INDEX idx_user_status (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;座位表的status字段很多版本只做了0和1我建议拆成三种状态空闲、锁定、已售出。用户选座但还没支付时座位要临时锁定防止两个人在同一时间选同一个位置导致超卖。这个细节论文里值得单独写一段。orders表里冗余一个seat_ids字符串字段是一种实用取舍。按规范可能要拆一张订座明细表但对课程设计来说订单详情页直接拆字符串展示座位号比多表关联简单得多答辩也讲得清。3. 核心代码实现从JDBC工具类到DAO层到登录再到列表页3.1 JDBC工具类连接管理和资源释放是地基整个项目里最值得先抄下来的代码就是DBUtil。它把驱动加载、连接获取、资源释放统一收到一个类里后面的DAO不用每一处都写Class.forNamepackage com.cinema.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/cinema ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; private static final String DRIVER com.mysql.cj.jdbc.Driver; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码里的参数每个都有理由。serverTimezoneAsia/Shanghai是MySQL 8驱动的要求不指定时区驱动默认拿UTC查询出来的DATETIME字段会差8小时。场次时间错8小时意味着页面显示和数据库存的对不上答辩演示时直接翻车。驱动类名com.mysql.cj.jdbc.Driver是8.x版本的老教程里写的com.mysql.jdbc.Driver在8.0.11之后已经被移除了。如果你的lib目录下是8.x的jar类名必须用新的如果用5.x的jar类名可以不变但5.x驱动连MySQL 8的默认认证插件会有兼容问题所以建议统一用8.x。资源释放的顺序也有讲究先关ResultSet再关Statement最后关Connection。连接从连接池拿的时候close只是归还给池不会真正断开此时ResultSet和Statement不手动关内存里会堆一堆没释放的结果集对象。3.2 DAO层把SQL从Servlet里拆出去很多不规范的源码喜欢在Servlet里直接写SQL看起来省事但一旦加需求就要大面积改动。标准做法是SQL只出现在DAO层。看UserDao的写法package com.cinema.dao; import com.cinema.entity.User; import com.cinema.util.DBUtil; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.SQLException; public class UserDao { public User findByUsername(String username) throws SQLException { String sql SELECT id, username, password, realname, phone FROM user WHERE username ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return mapRow(rs); } } } return null; } public boolean insert(User user) throws SQLException { String sql INSERT INTO user(username, password, realname, phone) VALUES(?, ?, ?, ?); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, user.getUsername()); ps.setString(2, user.getPassword()); ps.setString(3, user.getRealname()); ps.setString(4, user.getPhone()); return ps.executeUpdate() 0; } } private User mapRow(ResultSet rs) throws SQLException { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setPassword(rs.getString(password)); u.setRealname(rs.getString(realname)); u.setPhone(rs.getString(phone)); return u; } }这里用了JDK 7的try-with-resourcesConnection和PreparedStatement都实现了AutoCloseable方法结束自动关闭代码比手写finally干净很多。mapRow抽成一个私有方法以后加字段只改一处不用每个方法都改rs.getXxx。注意PreparedStatement的占位符。传参一律用setString、setInt不要拼字符串。拼字符串的问题在下面登录代码里会专门展开防SQL注入是答辩时最稳的一个加分点。3.3 登录与注册Servlet、Session和页面跳转登录是系统入口也是Servlet代码最典型的一段。看LoginServletpackage com.cinema.servlet; import com.cinema.dao.UserDao; import com.cinema.entity.User; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { 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); if (username null || username.trim().isEmpty() || password null || password.isEmpty()) { request.setAttribute(error, 用户名和密码不能为空); request.getRequestDispatcher(jsp/login.jsp).forward(request, response); return; } UserDao userDao new UserDao(); try { User user userDao.findByUsername(username); if (user ! null user.getPassword().equals(password)) { request.getSession().setAttribute(loginUser, user.getUsername()); request.getSession().setAttribute(userId, user.getId()); response.sendRedirect(request.getContextPath() /movieList); } else { request.setAttribute(error, 用户名或密码错误); request.getRequestDispatcher(jsp/login.jsp).forward(request, response); } } catch (Exception e) { e.printStackTrace(); throw new ServletException(登录时数据库操作异常, e); } } }request.setCharacterEncoding(UTF-8)必须放在读取参数之前因为POST请求的编码由请求体决定晚一步参数就乱码了。GET请求的乱码问题是另一条路径要改Tomcat的server.xml这个后面避坑章节详细说。这里查用户用的是findByUsername取回来再比对密码。为什么不直接WHERE username? AND password?因为后面要改成密码加盐存储到时候SQL条件没法直接比原始密码必须先拿到用户记录再校验。先在DAO里留出这个结构后面优化会省事很多。代码里有两条跳转路径登录失败forward到login.jsp携带error提示登录成功sendRedirect到movieListforward和sendRedirect的区别是高频面试题也是答辩大概率会问的点。forward是服务端内部跳转浏览器地址栏不变request中的数据能带到目标页sendRedirect是告诉浏览器重新发起一次GET请求地址栏变化原request里的数据全部丢失。登录成功后必须用重定向否则刷新页面会重复提交表单后果在避坑章节会讲。3.4 JSP视图层EL表达式和JSTL把Java代码请出页面登录成功后进入电影列表页。业务逻辑在MovieListServlet里WebServlet(/movieList) public class MovieListServlet extends HttpServlet { Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListMovie movieList MovieDao.findByStatus(1); request.setAttribute(movieList, movieList); request.getRequestDispatcher(jsp/movieList.jsp).forward(request, response); } }对应的JSP页面% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title正在热映/title /head body h2正在热映/h2 c:forEach varmovie items${movieList} div classmovie-card h3${movie.name}/h3 p片长${movie.duration} 分钟/p p${movie.description}/p a href${pageContext.request.contextPath}/movieDetail?id${movie.id}选座购票/a /div /c:forEach /body /htmlEL表达式${movie.name}走的是Movie.getName()方法不是直接读字段。所以实体类加字段后忘了生成getter页面上会取到null这是那种“找不到原因但就是显示空白”的问题排查很久最后发现是个getter缺失。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %这一行依赖jstl-1.2.jar这个jar在lib目录里必须存在。很多源码包不带这个jarJSP一运行就报org.apache.jasper.JasperException: Unable to load class for JSP不是你的代码问题是依赖缺失。工程里的URL拼接用了${pageContext.request.contextPath}这个一定要习惯——它动态获取应用上下文路径这样部署到任何Tomcat目录下都不会因为路径问题404。到这里登录、查列表两个核心链路已经通了。接下来单独开一章说那四个让人debug到怀疑人生的坑。4. 避坑与排查四个让我熬夜调试的问题4.1 ClassNotFoundExceptionMySQL 8的驱动类名迁移现象Tomcat能启动但访问任何涉及数据库的接口控制台直接报ClassNotFoundException: com.mysql.jdbc.Driver。原因lib目录下的mysql-connector-java.jar是8.x版本8.0.11之后官方把驱动类从com.mysql.jdbc.Driver迁移到了com.mysql.cj.jdbc.Driver旧类名直接删掉。源码里如果还写旧类名Class.forName瞬间崩。解决改DBUtil里的驱动类名同时连接串补上serverTimezone参数。如果你的数据库账号加密方式还是caching_sha2_password而驱动版本偏老再执行一条命令把认证插件换回mysql_native_passwordALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;这个坑排查起来其实很快看jar包版本再看代码里的类名两个不匹配就对了。4.2 中文乱码编码链路每一环都要通现象登录后电影名称显示成????后台打印SQL参数也是乱码。原因中文乱码是链条式问题任何一环断了都会出。最常踩的三处JSP头部没指明contentType、Servlet没调用setCharacterEncoding(UTF-8)、数据库连接串缺characterEncodingutf8。另外还有一个隐藏点——Tomcat对GET请求默认编码是ISO-8859-1GET参数里的中文到了Servlet已经乱了这时候在Servlet里设置编码也救不回来。解决四层一起配基本能根治JSP页面头部% page contentTypetext/html;charsetUTF-8 languagejava %Servlet的doGet和doPost第一行各加request.setCharacterEncoding(UTF-8);和response.setCharacterEncoding(UTF-8);JDBC连接串加上useUnicodetruecharacterEncodingutf8Tomcat的conf/server.xml里Connector节点加URIEncodingUTF-8我的习惯是配完这四层写一个测试页插入一条中文数据再读出来把整个编码链路验证通了再继续往下开发而不是写完所有页面才发现乱码再回过来排查是哪一层漏了。4.3 刷新页面导致重复下单POST之后必须重定向现象用户点“确认购票”生成了订单按一下F5刷新数据库里多了一条一模一样的订单。原因购票提交走了forward到结果页。forward不会改变浏览器地址栏URL当前地址还是那个处理订单的Servlet地址F5刷新等于又发了一次POST服务端又把下单逻辑执行了一遍。解决所有写操作处理完之后一律sendRedirect到查询型页面。比如下单成功后重定向到订单详情页URL带上订单号新的请求是GET不会执行第二次下单。这是Web开发里很经典的POST-Redirect-GET模式答辩时把这个词说出来评委就知道你是真跑过项目踩过坑的人。4.4 连接没关导致数据库连接数被打满现象系统跑了一段时间控制台开始报Connection is not available, request timed out同时MySQL的show processlist能看到一大堆Sleep状态的连接。原因DAO里每次getConnection()之后如果前面有SQL异常后面的close()可能根本没走到。加上某些源码直接new Connection而不用连接池高并发请求一多数据库的151个默认连接数瞬间就被占满。解决所有close()放进finally块或者用try-with-resources自动关闭。基础写法Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); ps conn.prepareStatement(sql); // 执行操作 } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); }再进一步接一个连接池这个问题就彻底不出现了。连接池的接入方式放到最后一章讲那是答辩前值得做的事。5. 部署与购票事务从Tomcat跑通到下单不丢数据5.1 部署到Tomcat版本选择和war包方案先说一个2024年之后特别容易踩的坑Tomcat版本。Tomcat 10开始把Java EE的命名空间从javax.*改成了jakarta.*ServletJSP老项目如果用了javax.servlet.http.HttpServlet直接丢到Tomcat 10运行编译期报一堆ClassNotFoundException。对于这套JDBCServlet项目最稳的是Tomcat 9和javax.*完全兼容。如果老师指定了高版本Tomcat那就得把所有javax.servlet导入改成jakarta.servlet工作量不小一般不值得。部署方式两种第一种把整个WebContent目录连同lib下的jar包打包成war文件。eclipse里Export→WAR file或者直接jar -cvf cinema.war *然后丢到Tomcat的webapps目录下启动时自动解压。这种方式适合最终交付。第二种开发期直接在IDE里配置Tomcat Server把项目添加进去IDE自动完成部署和热更新。调试阶段用这个改完JSP刷新就能看效果。启动之前检查三件事MySQL服务是否在跑、DBUtil里的用户名密码是否匹配、lib目录下的mysql驱动jar是否复制到了Tomcat的classpath。最后一个问题常见于直接把项目文件夹拷到webapps下但忘记带lib目录启动时数据库操作全报ClassNotFoundException: com.mysql.jdbc.Driver控制台刷屏但Tomcat本身没崩看半天才能定位到是lib缺失。5.2 购票事务多步更新必须包在同一个事务里购票这个动作牵扯三步检查座位状态、更新座位为已售、插入订单记录。这三步必须在一个事务里完成否则中间任何一步失败数据库就处于脏数据状态——座位卖出去了但没订单或者订单生成了但座位还是空的。看一个事务的标准写法Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 1. 检查座位状态 String checkSql SELECT status FROM seat WHERE id ? AND schedule_id ?; // 2. 更新座位状态为已售 String updateSql UPDATE seat SET status 2 WHERE id ? AND status ! 2; // 3. 插入订单记录 String insertSql INSERT INTO orders(order_no, user_id, schedule_id, seat_ids, total_price) VALUES(?, ?, ?, ?, ?); conn.commit(); // 全部成功统一提交 } catch (SQLException e) { try { if (conn ! null) conn.rollback(); // 任何一步失败全部回滚 } catch (SQLException ignored) {} e.printStackTrace(); } finally { DBUtil.close(null, null, conn); }这里有一个关键点更新座位时SQL里带了status ! 2条件executeUpdate返回0说明座位已经被别人买走了这种情况要主动抛异常触发回滚。这就是乐观锁的思路——不提前锁表更新时检查条件冲突直接放弃。用SELECT ... FOR UPDATE是更强硬的做法但课程设计里容易引出死锁问题我一般建议用status ! 2这种条件更新方式。事务代码写完之后可以手动做一个测试验证批量性能开两个浏览器窗口同时操作同一个座位一个成功另一个必须看到“已被购买”的提示而不是两个都下单成功。5.3 账户与连接参数部署前的最后配置检查部署前最后过一遍配置文件配置项建议值说明mysql连接串useSSLfalse本地开发不需要SSLserverTimezoneAsia/Shanghai避免时间差8小时MySQL password与代码一致两个地方必须同步Tomcat端口8080被占用时改server.xmlJSP页面编码UTF-8三处编码保持一致MySQL 8的默认认证插件对老驱动不友好如果你发现本地跑通的项目换一台机器Access denied报错优先检查认证插件和驱动版本的兼容组合。平时准备一个好用的MySQL管理工具比如Navicat或DBeaver导入sql、看数据、调整权限都比命令行高效很多。6. 答辩前值得做的三个优化连接池、密码加盐和一个验证习惯6.1 用Druid连接池替换原生JDBC连接原生DriverManager.getConnection每调用一次就建一个物理连接性能差且连接数不可控。答辩前花半小时接入一个连接池效果立竿见影。以Druid为例把DBUtil改造一下private static DruidDataSource dataSource; static { dataSource new DruidDataSource(); dataSource.setUrl(URL); dataSource.setUsername(USER); dataSource.setPassword(PASSWORD); dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(5); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }DBUtil.close不用改因为池化连接在close时只是归还给连接池不是真的断开但你仍然需要手动关闭ResultSet和Statement否则长期跑下来结果集对象堆积。接入连接池后之前说的“连接打满”问题基本绝迹。6.2 用户密码加盐存储直接明文存密码是最容易被答辩老师挑刺的一点。把密码改成加盐存储public static String encrypt(String password, String salt) { String input salt password; return DigestUtils.md5Hex(input); // 或用SHA-256 }注册时生成随机salt存到用户表的salt字段。登录时按用户名查出salt再对输入密码做同样的加密与库里存的密文比对。这个改动只涉及注册和登录两处逻辑DAO层SQL不用大改但加了这个细节整个项目的安全意识就上一个档次。6.3 一个验证习惯做完以上优化建议你走一遍完整的验收清单注册新账号、用错误密码登录一次、正常登录、查看电影列表、选座下单、支付、查订单、刷新页面确认没有重复订单全部过一遍再准备答辩。我之前就吃过亏项目本地跑得好好的答辩前换了台演示机器Tomcat和MySQL装好后忘了改DBUtil里的数据库密码演示时第一个登录接口就报错在台上满头大汗改配置那感觉太狼狈了。从那以后我每次换机器部署都强制走一遍这个清单先从数据库连接验证开始再一个接口一个接口测确认无误才敢上台。这个习惯救了我好几次希望帮到你。本文还有配套的精品资源点击获取