
简介一份面向JavaWeb初学者的完整课程设计源码基于Servlet、JSP、JavaBean等技术实现水果销售系统的商品展示、购物车、订单管理、用户登录等核心功能涵盖从商品列表浏览、加入购物车到下单结算的完整业务闭环。压缩包共843个文件、约16.2MB包含38个JSP页面、26个Java类、31个JAR依赖库、SQL数据库脚本以及大量JS、CSS、GIF/PNG图片等前端资源JS负责页面交互、CSS控制样式、GIF/PNG构建商品图与界面元素SQL脚本可直接初始化数据表文件类型覆盖页面、逻辑、配置、静态资源多个层面整体结构便于查找和修改。项目采用MVC模式组织代码覆盖JDBC数据库操作、Session会话管理、Filter过滤器、请求转发与重定向等高频知识点并附演示视频演示部署和运行效果帮助理解请求从页面到数据库再返回页面的完整链路。已有3002人学习下载既能配合课程设计答辩使用也可作为学习JavaWeb全链路开发的实战样本和功能扩展基础。1. JavaWeb项目源码水果销售系统先搞清楚这到底是什么一个能跑的JavaWeb项目源码和你在课程里敲的增删改查练习差的不是代码量是它有没有把“买卖水果”这件事讲圆。水果销售系统听起来土但它恰好覆盖了JavaWeb最常被问到的完整链路商品展示、购物车、订单、库存、会员管理。我刚带团队时见过太多人拿着源码跑起来就以为完事了结果连数据库脚本都不看一换环境就翻车。这篇就按我做项目的顺序把怎么读、怎么跑、怎么改成自己的东西讲清楚。适合谁看准备交毕设、想应付课程设计、或者刚入职要快速理解一个老项目的JavaWeb初学者。它能帮你少走弯路但前提是你得愿意动手敲一遍而不是只点“运行”按钮。目标只有一个让你拿到这套源码后能在半天内跑起来并且知道每一张表、每一个Servlet在干什么。2. 先把项目骨架拆开从web.xml到数据库脚本一条请求到底走几次跳转拿到JavaWeb项目源码的第一步不是急着配Tomcat而是先认路。水果销售系统这种典型SSH或ServletJSP结构的项目它的请求路径是有迹可循的。你要是不先把骨架看清后面改一个功能就得翻半天。2.1 读懂分层Servlet处理请求Service写业务DAO查数据库常见的做法是三层架构Controller层Servlet接收前端请求Service层处理业务逻辑DAO层操作数据库。水果销售系统里你大概率会看到FruitServlet、OrderServlet、UserServlet这几个入口它们在web.xml里配置了URL映射。我一般会先把Servlet类的前十行读完看它继承的是HttpServlet还是BaseServlet。如果是BaseServlet说明项目做了反射调用方法的封装URL会带上?methodadd这样的参数如果是传统写法就是一个方法一个doGet或doPost。这一步决定你后面调试时断点打在哪儿。// BaseServlet的典型封装CommonBaseServlet.java public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 从请求里取method参数比如 ?methodaddFruit String method req.getParameter(method); if (method null || method.isEmpty()) { method execute; // 默认方法 } try { // 通过反射调用当前Servlet里同名的方法 Method m this.getClass().getMethod(method, HttpServletRequest.class, HttpServletResponse.class); m.invoke(this, req, resp); } catch (Exception e) { // 反射调用异常通常是因为方法名写错或参数不匹配 throw new ServletException(反射调用方法失败: method, e); } } }这段代码的逻辑是前端请求FruitServlet?methodadd时service方法会拿到method参数值add通过反射调用FruitServlet类里的add(HttpServletRequest, HttpServletResponse)方法。参数说明method参数名必须和前端表单或超链接里写的namemethod一致否则默认执行execute方法这是新手最常见的“点按钮没反应”的原因。反射调用的好处是一个 Servlet 可以处理多个操作不用写一堆 if-else。2.2 数据库脚本怎么看先找水果表和订单表的外键关系水果销售系统最核心的表不会超过十张用户表、水果分类表、水果表、购物车表、订单表、订单项表。我拿到SQL脚本后会先看CREATE TABLE的数量再看外键约束和字段注释。-- 水果表 CREATE TABLE fruit ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 水果ID, category_id INT NOT NULL COMMENT 分类ID关联fruit_category.id, name VARCHAR(50) NOT NULL COMMENT 水果名称, price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, image VARCHAR(255) DEFAULT NULL COMMENT 图片路径, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ) COMMENT水果表; -- 订单项表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL COMMENT 关联order.id, fruit_id INT NOT NULL COMMENT 关联fruit.id, quantity INT NOT NULL COMMENT 购买数量, price DECIMAL(10,2) NOT NULL COMMENT 成交单价 ) COMMENT订单明细表;这里要注意DECIMAL(10,2)而不是FLOAT因为水果价格涉及金额浮点运算会有精度误差这是老项目里能看到的规范写法。order_item表同时外联order表和fruit表这就是订单和商品的多对多关系拆出来的关联表。如果你看到fruit表里没有category_id而是用type字符串字段说明这个项目版本较老后续扩展分类会很痛苦。2.3 导入SQL的三种姿势命令行NavicatIDE内置导入数据库脚本时我吃过亏用Navicat直接运行整个脚本遇到DROP TABLE IF EXISTS和中文注释乱码就会中断。后来固定用命令行导入干净利落。# 先建库再指定字符集导入避免中文乱码 CREATE DATABASE IF NOT EXISTS fruit_sales DEFAULT CHARACTER SET utf8mb4; USE fruit_sales; # 在终端执行导入 mysql -uroot -p --default-character-setutf8mb4 fruit_sales /path/to/fruit_sales.sql命令行导入的好处是能看到每一行报错而不是像图形工具那样弹个“Error 1064”就完事。参数说明--default-character-setutf8mb4必须和脚本文件里保存的编码一致Windows下脚本常用UTF-8但MySQL服务端可能默认是latin1不一致就会出现中文问号。导入成功后先查一下表数量再随便select一条数据确认中文正常。3. 把项目跑起来IDEA配置Tomcat和数据库连接池的血泪经验源码能读懂了接下来就是环境。这一步翻车率最高而且翻车点高度集中JDK版本不匹配、Tomcat版本不兼容、数据库驱动缺失、连接池配错。我带你按顺序来。3.1 IDEA运行JavaWeb项目配置从Project Structure到Artifact先说结论新版IDEA跑JavaWeb项目本质是三件事——让编译器认识依赖、让Tomcat认识Web应用、让Web应用认识数据库。新手最容易卡在Artifact这一环。# 典型目录结构Maven或普通Web src/main/java/ # Java源码 src/main/webapp/ # JSP页面、静态资源 WEB-INF/ # web.xml、classes、lib我一般会在IDEA里依次点开File → Project Structure → Artifacts点绿色加号选择Web Application: Exploded然后右键新建的Artifact选择Put into Output Root把依赖的jar包加进去。这个操作的目的是让Tomcat运行时能在WEB-INF/lib下找到所有需要的类。如果你发现启动Tomcat后ClassNotFoundException: com.mysql.jdbc.Driver基本就是Artifact里没把MySQL驱动打进去。端口配置在Run → Edit Configurations里选Tomcat ServerHTTP port默认8080如果被占用我改成8081。Application context我习惯设成/fruit这样访问路径就是http://localhost:8081/fruit/index.jsp。这里有两个坑第一是Open browser勾选后会自动打开浏览器但有时候Tomcat还没起完就报404建议去掉勾选手动刷第二是On frame deactivation默认Update classes and resources改JSP会自动热部署改Java类不一定生效需要点一下Redeploy。3.2 数据库连接池参数怎么调c3p0换成Druid顺手解决连接泄露老项目里常用c3p0配置在c3p0-config.xml。但c3p0的默认参数在并发稍微上来的时候容易卡死我一般直接换Druid代码改动极小收益明显。!-- druid配置替换原来的c3p0-config.xml -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver / property nameurl valuejdbc:mysql://localhost:3306/fruit_sales?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghai / property nameusername valueroot / property namepassword value123456 / !-- 初始连接数 -- property nameinitialSize value5 / !-- 最小空闲连接数 -- property nameminIdle value5 / !-- 最大活跃连接数 -- property namemaxActive value20 / !-- 获取连接时最大等待时间单位毫秒 -- property namemaxWait value60000 / /bean参数说明initialSize建议和minIdle一致避免频繁创建连接maxActive不是越大越好MySQL默认最大连接数是151你配200反而会报Too many connectionsmaxWait是获取连接的超时时间60000毫秒意味着连接池满了以后请求会等一分钟适合教学项目生产环境下一般30000以内。serverTimezoneAsia/Shanghai这个是新版MySQL驱动必填的参数不填会报时区错误这也是老项目跑在新环境下的经典翻车点。如果你看到连接池配置里没有testWhileIdle和validationQuery记得补上property nametestWhileIdle valuetrue / property namevalidationQuery valueSELECT 1 /这两行的作用是定期检查空闲连接是否还有效。MySQL默认wait_timeout是8小时超过这个时间连接会被服务端断开但连接池不知道下次取出来用就报Communications link failure。加了上面的配置连接池会在空闲时主动探活这算是我被坑了无数次后总结的后悔药。3.3 常见问题JDK版本不匹配和JSP编译报错如果你用的JDK17跑JavaWeb项目大概率会踩两个坑第一个是Tomcat 9.0以下不支持JDK17的字节码启动直接报UnsupportedClassVersionError第二个是JavaBean的useBean标签在JSP里实例化时要求类必须放在WEB-INF/classes下而不是仅存在于源码目录。# Java版本检查和Tomcat版本匹配建议 java -version # 如果是17Tomcat建议用10.1.x # 如果是8Tomcat建议用8.5.x原因Tomcat 10把javax.servlet包名换成了jakarta.servlet如果你的源码里写的还是import javax.servlet.http.HttpServlet用Tomcat 10编译会直接报包不存在。所以这两个版本必须绑定JDK8配Tomcat8.5或9JDK17配Tomcat10.1但源码也要跟着改包名。对水果销售系统这种老项目我建议直接用JDK8 Tomcat8.5省去改代码的时间。4. 核心功能实现拆解购物车和订单怎么联动库存项目跑起来了就该看业务了。水果销售系统的核心不是页面好看而是购物车加购、订单提交、库存扣减这三件事的联动。这是面试也爱问、功能也爱考的重点。4.1 购物车存储选Session还是Cookie老项目的购物车通常存在Session里好处是服务端可控坏处是重启Tomcat就没了。Cookie方案能持久化但每次请求都要传输数据水果种类多的时候会撑大报文。// 购物车核心类Cart.java public class Cart { // 用Map存购物车项key是水果IDvalue是CartItem private MapInteger, CartItem items new HashMap(); // 添加水果到购物车 public void addFruit(Fruit fruit, int quantity) { CartItem item items.get(fruit.getId()); if (item null) { item new CartItem(fruit, quantity); items.put(fruit.getId(), item); } else { // 已存在则累加数量 item.setQuantity(item.getQuantity() quantity); } } // 获取购物车总价 public double getTotalPrice() { double total 0; for (CartItem item : items.values()) { total item.getSubtotal(); } return total; } }代码逻辑说明items用MapInteger, CartItem而不是ListCartItem是为了避免重复添加同一水果时遍历列表。getSubtotal()返回的是price * quantity。注意这里没用BigDecimal这是老项目通病你以为算对了两毛钱实际浮点运算里一堆0.999999。如果只做毕设影响不大如果要做真实交易建议把所有价格计算改成BigDecimal。这个类不涉及数据库操作所以不需要DAO层直接存在Session里// 添加购物车的Servlet方法 public void add(HttpServletRequest request, HttpServletResponse response) throws Exception { // 获取当前Session里的购物车如果没有则新建一个 Cart cart (Cart) request.getSession().getAttribute(cart); if (cart null) { cart new Cart(); request.getSession().setAttribute(cart, cart); } int fruitId Integer.parseInt(request.getParameter(fruitId)); int quantity Integer.parseInt(request.getParameter(quantity)); Fruit fruit fruitService.findById(fruitId); // 检查库存超库存要拦下来 if (fruit.getStock() quantity) { request.setAttribute(msg, 库存不足当前剩余 fruit.getStock() 斤); request.getRequestDispatcher(/cart.jsp).forward(request, response); return; } cart.addFruit(fruit, quantity); // 重定向避免刷新页面重复提交 response.sendRedirect(request.getContextPath() /cart.jsp); }这段是最贴近实际业务逻辑的先取Session里的购物车没有就新建Integer.parseInt如果前端传的不是数字会抛NumberFormatException新手很容易忽略建议用try-catch包一层出现异常时默认设置数量为1库存校验放在加购时做但这样还不够因为用户加购后可能修改数量真正扣减库存应该发生在订单提交时。4.2 订单提交和库存扣减的先后顺序订单提交是事务操作的经典教学场景。核心逻辑是生成订单记录 → 生成订单项列表 → 扣减库存。这三步必须在一个事务里否则会出现订单生成了但库存没扣或者库存扣了但订单失败。// OrderService.createOrder 方法注意事务注解或手动控制 public int createOrder(Order order, ListOrderItem orderItems) { // 第一步向order表插入订单主表 String sql INSERT INTO order (user_id, total_price, status, create_time) VALUES (?, ?, ?, ?); // ... 执行插入返回自增主键 orderId // 第二步向order_item表批量插入订单项 String itemSql INSERT INTO order_item (order_id, fruit_id, quantity, price) VALUES (?, ?, ?, ?); // ... 循环执行插入或者用batch // 第三步扣减库存注意SQL里带条件防止超卖 String updateSql UPDATE fruit SET stock stock - ? WHERE id ? AND stock ?; int rows ...; // 执行更新 if (rows 0) { throw new RuntimeException(水果库存不足订单回滚); } return orderId; }为什么第三步的SQL要写AND stock ?因为在并发场景下两个人同时买同一个水果如果用“先查库存再判断再更新”的写法会存在竞态条件两个请求都查到库存还有1斤然后都执行更新最后库存变成-1。这个stock ?是让数据库自己判断如果条件不满足影响行数为0我们就知道库存不足了。这是最便宜的防超卖方案不需要加锁也不需要分布式锁。事务控制在Servlet层调用Service时开启// 用ThreadLocal或者Spring的TransactionTemplate管理事务 public void submitOrder(HttpServletRequest request, HttpServletResponse response) throws Exception { // 常见的做法是用Spring的声明式事务老项目里可能就是手动 // Connection conn DBUtils.getConnection(); // conn.setAutoCommit(false); try { // 执行上面三步 orderService.createOrder(order, orderItems); // 提交事务 // conn.commit(); // 清空Session里的购物车 request.getSession().removeAttribute(cart); response.sendRedirect(request.getContextPath() /order_success.jsp); } catch (Exception e) { // 回滚事务 // conn.rollback(); request.setAttribute(msg, 订单提交失败 e.getMessage()); request.getRequestDispatcher(/cart.jsp).forward(request, response); } }这里最容易被忽视的是setAutoCommit(false)之后finally里必须把连接关闭和恢复自动提交做干净。如果连接池里的连接没恢复自动提交下一次从连接池取出来使用时所有SQL都会在一个未提交的事务里执行数据永远写不进库且不报错。我接过的项目里有好几起“数据时有时无”的诡异bug最后查到都是连接状态污染。4.3 库存扣减后如何刷新页面上的剩余量扣库存成功后购物车和商品列表页的库存数字不会自动变。老项目是不做实时推送的通常就是订单成功页显示商品和数量用户返回商品页时重新查库存。如果实现了JSP的标准用法c:forEach遍历时每次都会从数据库读所以刷新后自然是最新的。%-- 商品列表页JSTL遍历商品列表 --% c:forEach items${fruitList} varfruit div classfruit-item img src${fruit.image} alt${fruit.name} / span${fruit.name}/span span${fruit.price}/span span库存${fruit.stock}/span a hrefFruitServlet?methodaddfruitId${fruit.id}quantity1加入购物车/a /div /c:forEach注意一个细节如果FruitServlet?methodadd在库存扣减失败时转发回了商品页那fruitList这个Attribute必须在转发前重新set到request里否则JSP取不到值报500错。常见的做法是商品列表和购物车操作共用同一个FruitServlet所以在add方法处理完库存失败后要主动调用一次fruitService.findAll()再setAttribute(fruitList, fruitList)确认RequestDispatcher.forward到了index.jsp。5. 避坑指南跑JavaWeb项目源码时最容易翻车的5个点源码能跑是一回事跑得稳是另一回事。下面这5个坑我几乎每次帮人调项目都会碰到至少一个。按“现象→原因→解决”写清楚你遇到类似报错直接对照着查。5.1 Tomcat启动秒退双击startup.bat窗口一闪而过现象在IDEA里配置好Tomcat点运行后Console里日志还没输出窗口就消失浏览器访问404。原因大多数情况是环境变量JAVA_HOME没有指向JDK或者Tomcat版本和JDK不兼容。Tomcat的startup.bat会调用%JAVA_HOME%\bin\java如果JAVA_HOME配的是JRE或者根本没配Tomcat会找不到Java环境直接退出。解决先确认JAVA_HOME指向的是JDK安装目录不是bin目录然后在IDEA里Run → Edit Configurations检查Tomcat Server的JRE选项是否选择正确。如果还不行打开命令行手动运行Tomcat看具体报错# 进入Tomcat的bin目录运行startup.bat cd C:\apache-tomcat-8.5.100\bin startup.bat窗口会保留并打印日志绝大多数原因都会在最后几行显示比如UnsupportedClassVersionError或者端口被占用。比起在IDEA里看隐藏日志命令行是最直接的排查方式。另外注意如果IDEA里配的是Tomcat路径但用的是内置Tomcat那JAVA_HOME就失效了要在IDEA的Project Structure → SDK里确认项目SDK是JDK而不是JRE。5.2 中文乱码页面显示问号数据库里的数据也是问号现象JSP页面上商品名称显示???或者数据库表里数据正常但页面显示乱码。原因有三个层面的编码不一致——JSP文件保存格式、JSP页面pageEncoding声明、MySQL表字符集。三处只要有一处是ISO-8859-1或GBK而其他是UTF-8就会乱。解决把三处统一为UTF-8。JSP文件顶部写page pageEncodingUTF-8 contentTypetext/html;charsetUTF-8数据库建库时用utf8mb4MySQL连接URL加characterEncodingutf8。5.3 注册功能报SQL语法错误但直接在Navicat里执行没问题现象注册页面填写信息提交后报You have an error in your SQL syntax但在Navicat里手动执行同样的SQL却正常。原因这个坑大多数时候不是SQL本身错而是user表用了数据库的关键字作字段名。比如name、order、desc在MySQL里是保留字。水果销售系统里订单表一般叫order如果SQL里写INSERT INTO order (user_id, ...)MySQL会把order当作关键字解析直接报错。解决看报错信息里那句SQL把关键字表名或字段名用反引号包起来INSERT INTO order (user_id, total_price, status, create_time) VALUES (?, ?, ?, ?)注意包的是反引号不是单引号。如果SQL是拼接出来的比如String sql INSERT INTO order ...把它改成 INSERT INTOorder... 。同时检查DAO层用的是PreparedStatement还是Statement。如果是后者拼接用户输入会导致SQL注入风险建议统一改成预编译。5.4 登录功能验证码永远提示错误明明输对了现象登录页有验证码输入图片里显示的字符后提交后台提示验证码错误刷新验证码再试还是错。原因验证码实现里Servlet在往Session里存验证码字符串之前先响应输出了图片流。而JSP在页面加载时又执行了一次验证码刷新导致图片里的文字是新生成的Session里存的却是旧的或者是空。典型代码是img标签直接指向验证码Servlet每次img src请求都会生成新验证码并覆盖Session中的值用户提交时用Session里的校验自然对不上。解决验证码生成逻辑只能在一个地方写通常是在生成图片的Servlet里并且不要让页面额外调用。常见做法是把验证码字符串存到Session在同一请求里输出图片。如果JSP里写了类似jsp:include pagecaptcha.jsp或另一个img srccaptchaServlet同时又在登录Servlet里生成了一次验证码要删掉后者的生成逻辑。另外注意校验时先看Session里是否存在如果不存在直接提示“请刷新验证码”区分“无验证码”和“验证码错误”两种提示排查起来会清楚很多。5.5 改完Java代码点Redeploy后发现数据丢了重新登录失效现象在IDEA里修改了Java类点Update resources或Redeploy后页面跳回登录页订单数据也消失。原因指向target目录的Artifact是Exploded形式Redeploy时IDEA会把整个WEB-INF/classes目录重写并重启Web应用上下文。Session数据存在JVM内存里应用上下文重启后Session就失效了所以登录状态丢失。这不是代码bug是开发模式的固有行为。解决如果你是频繁改Java类建议每次改完直接点右上角的“重启”按钮让Tomcat整个进程重启而不是Redeploy如果你只是改JSP用Update resources就够了Session不会丢。在IDEA里把On frame deactivation设置为Do nothing手动控制部署时机避免它自动更新打断调试。血泪经验调试购物车和登录状态时尽量不要用热部署强行用只会让你觉得代码没改对其实只是Session丢了。6. 把二手源码变成自己的三个必改的技巧和验证方法你能把源码跑通只是第一步毕设答辩或领导验收时最怕被问“你做了什么”。水果销售系统这种大众题目光抄肯定不行。下面这些改动是我觉得性价比最高的三个方向。6.1 把下拉框改成级联筛选分类和价格区间联动原项目通常是单个分类下拉框页面刷新后再查商品列表。你可以改成两级联动选分类后价格区间下拉框动态更新。这一步做得好答辩时能讲出“用户体验”四个字。// 在index.jsp里加一段原生JavaScript不引第三方库 function loadPriceRange(categoryId) { var priceSelect document.getElementById(priceRange); priceSelect.innerHTML ; // 先清空 if (categoryId ) { priceSelect.options.add(new Option(全部价格, )); return; } // 动态获取该分类下的价格区间 fetch(FruitServlet?methodgetPriceRangecategoryId categoryId) .then(res res.json()) .then(data { data.forEach(range { var opt new Option(range.label, range.value); priceSelect.options.add(opt); }); }); }代码逻辑说明loadPriceRange在分类下拉框的onchange事件里调用拿到categoryId后发异步请求到FruitServletServlet返回JSON数组再填充到价格区间下拉框。这里用fetch而不是XMLHttpRequest代码更简洁适配现代浏览器。注意当你加了异步请求你的FruitServlet需要支持返回JSON的getPriceRange方法否则前端会报404。JSON格式需要引入Jackson或fastjson老项目默认没有需要在pom.xml或lib加上依赖。6.2 防重名注册异步校验用户名原项目的注册页面用户填完用户名必须提交后才知道是否重名体验很差。你可以改成输入框失焦时异步校验有重名立刻提示。$(#username).blur(function() { var username $(this).val().trim(); if (username.length 0) return; $.get(UserServlet, { method: checkUsername, username: username }, function(result) { if (result exists) { $(#msg).text(用户名已被占用).css(color, red); } else { $(#msg).text(用户名可用).css(color, green); } }); });逻辑说明blur事件是输入框失焦时触发get请求带methodcheckUsername和username参数。UserServlet里对应的方法要只做查重并返回字符串exists或available不要重定向也不要返回页面。这个代码看起来简单但面试里常被问“如果这个方法返回500错误前端会怎样”答案是$.get的回调不执行页面无提示。所以要同时写.fail(function(){ $(#msg).text(网络异常) })。这也是把老项目改成前后端分离思路的第一步。6.3 验证跑通后的数据完整性压测一下库存防超卖改完功能后你怎么证明它真的没问题不能光靠“我点了一下没报错”。验证库存防超卖的常见做法是开两个浏览器窗口同时登录两个账号抢购同一件库存只剩1的商品。# 模拟并发下单的简易验证用curl多次请求 for i in $(seq 1 20); do curl -b cookies.txt -c cookies.txt \ http://localhost:8081/fruit/OrderServlet?methodsubmitOrderfruitId1quantity1 done wait # 执行后查数据库确认库存不为负数 mysql -uroot -p fruit_sales -e SELECT stock FROM fruit WHERE id1;这个脚本不在生产环境跑仅用于本地验证。原理是发起20个并发请求最后检查库存字段。如果SQL里写了AND stock ?最终库存只会是0如果没写库存可能变成负数说明事务逻辑有漏洞。参数说明-b cookies.txt -c cookies.txt是让curl携带Session Cookie因为你的下单接口依赖登录态如果不带Cookie请求会被登录过滤器拦下来。循环里每个请求都带同一个Cookie文件模拟同一个用户并发下单一般是够用的。这个验证做完你可以很自信地在答辩里说“用脚本压过库存不会超卖”。最后说两句我的习惯。接手任何JavaWeb源码我第一件事永远是复制一份原始SQL脚本和项目压缩包存好任何改动都在副本上进行。哪怕是个毕设级的小系统你也不知道哪次改坏了想回退。改代码时顺手把关键业务逻辑的注释补上不光是给别人看更是给两周后的自己看。希望帮到你。本文还有配套的精品资源点击获取