简介:一套面向JavaWeb初学者的简易购物车完整项目源码,适合学习Servlet、JSP、Session管理、JDBC数据库操作及MVC分层思想。压缩包共54个文件,核心代码为14个Java源文件、6个JSP页面,同时附带数据库脚本、配置文件、项目说明和开发文档,整体仅1.58MB,便于直接导入IDE运行调试。项目实现在线购物车的商品加入、删除、数量更新、金额统计等典型功能,并配有逐功能笔记与安装部署说明,可帮助读者串联前后端交互逻辑。已有4125人学习下载,对想通过完整案例掌握JavaWeb开发流程的开发者有较高参考价值。
1. javaWeb 简易购物车:一个能跑通全流程的最小实现到底长什么样
刚做完一个 javaWeb 课程设计,或正在准备毕设的人,多半会被“购物车”这三个字卡住。搜“javaWeb 简易购物车源代码”,得到的基本是两种情况:要么是几百兆的 SSH 古董项目,JDK 版本比你电脑还老;要么是只贴了 CartServlet 和 JSP 页面的碎片,数据库脚本没有,跑起来直接 404。我的看法是,购物车在 javaWeb 里是个特别合适的练手项目——它同时牵扯 Session 状态管理、Servlet 生命周期、JSP 取值、JDBC 查询、表单重复提交这几个必考知识点,但逻辑本身又足够简单,不涉及复杂的业务规则和事务。本文要给的是一套我常用的最小实现方案:三张表、五个类、六个页面,不用框架,不引 Maven 依赖,只用 Tomcat 自带的 servlet-api 就能在 IDEA 里跑通。适合的人群很明确:正被 javaWeb 大作业卡住的学生、想从 SSH 旧代码迁移到 Servlet 3.0 写法的人,以及想知道“购物车到底该不该存数据库”的初学者。下文所有代码都按这个思路来——不贴完整源码包,但把每一步拆到你照着敲就能跑。
2. 先想清楚:购物车的数据放在哪里,三种存法的取舍
2.1 只用 Session 存:最快跑通,但关掉浏览器就丢
大多数简易购物车的实现首选 Session,因为购物车的核心特征就是“临时性”。用户逛店铺、加购物车、结账,这中间的状态没必要写进数据库,等用户关掉浏览器,购物车里的东西本来也应该清空。用 Session 实现的核心逻辑就一句话:第一次访问时创建一个空的 HashMap 放进 Session,之后每次“加购”请求都从 Session 里把这个 Map 取出来,修改后再放回去。好处是代码量极少,不需要为购物车建表,也不需要考虑多用户之间的数据隔离——Session 天然属于单个用户。
但 Session 方案的短板也很明显:Session 默认存活时间是 Tomcat 里配置的 30 分钟,用户加完购物车,逛了四十分钟再回来点结算,购物车已经空了。更麻烦的是,Session 是保存在服务器内存里的,用户量大时,每个用户的购物车都占一块内存,这会让服务器压力非常大。简易项目里这个坑不明显,但如果想把这个项目扩展成能上线的版本,就必须考虑把购物车持久化。
2.2 存数据库:多端同步,但要处理“登录”和“合并”两件麻烦事
把购物车落库,常见做法是建一张 cart 表和一张 cart_item 表,前者记录购物车归属,后者记录每个商品的数量。伪代码是cart(user_id, created_at)和cart_item(cart_id, product_id, quantity, added_at)。这个方案的第一个麻烦是用户必须登录——如果允许匿名加购,你得给每个匿名访客生成一个临时标识,否则根本不知道购物车属于谁。第二个麻烦是合并逻辑:用户在手机上加了一个商品,又在电脑上加了一个,两边购物车怎么合并?需要一个按 user_id 查询然后把两个列表合并的接口。
对一个“简易”购物车,我一般不建议第一步就上数据库。如果你的题目要求里写了“购物车数据需要持久化”或者“支持未登录状态加购物车”,那可以考虑用 Cookie 存购物车 ID,把购物车数据本身放数据库。如果没有这个要求,Session 方案足够交差。
2.3 折中方案:Cookie 存购物车明细,适合匿名用户的场景
还有一个容易被忽略的存法:把购物车整个序列化成 JSON 字符串放进 Cookie。这个方案适合“用户根本不登录”的简易商城——比如一个内部演示项目。好处是服务器完全无状态,购物车跟着浏览器走,关掉再打开也还在。坏处是 Cookie 大小限制 4KB,只能放十几个商品,而且数据对用户可见,价格字段一改就出大事。所以这个方案只能用来装“商品 ID + 数量”,价格、库存必须在结算时从数据库重新读取。
我实际做项目时见过有人把单价也塞进 Cookie,结果用户改个 Cookie 就能以一折买货。这是购物车实现里最经典的翻车点之一。简易项目图省事可以接受 Cookie,但结算时务必以数据库的价格为准。
3. 从零写一个 Servlet + JSP 的购物车:建表、加购、减购、清空
3.1 数据库脚本:三张表就够,别迷信外键
这个方案的数据库只有三张表:用户表(user)、商品表(product)、购物车表(cart,其实叫 cart_item 更准确)。为了保持“简易”,我不建独立的 cart 主表,直接把 user_id 挂在 cart_item 上,用户第一次加购时插入记录,之后按 user_id 查。
CREATE DATABASE IF NOT EXISTS cart_demo DEFAULT CHARSET utf8mb4; USE cart_demo; CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(100) NOT NULL ) ENGINE=InnoDB; CREATE TABLE `product` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL, `stock` INT NOT NULL DEFAULT 0, `image` VARCHAR(255) ) ENGINE=InnoDB; CREATE TABLE `cart_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `product_id` INT NOT NULL, `quantity` INT NOT NULL DEFAULT 1, UNIQUE KEY `uk_user_product` (`user_id`, `product_id`) ) ENGINE=InnoDB;这里的核心设计是UNIQUE KEY uk_user_product,它保证同一个用户对同一个商品只有一条记录,重复加购时走 UPSERT 而不是再插一条。很多初学者会把 quantity 设成主键之外随意更新,结果同一个商品出现两行,数量分别在两个行里。DECIMAL(10,2)是价格字段的正确姿势,不要用DOUBLE——浮点数计算金额会有精度误差,后面避坑章细说。外键我故意不建,不是因为外键不好,而是简易项目常常要手动清数据、改数据,外键约束反而碍事。
初始化数据时插入两个商品用于测试:
INSERT INTO `product` (`name`, `price`, `stock`) VALUES ('Java核心技术卷I', 119.50, 100), ('Spring实战第6版', 89.00, 80);3.2 购物车相关的 JavaBean 设计:两个类,别多写
实体类我一般分两层:Product 对应商品表,CartItem 对应一行购物车记录。CartItem 里除了 productId 和 quantity,还需要一个冗余的 productName 和 price 字段,因为在 JSP 页面展示购物车时,不可能每次遍历都再去查一次数据库——那样会产生 N+1 查询问题。如果你只建了 CartItem 和数据表字段一一对应,JSP 里展示商品名称就得在循环里调用 ProductDao,这是最常见的性能问题。
public class CartItem { private int productId; private String productName; private BigDecimal price; // 用 BigDecimal,别用 double private int quantity; // getter / setter 省略,IDEA 自动生成 public BigDecimal getSubtotal() { return price.multiply(BigDecimal.valueOf(quantity)); } }注意getSubtotal()这个方法是计算一行购物车的小计金额,它在 EL 表达式里可以直接用${item.subtotal}取值,不用在 Servlet 里算好再塞进作用域。BigDecimal 的multiply接收的是 BigDecimal 对象,所以要先BigDecimal.valueOf(quantity)把 int 转换一下。这里还有一个细节:price字段要从ResultSet.getBigDecimal()取,不要图省事先getString()再new BigDecimal(...),多一次转换就多一个报错的可能。
ProductDao 和 CartDao 各自提供最少的查询方法。CartDao 至少要有这三个方法:findByUserId(int userId)、addOrUpdate(CartItem item)、deleteByUserIdAndProductId(int userId, int productId)。addOrUpdate 对应 SQL 就是前面那个唯一键的 UPSERT:
INSERT INTO cart_item (user_id, product_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)VALUES(quantity)这个写法在 MySQL 8.0.20 之后会提示弃用,但对简易项目来说完全够用,不用刻意改成别名写法。不推荐在 DAO 里写复杂的 SQL 拼接字符串,就用 PreparedStatement 占位符,防 SQL 注入的同时也避免单引号把 SQL 炸掉。
3.3 加购和减购的 Servlet 实现:一个 Servlet,四个 action 参数
很多项目给每个操作建一个 Servlet,加购一份、删除一份、清空一份,结果 web.xml 里堆了一堆映射。我的习惯是一个 CartServlet 接收action参数,分发到四个操作:add、minus、remove、clear。这不是偷懒,而是购物车的操作本质都是“修改同一份购物车数据”,集中在一个类里更好维护。
@WebServlet("/cart") public class CartServlet extends HttpServlet { private final CartDao cartDao = new CartDao(); private final ProductDao productDao = new ProductDao(); @Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String action = req.getParameter("action"); HttpSession session = req.getSession(); Object userIdObj = session.getAttribute("userId"); if (userIdObj == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } int userId = (Integer) userIdObj; switch (action == null ? "" : action) { case "add": doAdd(req, resp, userId); break; case "minus": doMinus(req, resp, userId); break; case "remove": doRemove(req, resp, userId); break; case "clear": doClear(req, resp, userId); break; default: resp.sendRedirect(req.getContextPath() + "/cart/list"); } } // 以下四个方法省略内部实现,逻辑见下方说明 }service()方法统一处理编码和登录校验,四个私有方法只关心自己的业务。这里有两个必须注意的点:req.setCharacterEncoding("UTF-8")必须放在第一次读取请求参数之前,否则 POST 请求的中文商品名会乱码;登录校验要放在参数处理之前,防止未登录用户通过直接构造请求 URL 调加购接口并导致空指针——userId从 Session 里取出来如果是 null,后面的 DAO 查询会直接爆炸。
doAdd 方法里,先从请求里拿productId,转换成 int,然后构造 CartItem,把数量设成 1,调cartDao.addOrUpdate(item)。加购之前要从 ProductDao 查一次商品价格和库存,把价格塞进 CartItem,商品名也一并塞进去。这样虽然多了一次数据库查询,但能保证购物车里的价格永远是新价格,而不是某个历史价格。
private void doAdd(HttpServletRequest req, HttpServletResponse resp, int userId) throws IOException { int productId = Integer.parseInt(req.getParameter("productId")); Product product = productDao.findById(productId); if (product == null) { resp.getWriter().write("商品不存在"); return; } CartItem item = new CartItem(); item.setUserId(userId); item.setProductId(productId); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setQuantity(1); cartDao.addOrUpdate(item); resp.sendRedirect(req.getContextPath() + "/cart/list"); }这里不做库存校验和数量上限控制,是刻意为之——简易购物车不承担下单逻辑,库存校验应该留给结算阶段。如果你在简化项目里加了“库存不足不允许加购”,反而会引入一个复杂问题:加购后商品又被别人买完了,此时购物车里数量还在,结算时怎么提示?这个矛盾留到第 6 章说。
3.4 展示购物车列表:Servlet 查完放进 Request,JSP 用 EL + JSTL 遍历
展示页面需要两个数据:当前用户的购物车列表,和合计金额。Servlet 里的写法是:
List<CartItem> items = cartDao.findByUserId(userId); req.setAttribute("cartItems", items); req.getRequestDispatcher("/cart.jsp").forward(req, resp);这里用forward而不是sendRedirect,因为要带数据给 JSP。重要性在这:如果用sendRedirect,购物车数据存在 request 作用域里会丢,用户看到的永远是空购物车。新手最容易犯的错误是把这两个 API 搞混,然后反复查“为什么 list 为 null”。sendRedirect只在“操作完跳转到另一个 URL”时用,比如加购完成后跳回商品列表;展示数据时必须用 forward。
JSP 这边的核心遍历代码是:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:forEach items="${cartItems}" var="item"> <tr> <td>${item.productName}</td> <td>¥${item.price}</td> <td> <a href="cart?action=minus&productId=${item.productId}">-</a> <span>${item.quantity}</span> <a href="cart?action=add&productId=${item.productId}">+</a> </td> <td>¥${item.subtotal}</td> <td><a href="cart?action=remove&productId=${item.productId}">删除</a></td> </tr> </c:forEach>注意 JSP 里调${item.subtotal}实际是调 CartItem 的 getSubtotal() 方法,EL 表达式会自动把属性名首字母大写并拼上 get 去找方法。这就是为什么 JavaBean 里要写 getter——JSP 只认 getter,不认字段。另一个细节是数量的“+”号用的是action=add而不是单独写一个“updateQuantity”接口,因为 addOrUpdate 的 SQL 本身就是增量更新,传入 quantity=1 再加一次就等于数量 +1,逻辑统一,这是故意复用同一个方法的。
JSTL 标签库需要把两个 jar 包放到 WEB-INF/lib 下:jstl.jar 和 standard.jar(或 javax.servlet.jsp.jstl-api-1.2.jar 等 Tomcat 对应版本)。如果你遇到 JSP 报“can not find tag library”或者页面直接 500,先查这两个 jar 在不在。另外,页面顶部还要加一句:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>少这一行,JSP 输出的中文在浏览器里就会变成乱码。Tomcat 默认对 JSP 响应的编码是 ISO-8859-1,必须手动指定 UTF-8。这个和前面 Servlet 里req.setCharacterEncoding是一对,一个管请求,一个管响应,漏哪个都会出乱码。
3.5 在 IDEA 里跑起来的完整配置:Tomcat 版本选择与本项目相关的坑
IDEA 里跑 javaWeb 项目的标准流程是:新建一个普通 Java 项目,添加 Web 方面(右键项目 → Add Framework Support → Web Application),然后把项目结构改成 Maven 或直接把 jar 拖进 WEB-INF/lib。我强烈建议用 Maven,因为用 IDEA 内置的 Tomcat 集成时,Maven 项目能自动把依赖打包进 artifact,而手工建的项目经常会碰到“类找不到”的玄学问题——实际上类就在 WEB-INF/lib 里,但 IDEA 的 artifact 构建配置没把 lib 目录放进去。
具体配置步骤分四步:1)确认 Tomcat 版本。Java 8 推荐 Tomcat 8.5 或 9.0,Java 11 以上推荐 Tomcat 9.0 或 10.1。注意 Tomcat 10 的 Servlet 包名从 javax.servlet 改成了 jakarta.servlet,网上大量的旧教程代码用的是 javax,直接放到 Tomcat 10 会报 NoClassDefFoundError,这是最常见的一个坑。2)IDEA 里 Run → Edit Configurations,点加号选 Tomcat Server → Local,Deployment 页签里把 artifact 选成xxx:war exploded,而不是 war 包——war exploded 是解压目录,Tomcat 可以直接加载,启动快,改 JSP 不用重新打包。3)Application context 填/cart或者留空。很多人在这里填了/,然后访问页面时加上项目路径就 404。4)Server 页签里 On Update Action 选 Update resources,这样改 JSP 后按 Ctrl+F10 能热更新,不用重新启动服务器。
启动后访问http://localhost:8080/cart/list,如果出现 404,先去 Tomcat 的部署目录(%CATALINA_HOME%/webapps或 IDEA 输出目录)看有没有生成对应的文件夹,找不到就说明 artifact 没构建成功。如果出现 500,看 IDEA 控制台的异常栈,最常见的三类:ClassNotFoundException、SQLException、NullPointerException。这三类的排查方向分别是 jar 依赖、数据库连接配置、Session 里取不到值。
4. 购物车测试点:从正常加到边界情况,测试用例怎么写
4.1 核心功能测试点:加购、修改数量、删除、清空
购物车的测试点,按优先级排列:能不能正常加购;同一商品加两次会不会出现两行(应该是数量变为 2);减购到 0 会不会出现负数;删除单条购物车会不会顺手把别人的也删了;清空后再次访问购物车页面会不会报空指针。这里每一个点都对应前面代码里的一处设计:同一商品两行的问题由唯一键解决;减到 0 的问题要在 doMinus 方法里判断,如果当前 quantity 是 1,再减就该执行 delete 而不是 update;删除时 SQL 的 WHERE 条件必须是user_id = ? AND product_id = ?,漏了 user_id 会把所有用户的这个商品全删光。
测试入口建议用浏览器无痕窗口而不是开发者工具里模拟移动端——无痕窗口能保证 Session 是新开的,不会残留上次登录状态。手动测试用例可以这样列:登录后点加购三次同一商品,购物车数量应为 3;点减号两次,数量应为 1;再点一次减号,该商品应从购物车消失;点“清空购物车”,列表应为空;再刷新页面,列表仍然为空,且页面不报错。注意“刷新后仍然为空”这个用例很关键,因为有的实现是“从内存 list 里 remove”,刷新后 Session 里的 list 又被重新初始化成空数组,那其实是错误地初始化了购物车。
4.2 会话与权限相关的测试点:未登录加购、Session 过期、重复提交
购物车最容易“翻车”的不是 CRUD,而是会话边界。常见场景:用户未登录直接手动访问cart?action=add&productId=1,后端要返回 302 跳登录页而不是抛异常;用户登录后加购了几个商品,然后清浏览器 Cookie(Session 依赖 Cookie 里的 JSESSIONID 保持会话),再回页面点结算,购物车应该是空而不是报错;用户在商品详情页疯狂点“加入购物车”按钮 50 次,购物车数量应该加 50 而不是只加了一次,也不应该出现“重复提交表单”的 500。
这些测试点在代码里的落实是:登录校验在 Servlet 里统一拦截(前面已给出),Session 过期后的表现依赖 JSP 里对 null 值的处理——用<c:if test="${empty cartItems}">显示“购物车是空的”而不是后端直接往 request 里塞 null。至于重复提交,简易项目不用加 token 机制,但前端可以加“点击后禁用按钮”的 JavaScript,这个不算防重但能兜底。我一直跟人说,购物车的防重和订单的防重是两种难度——购物车是 idempotent 的(加一次和加两次本质是数量 +1),数量不对还能改,订单一旦重复提交就是真实扣款,所以简易购物车完全不用过度设计。
5. 避坑与常见问题:购物车实现里的五个经典踩坑现场
5.1 现象:加购后刷新页面,商品数量翻倍
这是新手必踩的坑。加了 1 个商品,点浏览器刷新,数量变成 2;再刷新,变成 3。原因是刷新页面会重新提交上一次的 POST 请求,浏览器对 POST 的“再次提交确认”被用户点了确定后,Servlet 又执行了一次加购逻辑。这不是购物车代码 bug,是表单重复提交问题。
解决:加购接口改用 POST,完成操作后立即resp.sendRedirect到购物车列表页。这样用户刷新的是 GET 的列表页,不会重复触发加购。如果项目是为了演示方便把加购也用了 GET 链接,那刷新确实会重复触发,除非每次加购时判断“Session 里上次加购时间”。更直接的建议是:写操作一律 POST,写完后 302 跳转。
5.2 现象:中文商品名在购物车页面显示为问号
现象分两种:数据库里存的是正确中文,页面显示 ????;或者数据库里存的就是乱码。第一种是响应编码问题,检查 JSP 头部有没有<%@ page contentType="text/html;charset=UTF-8" %>,没有就补上;第二种是数据库连接问题,JDBC URL 必须带useUnicode=true&characterEncoding=UTF-8,例如jdbc:mysql://localhost:3306/cart_demo?useUnicode=true&characterEncoding=UTF-8。还有一种隐蔽的情况是 MySQL 表本身的 charset 已经是 utf8mb4 了,但连接的编码不对,此时看数据库里存的数据是好的,就是查出来乱码。这个坑花了我一晚上才定位到,结果就是 JDBC URL 少了参数。
5.3 现象:购物车里的价格是旧价格,结算时和数据库对不上
根源是加购时把价格存进了 cart_item 表。商品后来涨价了,用户的购物车还留着旧价格。这个“对不对”取决于产品设计——如果购物车页面想展示对用户友好的“下单时价格”,那存一份快照没问题;但结算必须以数据库当前价格为准,否则就是价格漏洞。
解决:cart_item 里不存 price,存 product_id 和 quantity,展示时联表查询价格。或者在结算接口里重新SELECT price FROM product WHERE id = ?,并忽略购物车里的价格字段。前者的 SQL 扩展性更好,推荐第一版就这么做。
5.4 现象:两个用户同时加购,购物车数量互相干扰
原因是 Session 里存的购物车对象是同一个引用。常见写法是:
HashMap<Integer, Integer> cart = (HashMap) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); }这个写法本身没问题,问题出在把购物车对象定义成 Servlet 的成员变量:
public class CartServlet extends HttpServlet { private HashMap<Integer, Integer> cart = new HashMap<>(); // 错误写法 }Servlet 是单例多线程的,所有用户共享同一个 CartServlet 实例,所以这个 map 被所有人共享。症状就是用户 A 加购商品 1,用户 B 的购物车里也出现了商品 1。解决:购物车对象永远从 Session 取,绝不放在成员变量。换成人话——把数据放在“每个用户自己有的一份”里,而不是放在“所有用户共用的一份”里。
5.5 现象:IDEA 启动 Tomcat 后访问项目是 404,但 Tomcat 首页能开
页面 404 的常见原因按排查顺序排:1)IDEA 的 Application context 配置和浏览器访问路径不一致,比如配置的是/cart,但访问localhost:8080/;2)部署 artifact 没有成功,跑到 Tomcat 安装目录去看 webapps 下有没有对应文件夹;3)web.xml 的 servlet-mapping 写错,或者注解@WebServlet的路径以/开头写成了cart(少了斜杠也会报错,Tomcat 对非法 url-pattern 会在启动时直接 fail——但此时 IDEA 控制台可能没有红字,需要看 Tomcat Localhost Log 选项卡)。
如果你能看到 Tomcat 的默认首页但访问不了项目,基本可以断定是部署配置问题,去 Run → Edit Configurations → Deployment 页签看 artifact 是否已添加。还有一个容易被忽略的细节:IDEA 里 Tomcat 输出的“Artifact is being deployed”日志出现后,要等日志里出现 “deploy complete” 才代表真正部署完成,如果只是启动到一半就去访问,也会临时 404。
6. 从购物车到订单的“两次提交”改造:把简易购物车升级成可交作业的完整闭环
前面搭出来的购物车本质上只是“商品清单的增删改查”,离一个能让人眼前一亮的 javaWeb 大作业还差最后一步:购物车到订单的转换。这一步不需要建复杂的订单系统——用一个order表和order_item表就够了,但要在提交订单时完成一个“防重”设计,这个设计才是项目里最值得在答辩时讲的部分。
核心思路叫“两次提交”:第一次提交生成订单草稿,返回一个orderId和从购物车生成的订单明细快照;用户确认后第二次提交订单,此时后端只更新订单状态,不再从购物车读取数据。这样就把“购物车内容更改”和“订单已生成”两个事件解耦了。好处有三个:用户返回上一个页面修改购物车商品数量再重复提交,这个行为会自动变成“生成新订单”,而不是把同一个订单改得乱七八糟——因为订单数据已经快照在 order_item 里;第二个好处是结算时能重新校验库存和价格,校验不通过直接提示用户返回购物车修改,而不是先把订单生成了再告诉用户失败;第三个好处是订单状态字段可以从 0(草稿)走到 1(已确认),这个状态字段在答辩时很好讲故事——它对应了电商系统里的“待支付”。
// 第一次提交:从购物车生成订单快照 @WebServlet("/order/create") public class OrderCreateServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) { Integer userId = (Integer) req.getSession().getAttribute("userId"); List<CartItem> cartItems = cartDao.findByUserId(userId); if (cartItems.isEmpty()) { resp.sendRedirect("/cart/list"); return; } // 校验库存和价格,任何一个商品不满足,返回提示 for (CartItem item : cartItems) { Product dbProduct = productDao.findById(item.getProductId()); if (dbProduct.getStock() < item.getQuantity()) { req.setAttribute("error", "商品[" + dbProduct.getName() + "]库存不足"); req.getRequestDispatcher("/cart.jsp").forward(req, resp); return; } } // 插入 order,得到 orderId;再循环插入 order_item int orderId = orderDao.insert(userId); BigDecimal total = BigDecimal.ZERO; for (CartItem item : cartItems) { orderItemDao.insert(orderId, item); total = total.add(item.getSubtotal()); } orderDao.updateTotal(orderId, total); cartDao.clearByUserId(userId); // 清空购物车,防止再次提交 resp.sendRedirect("/order/confirm?orderId=" + orderId); } }这段代码里的关键是:生成订单后立即清空购物车,并且把需要展示给用户确认的订单明细放进了 order_item 表。如果不清购物车,用户刷新“确认”页面时 OrderCreateServlet 会被再次触发,第二次生成一个新订单——这算不算业务错误?算。因为用户只是刷新了一下,就凭空多了一个订单。防重这一步是购物车升级到订单后必须做的,比给 Servlet 加synchronized有用得多——你锁住的是当前这个用户的这个请求,没必要用全局锁。
这些年带人做 javaWeb 课设,看过的购物车代码少说也有几十份,最让我感慨的不是谁写得炫,而是很多人从一开始就把方向搞错了——花大量精力去调 CSS 让购物车页面好看,却不知道 Session 里那个 map 的并发问题才是真正会翻车的地方。我希望这篇东西能帮你在动手前先想清楚“购物车的数据到底该放在哪”,这个问题的答案决定了你的项目是只能演示还是能顶住真正的用户。至于要不要把购物车存数据库、要不要做订单快照,你可以在跑通上面的最小实现之后再决定——到那时你手里已经有了可运行的基线代码,改起来比从零开始踏实得多。希望帮到你。
本文还有配套的精品资源,点击获取