JSP网上书店系统开发全流程:从表结构到事务与避坑实战 简介一份基于JSP和MySQL技术实现的网上书店系统毕业设计文档面向计算机相关专业学生、Java Web初学者及需要完成类似课题的开发者。该毕业作品从典型电子商务应用场景出发对网上书店系统进行了全面的可行性研究与需求分析将系统划分为图书分类、用户注册登录、图书搜索、购物车管理、在线下单、订单处理以及后台图书维护等功能模块并针对各模块给出了详细的设计思路确保系统能够满足网上购书、图书信息展示、后台图书管理和订单处理等实际需求。文档在技术层面采用JSP作为动态网页开发技术搭配MySQL数据库存储与检索业务数据重点阐述了JSP相较于ASP在安全性、性能及跨平台支持上的优势同时展示了数据库表结构设计、前后端数据交互逻辑及关键代码实现段落。资源为1个doc文件包体大小仅1.09MB文件内含中英文摘要、目录、正文和参考文献结构清晰方便按章节浏览和修改复用。已有352人学习对于希望快速掌握JSP电商开发流程、理解MVC分层思想以及撰写毕业设计论文的读者具有较高的参考价值。1. 网上书店系统用JSP做到底在做一个什么作品网上书店系统是JSP课程设计里出现频率最高的课题之一一个前台让用户浏览图书、搜索、加入购物车、下单一个后台让管理员管理图书分类、库存和订单。很多同学觉得“这太老土”转头去写Spring Boot前端分离项目结果答辩时老师一句“你的购物车数据存在哪里并发情况下库存怎么保证”就沉默了。JSP这套技术方案虽然老但它把HttpSession、请求转发、JDBC事务、MVC分层这些最底层的东西全部暴露在眼前反而更容易讲清楚。这篇笔记按我自己的开发顺序从表结构到页面渲染、从分页到避坑把这个课题的完整落地路径拆给你。2. 先把架子搭起来JSPServletDAO的分层设计与环境选型2.1 为什么选JSP而不是纯Servlet或纯模板引擎JSP本质上就是Servlet被Tomcat编译成Java类再运行。你写一段JSP页面Tomcat会自动把它转成Servlet把HTML代码用out.write()一行行写出来。所以网上书店这种项目用JSP做展示层是成本最低的前端页面可以直接套HTML模板里面嵌JSTL标签或Java脚本不需要像纯Servlet那样用字符串拼接页面否则一个购物车页面能写到你想哭。有人会问现在网上书店类作品很多都用Spring Boot Thymeleaf为什么还抱着JSP我在帮学生改毕设时见过不少类似标题的团队作品比如“基于SpringBoot的图书借阅管理系统”“基于SpringBoot Vue商品管理系统”它们采用了前后端分离代码量大但答辩时很少能把“会话管理”讲透。而JSP方案里的Session、Cookie、Servlet生命周期都是教学大纲里的硬骨头老师也更容易针对性地追问。你不要觉得技术旧关键是答辩时你能扛住问。纯Servlet不是不能做但你要把前端页面拆成很多out.print(tr)一旦页面结构调整Java代码跟着改维护成本暴涨。JSP则允许你把HTML和Java逻辑混杂再配合JSTL和EL表达式把页面上的c:forEach循环和${book.price}这类取值写成接近前端模板的形态。另外Thymeleaf、FreeMarker这类模板引擎虽然更现代但很多学校教材没有把它们纳入课程体系你用了反而可能被认为是“从GitHub抄的”解释成本变高。所以我的建议很直接课程设计用JSPServletJavaBean把数据访问独立成DAO这就是最稳妥的组合。如果项目里还有定时任务、消息队列这些需求那才需要考虑换框架网上书店这个规模完全不需要。2.2 分层设计页面层、控制层、数据层的职责边界一个网上书店按照什么样的层级划分决定你后面写代码会不会把自己绕进去。常见做法是三层JSP页面负责收集参数和显示结果Servlet负责接收请求、调用业务逻辑、跳转页面DAO负责数据库增删改查。业务逻辑如果只有“判断库存是否充足”“计算订单总价”这种程度可以写在Servlet里但为了答辩时能说“面向接口编程”我一般会在Servlet和DAO之间多加一个Service层虽然它可能很薄但设计模式就自然出来了。我见过很多半成品项目的问题出在职责混乱JSP里直接写% Connection conn DBUtil.getConnection(); %做查询Servlet里又写HTML输出最后整个项目没有一个文件是干净的。规范的做法是JSP里禁止出现Java业务代码只通过EL表达式和JSTL取值Servlet里不写SQLDAO里不出现HttpServletRequest。这样即使团队两个人开发前端改JSP、后端改Servlet也不会互相踩脚。目录结构可以参考下面这个我在实际搭项目时会用Maven管理虽然不是必需但依赖管理更省事尤其是mysql-connector和jstl两个jar包的版本冲突在普通Web项目里很常见src/main/java/com/bookstore/ ├── controller/ # Servlet只做请求分发 │ ├── LoginServlet.java │ ├── RegisterServlet.java │ ├── BookListServlet.java │ ├── CartServlet.java │ └── OrderServlet.java ├── service/ # 业务逻辑 │ ├── UserService.java │ ├── BookService.java │ └── OrderService.java ├── dao/ # 数据访问 │ ├── UserDAO.java │ ├── BookDAO.java │ └── OrderDAO.java ├── entity/ # 实体类User、Book、CartItem、Order ├── filter/ # 编码过滤器、登录过滤器 ├── util/ # DBUtil、分页工具类 src/main/webapp/ ├── index.jsp ├── book_list.jsp ├── cart.jsp ├── order_confirm.jsp ├── admin/ ├── static/ # css、js、图片 └── WEB-INF/ ├── web.xml └── jstl.jar这个结构里每个包只干一件事答辩画架构图时也可以直接照着说controller - service - dao - database箭头是单向的不允许反向依赖。注意WEB-INF目录下放jstl的tld文件或jar包页面上的% taglib %指令才能生效。2.3 环境选型Tomcat版本、JDK版本、MySQL版本怎么搭配环境搭配上我常用的组合是JDK 8 Tomcat 8.5/9 MySQL 5.7/8 Eclipse或IntelliJ IDEA。如果你的学校机房还是老版本教材Tomcat 7也能跑但JDBC驱动连MySQL 8会有问题建议直接上Tomcat 9、JDK 8/11学到的内容也不会过时太多。注意两个坑很多人容易踩MySQL 8.0版本驱动名是com.mysql.cj.jdbc.Driver不是老的com.mysql.jdbc.Driver同时连接串必须带serverTimezoneAsia/Shanghai否则报时区错误。这两个参数在第5章避坑部分会专门展开。另外一个坑是Tomcat 9用Servlet 4.0规范你的Servlet要用javax.servlet包下的类不是jakarta.servletTomcat 10开始才换成jakarta。很多教程贴的是老写法你不注意就会引入一个Tomcat 10用的Servlet jar包结果运行时ClassNotFound。URL连接参数列举如下useUnicodetruecharacterEncodingutf8这两个参数直接解决数据库层面的中文乱码jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai其中useSSLfalse是关闭SSL加密握手本机开发时能减少一层连接失败概率serverTimezone必须与你系统时区一致否则MySQL会拿它当前的SESSION.time_zone做时间换算导致订单时间差8小时。如果你用的是MySQL 5.7驱动名可以保持com.mysql.jdbc.Driver但为了兼容8.0还是建议从一开始就写com.mysql.cj.jdbc.Driver。Maven项目的话在pom.xml里加依赖时注意scopemysql-connector-java的scope是runtime或compilejstl依赖用compileservlet-api用provided因为Tomcat自带了Servlet实现你再打进去会造成冲突。这个细节能避免很多“为什么Tomcat启动后项目找不到Servlet类”的报错。3. 核心功能拆解图书展示、购物车、订单与用户会话3.1 图书列表分页与模糊查询DAO层的关键写法网上书店最核心的页面就是图书列表它同时承担“分页浏览”和“按书名模糊搜索”两个能力。分页实现不算难原则是SQL层做分页不要一次查出全表然后在内存里截取。MySQL的LIMIT offset, count配合JDBC的PreparedStatement参数绑定即可。下面这段是BookDAO里面的分页模糊查询方法也是我平时会用的写法public ListBook findBooksByPage(String keyword, int currentPage, int pageSize) throws SQLException { String sql SELECT * FROM books WHERE title LIKE ? LIMIT ?, ?; ListBook list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, (currentPage - 1) * pageSize); ps.setInt(3, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Book book new Book(); book.setBookId(rs.getInt(book_id)); book.setTitle(rs.getString(title)); book.setAuthor(rs.getString(author)); book.setPrice(rs.getBigDecimal(price)); book.setStock(rs.getInt(stock)); book.setCoverUrl(rs.getString(cover_url)); list.add(book); } } return list; }这段代码里最关键的是LIMIT ?, ?的两个参数第一个offset是当前页第一条记录的位置假设每页显示8条第3页的offset就是(3-1)*816第二个pageSize是每页记录数。分页时还需要一条SELECT COUNT(*) FROM books WHERE title LIKE ?来得到总记录数用于计算总页数totalPages (int) Math.ceil(total * 1.0 / pageSize)。我曾经见过有人不写count查询而是用上一次查询结果的数量判断是否还有“下一页”这种方式一旦过滤条件变化就会出错不推荐。注意关键字搜索时一定要用PreparedStatement的?参数拼接不要用字符串拼成SELECT * FROM books WHERE title LIKE % keyword %。否则用户输入一个单引号SQL就被注入轻则查询报错重则被人拖走整张用户表。毕业论文查重时“防止SQL注入”是一个现成的安全卖点。3.2 购物车数据结构放Session还是数据库的取舍购物车是网上书店系统里最值得辩论的设计点。常见做法是“未登录时购物车存Session登录后合并进数据库”但作为课程设计你完全可以简化成“全部存Session”只要在答辩时能说清楚你的选择理由即可。我自己的做法是Session为主数据库只保留历史订单不单独建购物车表。原因是毕设项目没有跨设备同步需求Session天然能区分每个用户代码量还少。购物车的Java对象可以简单定义为下面这样public class CartItem { private Integer bookId; private String title; private BigDecimal price; private Integer stock; private Integer quantity; private BigDecimal subtotal; // 小计通常在getter里算 public BigDecimal getSubtotal() { return price.multiply(BigDecimal.valueOf(quantity)); } }购物车在Servlet里的操作就是维护一个HttpSession中名为cart的ListCartItem列表。每次加入图书前先遍历列表找到该书就更新数量找不到就新增。这里有个隐藏问题修改购物车时不要直接改Session里的引用对象应当先取出来拷贝一份操作完再放回Session否则并发情况下同一个Session的请求可能相互覆盖。课程设计虽然并发量低但这个细节写进论文里很加分。放数据库的另一种方案是额外建一张cart表和cart_item表按用户ID保存。它的优势是重启Tomcat后购物车不丢失也能实现多端共用但代价是每一次加购、改数量都要写SQL而且还要处理“用户未登录”的临时购物车复杂度上升不少。如果你的老师特别喜欢追问数据持久化再考虑数据库方案。3.3 订单生成与库存扣减事务是最后的底线订单模块是整个系统最容易扣分的点。因为“下单”至少要做两件事往orders表插入一条订单记录同时扣减books表的库存。如果这两条SQL中间任何一条失败系统就会库存对不上。正确做法是用同一个Connection开始事务执行多个SQL最后统一提交或回滚。事务代码的核心结构public int createOrder(int userId, ListCartItem items) throws SQLException { Connection conn null; PreparedStatement psOrder null; PreparedStatement psItem null; PreparedStatement psStock null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关掉自动提交开启事务 psOrder conn.prepareStatement(INSERT INTO orders(user_id, total_price, status) VALUES(?,?,?), Statement.RETURN_GENERATED_KEYS); // 先计算totalPrice再设置参数 psOrder.setInt(1, userId); psOrder.setBigDecimal(2, totalPrice); psOrder.setString(3, 待付款); psOrder.executeUpdate(); ResultSet keys psOrder.getGeneratedKeys(); int orderId keys.next() ? keys.getInt(1) : 0; for (CartItem item : items) { psItem conn.prepareStatement(INSERT INTO order_items(order_id, book_id, quantity, price) VALUES(?,?,?,?)); psItem.setInt(1, orderId); psItem.setInt(2, item.getBookId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPrice()); psItem.executeUpdate(); psStock conn.prepareStatement(UPDATE books SET stock stock - ? WHERE book_id ? AND stock ?); 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 orderId; } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) conn.setAutoCommit(true); // 恢复自动提交避免连接池污染 // 关闭所有资源 } }这段代码有三个细节值得注意。Statement.RETURN_GENERATED_KEYS的作用是拿到数据库自动生成的订单号如果不加这个参数getGeneratedKeys()返回空结果集扣库存的SQL里带上AND stock ?条件让它返回0行时抛异常触发回滚这样即使两个用户同时下单也只有库存充足的一方能成功最后在finally里把autoCommit恢复为true否则使用连接池时连接归还后可能仍处于事务中导致后续操作全是“隐式事务”。防重复提交这个问题也在这里冒出来。用户下单后如果刷新页面浏览器会再次提交上一次的表单导致重复订单。常见的处理是“PRG模式”下单成功后用response.sendRedirect重定向到订单详情页而不是request.getRequestDispatcher(...).forward(...)这样刷新的是GET请求不会重新触发POST。更严谨的方式是加一个隐藏的token字段提交时对比Session里的token我在第5.4节再讲。4. 从零写一个最小可运行的网上书店表结构、DAO、Servlet、JSP4.1 数据库表设计五张表就能把主流程跑通一个毕业设计级别的网上书店最少只需要五张表users用户表、books图书表、categories分类表、orders订单表、order_items订单明细表。购物车因为放在Session里不需要建表这是我推荐的简化方案。下面是建表SQL字段注释已经写在里面CREATE TABLE users ( user_id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存加盐后的哈希值 salt VARCHAR(32) NOT NULL, email VARCHAR(50) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE categories ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(30) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE books ( book_id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, cover_url VARCHAR(200) DEFAULT NULL, description TEXT, INDEX idx_cat (category_id), CONSTRAINT fk_book_cat FOREIGN KEY (category_id) REFERENCES categories(category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( order_id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, receiver_name VARCHAR(30), receiver_phone VARCHAR(20), receiver_address VARCHAR(200), status VARCHAR(20) DEFAULT 待付款, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user (user_id), CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES users(user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_items ( item_id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(order_id), CONSTRAINT fk_item_book FOREIGN KEY (book_id) REFERENCES books(book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;所有表统一用InnoDB引擎在你第3.3节写事务时才有意义。字符集选utf8mb4而不是utf8因为utf8mb4能存emoji避免用户昵称里有一个表情就报“Incorrect string value”错误。DECIMAL(10,2)用来存价格绝不可以用float或double否则0.10.2等于0.30000000000000004订单金额对不上触目惊心。用户表里的salt字段很关键后面登录注册时要用。4.2 用户登录与注册DAO写法和Servlet调用链用户模块是网上书店的第一个完整闭环。注册时密码不能明文存数据库常见做法是用salt加盐后算SHA-256。下面这段DAO方法的重点是PreparedStatement和结果映射public User findUserByUsernameAndPassword(String username, String passwordWithSalt) throws SQLException { 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, passwordWithSalt); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setUserId(rs.getInt(user_id)); user.setUsername(rs.getString(username)); user.setEmail(rs.getString(email)); user.setCreateTime(rs.getTimestamp(create_time)); return user; } return null; } } }调用这个方法前Servlet里需要做密码哈希先用用户名查到该用户对应的salt然后对用户输入的原始密码做SHA-256(密码 salt)再传给DAO。如果用户不存在直接从Cookie里读不到salt这时可以返回统一的“用户名或密码错误”不要区分“用户不存在”和“密码错误”浪费不了几行代码但能显得你有安全意识。注册Servlet里设置Session在用户操作之后别忘了把用户对象放进sessionrequest.getSession().setAttribute(user, user);之后页面上的“欢迎你${sessionScope.user.username}”就从Session里取出来。个人信息展示页面比如“我的资料”里通常显示用户名、邮箱、注册时间这些字段都可以从Session里的User对象拿不需要在每个页面都查数据库。JSP页面里用EL表达式取值时注意范围${user.username}默认会从page-request-session-application四个范围找明确写成${sessionScope.user.username}更严谨也避免和请求参数里的user冲突。4.3 图书列表JSP页面JSTL渲染与封面图片坐标定位图书列表页面是JSP展示层的重头戏。最稳妥的写法是用JSTL的c:forEach循环渲染表格配合EL表达式取字段尽量避免在页面里写% %脚本片段。一个简化版列表如下% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html body table tr th封面/thth书名/thth作者/thth价格/thth库存/thth操作/th /tr c:forEach items${pageBean.list} varbook tr td img src${pageContext.request.contextPath}${book.coverUrl} classbook-cover / /td td${book.title}/td td${book.author}/td td${book.price} 元/td td${book.stock}/td tda href${pageContext.request.contextPath}/cart/add?bookId${book.bookId}加入购物车/a/td /tr /c:forEach /table /body /html这里处理了两个“坐标”问题。${pageContext.request.contextPath}是项目上下文路径如果你的项目部署后是http://localhost:8080/bookstore/这个表达式会替换成/bookstore没有它图片路径会变成/upload/xx.jpg浏览器实际请求的是http://localhost:8080/upload/xx.jpg404不商量。另一个是图片在页面上的位置和尺寸控制我经常被问到“jsp图片如何对坐标定位”其实就是给img标签套一层CSS用width/height/object-fit固定盒子而不是在JSP里写绝对坐标.book-cover { width: 60px; height: 80px; object-fit: cover; /* 等比裁剪不会拉伸变形 */ border-radius: 4px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2); }object-fit: cover能保证所有图书封面在60×80的盒子里统一显示不管上传的原图是200×400还是500×300。它和CSS里的背景定位不同更推荐用在商品图上。如果你需要点击图片放大居中可以用position: relative嵌套img实现不过那是前端细节不要在答辩时讲太深容易偏题。4.4 购物车与订单流程走通从加入购物车到提交订单购物车流程至少需要三个Servlet动作add、update、remove。add的要点是先取Session里的购物车列表没有就新建一个。每次加购后不要forward到原页面而是sendRedirect回图书列表页防止按F5刷新时重复加购。String bookId request.getParameter(bookId); HttpSession session request.getSession(); ListCartItem cart (ListCartItem) session.getAttribute(cart); if (cart null) { cart new ArrayList(); } boolean exists false; for (CartItem item : cart) { if (item.getBookId() Integer.parseInt(bookId)) { item.setQuantity(item.getQuantity() 1); exists true; break; } } if (!exists) { // 根据bookId查出Book对象然后构造CartItem并加入cart } session.setAttribute(cart, cart); response.sendRedirect(request.getContextPath() /book/list);这个链路里的坑是Integer.parseInt(bookId)可能抛NumberFormatException如果用户手改URL参数成bookIdabcServlet直接500你可以在页面操作上只保留链接跳转并在Servlet里捕获异常后返回错误页面。另外一个坑是Session里的购物车列表是ArrayList属于引用类型多个请求同时修改时需要同步课程设计里一般不会触发但如果你用了Vector或Collections.synchronizedList能提高代码健壮性答辩时也是加分点。订单提交服务在3.3节已经写过事务部分Servlet里还需要做参数组装从购物车对象计算总价从请求里拿到收货人、电话、地址调用OrderService.createOrder。创建成功后清空Session里的购物车并把新订单ID放到请求参数里跳转防止用户再看一次购物车时发现商品还在。5. 避坑手册JSP网上书店最常见的翻车点与排查顺序5.1 中文乱码从请求参数到数据库再到页面按这四个方向查现象图书标题里的“设计”在页面上显示成“设计”或者从前台提交的中文信息存进数据库变成问号。原因乱码不是单点问题可能出现在三层HTTP请求编码、数据库连接编码、数据库表字符集。JSP页面明明写了charsetUTF-8但Servlet里没有设置请求编码Tomcat解码POST参数时用的是ISO-8859-1于是中文在入口就坏了。解决按顺序检查。Tomcat的server.xml中Connector节点增加URIEncodingUTF-8所有Servlet入口的doPost开头调用request.setCharacterEncoding(UTF-8)响应统一用response.setContentType(text/html;charsetUTF-8)JDBC连接串保留useUnicodetruecharacterEncodingutf8。更彻底的做法是写一个EncodingFilter拦截所有请求做统一编码这个类在毕业设计里虽小但很体面。public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }这个过滤器放在web.xml里配置url-pattern/*/url-pattern不要只配.jsp或.do因为静态资源和Servlet转发路径都可能走到过滤器。如果你用了Maven自带的Tomcat插件还要检查插件配置里的encoding属性否则IDE控制台中文日志乱码会误导你。5.2 MySQL 8连接失败驱动名、时区、公钥权限三连坑现象启动Tomcat后访问任意一个查询页面报错java.sql.SQLException: Unable to connect或Public Key Retrieval is not allowed。原因MySQL 8的驱动类名变了还引入了新的安全机制。老教程里的com.mysql.jdbc.Driver是MySQL 5版本的驱动用它连MySQL 8会直接抛ClassNotFound或URL参数未知serverTimezone不设置会提示The server time zone value Öйú±ê׼ʱ¼ä这个乱码会让你以为是编码问题其实是时区。解决驱动类换成com.mysql.cj.jdbc.Driver连接串加上serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue。allowPublicKeyRetrievaltrue是为了解决MySQL 8在非SSL连接下自动拉取公钥失败的问题本机开发没有风险部署到公网时再考虑关掉。如果还连不上用命令行mysql -uroot -p -h127.0.0.1先确认MySQL本身是否允许TCP连接很多人的MySQL只监听了socket文件JDBC里写localhost反而连不上。5.3 图片上传后路径对不上封面图不显示、Eclipse重启后消失现象后台成功上传图书封面图片也在项目目录里生成了但前台JSP页面就是加载不到重启IDE后图片文件消失。原因网上书店后台管理里经常有人用request.getServletContext().getRealPath(/upload)把文件写到Tomcat的webapps/项目名/upload目录这个目录在IDE重启时会被重新发布临时文件被清掉。即使你写到项目src目录下JSP里引用的/upload/xx.jpg也会被pageContext.request.contextPath解析到webapp根目录两边不一致。解决统一把图片存到Tomcat外部的固定物理目录再给Tomcat配置虚拟映射。比如开发环境下存到D:/bookstore_files/upload代码里通过常量管理然后通过Servlet或静态资源映射把/upload/*转发到这个目录。以Tomcat 9为例在conf/server.xml的Host节点里增加Context docBaseD:/bookstore_files/upload path/bookstore/upload reloadablefalse/这里docBase是实际文件目录path是URL访问路径必须和项目上下文路径保持一致。毕设答辩现场通常不会让你改Tomcat配置所以更省事的做法是直接把封面图放进webapp/upload然后用代码控制上传文件大小并把上传目录写死在常量里最后附一句“生产环境应改为对象存储”就能蒙混过关。你没看错课程设计里“知道哪里是临时方案”也是给分点。5.4 页面刷新导致订单重复提交把“刷新”和“主动提交”分开现象用户在下单页面点一次“提交订单”浏览器地址栏光标移动到地址栏按回车或者按F5刷新订单就变成两条一模一样的。原因表单提交用的是POST刷新页面时浏览器会重新提交上一次POST参数。购物车里的商品还在Session里请求处理逻辑再次执行于是重复生成订单。我曾经见过有同学为了让页面“加载完后刷新一次”在JSP里加了meta http-equivrefresh content0结果用户一进订单确认页就自动刷新反而把下单请求重新触发了这是个非常典型的错误场景。解决下单成功后一律使用sendRedirect重定向到order_detail.jsp页面不要forward。同时给下单表单增加一个一次性token字段。下面是简化实现// 订单确认页JSP中的表单 input typehidden nametoken value${sessionScope.orderToken} /下单Servlet在doPost入口先比较请求参数里的token和Session里的orderToken如果相同立即把Session里的orderToken置空然后才执行订单创建如果不相同说明是重复提交直接重定向到图书列表页。我这里想单独强调一下“jsp页面让加载完后刷新一次”这个热词。如果你真的想在页面加载完成后做局部刷新应该用JavaScript的window.onload加上AJAX请求某个接口而不是用meta refresh整页刷新。整页刷新会带着当前页面状态重新走一遍Servlet而AJAX刷新只更新你指定的DOM节点比如购物车角标的数量。5.5 跨浏览器兼容性JSP页面在Chrome好、IE错乱现象在Chrome里页面排版正常换到Firefox或Safari就出现表格错位、按钮显示不全管理员后台的弹窗按钮点不动。原因JSP生成的HTML里没有声明DOCTYPE浏览器进入了Quirks模式或者用了center、bgcolor这类HTML4标签再或者前端JS用了老IE才支持的attachEvent。网上书店项目往往混用Bootstrap 2、jQuery 1.x之类老库问题会被放大。解决所有JSP模板第一行写上标准文档声明!DOCTYPE html统一按HTML5渲染CSS尽量用Flex/Grid而不用float布局JavaScript写事件监听时用addEventListener不要用onclick属性绑定避免多个脚本互相覆盖。做到这三点跨浏览器支持就能写进论文第一章。你不需要在JSP里做浏览器判断更不应该依赖request.getHeader(User-Agent)去做页面切换那是在制造新坑。6. 让这个作品更像一个职业级的分页组件、日志、事务封装与答辩验证前面讲的是“跑起来”但答辩时老师更想看“你理解到什么程度”。我有几个低成本的增强方向不会让你多写很多代码但效果立刻不一样。第一个是把分页代码抽成PageBeanT。你在图书列表、订单列表、用户列表三个地方都用分页每次复制粘贴一大段DAO代码会显得很业余。写一个泛型类内部持有totalPages、list、currentPage等字段再配一个PaginationUtil生成页码条HTML。这样答辩时老师问“分页原理”你可以直接讲“当前页、每页条数、总记录数三个参数SQL层面算出offset”。第二个是用java.util.logging或SLF4J把System.out.println全部替换掉。你不一定需要引入Log4j但至少在OrderService里留下类似logger.info(create order success, orderId{}, orderId)这样的记录。老师问“你怎么排查问题时”你总要说“看日志定位”而不是“看控制台猜”。第三个是顺手验证一个订单边界场景当orders插入成功、order_items插入成功但扣库存时rows 0整个事务是否能回滚。你可以故意把库存设成1购物车加2件在下单页面提交恢复数据库后发现订单没有生成这就证明事务真正生效了。我自己做这类项目时习惯把这句“人为制造失败”的测试结果写进说明文档比广告腔地说“系统做了事务”更有说服力。最后给一个通用的事务封装工具把重复的获取连接和异常处理收敛起来public abstract class TransactionTemplate { public T T execute(CallbackT callback) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); T result callback.doInTransaction(conn); conn.commit(); return result; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } } }这个模板的用途是让业务代码只关心SQL不用每个方法都重复写commit/rollback。你如果在答辩时拿出这个类再说一句“我参考了Spring的TransactionTemplate思路”就能让老师觉得你不是只会复制代码。这个方案很老但很正经。当年的我在做这个课题时想着越炫越好塞了邮箱验证、评论、收藏一大堆功能结果上线三天跑出六个乱码答辩前一天还在改购物车Session的并发问题。现在回想网上书店系统能让你学到的是“一个流程从用户点击到数据库落地的完整链路”而不是功能多少。把图书搜索、购物车、订单、库存四个核心流程跑通把事务和防重复提交讲明白这个作品就立住了。希望你做完之后不只是拿到一个文档而是真能说清楚里面的每一次跳转和每一张表。希望帮到你。本文还有配套的精品资源点击获取