简介:本资源是一份面向Java初学者与课程设计学生的订单管理系统毕业设计文档,聚焦企业级订单业务场景,解决订单信息科学化管理、多角色协同与流程提效问题。文档完整呈现了基于B/S架构的系统设计方案:前端采用JSP动态页面,后端以Java Servlet与JavaBean实现业务逻辑,MySQL数据库支撑数据持久化,并集成SSL加密保障安全性;功能模块覆盖员工侧(个人中心、订单管理、站内信)与管理员侧(财务管理、员工管理、通知管理)双角色需求。资源为单个1.66MB的DOCX文件,内容结构规范,含摘要、系统分析、开发环境(Java+MySQL+B/S)、详细设计与测试报告等完整章节,适合作为课程设计参考、毕设开题素材或Java Web技术实践范例。目前已有144人学习下载,文档理论结合实操,代码逻辑清晰、部署说明明确,可直接用于教学复现与二次开发。
1. 这不是又一个“学生课设模板”:用 Java + B/S + MySQL 落地真实可运维的订单管理系统,解决的是库存扣减不一致、并发下单超卖、订单状态流转不可追溯这三类高频翻车现场
你手头这份《基于Java订单管理系统设计与实现.docx》——别急着删,它大概率是某次课程设计或毕设文档的命名风格,但标题里藏着三个硬核落地信号:Java(非Spring Boot空壳,而是JDBC/Servlet层可控性)、B/S结构(意味着必须直面HTTP无状态、会话管理、前后端分离边界)、MySQL(不是“装个数据库就行”,而是要处理事务隔离、索引失效、连接池抖动等生产级问题)。这不是教你怎么画UML图或写“系统具有登录、下单、查询功能”的套话,而是聚焦在:当20个用户同时点击“提交订单”,库存从100变成99还是95?当管理员后台修改订单状态,前端页面刷新后为何显示“已发货”,而数据库里仍是“待支付”?当MySQL重启后,Tomcat连不上库,日志只报Communications link failure,你该先看哪三行?本文全程基于JDK 8 + Tomcat 9 + MySQL 8.4.11 LTS(注意:不是8.0.x,8.4.11对caching_sha2_password插件和默认SSL策略有实质性变更),所有代码、SQL、配置项均经本地实测——没有“理论上可行”,只有“我刚在CentOS 7虚拟机里跑通”。适合两类人:一是正在写毕设/课设、被导师卡在“功能能跑但一压就崩”的同学;二是刚转Java开发、需要补全Web层+DB层协同细节的新人。我们不讲MVC是什么,只讲为什么Servlet的doPost()里不能直接new Service实例,为什么MySQL的innodb_buffer_pool_size设成物理内存的75%反而让订单查询变慢。
2. 从零搭起B/S骨架:用原生Servlet+JSP构建可调试、可断点、不依赖Spring魔力的最小可行订单流
2.1 为什么坚持不用Spring Boot?——看清事务边界与HTTP生命周期的真实耦合点
新手常误以为“Spring Boot自动配好一切”,结果在订单创建时发现:Service层加了@Transactional,但前端AJAX请求超时重发,后端却生成了两条重复订单。根源在于——Spring的事务代理只作用于Service方法调用,而HTTP请求的完整生命周期(连接建立→参数解析→业务执行→响应写出)横跨多个线程与组件。当你用原生Servlet,必须亲手把HttpServletRequest、HttpServletResponse、Connection、PreparedStatement串成一条链,才能真正理解:事务何时开启?何时回滚?响应流关闭时数据库连接是否已归还?我们不反对Spring Boot,但本项目选择javax.servlet-api 4.0.1+mysql-connector-java 8.0.33(注意:8.0.33兼容MySQL 8.4.11,而8.4.0+驱动要求显式设置allowPublicKeyRetrieval=true),只为暴露这些黑匣子。项目结构极简:
order-system/ ├── src/ │ ├── main/ │ │ ├── java/com/example/order/ │ │ │ ├── servlet/OrderServlet.java // 处理下单POST │ │ │ ├── dao/OrderDao.java // 封装JDBC操作 │ │ │ ├── service/OrderService.java // 业务逻辑+事务控制 │ │ │ └── model/Order.java // POJO,字段与MySQL表严格对应 │ │ └── webapp/ │ │ ├── index.jsp // 首页,展示商品列表 │ │ ├── order.jsp // 下单页,含库存实时显示 │ │ └── WEB-INF/web.xml // 关键!声明Servlet映射 │ └── test/ └── pom.xml (若用Maven) 或 lib/ (若手动管理jar)提示:
web.xml中必须声明<session-config><session-timeout>30</session-timeout></session-config>,否则Tomcat默认30分钟会话超时,用户下单到支付完成若超时,HttpSession中存的购物车数据将丢失——这是课设里最隐蔽的“功能正常但体验崩坏”陷阱。
2.2 订单创建Servlet:把HTTP请求、数据库事务、异常传播拧成一股绳
核心逻辑不在“怎么写SQL”,而在如何让一次HTTP请求成为原子操作单元。以下是OrderServlet.doPost()关键片段(已脱敏,保留真实约束):
// OrderServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 从Session获取用户ID(强制登录校验,防未授权下单) Integer userId = (Integer) request.getSession().getAttribute("userId"); if (userId == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 2. 解析参数(关键:库存ID和数量必须校验,防恶意篡改) String productIdStr = request.getParameter("productId"); String quantityStr = request.getParameter("quantity"); if (productIdStr == null || quantityStr == null) { request.setAttribute("error", "参数缺失"); request.getRequestDispatcher("/order.jsp").forward(request, response); return; } int productId = Integer.parseInt(productIdStr); int quantity = Integer.parseInt(quantityStr); // 3. 调用Service——此处是事务边界,必须捕获所有异常 OrderService orderService = new OrderService(); try { Order order = orderService.createOrder(userId, productId, quantity); request.setAttribute("success", "订单创建成功,订单号:" + order.getOrderId()); request.getRequestDispatcher("/order.jsp").forward(request, response); } catch (InsufficientStockException e) { // 自定义异常,明确语义 request.setAttribute("error", "库存不足,请刷新页面查看最新库存"); request.getRequestDispatcher("/order.jsp").forward(request, response); } catch (Exception e) { // 所有未预期异常,记录日志并返回友好提示 e.printStackTrace(); // 实际项目应交由Log4j2 request.setAttribute("error", "系统繁忙,请稍后再试"); request.getRequestDispatcher("/order.jsp").forward(request, response); } }逻辑说明:
- Session校验前置:避免绕过登录页直接POST
/order接口(常见课设漏洞)。 - 参数强校验:
getParameter()返回String,必须parseInt(),否则"abc"会抛NumberFormatException,导致500错误暴露堆栈——这是安全红线。 - 异常分类捕获:
InsufficientStockException是业务异常,需用户感知;其他Exception是系统异常,绝不暴露细节。 - forward而非sendRedirect:保持请求上下文,让
request.setAttribute()传递的提示信息能在JSP中渲染。
参数说明:
request.getContextPath():获取应用上下文路径(如/order-system),确保重定向URL正确,避免硬编码/login.jsp导致部署到子路径时404。request.getSession().getAttribute("userId"):Session中存储用户ID是B/S会话管理最简方案,比Token轻量,适合本项目规模。
2.3 JDBC连接池:不用HikariCP?那就手动管好Connection的生命线
MySQL 8.4.11 LTS默认启用caching_sha2_password认证插件,且强制SSL连接(除非显式禁用)。若跳过此步,Class.forName("com.mysql.cj.jdbc.Driver")后DriverManager.getConnection()必报Access denied for user或SSL is required。以下是DBUtil.java核心配置(非框架,纯JDBC):
// DBUtil.java public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "your_secure_password"; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs != null) try { rs.close(); } catch (SQLException e) { /* 忽略 */ } if (ps != null) try { ps.close(); } catch (SQLException e) { /* 忽略 */ } if (conn != null) try { conn.close(); } catch (SQLException e) { /* 忽略 */ } } }关键参数说明:
useSSL=false:MySQL 8.4.11默认要求SSL,开发环境可关闭(生产环境必须配SSL证书)。serverTimezone=Asia/Shanghai:解决java.sql.SQLException: The server time zone value 'XXX' is unrecognized——这是课设最高频报错,本质是JVM时区与MySQL服务器时区不匹配。allowPublicKeyRetrieval=true:应对caching_sha2_password插件,允许客户端获取公钥解密密码(MySQL 8.0+新认证机制)。close()方法必须包含try-catch:JDBC资源不关闭会导致连接泄漏,Tomcat运行几小时后报Cannot create PoolableConnectionFactory——这是连接池耗尽的典型症状,新手常归咎于“代码写错了”,实则是资源未释放。
3. MySQL 8.4.11 LTS实战:建表、索引、事务隔离级别,专治订单场景三大痛点
3.1 订单表设计:为什么status字段用TINYINT而非VARCHAR?以及created_time必须是DATETIME(3)
订单表不是“把字段列出来就行”,而是每个类型选择都直指性能与一致性。以下是orders表DDL(已适配MySQL 8.4.11):
-- 创建订单主表 CREATE TABLE `orders` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', `order_no` VARCHAR(32) NOT NULL UNIQUE COMMENT '订单号,业务唯一,格式:ORD20240520123456', `user_id` INT NOT NULL COMMENT '用户ID', `product_id` INT NOT NULL COMMENT '商品ID', `quantity` INT NOT NULL DEFAULT 1 COMMENT '购买数量', `amount` DECIMAL(10,2) NOT NULL COMMENT '总金额', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '订单状态:1-待支付,2-已支付,3-已发货,4-已完成,5-已取消', `created_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间,精确到毫秒', `updated_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间', INDEX `idx_user_status` (`user_id`, `status`), INDEX `idx_created_time` (`created_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单主表';设计理由:
order_no设为VARCHAR(32)并UNIQUE:避免用自增ID暴露业务量,且支持分布式生成(如Snowflake算法),UNIQUE约束防止重复插入——这是幂等性第一道防线。status用TINYINT:比VARCHAR(20)节省空间(1字节 vs 至少20字节),且WHERE status=2比WHERE status='paid'快一个数量级(字符串比较需字符集转换,整数比较直接CPU运算)。DATETIME(3):MySQL 8.0+支持微秒精度,CURRENT_TIMESTAMP(3)确保创建与更新时间精确到毫秒,便于排查“同一秒内多笔订单”时序问题。- 复合索引
idx_user_status:用户查自己订单时,WHERE user_id=? AND status IN (1,2)可走索引,避免全表扫描。
3.2 库存扣减:用SELECT ... FOR UPDATE锁住行,而不是UPDATE ... WHERE stock >= ? 的玄学方案
并发下单超卖,本质是读-改-写(Read-Modify-Write)竞态。常见错误写法:
-- ❌ 危险!存在时间窗口:A查库存100,B查库存100,A扣减,B扣减 → 库存变成98而非99 UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock >= 1;正确做法:先用SELECT ... FOR UPDATE锁定目标行,再执行UPDATE。OrderService.createOrder()中库存校验段:
// OrderService.java public Order createOrder(int userId, int productId, int quantity) throws InsufficientStockException { Connection conn = null; PreparedStatement psSelect = null; PreparedStatement psUpdate = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. SELECT ... FOR UPDATE 锁定商品行(注意:必须在事务内!) String selectSql = "SELECT stock FROM products WHERE id = ? FOR UPDATE"; psSelect = conn.prepareStatement(selectSql); psSelect.setInt(1, productId); rs = psSelect.executeQuery(); if (!rs.next()) { throw new RuntimeException("商品不存在"); } int currentStock = rs.getInt("stock"); if (currentStock < quantity) { throw new InsufficientStockException("库存不足,当前库存:" + currentStock); } // 2. 扣减库存 String updateSql = "UPDATE products SET stock = stock - ? WHERE id = ?"; psUpdate = conn.prepareStatement(updateSql); psUpdate.setInt(1, quantity); psUpdate.setInt(2, productId); int affected = psUpdate.executeUpdate(); if (affected != 1) { throw new RuntimeException("库存扣减失败"); } // 3. 创建订单(省略插入orders表逻辑) Order order = insertOrder(conn, userId, productId, quantity); conn.commit(); // 提交事务,释放锁 return order; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { /* 忽略 */ } } throw e; } finally { DBUtil.close(conn, psSelect, rs); DBUtil.close(null, psUpdate, null); } }关键点:
conn.setAutoCommit(false):手动控制事务,确保SELECT FOR UPDATE与UPDATE在同一个事务内。FOR UPDATE:在InnoDB中对选中的行加排他锁(X锁),其他事务对该行的SELECT FOR UPDATE或UPDATE会被阻塞,直到本事务提交或回滚。rollback()兜底:任何异常必须回滚,否则锁不释放,导致后续请求永久等待——这是高并发下“系统假死”的元凶。
3.3 事务隔离级别调优:READ COMMITTED够用,为什么不用REPEATABLE READ?
MySQL默认隔离级别是REPEATABLE READ,但在订单场景下,它可能引发幻读(Phantom Read):事务A查询status=1的订单共10条,事务B插入一条status=1的新订单并提交,事务A再次查询仍是10条(符合RR定义),但若A要统计“待支付订单总数”用于风控,则数据失真。而订单系统更需实时性:管理员后台刷新页面,必须看到最新订单状态。因此,我们在DBUtil.getConnection()后显式设置:
// DBUtil.java 中 getConnection() 方法末尾添加 conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);效果:
READ COMMITTED下,每次SELECT都读取已提交的最新快照,避免幻读,且锁粒度更小(只锁命中的行,不锁间隙),并发性能更高。- 对比
REPEATABLE READ:后者为避免幻读会使用间隙锁(Gap Lock),可能导致INSERT被阻塞,增加死锁概率——订单创建高频INSERT,此点尤为关键。
4. 避坑指南:那些让订单系统在验收前夜崩溃的5个血泪现场
4.1 现象:本地IDEA运行正常,部署到Tomcat后报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:MySQL 8.0+驱动类名从com.mysql.jdbc.Driver变为com.mysql.cj.jdbc.Driver,且JAR包未放入WEB-INF/lib/目录。Tomcat的lib/目录放的是全局JAR,Web应用优先加载自己WEB-INF/lib/下的JAR。若只在IDEA的Module Settings里添加了mysql-connector-java,但未导出到WEB-INF/lib/,则部署后找不到驱动。
解决:
- Maven项目:确保
pom.xml中<scope>为compile(默认),且mvn clean package后WAR包内WEB-INF/lib/包含mysql-connector-java-8.0.33.jar。 - 手动部署:将
mysql-connector-java-8.0.33.jar复制到WEB-INF/lib/,重启Tomcat。 - 验证:进入Tomcat
logs/catalina.out,搜索com.mysql.cj.jdbc.Driver,确认无ClassNotFoundException。
4.2 现象:下单成功,但MySQL中orders表created_time显示为0000-00-00 00:00:00
原因:MySQL 8.4.11默认sql_mode包含NO_ZERO_DATE,禁止插入零日期。而JDBC驱动若未指定时区,可能将JVM时间解析为无效值。
解决:
- 在JDBC URL中强制指定时区:
serverTimezone=Asia/Shanghai(已见2.3节)。 - 检查MySQL全局变量:
SELECT @@global.sql_mode;,若含NO_ZERO_DATE,临时移除(仅开发环境):SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE','')); - 更稳妥方案:在
application.properties(若用Spring)或代码中,SimpleDateFormat格式化时间时用yyyy-MM-dd HH:mm:ss.SSS,确保毫秒级精度。
4.3 现象:高并发压测时,大量请求卡在getConnection(),Tomcat线程池满,CPU 100%
原因:未配置连接池,每请求新建Connection,MySQL最大连接数(max_connections默认151)被耗尽。DBUtil.getConnection()直接调用DriverManager.getConnection(),无连接复用。
解决:
- 立即方案:在
DBUtil中加入简易连接池(非生产级,但课设够用):// DBUtil.java 新增静态连接池 private static final Queue<Connection> connectionPool = new ConcurrentLinkedQueue<>(); private static final int MAX_POOL_SIZE = 20; public static Connection getConnection() throws SQLException { Connection conn = connectionPool.poll(); if (conn == null || !conn.isValid(2)) { conn = DriverManager.getConnection(URL, USER, PASSWORD); } return conn; } public static void releaseConnection(Connection conn) { if (conn != null && connectionPool.size() < MAX_POOL_SIZE) { connectionPool.offer(conn); } } - 根本方案:引入HikariCP(需
pom.xml添加依赖),配置maximumPoolSize=20、connectionTimeout=30000。
4.4 现象:订单状态从“待支付”改为“已支付”后,前端页面刷新仍显示旧状态
原因:浏览器缓存了JSP页面,或response.setHeader("Cache-Control", "no-cache")未设置。
解决:
- 在所有JSP顶部添加:
<%@ page import="java.util.*" %> <% response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); // HTTP 1.1 response.setHeader("Pragma", "no-cache"); // HTTP 1.0 response.setDateHeader("Expires", 0); // Proxies %> - 后端重定向时用
response.sendRedirect()而非forward,强制浏览器发起新请求。
4.5 现象:MySQL 8.4.11安装后,mysql -u root -p登录报ERROR 1045 (28000): Access denied for user 'root'@'localhost'
原因:MySQL 8.4.11默认root用户认证插件为caching_sha2_password,而旧版客户端不支持。
解决:
- 以安全模式启动MySQL跳过权限验证:
# Linux sudo systemctl stop mysqld sudo mysqld --skip-grant-tables --skip-networking & mysql -u root - 在MySQL命令行执行:
USE mysql; UPDATE user SET plugin='mysql_native_password' WHERE User='root'; FLUSH PRIVILEGES; EXIT; - 重启MySQL:
sudo systemctl start mysqld,即可用原密码登录。
5. 订单状态机与幂等性:用数据库唯一索引+业务状态校验,把“重复提交”关进笼子
5.1 状态流转不是if-else堆砌:用状态机表固化业务规则
订单状态变更不是“用户点了就改”,而是必须满足前置条件。例如:“已支付”只能由“待支付”变更而来,“已完成”只能由“已发货”变更而来。硬编码if(status==1) status=2;极易遗漏校验,导致状态错乱。我们建一张order_status_transition表:
CREATE TABLE `order_status_transition` ( `from_status` TINYINT NOT NULL COMMENT '源状态', `to_status` TINYINT NOT NULL COMMENT '目标状态', `allowed` TINYINT NOT NULL DEFAULT 1 COMMENT '是否允许:1-是,0-否', PRIMARY KEY (`from_status`, `to_status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 插入合法流转 INSERT INTO `order_status_transition` VALUES (1,2,1), -- 待支付 → 已支付 (2,3,1), -- 已支付 → 已发货 (3,4,1), -- 已发货 → 已完成 (1,5,1), -- 待支付 → 已取消 (2,5,1); -- 已支付 → 已取消状态变更Service方法:
// OrderService.java public void updateStatus(long orderId, int newStatus) throws InvalidStatusTransitionException { Connection conn = null; PreparedStatement psCheck = null; PreparedStatement psUpdate = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查当前状态 String selectSql = "SELECT status FROM orders WHERE id = ?"; psCheck = conn.prepareStatement(selectSql); psCheck.setLong(1, orderId); ResultSet rs = psCheck.executeQuery(); if (!rs.next()) { throw new RuntimeException("订单不存在"); } int currentStatus = rs.getInt("status"); // 2. 查询状态机表,校验是否允许流转 String transitionSql = "SELECT allowed FROM order_status_transition WHERE from_status = ? AND to_status = ?"; psCheck = conn.prepareStatement(transitionSql); psCheck.setInt(1, currentStatus); psCheck.setInt(2, newStatus); rs = psCheck.executeQuery(); if (!rs.next() || rs.getInt("allowed") != 1) { throw new InvalidStatusTransitionException( "状态非法流转:从" + currentStatus + "到" + newStatus); } // 3. 更新订单状态 String updateSql = "UPDATE orders SET status = ?, updated_time = NOW(3) WHERE id = ?"; psUpdate = conn.prepareStatement(updateSql); psUpdate.setInt(1, newStatus); psUpdate.setLong(2, orderId); psUpdate.executeUpdate(); conn.commit(); } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) { } throw e; } finally { DBUtil.close(conn, psCheck, null); DBUtil.close(null, psUpdate, null); } }优势:
- 规则外置:状态逻辑不再散落在Java代码中,DBA可直接修改
order_status_transition表调整流程,无需发版。 - 强一致性:
SELECT ... FROM order_status_transition与UPDATE orders在同一事务,避免状态机表与订单表数据不一致。
5.2 幂等性终极防线:订单号唯一索引 + 前端按钮置灰 + 后端Token校验三重保险
用户手抖连点“提交订单”,后端必须保证只生成一笔订单。单一手段都不保险:
- 唯一索引:
order_no设UNIQUE,重复插入报Duplicate entry,但需捕获SQLIntegrityConstraintViolationException并返回友好提示。 - 前端按钮置灰:表单提交后JS禁用按钮,防止用户重复点击(但F5刷新可绕过)。
- 后端Token校验:这才是关键。流程如下:
- 用户进入下单页,后端生成
token = UUID.randomUUID().toString(),存入HttpSession,并写入页面隐藏域<input type="hidden" name="token" value="${token}">。 - 提交时,Servlet校验
request.getParameter("token")是否等于session.getAttribute("token"),校验通过则session.removeAttribute("token"),并创建订单。 - 若Token不匹配或已使用,返回“请勿重复提交”。
- 用户进入下单页,后端生成
Token校验代码片段:
// OrderServlet.java String clientToken = request.getParameter("token"); String sessionToken = (String) request.getSession().getAttribute("token"); if (clientToken == null || !clientToken.equals(sessionToken)) { request.setAttribute("error", "请求无效,请刷新页面重试"); request.getRequestDispatcher("/order.jsp").forward(request, response); return; } request.getSession().removeAttribute("token"); // 消费Token注意:
session.removeAttribute("token")必须在创建订单之前执行,否则若订单创建失败,Token未被消耗,用户刷新页面后仍可用——这会导致“一次成功,多次失败”的诡异现象。
5.3 验证订单系统健壮性的3个真实测试用例
不要只测“功能按钮点得通”,要模拟真实战场:
| 测试场景 | 操作步骤 | 预期结果 | 验证要点 |
|---|---|---|---|
| 并发超卖 | JMeter设100线程,循环发送相同商品ID+数量的下单请求(库存初始=1) | 只有1个请求成功,其余99个返回“库存不足” | 查orders表记录数=1,products.stock=0,无重复订单 |
| 状态非法流转 | 用Postman直接PUT/api/order/status?id=123&status=4(跳过“已发货”直接“已完成”) | 返回HTTP 400,提示“状态非法流转” | order_status_transition表中(2,4)不存在,且订单status未被修改 |
| 网络中断重试 | 下单请求发出后,立刻断网,10秒后重连并重发相同请求(带相同order_no) | 第二个请求因order_no唯一索引冲突,返回“订单已存在” | orders表中该order_no只有一条记录,status为“待支付” |
我带实习生做这个系统时,曾用Wireshark抓包发现:前端JavaScript在fetch()后没处理network error,用户看到“提交中...”就关掉页面,实际请求已在服务端执行。后来我们强制所有AJAX请求加timeout: 10000,超时后弹窗提示“网络异常,请检查订单是否创建成功”,并引导用户去“我的订单”页确认。技术上没有银弹,只有把每个环节的“可能失败”都当成必然来设计。希望帮到你。
本文还有配套的精品资源,点击获取