简介:在Java Web后端开发中,购物车是典型的会话状态管理场景。其核心原理在于通过Session保存用户临时选购数据,或通过数据库持久化购物车条目,从而支撑从“加入购物车”到“下单结算”的完整交易链路。针对这类需求,JSP+SQL Server组合凭借简单直接的页面渲染与关系型存储能力,常被用于课程设计及企业内部Demo搭建。实际开发中,还需考虑购物车条目合并、库存并发扣减、事务一致性以及SQL Server特有的连接配置与中文乱码等问题。一套完整的JSP+SQLServer购物车项目,往往从压缩包中解压即来,但关键价值在于数据库表设计、Servlet动作分发、JNDI数据源配置和常见排错经验。掌握这些,就能快速将老项目落地为可运行的工程。
1. 这套 JSP+SQLServer 购物车项目,到底能拿来做什么
从网上下载一个“JSP(SQLserver).rar”的压缩包,解压后里面通常是一个 JSP + Java + SQL Server 的购物车项目。这种老牌组合至今仍然大量出现在课程设计、毕业设计和小型电商原型里,因为它的技术栈足够简单:JSP 负责页面渲染,Servlet 处理请求,JavaBean 封装业务逻辑,SQL Server 存数据。这套项目能解决的核心问题是“让一个没登录的用户先选商品、临时存起来、最后一次性结算”,也就是购物车最常见的 session 模型。适合两类人:一类是想快速跑通一套完整 CRUD 加交易链路的入门者,另一类是公司内部要做演示 Demo、需要短时间交付小商城功能的开发。别被标题里的 .rar 劝退,真正值钱的是项目背后的数据库设计和购物车状态管理方式,这篇文章就把这两件事拆开讲,让你拿到任何同类压缩包都能快速落地。
2. 购物车背后的技术分工:为什么是 JSP + SQL Server + JavaBean
2.1 购物车状态该放 session 还是数据库
先想清楚购物车要存什么。用户在商品页点“加入购物车”,这个动作发生在会话中,不要求用户一定登录。最直接的方案是把购物车整体塞进 HttpSession,用一个 JavaBean 对象存商品 id、数量、单价、加入时间,每次请求从 session 里取出来修改再放回去。这个模型的好处是服务端无状态压力小,用户关浏览器购物车就消失,符合匿名购物场景。
放数据库是另一种方案:建一张 cart_item 表,用 sessionId 或 userId 标识购物车归属。这么做的好处是用户换设备、清 cookie 后购物车还在,也方便后台统计。但数据库版需要你在每次请求时先查库,购物车的增删改都变成 SQL 操作,开发量上了一个台阶。
我接手这类 JSP 购物车项目时的判断标准是:如果只是演示和交作业,session 方案够用;如果要做成企业里多人协作的电商基础,至少要拆出 cart 表和 cart_item 表,登录后把 session 里的临时购物车合并进数据库。标题里这个项目既然强调 Java + SQL Server,大概率是给学生准备的简化版,但我会按“session 为主,数据库保留最终订单”的方式来讲,这样既不绕远,又不至于让入门者卡死。
2.2 数据库设计:购物车表、订单表与商品表的关联关系
购物车本身不是数据库里的“一张表”那么简单。如果你只在 cart 表里存所有商品 id 和数量,一次加购你可能要更新几十个字段。正确的做法是拆分三张核心表:商品表 product、购物车项表 cart_item、订单表 orders 与 order_item。
product 表提供商品的原始信息:商品 id、名称、价格、库存。cart_item 表负责临时购物车:主键自增,加上 session_id 或 user_id,再关联 product_id 和 quantity。orders 表在结算时产生,记录订单号、总金额、下单时间。order_item 表用来锁定下单时的商品快照,价格要用快照而不是实时商品价,否则商家改了价格会牵连历史订单。
这样一个设计让购物车和订单解耦:购物车随时可以清空,订单一旦创建就不再被购物车状态影响。SQL Server 里做这三张表的关联,外键不是必须的,但如果用了外键,删除 parent 表时要注意约束顺序,否则会报“DELETE 语句与 REFERENCE 约束冲突”的错。
2.3 建表脚本:一套可直接执行的 SQL Server DDL
下面是一个可以直接在 SQL Server Management Studio 里执行的建表脚本,数据库名用 shop。这段脚本覆盖了商品、购物车项、订单主表和订单明细表,并特意处理了价格和数量这两个容易出问题的字段。
CREATE DATABASE shop; GO USE shop; GO CREATE TABLE product ( product_id INT IDENTITY(1,1) PRIMARY KEY, product_name NVARCHAR(120) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE cart_item ( cart_item_id INT IDENTITY(1,1) PRIMARY KEY, session_id NVARCHAR(64) NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, add_time DATETIME NOT NULL DEFAULT GETDATE(), CONSTRAINT FK_cart_product FOREIGN KEY (product_id) REFERENCES product(product_id) ); CREATE TABLE orders ( order_id INT IDENTITY(1,1) PRIMARY KEY, order_no NVARCHAR(32) NOT NULL UNIQUE, user_id INT NULL, total_amount DECIMAL(10,2) NOT NULL, order_status TINYINT NOT NULL DEFAULT 0, create_time DATETIME NOT NULL DEFAULT GETDATE() ); CREATE TABLE order_item ( order_item_id INT IDENTITY(1,1) PRIMARY KEY, order_id INT NOT NULL, product_id INT NOT NULL, product_name NVARCHAR(120) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, CONSTRAINT FK_order_item_order FOREIGN KEY (order_id) REFERENCES orders(order_id) );这段 DDL 里有两个关键设计。price 用 DECIMAL(10,2),绝不用 float 或 varchar。用 varchar 存价格是旧项目常见的坑,按价格排序时会先做字符串转数字,结果 9.9 排在 10.0 后面,这是条件反射一样的必踩点。quantity 默认 1,防止前端漏传导致负数或不存在的数量进入表。session_id 字段定成 NVARCHAR(64),因为 Tomcat 默认 sessionId 长度是 32 位,但如果用了 JVM 参数或自定义管理器,长度会变化,留到 64 稳一点。
SQL Server 与 MySQL 的差异在这套脚本里体现得很明显:自增列用 IDENTITY(1,1),默认当前时间用 GETDATE(),字符串类型用 NVARCHAR 而不是 VARCHAR,否则中文会乱码。如果你把这段脚本直接丢到 MySQL 里执行,IDENTITY 和 GETDATE 会直接语法报错,所以这一步不要跨数据库偷懒。
3. 把购物车跑起来:用户添加商品、改数量、结算的完整链路
3.1 环境清单与 JDBC 驱动配置
这套 JSP 购物车最常见的运行环境是 JDK 8 + Tomcat 8.5 + SQL Server 2008 R2 或 2012。如果你用 SQL Server 2012 以上版本,驱动要选对,否则会连不上。JDK 8 推荐用 sqljdbc4.jar 或 mssql-jdbc 8.2;JDK 11 以上要用 mssql-jdbc 9.2 以后,否则在类加载时会报“不支持 major version 55”的错误。把驱动 jar 放到 Tomcat 的 lib 目录下,再将以下连接参数单独写到一个 db.properties 文件里,方便后续修改。
jdbc.driver=com.microsoft.sqlserver.jdbc.SQLServerDriver jdbc.url=jdbc:sqlserver://localhost:1433;DatabaseName=shop;encrypt=false;trustServerCertificate=false jdbc.username=sa jdbc.password=your_password这段配置里,encrypt=false 是 SQL Server 2017 之后的常见必选项。新驱动默认开启传输加密,但很多教学用的数据库并未正确导入证书,连不上时去掉加密反而能通。trustServerCertificate=false 保持默认,不建议改 true,否则有图方便却留下中间人风险的可能。如果你在 Windows 上装的是 SQL Server 2022,连接前记得在“SQL Server 配置管理器”里打开 TCP/IP 协议,并把端口确认成 1433,否则 JDBC 会报“无法打开登录”。
3.2 商品列表页用 JSP + JSTL 渲染,并把「加入购物车」参数传对
核心购物车操作从商品列表页的“加入购物车”按钮开始。这里最容易翻车的是表单提交方式:如果把参数拼在 URL 上,点击按钮直接跳转到 AddCartServlet?productId=1,用户刷新一次页面就重复加一次商品。我一般把加入购物车做成 POST 表单,如下所示:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <form action="${pageContext.request.contextPath}/cart" method="post"> <input type="hidden" name="action" value="add" /> <input type="hidden" name="productId" value="${product.productId}" /> <input type="number" name="quantity" value="1" min="1" max="${product.stock}" /> <button type="submit">加入购物车</button> </form>这里用${pageContext.request.contextPath}生成项目根路径,避免 Servlet 映射地址在部署时因项目名修改而失效。input 里的 max 属性别依赖它做库存校验,因为前端可以被绕过。JSTL 的 c:forEach 负责遍历商品详情,如果项目没有引入 JSTL 依赖,运行时会报“org.apache.jasper.JasperException: Unable to compile class for JSP”,那就需要补上 jstl-1.2.jar 和 standard.jar。
3.3 购物车操作 Servlet:add、update、remove、clear 四种动作
购物车 Servlet 统一承接所有操作,通过 hidden 的 action 参数区分动作。我习惯用一个 CartItem 的 JavaBean 加一个 Cart 对象,Cart 内部用 HashMap 存商品 id 到 CartItem 的映射,这样 update 时不用遍历列表就能按 id 找到条目。Servlet 代码如下:
@WebServlet("/cart") public class CartServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); HttpSession session = request.getSession(); Cart cart = (Cart) session.getAttribute("cart"); if (cart == null) { cart = new Cart(); session.setAttribute("cart", cart); } if ("add".equals(action)) { int productId = Integer.parseInt(request.getParameter("productId")); int quantity = Integer.parseInt(request.getParameter("quantity")); cart.add(productId, quantity); } else if ("update".equals(action)) { int productId = Integer.parseInt(request.getParameter("productId")); int quantity = Integer.parseInt(request.getParameter("quantity")); cart.update(productId, quantity); } else if ("remove".equals(action)) { int productId = Integer.parseInt(request.getParameter("productId")); cart.remove(productId); } else if ("clear".equals(action)) { cart.clear(); } response.sendRedirect(request.getHeader("Referer") != null ? request.getHeader("Referer") : "cart.jsp"); } }这段代码里有几个参数值得注意。request.setCharacterEncoding("UTF-8")必须放在读取参数之前,否则中文商品名传到下一页会乱码。parseInt在参数非数字时会抛 NumberFormatException,前面应该加 try-catch 或直接用正则过滤,这是被问到“为什么加入购物车页面 500”的高频原因。response.sendRedirect回来源页是用户友好的做法,但 Referer 头可以被伪造,订单相关操作不要依赖这个回跳逻辑。
Cart 对象里的 add 方法要做同商品合并。连续点击两次“加入购物车”,第一次加入 quantity=1,第二次应该数量变成 2,而不是新增一行。这个合并逻辑必须在 JavaBean 里做,简单代码:
public void add(int productId, int quantity) { CartItem item = items.get(productId); if (item == null) { items.put(productId, new CartItem(productId, quantity)); } else { item.setQuantity(item.getQuantity() + quantity); } }我见过多个压缩包里的 Cart 是直接用 ArrayList,加购时每次都 add 一个新对象,于是同一个商品在购物车里出现三四行,结算金额还算不出对的数据。所以拿到任何 JSP 购物车源码,先看 Cart 内部是 Map 还是 List,是 List 基本可以断定要重写合并逻辑。
3.4 结算页与订单生成:事务怎么写
购物车展示页遍历 Cart 里的条目,显示商品名、单价、数量、小计。结算时不能只把购物车里的值直接做 INSERT,而是要在同一个事务里做三件事:验证库存、扣减库存、插入订单表。
Connection conn = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); String checkStockSql = "SELECT stock FROM product WHERE product_id = ? FOR UPDATE"; String updateStockSql = "UPDATE product SET stock = stock - ? WHERE product_id = ? AND stock >= ?"; for (CartItem item : cart.getItems()) { PreparedStatement check = conn.prepareStatement(checkStockSql); check.setInt(1, item.getProductId()); ResultSet rs = check.executeQuery(); if (!rs.next() || rs.getInt("stock") < item.getQuantity()) { throw new RuntimeException("商品库存不足"); } PreparedStatement update = conn.prepareStatement(updateStockSql); update.setInt(1, item.getQuantity()); update.setInt(2, item.getProductId()); update.setInt(3, item.getQuantity()); if (update.executeUpdate() != 1) { throw new RuntimeException("更新库存失败"); } } // 插入 orders 表,再获取自增 order_id,批量插入 order_item conn.commit(); } catch (Exception e) { conn.rollback(); throw new ServletException("结算失败", e); } finally { if (conn != null) try { conn.close(); } catch (SQLException e) {} }这段事务代码里的关键点是FOR UPDATE行锁。SQL Server 默认的 READ COMMITTED 隔离级别下,两条并发请求同时读到相同库存,然后各自扣减,就可能卖出 200 件但库存只有 100 件。FOR UPDATE不是 SQL Server 的标准语法,SQL Server 里对应的是WITH (UPDLOCK, ROWLOCK),但很多老项目会把 MySQL 教程里的写法直接搬过来,导致 SQL 执行直接报错。在 SQL Server 中正确写法是:
SELECT stock FROM product WITH (UPDLOCK, ROWLOCK) WHERE product_id = ?另外,插入 orders 表后要拿到自增主键,SQL Server 里可以用SELECT SCOPE_IDENTITY(),但前提是你和订单 INSERT 在同一个连接和事务中。若分开两次获取连接,SCOPE_IDENTITY 会取到当前会话的最近自增值,不是上一句话的,必须一起写。
4. 常见坑与排错:让 JSP 购物车项目少走三天弯路
4.1 SQL Server 连接失败:检查三处而不是只盯 URL
现象:Tomcat 启动正常,一访问商品列表页就报Cannot create PoolableConnectionFactory或The TCP/IP connection to the host has failed。大多数新手第一念头是改 JDBC URL,改半天没用。
原因:SQL Server 默认不开启 TCP/IP 协议。尤其你在本机安装 SQL Server 2008/2012 时,如果只装了默认实例,TCP/IP 可能是禁用状态,JDBC 走 1433 端口根本连不进去。另一个原因是你用了localhost,但 SQL Server 实例是命名实例,要写成jdbc:sqlserver://localhost\\\\SQLEXPRESS;DatabaseName=shop。
解决:打开“SQL Server 配置管理器”,在“SQL Server 网络配置”里选中“MSSQLSERVER 的协议”,把 TCP/IP 启用,然后在“IP 地址”选项卡里把 IPAll 的 TCP 动态端口改为 1433。改完必须重启 SQL Server 服务。如果还是连不上,打开 Windows 防火墙,放行 inbound 的 1433 端口。我多次遇到改完协议不重启服务,然后一直卡在同一报错上。
4.2 中文乱码:POST 和 GET 要分开处理
现象:商品列表页正常,但购物车页面里商品名显示成“???”。
原因:POST 请求没有设置request.setCharacterEncoding("UTF-8"),这是最常见的。但有的项目设置了仍然乱码,因为 Tomcat 8 开始 GET 请求的 URL 默认使用 UTF-8,而 SQL Server 数据库排序规则如果是 Chinese_PRC_CI_AS,那么数据本身没问题,问题出在数据库连接参数的字符集上。
解决:第一,在所有 Servlet 的 doPost 开头第一行加上request.setCharacterEncoding("UTF-8");第二,检查 jdbc.url 是否加了characterEncoding=UTF-8,旧版 sqljdbc 驱动不认这个参数,它靠的是数据库排序规则;第三,如果 GET 方式传中文,要在 Tomcat 的 conf/server.xml 中给 Connector 加URIEncoding="UTF-8"。从 SQL Server 读出来的中文乱码,还要确认建表时字段类型是 NVARCHAR,如果建表用了 VARCHAR,中文会以 GBK 存储,从 JDBC 读出来转 UTF-8 就会出现乱码。
4.3 购物车丢数据:session 超时与会话 Cookie
现象:用户把商品加到购物车,闲了一会儿点结算,发现购物车空了。
原因:HttpSession 默认超时时间是 30 分钟。如果用户在商品页停留超过这个时间,session 失效,购物车这个 attribute 自然被回收。另外,如果浏览器禁用了 Cookie,JSESSIONID 不会回传,每次请求都是新 session。
解决:根据需求调整 session 超时时间。在 web.xml 里加 session-timeout 配置,单位是分钟:
<session-config> <session-timeout>60</session-timeout> </session-config>但超过 24 小时的大购物车就不该只用 session 存了。我处理老项目时会给 session 里的购物车加一个“合并”按钮,用户登录后把 session 里的 cart_item 追加到数据库 cart_item 表,这样 session 再丢也不影响已入库条目。如果你只想快速补漏,也可以在购物车页面禁用浏览器后退,但这解决不了超时问题。
4.4 JDBC 驱动版本与 Java 版本不匹配
现象:Tomcat 启动时报UnsupportedClassVersionError: com/microsoft/sqlserver/jdbc/SQLServerDriver : Unsupported major.minor version 52.0,或者类找不到。
原因:驱动 jar 编译时的 Java 版本比 Tomcat 运行时高。比如你下载了 mssql-jdbc 9.4 需要的 JDK 8,但 Tomcat 是 6.0 下层 JDK 1.6,自然载不了;反过来,sqljdbc4.jar 在 JDK 11 下也会因为模块化访问问题报java.lang.ClassNotFoundException。
解决:先确认当前 Tomcat 用的是什么 JDK。Windows 下 Tomcat 的 bin 目录里 catalina.bat 会读JAVA_HOME环境变量,查一下echo %JAVA_HOME%。如果 JDK 是 1.8,就用 sqljdbc4.jar(Java 6 编译)或 mssql-jdbc 8.2(Java 8 编译);JDK 11 以上直接换 mssql-jdbc 9.2+。驱动不兼容的坑不是换一个 jar 就完,还要清掉 Tomcat 临时文件,否则 Tomcat 的 work/Catalina 缓存里还留着旧类文件,启动报错让你误以为配置没改。
4.5 rar 包里的项目直接导入不识别:项目目录结构与部署名
现象:解压“JSP(SQLserver).rar”后,用 Eclipse 或 IDEA 导入,项目前面有红叉,Tomcat 发布列表里找不到这个项目。
原因:压缩包里通常只有 src、webroot、sql 文件夹和几个配置文件,没有.classpath、.project,IDEA 不会自动识别为 Java Web 项目。还有的 old 项目用 MyEclipse 向导生成,里面缺少web.xml的 Servlet 版本声明,Tomcat 9 默认按 Servlet 4.0 处理,会报语法错误。
解决:如果只是要看代码,直接用 IDEA 的Open选择解压后的根目录,然后手动添加 Web 根目录和依赖 jar。如果是要跑起来,我会新建一个空 Maven 项目,把 src 下的 Java 文件拷进去,把 webroot 或 WebContent 里的内容复制到 src/main/webapp,再在 pom.xml 引入 JDBC 驱动和 JSTL。别指望老项目能一键导入,通常需要手动调整。另外注意 rar 中用中文命名的文件,解压时选 UTF-8 编码,否则文件名乱码,IDE 会报文件找不到。
5. 部署与交付:把 JSP+SQLServer 购物车打包成可运行的工程
5.1 Tomcat 数据源配置:用 JNDI 而不是每个页面连一遍
老 JSP 项目最常见的坏味道是每个 JSP 页面顶部都用Class.forName+DriverManager.getConnection新开连接。购物车这样的场景请求量一大,连接直接耗尽,页面打开极其缓慢。正确的落地做法是配置 Tomcat JNDI 数据源,把连接池交给 Tomcat 管理。
首先在META-INF/context.xml里定义数据源:
<Context> <Resource name="jdbc/shopDS" auth="Container" type="javax.sql.DataSource" driverClassName="com.microsoft.sqlserver.jdbc.SQLServerDriver" url="jdbc:sqlserver://localhost:1433;DatabaseName=shop;encrypt=false" username="sa" password="your_password" maxTotal="20" maxIdle="10" maxWaitMillis="-1"/> </Context>然后在 web.xml 里声明 resource-ref:
<resource-ref> <res-ref-name>jdbc/shopDS</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>获取连接的代码变成在 Servlet 里通过 InitialContext 查找:
Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/shopDS"); Connection conn = ds.getConnection();注意参数差异:Tomcat 8 用maxTotal,Tomcat 7 用maxActive。如果你把老项目搬到 Tomcat 8 以上,原来的 maxActive 不会生效,连接池默认最大 8 个连接,并发稍微一高就会出现连接等待超时。JNDI 数据源第一个好处是可以集中改密码,不用动 Java 代码;第二个好处是连接失效后连接池会自动复活连接,不会像 DriverManager 形式那样每次都要重启 Tomcat。
5.2 数据库初始化脚本与数据文件的移交清单
交付一个 JSP+SQLServer 购物车项目给别人时,不能只扔一个 rar。至少要有四类文件,否则对方大概率跑不起来。下面是一份实际工作中要用到的移交清单。
| 文件/目录 | 作用 | 注意事项 |
|---|---|---|
| sql/shop_init.sql | 建库建表及商品示例数据 | SQL Server 2008 R2 及以上兼容 |
| lib/mssql-jdbc-x.x.jar | JDBC 驱动 | 给出版本号和适用的 JDK 版本 |
| context.xml | JNDI 数据源配置 | 密码用占位符,不要写明文 |
| README.txt | 部署步骤和环境要求 | 写清单机步骤,不要只写“解压后部署” |
我一般会把商品表的 INSERT 示例数据也放进 init.sql,方便对方第一次打开页面就能看到商品。示例数据里如果包含中文,要注意 SQL 文件保存为 UTF-8 格式,然后在 SQL Server Management Studio 里执行时选择“脚本编码为 Unicode”,否则中文会写入一堆乱码。另外,SQL Server 2012 以后的分页写法是 OFFSET FETCH,如果 init.sql 里带了老旧的 TOP 100 行语法,部署到 2012 以上版本不影响,但反过来的老版本会报错——尽量在脚本里写兼容语法的标准 SQL。
5.3 修改连接参数后如何验证配置生效
部署完成最怕的不是配置错了,而是你自认为改对了,实际没生效。修改连接参数后,验证要分三步。第一步,重启 Tomcat,观察 logs/catalina.out 里有没有SocketTimeoutException或Login failed for user 'sa'。第二步,访问一个最基础的 JSP 页面,如果它能进去,说明 Tomcat 本身没问题;再打开商品列表页,看是否有 500 错误。第三步,用一个数据库客户端连接同样的连接字符串,确认数据库登录名和密码本身没问题。
如果 JSP 页面能打开但查询为空,很可能是 SQL Server 数据库名错误或表属于另一个数据库。比如你建库时用了shop,但 JDBC URL 里DatabaseName=shop写成了db_shop,Tomcat 不会在启动时报错,因为连接是懒加载——第一次查询才报Invalid object name 'product'。这类报错在日志里表现为“对象名无效”,跟表不存在一样,很多人会去查表是否创建,实际是连错了库。
另一个验证技巧:在 JSP 里加一行临时调试输出,显示当前连接是否来自数据源,而不是直接输出连接对象(那个对象是代理,不能直接查看)。我自己常用一个最小的 servelet:读取 JNDI 数据源然后执行SELECT 1,能返回 1,说明连接链路全部通了。这个小验证页面在交付时记得删掉,否则会暴露数据源密码。
6. 让购物车真正可用:补充防重、幂等与数据校验的最后一个技巧
购物车从“能跑”到“可用”,只差最后一道工序:把用户不小心触发两次提交、以及恶意绕过前端的情况堵住。这一章不讲大道理,只给一个具体的补强方案——用时间戳 token 做防重复提交,同时把参数校验从 Servlet 提一层。
我的做法是在购物车 JSP 页面生成 token 存入 session:
<% String cartToken = UUID.randomUUID().toString(); session.setAttribute("cartToken", cartToken); %> <input type="hidden" name="token" value="<%= cartToken %>" />Servlet 里在读取 action 之前先校验:
String token = request.getParameter("token"); String sessionToken = (String) request.getSession().getAttribute("cartToken"); if (token == null || !token.equals(sessionToken)) { response.sendError(400, "invalid token"); return; } request.getSession().removeAttribute("cartToken");这段代码能防止用户点击两次“提交订单”产生两笔订单。提交成功后立刻移除 token,第二次请求拿不到 token 就直接返回 400。注意这里的 token 必须在生成订单的 Servlet 中移除,不要在添加购物车时移除,否则用户正常操作也会被拦。
另一个必须补的校验是库存不要信前端:我在第 3.4 节提过WITH (UPDLOCK, ROWLOCK),那是最后一层安全网,下单前一定要真实查库。很多老项目在 JSP 页面上用<c:if test="${product.stock > quantity}">做判断,这只能给用户看,不能作为业务逻辑。真正上线前改掉这种写法,用 PreparedStatement 把 productId 和 quantity 绑定到 SQL Server 的库存更新语句上,一是防 SQL 注入,二是防并发超卖。
还有一个小细节:购物车里的商品数量上限。有人会把前端 max 改成 9999,然后结算时数据库溢出或库存变负数。在 Cart.add 方法里加一个Math.min(quantity, 99)的阈值,这是我做项目时踩过最无语的坑。对方明明是正常操作,只是多按了几下,然后整个事务回滚,订单没生成,还带着一个晦涩的 SQL 异常。
这个项目做完之后最深刻的体会是:JSP+SQLServer 这一套老技术组合,坑不在语法,而在状态管理。session 容易丢,SQL Server 连接配置繁琐,并发时要谨慎加锁。把状态管理想清楚,把 token 加进去,把库存校验放在事务里,这个购物车项目就真的能从作业变成能给人用的东西。希望帮到你。
本文还有配套的精品资源,点击获取