ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

JavaWeb网上购物书城课设全攻略:JSP+Servlet+MySQL三层架构与避坑指南

JavaWeb网上购物书城课设全攻略:JSP+Servlet+MySQL三层架构与避坑指南 简介一份JavaWeb数据库课程设计/期末大作业以网上购物书城为主题面向计算机相关专业学生、教师及初学JavaWeb的开发者适合作为课程设计、期末大作业、毕业设计或项目初期演示。项目覆盖用户注册登录、商品分类展示、购物车、订单管理、后台管理等典型电商模块附有数据库SQL脚本与文档说明。压缩包共140个文件以JSP页面、Java类、JavaScript、HTML、CSS及XML配置为主还包含数据库SQL脚本、2个jar包及README说明文档整体仅2.28MB结构清晰便于直接部署学习。代码均通过运行测试功能完整曾用于课程设计答辩并获得96分目前已有391人学习/下载。下载后可获取可直接运行的书城系统源码、前端页面与数据库交互逻辑既适合快速上手JavaWeb整合开发也便于二次扩展或用于课设演示按README操作即可遇到运行问题还可联系作者远程指导。1. 拿到「网上购物书城」课设题先搞清要交付什么「网上购物书城」大概是 JavaWeb 数据库课程设计里出镜率最高的题目。每到期末总会有人传一份「JavaWeb数据库课程设计/期末大作业-网上购物书城源代码文档说明数据库sql」的压缩包下载下来却不知道从哪下手文档没给运行顺序、SQL 脚本导入报错、Tomcat 起不来。下面按一条能直接落地的路径把这个项目讲透它本质上是一个基于 JSP Servlet MySQL 的经典三层架构应用核心交付物是能跑通的 Web 页面、能导入的数据库脚本、能拿来讲的课设文档。适合正在焦头烂额做课程设计的学生也适合想用小而全的项目把 JavaWeb 主线串起来的自学者。不需要会 Spring Boot能把登录、购物车、下单跑通就是合格交付。2. JavaWeb 书城用什么架构最稳JSP Servlet MySQL 的选型理由与分层实现书城项目看着简单落到代码里就涉及用户登录、分类浏览、购物车、下单、订单查询好几条线。架构选错了后面每加一个功能都想删掉重写。先给结论课程设计场景下JSP Servlet JavaBean MySQL 是兼容性最高、也最好向老师讲清楚的选择。下面把选型逻辑和一条完整的请求链路拆开说。2.1 为什么不直接上 Spring Boot课设场景下的选型约束Spring Boot 不是不好而是对「课程设计」这个交付场景不一定合适。首先是课程约束不少学校的 JavaWeb 课程大纲就框定在 JSP/Servlet/JDBC老师验收时看的也是这套东西交个 Boot 项目容易被认为偏题。其次是答辩可讲性Boot 把请求分发、事务、连接池、内嵌 Tomcat 全自动装配了被问到「一个请求从页面到数据库到底经过几个环节」时很难拆到一个能讲清楚 Tomcat、Servlet、Session 的粒度。第三是学习路线的对接问题网上大量 JavaWeb 入门教程黑马那套经典笔记就是典型覆盖的正是 JSP/Servlet/MySQL 这条线跟着能少踩很多没人给你填的坑。我也知道「JSP 过时了」的说法真实存在。但课设的目标是证明你理解了课程范围内的知识之后找工作补 Spring Boot 完全不冲突。Servlet 里的 Filter、生命周期、Session、转发与重定向这些概念在 Boot 里都被藏起来了反而只有在原生写法里你才能真正感受到它们各自干了什么。2.2 三层架构下的请求链路从登录表单到数据库的一条完整路径书城项目按三层拆JSP 负责展示Servlet 负责接收请求和跳转Service 放业务规则DAO 只碰 SQL。以登录为例一条完整链路拆出来是下面三块代码。// LoginServlet.java - 控制层接收参数、调用业务、决定跳转 WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserService(); 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 userService.login(username, password); if (user ! null) { req.getSession().setAttribute(loginUser, user); // 重定向避免刷新页面时表单重复提交 resp.sendRedirect(req.getContextPath() /book/list); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }// UserService.java - 业务层参数校验、调用 DAO、必要时包装业务异常 public class UserService { private UserDao userDao new UserDao(); public User login(String username, String password) { if (username null || password null || username.trim().isEmpty() || password.isEmpty()) { return null; } return userDao.findByUsernameAndPassword(username, password); } }// UserDao.java - 数据访问层只负责一条 SQL 的执行与结果映射 public class UserDao { public User findByUsernameAndPassword(String username, String password) { String sql SELECT id, username, nickname, email FROM t_user WHERE username ? AND password ?; // PreparedStatement 预编译? 占位符由驱动转义从根上防 SQL 注入 try (Connection conn DBUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User u new User(); u.setId(rs.getInt(id)); u.setUsername(rs.getString(username)); u.setNickname(rs.getString(nickname)); u.setEmail(rs.getString(email)); return u; } } } catch (SQLException e) { e.printStackTrace(); // 课设阶段打印异常栈方便本地排查 } return null; } }这段链路说清楚几个点答辩基本就稳了一半Servlet 里 getParameter 拿到的全是字符串转 int、校验空值这些动作放在 Service 而不是 DAO职责边界才清晰。DAO 用 PreparedStatement 而不是字符串拼接 SQL这是防 SQL 注入的标准姿势。用加号拼 sql 虽然也能查出来但老师反问一句「单引号怎么处理」就露馅了。登录成功后把 user 对象塞进 Session后续页面靠它判断是否登录。注意查询语句只 select id、username、nickname、email刻意不查 password避免敏感字段在 Session 里多待一秒。sendRedirect 与 forward 的区别要能讲重定向是「浏览器再发一次新请求」URL 会变转发是「服务端内部代转」URL 不变。写操作之后用重定向否则 F5 就会重复执行上一次请求。2.3 数据库连接池选型用 Druid 还是 C3P0参数怎么调原生 JDBC 每次操作都新开连接、用完断开开销大且代码冗余课设虽然访问量低但连接池是老师爱问的知识点。常见三个选择DBCP 最老牌但维护节奏慢C3P0 是 Hibernate 时代常用的配置项偏多Druid 是阿里开源的自带监控页面和日志中文资料最全以后做 Spring Boot 项目也能直接迁移过去。优先选 Druid理由很现实出了问题搜得到中文答案。一份精简的 druid.properties 配置长这样# druid.properties driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/bookstore?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingUTF-8 usernameroot password123456 initialSize5 maxActive20 maxWait3000 validationQuerySELECT 1四个参数按课设场景解释initialSize5启动时预建 5 条连接放在池里避免第一次请求才建连的冷启动延迟。maxActive20池中最大连接数。课设并发小20 完全够调太大 MySQL 默认 max_connections 也只有 150 左右池设 100 以上纯属浪费。maxWait3000从池里借连接的最长等待时间毫秒。借不到就抛异常而不是无限阻塞把 Tomcat 线程挂死。validationQuerySELECT 1每次借出连接前先执行这条廉价 SQL验证连接没被 MySQL 断开。MySQL 默认 wait_timeout 是 8 小时连接闲置过久会被服务端回收没有这条验证就会拿到坏连接。连接池参数里最容易翻车的反而是「用完没还」。借出的连接如果不调用 close()它不会真正断开而是永远占在池里多刷几次页面就达到 maxActive表现为「第一次能查、越刷新越卡」。后面避坑章会再展开。3. 数据库设计是书城的命根子E-R 图、建表 SQL 与初始化数据写业务代码之前先把数据模型立住。书城项目功能看着多核心实体就四类用户、图书、分类、订单再配两张辅助表购物车项、订单项。表结构建对后面的增删改查只是体力活表建歪了每个查询都绕路答辩时还会被数据一致性追问到哑火。3.1 先理清实体关系用户、图书、分类、订单四组核心表实体关系先用手画清楚再用工具画成 E-R 图放进文档这一页能撑起文档五分之一的印象分用户与订单一对多一个用户有多张订单。订单与订单项一对多一张订单包含多个商品行订单项通过 book_id 指向图书。分类与图书一对多一个分类下有多本图书。购物车项挂在用户下存 user_id、book_id、quantity同一用户同一本书只留一行数量累加。购物车要不要用表存储是个值得想一下的设计点。用 Session 存购物车代码简单但刷新页面可能丢而且数据库增删改查的演示场景少了一块。课程设计的要求里明确写了「数据库」把购物车落表每个操作都能在数据库里看到对应行演示和答辩都更占便宜。所以结论是购物车用表不要用 Session。订单项里有两个字段是故意冗余的book_title 和 price。原因是价格快照——下单那一刻的商品价格必须固定下来否则后台改一次图书定价历史订单的金额就跟着变了这在真实电商系统里是不可接受的。冗余看似违背三范式但属于数据分析里最常见的取舍答辩被问到也能从容回答。3.2 建表 SQL 落地字符集、保留字与字段类型直接给一份可以原样导入的建库建表脚本-- 建库utf8mb4 才能完整存中文与特殊符号 CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE bookstore; -- 用户表 CREATE TABLE t_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(32) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(32), email VARCHAR(64), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 图书分类表 CREATE TABLE t_category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(32) NOT NULL ); -- 图书表 CREATE TABLE t_book ( id INT AUTO_INCREMENT PRIMARY KEY, category_id INT NOT NULL, title VARCHAR(128) NOT NULL, author VARCHAR(64), price DECIMAL(10,2) NOT NULL, -- 金额用 DECIMAL不用 FLOAT stock INT UNSIGNED NOT NULL DEFAULT 0, sales INT UNSIGNED NOT NULL DEFAULT 0, description TEXT, image_url VARCHAR(256), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_book_category FOREIGN KEY (category_id) REFERENCES t_category(id) ); -- 购物车项表 CREATE TABLE t_cart_item ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT NOT NULL, book_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, add_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_book (user_id, book_id), -- 同用户同书只保留一行 CONSTRAINT fk_cart_user FOREIGN KEY (user_id) REFERENCES t_user(id), CONSTRAINT fk_cart_book FOREIGN KEY (book_id) REFERENCES t_book(id) ); -- 订单表order 是 MySQL 保留字表名统一用 t_order CREATE TABLE t_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, -- 对外展示的订单号 user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2已发货 3已完成 4已取消 receiver_name VARCHAR(32), receiver_phone VARCHAR(20), receiver_address VARCHAR(128), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_order_user FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 订单项表冗余 book_title 与 price保存下单快照 CREATE TABLE t_order_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, book_id INT NOT NULL, book_title VARCHAR(128), price DECIMAL(10,2) NOT NULL, -- 下单那一刻的价格之后图书改价不影响历史订单 quantity INT NOT NULL, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES t_order(id) ); -- 高频查询走索引避免全表扫描 CREATE INDEX idx_order_user ON t_order(user_id); CREATE INDEX idx_book_category ON t_book(category_id);几个字段层面的决定值得照着抄表名一律 t_ 前缀直接绕开 order 这类 MySQL 保留字。网上很多老教程写 order 表名能跑是因为旧版本没把它当保留字新版本一个 Syntax error 甩脸上。password 长度给了 64为哈希预留。即使课设存明文也别用 32 卡死后面想改成 MD5 存储时不用改表结构。DECIMAL(10,2) 不是随便写的FLOAT/DOUBLE 是二进制浮点0.10.2 会变成 0.30000000000000004金额差一分钱在财务语义上就是事故。只要涉及钱必须 DECIMAL。t_cart_item 上的 UNIQUE KEY (user_id, book_id) 配合 INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1加购逻辑就从「先查再插」变成一条原子语句也杜绝了购物车重复行。订单状态用 TINYINT 而不是字符串是为了后面扩展状态机好做比较展示层用 Map 或 switch 把 0 翻译成「待支付」即可。3.3 初始化数据与联表查询让页面先有东西可显示代码写完 SQL 导入如果表里空空如也登录进去一个商品都看不到演示效果直接减半。建表之后顺手插几条干净的测试数据INSERT INTO t_category (name) VALUES (计算机), (文学), (历史); INSERT INTO t_book (category_id, title, author, price, stock, sales, description) VALUES (1, Java核心技术 卷I, 凯·S·霍斯特曼, 118.00, 50, 120, Java 基础经典), (1, 深入理解Java虚拟机, 周志明, 129.00, 30, 88, JVM 进阶必读), (2, 平凡的世界, 路遥, 88.00, 100, 300, 茅盾文学奖获奖作品), (3, 明朝那些事儿, 当年明月, 98.00, 80, 260, 通俗历史读物); INSERT INTO t_user (username, password, nickname, email) VALUES (admin, 123456, 管理员, adminexample.com), (student, 123456, 测试用户, stuexample.com);订单列表页要展示订单号、用户昵称、金额、状态靠的是联表查询这也是课设文档里必须出现的一类 SQLSELECT o.order_no, u.nickname, o.total_amount, o.status, o.create_time FROM t_order o JOIN t_user u ON o.user_id u.id WHERE u.username student ORDER BY o.create_time DESC;这条查询的过滤条件是 t_user.username走的是「先通过索引找到用户再关联订单」的路子如果没有 idx_order_user 索引MySQL 会先扫全部订单再逐个匹配用户数据量一上来就是慢 SQL。课设数据量小感觉不到但「索引对查询性能的意义」这句话答辩时值得主动讲出来。另一个常见场景是查某用户买过哪些书JOIN 到订单项再 JOIN 到图书如果用户重复购买同一本会出现重复行这时才用 SELECT DISTINCT 去重。去重的前提是先搞清楚重复是业务允许的还是 JOIN 产生的笛卡尔积——别上来就 DISTINCT。4. 从零跑通「源代码」IDEA Tomcat MySQL 环境配置与最小启动链路拿到源代码包第一件事不是往 IDEA 里拖而是先把 JDK、Tomcat、MySQL、驱动这四件套的版本对齐。课设项目最常见的翻车不是代码逻辑而是环境压根没配对。我一般先定版本再配部署最后才启动。4.1 版本匹配先做对JDK、Tomcat、MySQL 驱动的关系版本组合直接决定了你会踩哪类坑照着下表配是最省事的组件推荐版本说明JDK8 或 11Tomcat 8.5 配 JDK 8Tomcat 9 配 JDK 8/11 均可Tomcat8.5 或 9.0网上的教程和源码大多按这两个版本写资料最多MySQL5.7 或 8.08.0 的驱动类名变成 com.mysql.cj.jdbc.DriverJDBC 驱动mysql-connector-java 5.1.49 / 8.0.x5.x 驱动连不上 MySQL 8 的默认认证IDEA2020 以后社区版就够课设不需要旗舰版的数据库工具MySQL 8.0 的坑要提前说它的默认认证插件是 caching_sha2_password老驱动 mysql-connector-java 5.1.x 连上去直接报 Unable to load authentication plugin。解决办法是优先换 8.0.x 驱动而不是去改 MySQL 用户认证插件改插件属于动了数据库的全局配置容易把自己绕晕。注意Tomcat 8.5 / 9 用的还是 javax.servlet 包名Tomcat 10 开始改成 jakarta.servlet。网上大量旧源码是按 javax 写的直接扔到 Tomcat 10 上会报 NoClassDefFoundError而且不是缺 jar是包名整个变了。课设阶段别追新用 8.5 或 9 最稳。4.2 在 IDEA 里部署 Web 项目的三个关键配置IDEA 运行 JavaWeb 项目配置和双击 Tomcat 的 startup.bat 是两回事。IDEA 的启动按钮会先编译、再打包成 Artifact、然后发布到配置好的 Tomcat 实例里。三个关键位置对应三类最常见的问题第一处Project Structure → Modules。如果项目不是 Maven 结构而是一份手动拷 jar 的普通项目需要把 WEB-INF/lib 下的 jar 选中并右键 Add as Library否则 IDE 编译期就看不到类整篇代码标红。第二处Project Structure → Artifacts。确认存在一个类型为 Web Application Exploded 的 artifact右侧 Output Layout 里 WEB-INF/lib 下能看到 mysql-connector、druid 等 jar 包。这里看不到 jar运行时就一定会报 ClassNotFoundException排查顺序永远先看这里。第三处Run/Debug Configurations → Tomcat Server Local。先选本地 Tomcat 目录再切到 Deployment 页签点 添加项目 Artifact最后确认 Application context 是 /bookstore。这一处配错启动后访问 http://localhost:8080/bookstore/login.jsp 就是 404。提示配好之后先访问 Tomcat 自带的首页 http://localhost:8080/ 确认 Tomcat 本身活着再访问你的项目路径。这个动作能把「Tomcat 坏了」和「项目没发布上去」两类问题当场分开。4.3 数据库连接与连接池配置一个能直接用的 Druid 工具类第 2 章给了 druid.properties这里补上加载它的工具类。连接池在整个应用里只需要初始化一次用静态代码块做这件事最合适// DBUtils.java - 静态代码块加载连接池配置全局只初始化一次 public class DBUtils { private static DruidDataSource dataSource; static { try (InputStream in DBUtils.class.getClassLoader() .getResourceAsStream(druid.properties)) { Properties props new Properties(); props.load(in); dataSource (DruidDataSource) DruidDataSourceFactory .createDataSource(props); } catch (Exception e) { e.printStackTrace(); // 静态块里异常必须打出来否则后续报错无从查起 } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); // 从池中借用而不是新建连接 } }这个类有三点值得嚼一下static 块在类加载时执行一次initialSize 配置的 5 条连接在这时就已经建好并放进池里了。getConnection() 不是新建物理连接而是从池里「借」。用完的连接必须 close()这个 close 不是真断开而是归还池中复用。druid.properties 要放在类路径根目录src/main/resources 或 classes 下用类加载器才能读得到放在 web 目录下是读不到的这属于最常见的低级错误。配套的连接释放模板也一起给出来DAO 里所有方法都按这个结构写Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtils.getConnection(); ps conn.prepareStatement(sql); // 设置参数、执行查询、封装结果 } catch (SQLException e) { e.printStackTrace(); } finally { // 释放顺序与申请顺序相反ResultSet - Statement - Connection if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } }finally 里手动关连接虽然啰嗦但能把「释放顺序」这个知识点明明白白展示出来面试和答辩被问到 JDBC 资源管理时这就是标准答案。JDK 7 之后可以用 try-with-resources 写得更短课设里二选一都行但别只贴一段你不懂原理的代码。5. 避坑与排错JavaWeb 书城课设最容易翻车的 5 个地方环境配了、代码导了、数据也导进去了能不能一次跑起来坦白说多数人第一次都跑不起来。这五条是课设答疑里被问烂的问题也是我自己当年一个个踩过来的按「现象 → 原因 → 解决」直接对着排查。5.1 驱动包没加载ClassNotFoundException 的根源与处理现象Tomcat 启动正常一提交登录表单就报 java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver有的项目里写的是 com.mysql.jdbc.Driver。原因mysql-connector 的 jar 没有进入最终发布的 Artifact。在 Project Structure 的 Libraries 里加了依赖IDEA 编译期能过但 Tomcat 运行时用的是 Artifact 里 WEB-INF/lib 目录下的 jar两边是独立的。另一个原因是驱动类名写错MySQL 5.x 用 com.mysql.jdbc.DriverMySQL 8.x 用 com.mysql.cj.jdbc.Driver把 8.x 驱动配成 5.x 的类名照样报这个错。解决先打开 Project Structure → Artifacts看 Output Layout 里 WEB-INF/lib 下有没有 mysql 和 druid 的 jar没有就右键 → Put into /WEB-INF/lib然后 Build → Rebuild Project。如果是 Maven 项目先看右侧 Maven 面板里依赖有没有红色报错刷新一下再重新部署。这条路走一遍九成的驱动问题都能消失。5.2 端口被占与 404Tomcat 部署方式的坑现象点启动按钮日志秒报 Port 8080 required by Tomcat v8.5 Server is already in use或者 Tomcat 明明起来了访问 http://localhost:8080/bookstore/login.jsp 一直 404。原因端口被上一个没关干净的 Tomcat 实例或其它程序占着。404 则是 Deployment 配置缺了关键一步要么没把 Artifact 加到部署列表要么 Application context 路径配的不是 /bookstore。解决端口占用的处理Windows 用 netstat -ano | findstr 8080 查到 PID 再在任务管理器结束进程也可以直接改 Tomcat 的 conf/server.xml把 Connector 端口从 8080 改成 8081一劳永逸但改完访问路径也要跟着换端口。404 就去 Run/Debug Configurations → Deployment 里确认加了 Artifact、确认 context 是 /bookstore。判断技巧访问 8080 根路径能出 Tomcat 默认首页说明 Tomcat 没问题接下来只查项目部署配置连默认首页都出不来就先处理 Tomcat 本身。5.3 中文乱码连接 URL 和建表字符集必须一致现象页面书名全是「???」或者表单提交的中文进数据库后变乱码更诡异的是页面显示正常数据库里却是乱码。原因字符集合在三个环节各管一段——JSP 页面、MySQL 表、JDBC 连接 URL。三处只要有一个不是 UTF-8中文就断。最常见的是建库时没指定字符集MySQL 默认继承成了 latin1。解决三处一起改。建库语句写成 CREATE DATABASE ... DEFAULT CHARACTER SET utf8mb4JDBC URL 里加 characterEncodingUTF-8JSP 页面顶部写 % page contentTypetext/html;charsetUTF-8 languagejava %Servlet 读取表单参数前先执行 req.setCharacterEncoding(UTF-8)。注意 setCharacterEncoding 必须在第一次 getParameter 之前调用否则当前请求的参数已经按默认编码被解析完了之后调用无效。这个属于「后悔药」型问题改完重启 Tomcat、清一下浏览器缓存基本能好。5.4 order 表建不出来MySQL 保留字与反引号现象从网上抄的建表 SQL 粘进命令行或图形化工具执行到 CREATE TABLE order 报 Syntax error来回看 SQL 都看不出毛病。原因order 是 MySQL 保留字直接当表名必须加反引号。老教程能跑是因为旧版本或特定 sql_mode 下没把它当保留字新版本严格校验直接报错。解决两条路。要么表名统一用 t_order 这种带前缀的彻底绕开保留字问题这也是我建表一贯的做法要么非要用 order就写成order反引号包起来。列名同理desc、group、rank、key 都是高危词。这类报错看着玄学其实最有规律——拿一张 MySQL 保留字表对一遍表名和列名比瞎试快得多。5.5 购物车加了又加表单重复提交与 PRG 模式现象购物车点「加入购物车」后按 F5商品数量变成 2、3、4或者下单成功后刷新页面订单重复生成。原因加购请求是 GET 超链接或 Servlet 处理完业务后直接用 forward 跳回页面。forward 是服务端内部跳转浏览器地址栏的 URL 没变F5 会把上一次请求原样重发一遍于是又执行了一次加购或下单逻辑。解决标准做法是 PRGPost/Redirect/Get。所有写操作的 Servlet 处理完业务后不要 forward用 resp.sendRedirect(req.getContextPath() /cart/list)让浏览器重新 GET 一次展示页。同时加购入口必须用 form 表单提交POST 方法不能写成 这种 GET 链接——GET 语义上就不该产生副作用浏览器预加载、收藏夹都会把它当普通请求发出去。把这两处改掉购物车翻倍、订单重复两个问题一次清干净。6. 答辩前的验证清单和一个加分技巧下单事务怎么实现6.1 十分钟验证清单核心功能是不是真的可用演示现场现写 bug 是最尴尬的没有之一。答辩前一天按这条链路完整过一遍截图或录屏留档注册一个新用户用新账号登录再退出从分类页点进「计算机」能看到对应图书列表加购两本不同的书再加购其中同一本确认数量变成 1 而不是多出一行从购物车删除一项再结算下单下单后去数据库查 t_book 里对应图书的 stock 是否减少订单列表能看到刚下的单点详情能看到商品行、数量、价格。这十几分钟花得很值。课设翻车大多不是功能没做而是某个边角没跑通临场才发现。6.2 加分项用事务把「扣库存 写订单」绑在一起下单这个动作至少包含三步扣减库存、插入订单、插入订单项。如果扣了库存但订单没生成数据就是脏的。教程里天天讲事务课设代码里真正写了的没几个你补上就是实打实的差距。核心写法如下// OrderService.createOrder() 中事务的核心写法 Connection conn null; try { conn DBUtils.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 bookDao.decreaseStock(conn, bookId, quantity); // 第一步扣库存 long orderId orderDao.insert(conn, order); // 第二步写订单 orderItemDao.insert(conn, orderId, items); // 第三步写订单项 conn.commit(); // 全部成功才提交 } catch (SQLException e) { if (conn ! null) conn.rollback(); // 任一步失败整体回滚 e.printStackTrace(); } finally { if (conn ! null) conn.setAutoCommit(true); // 归还连接前恢复默认 if (conn ! null) conn.close(); }这段代码最大的坑不在事务本身而在传参三个 DAO 方法都必须接收外部传入的 conn不能在 DAO 内部自己 getConnection 再关闭。如果三个操作各用一条连接setAutoCommit(false) 只管其中一条事务根本建立不起来。这也是答辩时老师常问的那个「插入订单和扣库存是不是同一个事务」——能答出「同一个连接 setAutoCommit(false) 统一 commit/rollback」这条就过了。finally 里把 autoCommit 恢复成 true 再归还连接是防止连接池里的连接残留事务状态属于池化环境下的卫生习惯。我当年这份书城课设就是漏了事务。演示时一切正常被问了一句「两个人同时下单库存会不会超卖」当场答不上来。后来把这三行补上才想明白课设评分看的不只是功能多炫而是你有没有把边界想清楚。登录验证码、分页查询、订单状态流转都是在这个基础上按精力锦上添花的东西。希望帮到你。本文还有配套的精品资源点击获取
返回列表