简介:这是一份基于JSP技术的网上书店系统毕业设计文档,面向计算机相关专业学生、毕业设计选题者及初学JavaWeb开发的人员。文档从系统需求分析、功能模块划分、数据库模式设计到前后端实现作了完整阐述,涵盖图书展示、在线订购、后台图书管理、订单处理等典型电子商务功能,并对比了JSP与ASP在安全性、性能及跨平台方面的差异。资源为1份doc文档,整体包大小约1.09MB,文字内容完整,便于查阅和参考目录结构展开学习。目前已有352人学习下载。通过阅读这份设计,读者可以了解基于JSP+MySQL构建B2C书店系统的整体流程,包括数据库表结构设计思路、关键功能模块划分、以及毕业设计论文的撰写框架,适合用于开题参考、系统设计借鉴或论文格式参照。
1. 基于JSP的网上书店系统,值不值得在毕设里选它
毕业季的Java Web选题里,“基于JSP的网上书店系统的设计与实现”几乎是每年都能见到的面孔。需求听着简单,做起来翻车最多的反而是一堆细节:表结构怎么设计、购物车放Session还是数据库、下单要不要开事务。这套系统的价值在于能把JSP、Servlet、JDBC和MySQL在一个完整项目里串起来,适合刚学完Java Web、想要一份能跑通也能答辩的毕业作品的读者。后面按“建表分层 → 会员模块 → 图书与订单 → 部署排错 → 安全收尾”的顺序展开,能照着做,也能拿来应付答辩老师对原理的追问。
2. 表结构设计与JSP分层先行:五张表撑起网上书店全部流程
2.1 网上书店的ER图与五张核心表
网上书店在毕设里算体量适中的题目:需求容易讲清楚,功能无外乎用户注册登录、图书浏览、加入购物车、下单、后台管理,数据库五张表就能全覆盖。常见做法是先画ER图,把实体和关系理清,再落成建表SQL。表设计是整个系统的地基,表设计翻车,后面代码全要返工。
用户、图书、购物车、订单、订单明细是五张核心表。购物车表可以先用Session实现而不建表,但作为设计与实现的完整方案,建议还是建上,哪怕代码里暂时用Session,结构上至少有据可查。订单和明细必须分开:一个订单对应多条明细,明细里保存下单时的书名和价格快照,避免图书改价后历史订单跟着变。
CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT, book_name VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10, 2) NOT NULL, stock INT DEFAULT 0, cover_image VARCHAR(255), category VARCHAR(30), description TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES users(id), CONSTRAINT fk_cart_book FOREIGN KEY (book_id) REFERENCES books(id) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10, 2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_order_user (user_id) ); CREATE TABLE order_items ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, book_id INT, book_name VARCHAR(100), price DECIMAL(10, 2), quantity INT, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id) );建表SQL里有几个参数值得注意。username加UNIQUE约束,注册时数据库层就能拦住重复账号,业务代码里只需捕获异常。price用DECIMAL(10,2)而不用FLOAT,金额计算不会出现累积误差。order_no用VARCHAR(32)存业务订单号,自增id只作为内部主键对外不暴露。订单明细冗余book_name和price,这是电商系统常见的快照思路。status字段的TINYINT注释,方便答辩时解释状态机。
2.2 JSP项目不只有JSP:Model 2分层与包结构
很多初学者把JSP当成了全部,写出的页面里又是连数据库又是写业务,这就是Model 1模式。项目小时能跑,页面一多就全乱。正规的做法是Model 2,也就是MVC:JSP只负责展示,Servlet负责接收请求和控制跳转,DAO负责数据库操作,JavaBean负责封装数据。
这套分层对于毕业设计非常有利,论文里的“系统设计”部分可以直接画三层架构图,答辩时老师问“为什么用MVC”,也能答出职责分离、便于维护的点。我一般会建下面这个包结构:
src/main/java/com/bookstore/ ├── bean/ # User, Book, CartItem, Order ├── dao/ # UserDAO, BookDAO, OrderDAO ├── servlet/ # UserServlet, BookServlet, CartServlet, OrderServlet ├── util/ # DBUtil src/main/webapp/ ├── index.jsp ├── login.jsp ├── register.jsp ├── bookList.jsp ├── cart.jsp ├── orderConfirm.jsp ├── WEB-INF/web.xml └── css/各层依赖关系是单向的:Servlet调用DAO,DAO返回Bean,JSP通过EL表达式读取Bean。反过来不行,JSP不能直接写JDBC代码,这是这条目录结构的基本纪律。新人最容易犯的错就是图省事在JSP里写Java代码,最后页面改一个按钮都要重新编译,完全没有分层意义。
Tomcat的web.xml也要在项目初期配好,我默认放4.0版本的头部,兼容Servlet 3.1以上注解扫描:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <display-name>bookstore</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> <session-config> <session-timeout>30</session-timeout> </session-config> </web-app>web-app_version="4.0"决定了容器按Servlet 4.0规范解析@WebServlet注解。如果头版本写成2.5或更低,注解可能被忽略,所有Servlet只能靠XML配置,这是后面部署时的一个大坑。session-timeout设30分钟,默认值就够了。
2.3 用DBUtil统一管理数据库连接
数据库连接不能散落在每个DAO里,工具类集中管理是入门项目最稳妥的方案。用DriverManager直连是JSP毕设最常见做法,代码简单、原理好讲,后续可平滑换连接池。
package com.bookstore.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf-8&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.jdbc.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) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }URL参数里useUnicode=true和characterEncoding=utf-8必须成对出现,少一个都可能让插入的中文变乱码。useSSL=false避免MySQL 8.x连接时报警告。驱动类com.mysql.jdbc.Driver对应MySQL 5.x的驱动jar;如果你用的是MySQL 8.x,要改成com.mysql.cj.jdbc.Driver,否则启动报ClassNotFoundException。这里我吃过亏,升级数据库后忘了同步改驱动名,排查了半个下午才发现。
关闭连接的顺序是ResultSet、Statement、Connection,理论上前者关闭后后者才能安全释放。用try-with-resources可以简化,但如果要配合手动事务,DBUtil里的close仍然有用,因为事务提交后连接要回到连接池或真正关闭。
3. 用IDEA新建JSP项目到会员模块落库:注册、登录与个人信息展示页面
3.1 从IDEA新建JSP项目开始
IDEA里新建JSP项目有两条路。一条是新建时选Java Enterprise,勾选Web Application,但这会引入Jakarta EE的包名,和Tomcat 8.5的javax包冲突,运行时报NoClassDefFoundError。另一条更稳妥:新建普通Java项目,然后右键项目选择Add Framework Support,勾选Web Application,再手动配置Tomcat。
推荐用第二种方式,包名是javax.servlet,对Tomcat 8.5兼容性最好。项目建好后检查Project Structure里的Artifacts,确认生成了Web Application Exploded结构。运行配置里选Tomcat Server,Deployment选项卡把项目加到Server下,Application context设为/bookstore,访问路径就是http://localhost:8080/bookstore。
这个步骤最容易出问题的就是Artifacts。IDEA里代码编译正常,Run却提示404,基本是Artifacts没配置好,项目没被打成war或exploded目录。检查Artifacts栏里有没有项目名:war exploded,没有就手动新建一个Web Application类型的Artifacts。
3.2 UserDAO + UserServlet:登录的黑匣子拆开看
登录功能的完整链路是:login.jsp表单POST到UserServlet,UserServlet调用UserDAO查询数据库,成功就把User对象放进Session,跳转到首页;失败则携带错误信息转发回登录页。这套流程看着简单,但每一层都有细节。
先写UserDAO,这是数据访问的核心:
package com.bookstore.dao; import com.bookstore.bean.User; import com.bookstore.util.DBUtil; import java.sql.*; public class UserDAO { public User findUser(String username, String password) { String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(rs.getString("username")); user.setNickname(rs.getString("nickname")); user.setPhone(rs.getString("phone")); return user; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }SQL用?占位符配合PreparedStatement,而不是字符串拼接,这是JSP项目里最基础也最重要的安全习惯。字符串拼接的SQL一旦用户名里输入' or '1'='1,登录黑匣子就被撬开了。PreparedStatement除了防注入,还能预编译提升执行效率,参数setString和setInt的序号从1开始。
再写UserServlet,用@WebServlet注解注册路由:
package com.bookstore.servlet; import com.bookstore.bean.User; import com.bookstore.dao.UserDAO; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/login") public class UserServlet extends HttpServlet { private UserDAO userDAO = new UserDAO(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("utf-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDAO.findUser(username, password); if (user != null) { HttpSession session = req.getSession(); session.setAttribute("loginUser", user); resp.sendRedirect("index.jsp"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("login.jsp").forward(req, resp); } } }注意两个细节。第一,doPost里第一行必须调req.setCharacterEncoding("utf-8"),不调的话中文用户名在POST参数里就是乱码。第二,登录成功用sendRedirect,失败用forward。成功后如果还用forward,刷新页面会重复提交表单;重定向让浏览器发起新请求,地址栏变成index.jsp,刷新也不会重复登录。
3.3 注册去重与个人信息展示页面
注册逻辑比登录多一步:先查用户名是否已存在,再插入新用户。数据库有UNIQUE约束,但业务层先查一次,能让错误信息更友好。
public boolean register(User user) { String checkSql = "SELECT COUNT(*) FROM users WHERE username = ?"; String insertSql = "INSERT INTO users (username, password, nickname, phone) VALUES (?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement checkPs = conn.prepareStatement(checkSql)) { checkPs.setString(1, user.getUsername()); try (ResultSet rs = checkPs.executeQuery()) { if (rs.next() && rs.getInt(1) > 0) { return false; } } try (PreparedStatement insertPs = conn.prepareStatement(insertSql)) { insertPs.setString(1, user.getUsername()); insertPs.setString(2, user.getPassword()); insertPs.setString(3, user.getNickname()); insertPs.setString(4, user.getPhone()); return insertPs.executeUpdate() == 1; } } catch (SQLException e) { e.printStackTrace(); return false; } }注册接口的返回boolean,Servlet再根据结果决定跳转还是回显错误,逻辑清晰。密码存的是明文,这在毕设里常见,但答辩容易被追问。第6章会给一个MD5加盐的改进版本,那才是能拿得出手的写法。
个人信息展示页面,也就是热词里常见的“jsp个人信息展示页面”,核心是把Session里的User读出来展示。JSP有两种做法,传统的是useBean标签:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <jsp:useBean id="loginUser" scope="session" class="com.bookstore.bean.User" /> <html> <body> <h2>个人中心</h2> 欢迎您:<jsp:getProperty name="loginUser" property="nickname" /> <p>手机号:<jsp:getProperty name="loginUser" property="phone" /></p> </body> </html>useBean标签看着正式,但有个隐患:session里没有loginUser属性时会new一个空的User对象,页面显示null。更推荐EL表达式:${loginUser.nickname},属性为null时JSP页面显示空字符串,同时配合<c:if>标签判断是否登录,未登录跳回login.jsp。EL表达式在答辩时也是加分项,说明你用了JSP 2.0之后的规范。
4. 把图书列表、购物车和订单串起来:分页、Session购物车与下单事务
4.1 图书列表分页:一次查一页,别一次性查全表
图书列表是网站的门面。最直观的写法是SELECT * FROM books把全部图书查到List里,再在JSP页面循环输出。数据量小没问题,一旦图书几百本,页面明显变慢。分页是这里必须有的设计,也是答辩老师最常问的功能点之一。
分页的核心参数是currentPage和pageSize,SQL用LIMIT子句控制偏移量:
public List<Book> findBooksByPage(int currentPage, int pageSize) { List<Book> books = new ArrayList<>(); String sql = "SELECT * FROM books ORDER BY id LIMIT ?, ?"; int offset = (currentPage - 1) * pageSize; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book book = new Book(); book.setId(rs.getInt("id")); book.setBookName(rs.getString("book_name")); book.setAuthor(rs.getString("author")); book.setPrice(rs.getBigDecimal("price")); book.setStock(rs.getInt("stock")); books.add(book); } } } catch (SQLException e) { e.printStackTrace(); } return books; }LIMIT的第一个参数是偏移量,从0开始。第1页offset是0,第2页是pageSize,公式(currentPage - 1) * pageSize要记牢。如果直接传currentPage进去,第2页会把第pageSize条到第2*pageSize条的数据错位展示,这是分页最典型的翻车点。
还要统计总数计算总页数。BookDAO里加一个countBooks()方法,执行SELECT COUNT(*) FROM books,然后Servlet里算totalPages = (total + pageSize - 1) / pageSize。这个公式用向上取整避免最后一页少算。BookServlet里把books、currentPage、totalPages都set进request,forward到bookList.jsp,页面底部渲染上一页、下一页和页码列表。
4.2 购物车放Session还是数据库:先选型再写AddCartServlet
购物车是网上书店最有讨论价值的设计点。常见方案有两种:存Session和存数据库表。存Session简单,购物车跟着用户会话走,不需要额外的表操作,但清浏览器或会话过期就丢。存数据库持久化好,但每次加购都要读写cart表,还要处理未登录用户。
毕设项目我建议用Session,理由有三:需求简单不需要跨设备同步;代码量少一半;Session本身就是HttpSession,答辩解释“购物车属于临时性数据”说得通。如果做的是带会员等级的完整商城,再考虑落库。下面的AddCartServlet是Session方案的典型写法:
@WebServlet("/addCart") public class AddCartServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int bookId = Integer.parseInt(req.getParameter("bookId")); HttpSession session = req.getSession(); LinkedHashMap<Integer, CartItem> cart = (LinkedHashMap<Integer, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<>(); } CartItem item = cart.get(bookId); if (item == null) { item = new CartItem(); item.setBookId(bookId); item.setQuantity(1); cart.put(bookId, item); } else { item.setQuantity(item.getQuantity() + 1); } session.setAttribute("cart", cart); resp.sendRedirect("cart.jsp"); } }用LinkedHashMap而不是List存购物车,是一个值得说的细节。Map的key是bookId,判断“这本书已经在购物车里”只需一次get,时间复杂度从O(n)降到O(1),同时LinkedHashMap保持插入顺序,页面展示按加入顺序排列。CartItem里只需要bookId和quantity,书名价格在cart.jsp展示时再查库,这样购物车只存最小必要信息,不存一份可能过期的图书价格快照。
Session购物车的坑在于,用户未登录也能加购,但下单时必须有登录用户。跳转下单页面时要判断session里有没有loginUser,没有就重定向到login.jsp并在URL里带returnUrl参数,登录成功再跳回来。这个逻辑属于在线书店购物流程的必修课,能写进设计说明里。
4.3 下单事务:多张表一起写,回滚才是后悔药
下单是整个系统里唯一涉及多张表写入的操作:生成订单主表记录、生成订单明细、扣减图书库存、清空购物车。四件事必须同时成功或同时失败,这就是数据库事务。少了事务,订单建好了库存没扣,或者订单明细写一半订单主表没提交,系统就处在脏数据状态。
订单DAO的写法要注意,Connection不能由每个方法自己获取又自己关闭,必须由下单逻辑统一控制:
public boolean createOrder(int userId, double totalPrice, List<CartItem> cart) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String insertOrder = "INSERT INTO orders (order_no, user_id, total_price, status) VALUES (?, ?, ?, 0)"; PreparedStatement psOrder = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, generateOrderNo()); psOrder.setInt(2, userId); psOrder.setDouble(3, totalPrice); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); int orderId = 0; if (keys.next()) { orderId = keys.getInt(1); } String insertItem = "INSERT INTO order_items (order_id, book_id, book_name, price, quantity) VALUES (?, ?, ?, ?, ?)"; for (CartItem item : cart) { PreparedStatement psItem = conn.prepareStatement(insertItem); psItem.setInt(1, orderId); psItem.setInt(2, item.getBookId()); psItem.setString(3, item.getBookName()); psItem.setBigDecimal(4, item.getPrice()); psItem.setInt(5, item.getQuantity()); psItem.executeUpdate(); } String updateStock = "UPDATE books SET stock = stock - ? WHERE id = ? AND stock >= ?"; for (CartItem item : cart) { PreparedStatement psStock = conn.prepareStatement(updateStock); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getBookId()); psStock.setInt(3, item.getQuantity()); int rows = psStock.executeUpdate(); if (rows == 0) { throw new SQLException("库存不足,事务回滚"); } } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } e.printStackTrace(); return false; } finally { DBUtil.close(null, null, conn); } }这段代码有三个关键点。第一,setAutoCommit(false)之后,所有SQL都在同一个事务里,直到commit才真正生效。第二,扣减库存的SQL带了AND stock >= ?条件,这条SQL的返回行数既是结果也是校验——库存不足时更新0行,直接抛异常触发回滚,从根上防止超卖。第三,Statement.RETURN_GENERATED_KEYS配合getGeneratedKeys()拿到自增主键,订单明细才能关联到正确的订单。
事务的console日志在出错时能看到SQLException,但看不到具体是哪一步失败。建议在catch块里把e.getMessage()打出来,同时把事务里的每一步包成try-catch分别记录,排查时会少走很多弯路。这也是我早期做项目时不重视的细节:事务一旦回滚,没有日志等于黑匣子,只能靠猜。
5. JSP网上书店部署避坑:打war包到Tomcat常踩的6个坑
5.1 页面中文全变问号:编码不统一
现象:本地IDEA里运行一切正常,部署到独立Tomcat后,所有中文内容变成问号。
原因:开发环境IDEA默认UTF-8,但Tomcat处理请求和响应的编码不一致。Tomcat 8.5后GET请求默认UTF-8,POST请求默认ISO-8859-1;JSP页面本身的pageEncoding没设UTF-8时,Tomcat也按ISO-8859-1读。
解决:三处都改成UTF-8。JSP文件头部写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;写一个全局过滤器,在doFilter里对request和response都调setCharacterEncoding("utf-8");必要时在server.xml的Connector上加URIEncoding="UTF-8"。只改其中一处,问题还会在别的页面复现。
5.2 IDEA里能跑,部署到Tomcat却ClassNotFound
现象:本地运行正常,把war包丢到Tomcat的webapps目录启动,报java.lang.ClassNotFoundException: com.mysql.jdbc.Driver。
原因:IDEA运行项目时,驱动jar在IDEA的Classpath里;打war包时,jar没有打进WEB-INF/lib目录。Artifacts的输出布局里默认只打包编译后的class和资源,外部库需要手工加进去。
解决:Project Structure → Artifacts → Output Layout,右键选择Put into Output Root或加lib目录,把mysql-connector-java.jar放进去,重新Build Artifacts。检查war包可以用压缩软件打开看WEB-INF/lib下有没有jar,这个习惯能省大量部署时间。
5.3 @WebServlet没生效:web.xml版本太老
现象:访问/login地址404,Tomcat控制台没有报错。
原因:web.xml的头部声明是Servlet 2.5甚至2.3版本,容器按老规范解析,@WebServlet注解被忽略。
解决:把web.xml头部换成4.0版本,就是第2章写的那份。如果项目里没有web.xml,Tomcat默认按Servlet 4.0处理,注解反而能用。所以出现注解不生效时,先别怀疑代码,把web.xml版本号改了或者删掉重试。
5.4 JSP改了不生效:Artifacts没重新构建
现象:修改了JSP页面,浏览器强制刷新还是旧内容。
原因:IDEA的Run tomcat时,Tomcat看到的是Artifacts输出目录里的副本。修改了源码树下的JSP,没有重新构建Artifacts,副本还是旧文件。
解决:菜单Build → Build Artifacts → Rebuild,然后重启Tomcat。更省事的方法是给Tomcat配置部署方式时选择war exploded,并把Application context下的目录指向项目输出,配合IDEA的Deployment选项。这个问题不算难,但在毕设冲刺阶段改页面最频繁,每次都要等rebuild,提前养成顺手rebuild的习惯能省不少火气。
5.5 orders表名与MySQL保留字冲突
现象:执行INSERT INTO orders一直报syntax error near 'orders',检查多遍SQL看不出问题。
原因:MySQL里ORDER是保留字,表名orders虽然没有被完整保留,但某些版本和模式下,和保留字相近的表名也会在语句解析时出问题。
解决:统一用反引号包住表名:INSERT INTO \orders``。或者干脆把表名改成t_order、book_order这种带前缀的名字,一劳永逸。这个坑在MySQL 5.7和8.0都遇到过,建议建表时就避开,后面写SQL不折腾。
5.6 nginx不能直接跑JSP:别指望反代代劳
现象:试图用nginx直接部署JSP项目,访问.jsp文件时要么返回源码要么下载文件。
原因:nginx本身不是Servlet容器,它不解析JSP。JSP文件的编译和执行必须由Tomcat这类Servlet容器完成,nginx只负责静态文件和反向代理转发。
解决:网上书店的常规部署是nginx监听80端口,静态资源(css、js、图片)由nginx直接响应,动态.jsp请求通过proxy_pass转发给Tomcat的8080端口。Tomcat处理完把响应返回nginx,nginx再回给浏览器。毕设答辩演示时,直接让Tomcat监听8080就行,nginx不是必须项。但知道nginx只能代理不能解析JSP,面试和答辩时能体现你对部署架构的理解。
6. 给网上书店补一层答辩底气的安全与性能写法
Session购物车虽然简单,但有两件事决定这套系统能不能从“跑得通”升级到“讲得清”。第一是数据库连接池,第二是密码加密。
JDBC直连在每次请求时都经历“创建连接→执行SQL→销毁连接”,压力一大就慢。常见做法是换成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.setMaxWait(3000); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }initialSize是连接池启动时预建的连接数,maxActive是最大连接数,maxWait是拿不到连接时的最大等待毫秒数。三个参数分别回答“池子里常备几个连接”“最多允许多少并发”“排队等多久就放弃”,答辩时能讲清参数含义就是加分项。
密码加密是另一个必改点。登录表里存明文密码,放在本地项目没人看,一旦部署到公网就是漏洞。最简单可靠的写法是MD5加盐:
public static String md5WithSalt(String password, String salt) { String text = salt + password; return DigestUtils.md5Hex(text); }salt可以用用户名,注册时把salt + 明文的MD5值入库。登录时先用同样的规则算出哈希再比对。这版完整流程是:用户注册 → 计算md5(username + password)→ 存入password列;登录时 → 用输入的用户名和密码计算哈希 → 与库里的列比对。数据库泄露了,攻击者拿到的也是哈希,比明文安全一个量级。
最后说个做毕设期间的教训:我当时把全部精力放在把功能跑通上,到了答辩演示,老师一句“你密码怎么存、并发下单会不会超卖、购物车刷新之后还在不在”,才发现靠裸DriverManager、明文密码、无事务的版本连及格线都勉强。后来补上连接池、事务和MD5加盐,代码结构才真正立住。这套JSP网上书店做的不是发明新框架,而是把每个环节用最标准的方式串起来,这个过程本身比功能值钱。希望帮到你。
本文还有配套的精品资源,点击获取