资讯详情

资讯详情

JavaWeb订餐系统源码拆解:从环境部署到订单事务与权限控制

简介这是一套基于JavaWeb实现的必胜客在线订餐系统项目包面向正在学习JavaWeb、准备课程设计或毕业设计的学生与初级开发者围绕用户注册登录、菜单浏览、购物车、下单支付及后台菜品与订单管理等典型餐饮业务场景展开可帮助读者打通从需求分析、数据库设计到前后端交互的完整链路。压缩包共1307个文件约66.88MB以png、jpg等页面图片素材java源码、class编译文件、jsp与html视图、css与js脚本以及jar依赖、xml与properties配置和sql建表脚本为主基本覆盖开发、部署与数据初始化所需内容。目前已有2518人学习。项目采用MVC思路组织Servlet、JSP与JavaBean配合数据库访问层完成业务逻辑读者可重点研读请求处理、数据校验、订单状态流转与页面异步交互也能从配置和目录结构中理解Tomcat部署方式适合作为课程设计模板或JavaWeb综合实践参考。1. 拿到一个 JavaWeb 订餐系统源码包先分清它是 JSPServlet 还是 SSM把压缩包解开之后第一件事不是打开 IDE 新建工程而是先ls一遍目录。JavaWeb 这个词在课程设计资源里覆盖的跨度极大一种是最经典的 JSP Servlet JDBC 直连 MySQL 的三层结构另一种是 Spring SpringMVC MyBatis 的整合工程两者的启动方式、依赖管理甚至配置文件的后缀都不一样。这个在线订餐系统属于哪一类决定了你接下来半小时是顺风顺水还是连续翻车。它真正值钱的地方在于提供了一条完整的点餐链路菜单浏览、加入购物车、提交订单、后台改单四段都能跑通而不是一堆互不相干的 CRUD 页面拼起来的样子货。适合两类人赶课程设计需要一套能演示、能讲清逻辑的骨架以及刚学完 Servlet 想找一个有业务味道的练手项目而不是继续在图书管理系统的增删改查里打转。下面按「先判断、再跑通、后拆解、最后避坑」的顺序走每一步都落到你能直接抄的命令和参数上。2. 环境对齐JDK、Tomcat、MySQL 三件套版本核对这类包最容易出问题的地方不在代码而在版本。课程设计资源普遍有年份包里的mysql-connector-java-5.1.x.jar配 MySQL 8 会直接报驱动类找不到javax.servlet与jakarta.servlet混用则会在 Tomcat 10 上整站 404。我一般会先把版本关系钉死再谈别的。2.1 看目录判断技术栈用一次 find 摸清家底传统工程和 Maven 工程的区分非常明显看有没有pom.xml就够了。但光看这个还不够得顺手把 jar 包、SQL 脚本、配置文件全捞出来心里有张地图后面排错才不会靠猜。你看到的目录/文件大概率是什么后续动作WEB-INF/web.xmlWEB-INF/lib/*.jarsrc传统 JSPServlet 工程按普通 Web 工程导入jar 手动挂到 classpathpom.xmlsrc/main/javasrc/main/webappMaven 工程多为 SSM先mvn dependency:tree看依赖有没有冲突applicationContext.xml/spring-mvc.xmlSpring 系整合检查包扫描路径和数据源 Bean*.sql常见名order_db.sql、db.sql建库建表脚本先导库再改连接参数jdbc.properties/db.properties连接参数外置优先改这里别动 Java 源码里的硬编码# 解压后先做一次结构扫描别急着导 IDE unzip -q online-order.zip -d order-src cd order-src # 1. 看清目录层级判断是 webapp 结构还是标准 Maven 结构 find . -maxdepth 3 -type d | sort # 2. 把所有 jar 列出来重点看 mysql 驱动和 servlet-api 的版本 find . -name *.jar -exec basename {} \; | sort -u # 3. 定位三个关键文件web.xml、pom.xml、建库脚本 find . \( -name web.xml -o -name pom.xml -o -name *.sql -o -name jdbc.properties \) -print这三条命令里第一条决定导入方式第二条决定版本兼容性第三条决定你改配置时该打开哪个文件。第二条尤其关键如果看到mysql-connector-java-5.1.47.jar而你本机是 MySQL 8就要么换成mysql-connector-java-8.0.x要么把驱动的类名从com.mysql.jdbc.Driver改成com.mysql.cj.jdbc.Driver两者只能选一个混着来必报ClassNotFoundException。servlet-api 的 jar 如果出现在WEB-INF/lib里部署前记得删掉容器自己会提供重复了会引发类加载冲突。2.2 数据库先跑通建表、导数据、改四个连接参数代码可以慢慢看数据库必须先活。绝大多数「页面空白、后台报 500」的根因都是连接串里缺参数或者库名对不上。-- 建库时字符集一次到位别用默认的 latin1 再回头改 CREATE DATABASE IF NOT EXISTS order_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE order_db; -- 菜品表status 控制上下架stock 用于下单时扣减 CREATE TABLE dish ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, img VARCHAR(255) DEFAULT NULL, -- 只存相对路径如 /upload/dish/1.jpg status TINYINT NOT NULL DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单主表total 是下单瞬间的金额快照 CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0待支付 1已支付 2配送中 3已完成 4已取消 create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;连接参数建议单独放一个 properties 文件改的时候不用重新编译# src/main/resources/jdbc.properties jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/order_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password改成你自己的密码连接串里的四个参数各自管一件事缺一个就会以很奇怪的方式失败characterEncodingutf8决定中文能不能正常入库serverTimezoneAsia/Shanghai不加MySQL 8 会报时区异常订单时间直接是 nulluseSSLfalse是为了本地开发省掉证书握手allowPublicKeyRetrievaltrue是 MySQL 8 默认认证插件下必须开的否则第一次连接就被拒绝。密码不要留在代码里也不要提交到任何仓库这是血泪经验。// 读取配置并统一管理连接避免每个 DAO 里都写一遍 DriverManager public class DBUtil { private static final Properties PROPS new Properties(); static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(jdbc.properties)) { PROPS.load(in); Class.forName(PROPS.getProperty(jdbc.driver)); // 老驱动这里会抛异常 } catch (Exception e) { throw new ExceptionInInitializerError(数据源初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection( PROPS.getProperty(jdbc.url), PROPS.getProperty(jdbc.username), PROPS.getProperty(jdbc.password)); } }这段代码的关键在静态块Class.forName一旦失败整个类初始化就崩报错会非常明确地指向驱动问题比运行时在某个 DAO 里抛NullPointerException好排查得多。getResourceAsStream用的是 classloader 路径所以jdbc.properties必须放在src/main/resources或者WEB-INF/classes下放错位置读出来就是 null。2.3 编译、打包、部署war 包怎么放context path 怎么定Maven 工程一条命令解决传统工程走 IDEA 的 Artifacts 配置两条路都要清楚。# Maven 工程跳过测试直接打包 mvn -DskipTests clean package # 产物一般在 target 下确认 war 包名字 ls -lh target/*.war # 丢进 Tomcat 的 webappswar 名就是 context path cp target/order.war $CATALINA_HOME/webapps/ # 启动并观察日志重点看有没有 SEVERE 和数据库连接异常 $CATALINA_HOME/bin/startup.sh tail -f $CATALINA_HOME/logs/catalina.out如果 war 包叫order.war访问地址就是http://localhost:8080/order/末尾的斜杠不能省省了在部分容器版本上会 404。非 Maven 工程则要把WEB-INF/lib下所有 jar 一起打进 artifact 的WEB-INF/lib漏一个就是NoClassDefFoundError。启动日志里如果出现Communications link failure说明 MySQL 没起来或端口不通出现Unknown database order_db说明建库脚本没执行或者库名改了没同步这两类错误占首次部署失败的八成以上先看日志再改代码。3. 订单主链路从加购物车到落库的四个关键节点点餐系统的业务复杂度几乎全压在订单上。菜单和用户是静态数据订单是有状态、有并发、有金额的写得好不好在这里拉开差距。这一段拆的是包里最值得看的部分也是二次改造时最容易改出 bug 的地方。3.1 购物车放 Session 还是放数据库课程设计级项目基本都是 Session简单直接但也埋着坑。// 购物车结构dishId - {数量, 下单时的单价快照} // 不直接缓存 Dish 实体避免菜品改价后购物车金额跟着漂 public class CartItem implements Serializable { private Integer dishId; private String name; private BigDecimal price; // 加入购物车时锁定的价格 private int qty; // getter / setter 省略 } // 加购已存在就累加数量不存在就新建 WebServlet(/cart/add) public class CartAddServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int dishId Integer.parseInt(req.getParameter(dishId)); int qty Integer.parseInt(req.getParameter(qty)); HttpSession session req.getSession(); MapInteger, CartItem cart (MapInteger, CartItem) session.getAttribute(cart); if (cart null) { cart new LinkedHashMap(); // 保持加入顺序页面显示更稳定 session.setAttribute(cart, cart); } CartItem item cart.get(dishId); if (item null) { Dish dish dishService.findById(dishId); // 查一次库拿名称和价格 item new CartItem(dishId, dish.getName(), dish.getPrice(), 0); cart.put(dishId, item); } item.setQty(item.getQty() qty); resp.sendRedirect(req.getContextPath() /cart/list); } }用LinkedHashMap而不是HashMap是为了让购物车列表按用户加购顺序展示HashMap的遍历顺序在不同 JDK 版本上并不一致偶尔会出现「加了两道菜顺序乱跳」的玄学现象。价格在加购时锁进CartItem是为了给下面的事务做铺垫——下单时必须用锁定的价而不是下单瞬间重新查库的价。Session 方案的天花板也很清楚换浏览器购物车就没了多实例部署时不共享。要往生产方向走就得把购物车落到cart表用user_id dish_id做唯一键INSERT ... ON DUPLICATE KEY UPDATE qty qty ?一条 SQL 搞定累加。这一步改造量不大但讲清楚为什么改比直接堆一个 Redis 更有说服力。3.2 下单事务金额快照、明细落库、库存扣减必须在一个事务里这是整个项目最该逐行读的方法。写错的话线上表现就是「订单成功了但库存没动」或者「扣了库存订单没生成」。public String submitOrder(int userId, MapInteger, CartItem cart) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交事务从这里开始 // 1. 先算总额并生成订单号简化版生产建议用雪花或号段 String orderNo OD System.currentTimeMillis(); BigDecimal total BigDecimal.ZERO; for (CartItem it : cart.values()) { total total.add(it.getPrice().multiply(BigDecimal.valueOf(it.getQty()))); } // 2. 主表落库status0 待支付 long orderId orderDao.insert(conn, orderNo, userId, total, 0); for (CartItem it : cart.values()) { // 3. 明细表存价格快照历史订单不受后续改价影响 orderItemDao.insert(conn, orderId, it.getDishId(), it.getPrice(), it.getQty()); // 4. 带条件的库存扣减影响行数为 0 说明库存不够 int affected dishDao.reduceStock(conn, it.getDishId(), it.getQty()); if (affected 0) { throw new SQLException(库存不足, dishId it.getDishId()); } } conn.commit(); return orderNo; } catch (SQLException e) { if (conn ! null) conn.rollback(); // 任何一步失败整体回滚 throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }配套的扣减 SQL 必须是带条件的更新不能先 select 再 update-- 用一条语句完成「判断 扣减」把并发判断交给数据库 UPDATE dish SET stock stock - #{qty} WHERE id #{dishId} AND stock #{qty};AND stock qty这个条件就是并发防线。两个用户同时抢最后一份MySQL 的行锁会让第二条更新看到扣减后的值条件不成立返回影响行数 0Java 层据此抛异常回滚。如果改成先SELECT stock判断够不够、再UPDATE中间那段窗口期足够插进另一个事务超卖就发生了。同一个Connection贯穿所有 DAO 也是必须的很多人图省事在每个 DAO 里各自getConnection()那种写法下setAutoCommit(false)完全失效回滚只回滚了个寂寞。3.3 订单状态机别让后台能随便改状态状态字段用数字没问题问题在于没有约束。后台管理页面上一个「修改状态」的下拉框如果直接UPDATE orders SET status ?运营手一抖就能把已完成的订单改回待支付。-- 只允许合法跃迁待支付(0) - 已支付(1) - 配送中(2) - 已完成(3) -- 取消只允许从 0 或 1 发起 UPDATE orders SET status #{to} WHERE id #{orderId} AND status #{from};执行后检查影响行数为 0 就提示「订单状态已变化请刷新后重试」。这一招顺带解决了重复提交用户连点两次「确认收货」第二次的from已经不是 2 了更新自然失败不会重复触发积分或通知逻辑。状态跃迁表建议直接写死在 Service 里用一个MapInteger, SetInteger存允许的下一状态比散落在各个 if 里安全得多。这里不需要上状态机框架但要有一张明确的表至少自己排错时能一眼看出哪条路径是非法的。3.4 分页与菜品图片两个小功能两个大坑分页别用LIMIT offset, size配全表COUNT(*)就完事菜品表数据量小的时候看不出问题做二次开发一造几万条数据就慢了。常规写法是两条 SQL一条SELECT ... ORDER BY id DESC LIMIT ?, ?一条SELECT COUNT(*) FROM dish WHERE status 1两条共用同一套 where 条件条件拼错就会导致「总数 100 条但翻到第 3 页就空了」。// 分页参数做边界收敛防止 page0 或 size10000 这类脏输入 int page Math.max(1, Integer.parseInt(req.getParameter(page) null ? 1 : req.getParameter(page))); int size 8; // 每页固定 8 道菜前端三列布局刚好 int offset (page - 1) * size; ListDish list dishDao.findByPage(offset, size); int total dishDao.countOnSale(); int pages (total size - 1) / size; // 向上取整别用整除图片上传这块包里常见的做法是把文件存到webapp/upload/下数据库里存相对路径/upload/dish/xxx.jpg。存相对路径而不是D:/tomcat/webapps/...这种绝对路径是能不能换机器部署的分水岭。文件名一定要重命名成 UUID 或时间戳别用原始文件名中文名加特殊字符在部分容器上会导致 404上传目录也别放在WEB-INF里面那个目录浏览器根本访问不到。4. 登录、权限与后台管理过滤器与参数绑定后台管理是这类项目里最容易被忽略的部分因为它「能跑就行」。但恰恰是后台页面越权访问、SQL 拼接、明文密码三个问题经常同时出现。4.1 编码过滤器与登录拦截两个 Filter 各管一件事编码问题统一在入口解决别在每个 Servlet 里写setCharacterEncoding。WebFilter(/*) // 拦截所有请求顺序上要排在登录过滤器之前 public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); // 只管 POST 体 resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } } WebFilter(urlPatterns {/order/*, /cart/*, /admin/*}) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest r (HttpServletRequest) req; HttpSession session r.getSession(false); // 注意false别顺手创建新会话 if (session null || session.getAttribute(loginUser) null) { ((HttpServletResponse) resp).sendRedirect(r.getContextPath() /login.jsp); return; // 不往下走请求到此为止 } chain.doFilter(req, resp); } }getSession(false)这个参数是重点。写成getSession()或getSession(true)每个未登录请求都会创建一个空会话内存里堆一堆无用的 Session 对象压测时看着内存曲线往上走还找不到原因。两个过滤器的执行顺序由web.xml里的filter-mapping声明顺序决定或者在注解场景下靠过滤器名字母序所以正式项目里建议统一写在web.xml里顺序看得见。4.2 后台越权前端隐藏菜单不叫权限控制后台页面常见做法是登录后按角色渲染菜单管理员看得到「菜品管理」普通用户看不到。但菜单只是a标签地址栏直接敲/admin/dish/list照样能进。真正的拦截必须在服务端做。// 后台拦截角色不匹配直接打回首页别只靠隐藏链接 WebFilter(/admin/*) public class AdminFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest r (HttpServletRequest) req; User user (User) r.getSession().getAttribute(loginUser); if (user null || user.getRole() ! 1) { // 1 管理员 ((HttpServletResponse) resp).sendError(403); return; } chain.doFilter(req, resp); } }sendError(403)比sendRedirect更合适越权访问不该被静默重定向成「正常」流程日志里也更容易定位。角色字段用整数没错但要定义成常量或者枚举别在十个地方散写role 1后期加一个「店长」角色就得全局搜索替换。4.3 密码存储与 SQL 注入两道必须自己守的线用户表里存明文密码是这类课程设计包里最常见的减分项。改造成本其实很低。// 注册与登录都用占位符字符串拼接一律不许出现 public User login(String username, String rawPwd) throws SQLException { String sql SELECT id, username, pwd_hash, salt, role 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 null; String salt rs.getString(salt); String expect rs.getString(pwd_hash); // 加盐摘要同一密码在不同用户下哈希值不同彩虹表失效 String actual DigestUtils.sha256Hex(salt rawPwd); return expect.equalsIgnoreCase(actual) ? mapRow(rs) : null; } } }PreparedStatement的占位符不只是防注入还能让数据库复用执行计划username a OR 11这种输入会被当成普通字符串处理。至于密码最低要求是加盐哈希别用 MD5 裸哈希要更稳一点就换 BCrypt自带盐值和计算强度代价是登录时多花几十毫秒。这里还有一个细节登录失败时不要区分「用户不存在」和「密码错误」统一提示「用户名或密码错误」否则等于免费提供账号枚举接口。5. 避坑与排查五条踩过的记录按现象对号入座以下五条是我拆这类包时按出现频率排的基本覆盖了从解压到跑通的全过程。遇到问题先按「现象」对照比漫无目的地翻代码快得多。5.1 启动与部署类问题记录一访问首页 404日志里没有任何异常现象Tomcat 启动成功localhost:8080能出猫页但访问项目路径就是 404catalina.out干干净净。原因war 包没有真正解压或者 context path 和你想的不一样。常见于 war 包名带版本号如order-1.0.war实际路径是/order-1.0/也可能是web.xml里url-pattern写成了/index而首页文件叫index.jsp容器没配欢迎页。解决先ls $CATALINA_HOME/webapps/确认解压出来的目录名访问地址严格按目录名来检查web.xml里有没有welcome-file-list没有就手动访问/index.jsp验证最后看server.xml里 Connector 的端口是不是被改过。记录二启动报 ClassNotFoundException: com.mysql.cj.jdbc.Driver现象应用能起来但一碰数据库就抛异常堆栈里明确写着驱动类找不到。原因三种可能——jar 没打进WEB-INF/lib打进去了但是 5.1.x 版本类名应该是com.mysql.jdbc.Driver或者容器本身带了一个老版本驱动类加载时先命中了它。解决find . -name mysql-connector*.jar确认驱动文件存在MySQL 8 用com.mysql.cj.jdbc.Driver5.7 用com.mysql.jdbc.Driver两者不能混如果容器lib目录下也有同名驱动删掉容器里的那份让应用自带的生效。5.2 数据与编码类问题记录三菜品名称入库变成问号页面显示乱码现象后台新增一道菜数据库里查出来是???或者页面标题上出现一串菱形方块。原因链路上有三处字符集任何一处是 latin1 都会断。数据库/表没设utf8mb4连接串缺characterEncodingutf8JSP 页面没写% page contentTypetext/html;charsetUTF-8 %。解决先SHOW CREATE TABLE dish;看表字符集不对就ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4;再核对连接串的编码参数最后统一在过滤器和 JSP 头部各设一次三处齐了才不会漏。已经写成问号的数据改不回来只能重录所以建库那一步别偷懒。5.3 业务逻辑类问题记录四下单提示成功但库存数字纹丝不动现象订单列表里有新订单明细也对可菜品表里stock一直是原值。原因库存扣减的 SQL 放在了事务之外或者 DAO 各自开了连接主事务回滚了它也不知道还有一种情况是扣减语句写成了SET stock ?直接赋值而不是stock stock - ?看着执行成功其实把库存改成了固定值。解决确认所有 DAO 方法接收同一个Connection参数并且没有内部getConnection()确认扣减用的是stock stock - #{qty}并且带AND stock #{qty}最后在commit()之前打断点看每一步的影响行数是不是都在预期内。这类问题最典型的特征是「部分成功」一旦见到部分成功先怀疑事务边界。5.4 静态资源类问题记录五菜品图片上传成功页面上一片裂图现象上传接口返回成功数据库里img字段也有值但img src...显示不出来浏览器控制台 404。原因存的是绝对路径如D:\upload\1.jpg浏览器没法访问或者文件写到了WEB-INF/upload下这个目录对外不可见再或者img字段存的是upload/1.jpg而页面在/dish/list这种二级路径下相对路径解析错了。解决统一存以/开头的应用内相对路径页面渲染时拼上${pageContext.request.contextPath}上传目录放在webapp下而非WEB-INF下如果用到 SpringMVC检查mvc:resources有没有把/upload/**映射到实际目录漏了配置就算文件真实存在也是 404。6. 二次改造让这套骨架从「能演示」变成「能讲清楚」跑通只是及格线。这套代码真正的价值在于它的结构足够干净改起来不牵一发动全身。我一般的改造顺序是先补数据一致性再补交互最后才是加功能因为前两步决定了你改动之后还敢不敢继续动。改造点具体做法验证方式购物车持久化新建cart表user_iddish_id唯一键用ON DUPLICATE KEY UPDATE做累加换浏览器登录购物车内容还在订单号生成时间戳换成「日期 库自增序列」或雪花算法并发压 50 单order_no 无重复重复提交防护订单表加token字段页面渲染时下发提交时校验并删除连点提交按钮只生成一单后台权限细化角色常量抽成枚举AdminFilter按 URI 前缀分级普通账号直敲后台地址返回 403慢查询治理给orders(user_id, create_time)加联合索引EXPLAIN看 type 从 ALL 变 ref改造完怎么确认没改坏我的习惯是准备一份最小回归清单每次动完代码照着走一遍比凭记忆点页面靠谱得多# 用 curl 做一轮冒烟比手点快也方便比对返回 BASEhttp://localhost:8080/order # 1. 登录拿会话把 cookie 存下来后续请求带上 curl -s -c /tmp/ck.txt -d usernametestpassword123456 $BASE/login -o /dev/null -w login:%{http_code}\n # 2. 加购两道菜看是否 302 到购物车 curl -s -b /tmp/ck.txt -d dishId1qty2 $BASE/cart/add -o /dev/null -w add:%{http_code}\n # 3. 提交订单返回体里应带订单号 curl -s -b /tmp/ck.txt -X POST $BASE/order/submit | grep -o OD[0-9]* # 4. 直接查库核对库存是否真的减了 2 mysql -uroot -p -e SELECT id,name,stock FROM order_db.dish WHERE id1;这套脚本的意义在于把「页面看起来正常」换成「数据确实对上了」。第 3 步拿到的订单号要和第 4 步的库存变化对得上才算真的通过只看到跳转到成功页就收工很容易漏掉事务没生效的情况。还有一个容易被忽略的点改完之后记得把web.xml里的欢迎页、过滤器顺序、jdbc.properties三者再对一遍。我吃过一次亏把数据库从本地换到另一台机器只改了 properties 忘了改连接池的初始化参数结果应用启动正常第一次下单才报错排查了半天才想起是两个配置源。从那以后我每次动配置都强制先把「启动 → 登录 → 加购 → 下单 → 后台改状态」这条链路空跑一遍再改代码虽然多花五分钟但省下的往往是半个下午。这套订餐系统的包就放在那里按上面的顺序拆一遍比自己从零搭一个骨架要快得多希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →