资讯动态

电商项目复现指南:从单体架构到并发扣库存与幂等支付

发布时间:2026/10/14 12:56:43 来源:尧图企业网站定制
简介当当网作为国内知名电商平台其完整的项目源代码是初中级Java开发者学习真实业务系统的理想素材。这份代码展示了从用户登录、商品展示、购物车到订单地址管理等核心电商模块的实现方式适合用于训练代码阅读能力理解一个完整Web项目从前端交互到后端服务的整体链路。压缩包共406个文件大小约3.29MB以Java源文件、JSP页面、CSS样式和SQL脚本为主辅以9个jar依赖库和大量商品展示图片结构上包含前端界面、后端服务、数据库设计、接口定义与配置文件等部分。代码中体现了Spring Boot、MyBatis等主流框架的配合使用可帮助学习RESTful API的接口规范、ORM实体映射以及跨环境配置的差异处理。资源在CSDN已获得1221人学习浏览适合具有一定Java基础、希望从单体项目入手补充项目经验的开发者按模块拆解分析、跟踪调用链并结合实际编码练习能较快提升服务端开发与数据库设计的实战能力。1. 「整个项目源代码」到底意味着什么先想清楚再动手「整个项目源代码」这类标题最容易让想练手的人误判以为下载一个压缩包解压就能跑结果解压完看到几十个模块、三四份 README、没有数据库脚本连该从哪里启动都找不到。真正值得复现的不是一堆代码文件而是把商品、购物车、订单、库存、支付回调这一条交易链路跑通。这篇文章以某知名图书商城的完整项目复现为例讲清楚后端服务、前端商城、管理后台三端工程怎么组织核心表怎么设计下单扣库存怎么保证不超卖支付回调怎么做幂等以及本地环境怎么一键拉起来。适合两类人打算把它当练手项目、想给简历写完整项目经验的开发者以及接手了类似代码包但不知道从哪下手的同学。2. 先立技术底座分层架构与核心数据模型复现一个电商项目最容易犯的错是一上来就写业务代码结果写到一半发现表结构撑不住需求或者接口和前端约定对不上。我这里习惯的节奏是先定架构形态再定四张核心表最后出一份接口清单然后才开写。2.1 单体优先还是微服务小团队最稳的分层方式看到「整个项目」四个字第一反应可能是微服务网关、注册中心、配置中心、分布式事务中间件全上一套。我一开始也这么干过结果在本地跑五个服务用了两天还没开始写业务。后续做同类项目我通常默认单体优先。理由很直接一是部署成本低一个 Spring Boot 应用包加 MySQL 和 Redis 就能跑二是调试链路短下单接口从 Controller 穿到 Service 再穿到 Mapper断点可以从头跟到尾不用跨服务跳来跳去三是事务好处理订单和库存数据在同一库里本地事务就能保证一致性不需要引入分布式事务。微服务要处理的注册发现、配置同步、服务间鉴权对这个练手项目没有业务价值反而稀释了主链路的复杂度。那什么时候拆等订单量真的起来了或者商品团队、订单团队需要独立发版时再按域拆。拆的顺序一般是从订单域开始因为交易链路对稳定性要求最高。复现阶段把单体做干净后期拆分的成本反而低。分层上我一般保持 Controller、Service、Mapper 三层Controller 只做参数校验和路由Service 放业务规则Mapper 只碰 SQL。另外加一层 domain 放实体和状态枚举避免枚举值散落在业务代码里。2.2 商品、库存、订单、用户四张核心表的字段设计四张表是这套项目的骨架用户表、图书表、库存表、订单表。设计时有几个容易被忽略的点金额一律用分存整数不用 decimal 和 float避免浮点误差引起对账翻车订单表要冗余用户昵称和商品标题、封面防止商品下架后历史订单不可读库存单独拆一张表因为图书详情是读多写少库存是高频更新拆开之后图书缓存可以放心用扣库存不会把商品缓存弄失效。-- 用户表 CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(128) NOT NULL COMMENT 存 BCrypt 哈希不存明文, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 图书表 CREATE TABLE book ( id BIGINT NOT NULL AUTO_INCREMENT, title VARCHAR(128) NOT NULL, author VARCHAR(64) NOT NULL DEFAULT , category_id INT NOT NULL, price_cents INT NOT NULL COMMENT 价格单位分, cover_url VARCHAR(256) NOT NULL DEFAULT , status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 库存表 CREATE TABLE stock ( id BIGINT NOT NULL AUTO_INCREMENT, book_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表与订单项表 CREATE TABLE order ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, user_name VARCHAR(32) NOT NULL DEFAULT COMMENT 下单时冗余, status TINYINT NOT NULL COMMENT 10待支付 20已支付 30已取消 40已关闭, total_cents INT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id BIGINT NOT NULL AUTO_INCREMENT, order_id BIGINT NOT NULL, book_id BIGINT NOT NULL, book_title VARCHAR(128) NOT NULL DEFAULT , price_cents INT NOT NULL COMMENT 下单时单价快照, quantity INT NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段说明里最值得留意的三个设计取舍订单状态用 TINYINT 加注释而不是字符串理由是状态流转要频繁做条件更新数字比较比字符串快而且前期就把状态枚举定死后面写代码不会出现「已支付」和「支付成功」两种写法并存的问题。订单号单独建唯一索引不用自增主键对外暴露避免别人通过订单 ID 差值估算出货量。订单项里的 price_cents 是快照不是下单时现查图书表因为图书表价格随时会改历史订单必须反映成交那一刻的价格。2.3 先画接口清单再写代码十二个核心接口「整个项目」规模一大前后端最容易脱节。常见做法是先出一份接口清单前端拿到后可以并行开发后端拿着做任务拆分等联调时按清单逐条过谁缺谁补就清楚了。我通常把接口数量控制在能覆盖主链路的范围内不追求 REST 风格教条而是按业务动作命名。模块路径方法说明认证/api/auth/loginPOST登录返回 token认证/api/auth/registerPOST注册商品/api/booksGET分页查询支持分类筛选商品/api/books/{id}GET图书详情购物车/api/cartGET当前用户购物车购物车/api/cart/itemsPOST加入购物车购物车/api/cart/items/{id}DELETE移除购物车项订单/api/ordersPOST创建订单订单/api/orders/{id}GET订单详情订单/api/orders/{id}/cancelPOST取消订单限未支付支付/api/payments/notifyPOST支付回调管理后台/api/admin/booksPOST上架图书管理后台/api/admin/books/{id}/stockPUT调整库存接口清单定完还要定一个响应包装统一返回{ code, message, data }code 为 0 表示成功非 0 表示业务异常。错误码分区比如 10001 是参数错误20001 是库存不足30001 是订单状态冲突。这样前后端联调时看到 code 就能定位模块不用翻日志猜。3. 把「整个项目」拆成可运行的最小集合工程初始化架构定好下一步是把工程实际建出来。这里有个血泪经验一开始不要试图把「整个项目」准备得面面俱到先搭出能启动、能连库、能渲染首页的最小子集跑通后再往里面加模块。子集跑不起来后面加再多代码都是空转。3.1 三端工程怎么拆web、admin、server复现电商项目我一般拆三个工程用户商城前端、管理后台前端、后端服务。管理后台单独拆出来的原因有两个一是权限模型不同管理端需要管理员角色和操作审计和用户端混在一起会让鉴权代码变得复杂二是部署粒度不同运营人员改商品上架不需要发整个商城的前端包。三端放在同一个仓库用目录区分本地开发时各自独立启动。bookstore/ ├── server/ # Spring Boot 后端服务 │ ├── pom.xml │ └── src/main/ │ ├── java/com/example/bookstore/ │ │ ├── controller/ # HTTP 入口 │ │ ├── service/ # 业务逻辑 │ │ ├── mapper/ # SQL 映射 │ │ └── domain/ # 实体、枚举 │ └── resources/ │ ├── application.yml │ └── db/ # 初始化脚本 ├── web/ # 用户商城前端Vue ├── admin/ # 管理后台前端Vue └── deploy/ └── docker-compose.yml后端用 Spring Boot前端用 Vue这是目前市面上同类项目最常见的组合。前端选 Vue 而不是 React对练手项目来说更好上手而且 Element 组件库把表格、表单、弹窗这些管理后台高频组件都封装好了管理端能少写很多样板代码。如果团队更熟 React换成 React 也完全可行后端不感知前端框架差异接口清单是唯一的契约。3.2 后端工程初始化用脚手架建骨架后端骨架不手写 POM直接用 Spring Initializr 生成。命令行方式如下IDE 内置的初始化向导选同样的依赖也可以# 在 bookstore 目录下执行生成 server 工程 curl https://start.spring.io/starter.tgz \ -d dependenciesweb,mysql,data-redis,validation \ -d languagejava \ -d typemaven-project \ -d baseDirserver \ -d groupIdcom.example \ -d artifactIdbookstore-server \ -d packageNamecom.example.bookstore \ | tar -xzvf -四个依赖分别对应web 提供 Spring MVC 和内置 Tomcatmysql 是 MySQL 驱动data-redis 提供 Redis 客户端和 Spring Data Redis 封装validation 是参数校验的 JSR 303 实现后面写接收注册请求时直接加 Valid 就能校验字段。这里我一般会把 actuator 也加上虽然它不直接贡献业务功能但本地调试时看健康检查端点、确认服务是否真正起来了比看日志快得多。生成完的骨架可以空跑但连不上数据库因为还没有数据源配置和初始化脚本。下一步先把 application.yml 配好再准备建表和种子数据。3.3 数据库初始化脚本从建库到种子数据数据源配置里最容易踩的坑是时区。MySQL 8 对时区敏感连接串里不指定 serverTimezone启动时大概率报 CST 或 UTC 的时区错误或者不报错但时间和本地对不上。spring: datasource: url: jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: bookstore password: bookstore redis: host: localhost port: 6379 jackson: time-zone: Asia/Shanghai数据库初始化脚本放在 server 工程的 resources/db 下配合启动时执行。种子数据的目的是让首页第一次渲染时有内容不然前端联调时列表永远是空容易误判接口坏了。-- 初始化脚本建议在首次启动时自动执行 CREATE DATABASE IF NOT EXISTS bookstore DEFAULT CHARACTER SET utf8mb4; INSERT INTO book (title, author, category_id, price_cents, status) VALUES (示例图书·卷一, 示例作者A, 1, 4900, 1), (示例图书·卷二, 示例作者B, 1, 6800, 1), (示例图书·卷三, 示例作者C, 2, 3500, 1); INSERT INTO stock (book_id, quantity) VALUES (1, 100), (2, 80), (3, 50);注意初始化脚本要可重复执行或者只执行一次的机制否则第二次启动时直接报主键冲突。我一般用 Spring 的 SQL init 配置配合CREATE TABLE IF NOT EXISTS种子数据用INSERT ... ON DUPLICATE KEY UPDATE保证幂等。这一步做完后端服务应该能启动、能连库、能查出种子图书最小子集才算立住。4. 交易主链路落地从加购到支付回调骨架和表结构就绪后进入整个复现项目最核心的部分交易主链路。这条链路是电商系统区别于普通 CRUD 系统的分水岭也是最容易出现并发问题的地方。我的原则是先把下单、扣库存、支付回调三个动作写对再考虑查询优化和页面美化。4.1 下单与扣库存事务边界和行锁是关键加购接口本身不复杂本质是对购物车表的增删改查。真正有难度的是从购物车创建订单的那一步它同时要扣库存、写订单主表、写订单项表三个动作必须在一个事务里任何一个失败都要全部回滚。Transactional public Order createOrder(Long userId, ListCartItem items) { // 1. 汇总金额单位是分 long totalCents items.stream() .mapToLong(CartItem::getPriceCents) .sum(); // 2. 逐项扣库存UPDATE 带条件由数据库保证不超卖 for (CartItem item : items) { int updated stockMapper.deduct(item.getBookId(), item.getQuantity()); if (updated 0) { throw new BusinessException(库存不足: item.getBookId()); } } // 3. 生成订单号和订单记录 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(OrderStatus.WAIT_PAY); order.setTotalCents(totalCents); orderMapper.insert(order); // 4. 写订单项保存价格快照 for (CartItem item : items) { orderItemMapper.insert(convertToItem(order.getId(), item)); } return order; }对应的 Mapper SQL 是关键-- 条件扣减quantity #{qty} 是防超卖的核心 UPDATE stock SET quantity quantity - #{qty} WHERE book_id #{bookId} AND quantity #{qty}这段逻辑里最反直觉的地方是不要在 Java 代码里先 SELECT 库存再判断够不够、然后再 UPDATE。两个请求同时读到库存是 1都判断够都去 UPDATE结果库存变成 -1。正确做法是把库存判断放到 UPDATE 的 WHERE 条件里数据库的行锁会保证同一时刻只有一个事务能更新成功返回影响行数 0 就说明库存不足直接抛异常回滚。参数上有两个调法需要注意一是扣减的 quantity 要限制单次购买数量比如单本图书最多购买 5 册防止有人一次下几百册把库存打穿二是事务注解要加在 public 方法上同类内部调用会导致事务失效这是个经典翻车点。另外订单号生成我用的是时间戳加随机数和用户 ID 后四位长度控制在 32 位以内测试时肉眼也能读出大致时间。4.2 支付回调幂等处理不做好会翻车真实项目里支付回调是异步推送的可能同时来两条、也可能网络抖动后重试。如果回调处理不幂等订单状态可能被覆盖库存可能被重复回补。复现项目没有真实支付渠道常见做法是在本地模拟一个回调接口逻辑和真实支付平台一致App 跳转支付页 → 本地模拟支付成功 → 回调服务端。PostMapping(/api/payments/notify) public Result handleNotify(RequestBody PayNotifyRequest payload) { // 1. 查订单不存在直接返回失败 Order order orderMapper.findByOrderNo(payload.getOrderNo()); if (order null) { return Result.fail(30001, 订单不存在); } // 2. 已经支付直接返回成功保证重复回调不重复处理 if (order.getStatus() OrderStatus.PAID) { return Result.ok(); } // 3. 条件更新只有当前状态是待支付才能改为已支付 int updated orderMapper.updateStatus(order.getOrderNo(), OrderStatus.WAIT_PAY, OrderStatus.PAID); if (updated 0) { return Result.fail(30002, 订单状态冲突); } // 4. 到这里才真正完成支付触发后续流程 log.info(支付成功 orderNo{}, payload.getOrderNo()); return Result.ok(); }对应的 SQLUPDATE order SET status #{targetStatus}, paid_at NOW() WHERE order_no #{orderNo} AND status #{expectStatus}这里的状态条件更新值得多说一句判断当前状态和修改状态两个动作必须合并到一条 UPDATE 语句里而不是先 SELECT 判断再 UPDATE。先查再改在并发下会出问题两个回调同时进来都查到待支付状态都走更新逻辑第二个更新虽然能成功但触发两次后续发货流程。改成 WHERE status 期望前置状态 后数据库层面保证只有第一个请求能更新成功第二个请求影响行数为 0直接走状态冲突分支。调试时我会在本地模拟回调接口上打印完整的请求报文因为真实支付平台推送的字段格式经常比你定义的多几个用严格 JSON 反序列化会直接报错。处理方式是 DTO 里只定义需要的字段多余字段忽略不校验未知字段。这个习惯帮我省掉了大量联调时间。4.3 订单超时关闭定时任务扫表与延迟队列的取舍待支付订单不能一直挂着一般 30 分钟未支付就关闭并回补库存。这个动作的实现方式决定这个复现项目的「工程感」到底怎么样。最简单可靠的方案是定时任务扫表配上条件更新保证幂等。Scheduled(fixedDelay 60_000) Transactional public void closeExpiredOrders() { // 查询超时未支付的订单限制批次大小防止一次扫太多 ListOrder expired orderMapper.findExpired( LocalDateTime.now().minusMinutes(30), 200); for (Order order : expired) { int updated orderMapper.updateStatus(order.getOrderNo(), OrderStatus.WAIT_PAY, OrderStatus.CLOSED); if (updated 0) { // 只对真正关闭成功的订单回补库存 orderMapper.restoreStock(order.getOrderNo()); } } }用条件更新的好处是定时任务重复执行也不会重复关单因为第二次执行时订单状态已经不是待支付了。回补库存也只会执行一次。这个方案在订单量几千到几万的规模下完全够用定时任务每 60 秒扫一次最多会有 1 分钟的延迟误差对用户来说感知不到。什么时候换延迟队列当待支付订单量到了几十万一次扫 200 条需要循环很多次数据库压力明显上升时再换。常见做法是用 Redis 的过期事件或者 RabbitMQ 的延迟队列但这两者都引入了新的依赖和新的失败场景。本地复现阶段定时任务扫表已经能把「超时关单 → 回补库存」的完整链路跑通了我不建议一步到位上延迟队列。5. 避坑记录复现电商项目时最常踩的 5 个坑这类项目我在不同机器上搭过很多遍每一步的坑都熟。这一章不写理论直接记录 5 个高频踩坑点每一条都是「现象 → 原因 → 解决」。5.1 订单提前 8 小时被关闭超时判断全乱现象订单 14:00 创建设置 30 分钟超时结果 18:00 左右就被定时任务关闭关闭时间比预期晚了整整一个时区差。另一台机器上则相反订单刚创建 5 分钟就被关掉。原因应用服务和 MySQL 的时区不一致。应用默认 UTCMySQL 连接串没指定 serverTimezoneJava 侧读出的时间与数据库存储时间相差 8 小时定时任务拿本地时间比较自然错位。解决数据源连接串统一加serverTimezoneAsia/Shanghai同时把 Jackson 的 time-zone 也设成 Asia/Shanghai保证 JSON 序列化出来的时间戳对前端也友好。这个配置在 2.2 节的代码里已经给出来了建议所有环境统一不要靠服务器默认时区碰运气。5.2 并发下单把库存扣成负数现象对库存只有 1 本的图书发起 200 个并发下单请求最终订单成功数量和实际库存对不上库存出现负数。原因代码里先 SELECT 库存判断大于 0 再 UPDATE 扣减。两个请求同时在第一步读到库存 1都认为有货都执行 UPDATE库存变 -1。解决改成条件 UPDATE即UPDATE stock SET quantity quantity - #{qty} WHERE book_id #{bookId} AND quantity #{qty}判断库存充足与否的职责交给数据库的行锁和 WHERE 条件。影响行数为 0 的请求直接抛库存不足。这个方案的副作用是并发下单时会有一定比例的请求失败对一致性要求高的电商场景是可接受的失败请求让用户重试即可。5.3 支付回调重复推送导致库存多回补现象订单支付成功后用户申请退款退款流程刚走完支付平台的重试回调又来了系统把订单状态改回已支付同时执行了第二次库存回补。原因回调处理里先判断订单状态、再更新状态两步之间没有原子性。退款动作在两次回调之间执行第二次回调拿到的状态判断结果是旧值。解决所有状态流转都改成条件更新UPDATE order SET status #{target} WHERE order_no #{orderNo} AND status #{expect}。更新影响行数为 0 就说明状态已被其他流程改动直接拒绝本次回调。这条规则同样适用于退款、取消订单、超时关闭凡是状态机里的每一个跳转都必须带前置状态条件。5.4 前端跨域登录成功后续请求却全部 401现象前端跑在 8080后端跑在 8081登录接口调用成功返回了 token但紧接着的购物车接口全部返回 401登录状态像没有生效一样。原因CORS 配置里设置了跨域允许但没有开启 allowCredentials浏览器默认不允许跨域请求携带 Cookie。另一种常见原因是后端 Origin 配置写成了*而带凭证的跨域请求不允许使用通配符 Origin。解决后端 CorsFilter 里指定具体 Origin 列表开启setAllowCredentials(true)同时前端 axios 请求里加withCredentials: true。两边的配置是成对生效的缺一个就白调。排查这类问题最有效的办法是打开浏览器开发者工具看请求响应头里有没有 Access-Control-Allow-Credentials: true。5.5 换一台机器就跑不起来依赖版本不一致是玄学现象项目在本机跑得好好的打包发给别人后依赖各种报错。大部分是 Redis 连不上或者序列化异常少数情况是数据库驱动版本行为和预期不一致。原因复现项目时有人习惯直接引入最新版本依赖但最新版本在不同机器上的兼容行为可能有差异尤其是 Spring Data Redis 的序列化策略换过好几次默认值。解决固定依赖版本并在 POM 里显式声明不用父 POM 的隐式管理Docker Compose 里镜像 tag 也不要写 latest统一指定像 mysql:8.0、redis:7 这种明确版本。这样换机器时最差不过是重新拉镜像不会因为版本漂移出现偶发问题。6. 让它彻底跑起来并验证Docker Compose 一键启动与检查清单代码和表结构都有了最后一步是让整个项目在干净环境里一键启动并对照检查清单确认不是「能启动」而是「能走通」。这一步做完才算真正复现了这套项目。6.1 用 Docker Compose 起全套服务不污染本机环境我一般会在项目的 deploy 目录放一个 docker-compose.yml把 MySQL、Redis、后端服务一次性定义好。这样不管是自己换电脑还是把项目交给别人都不会被本机装没装 MySQL、版本对不对卡住。version: 3.8 services: mysql: image: mysql:8.0 container_name: bookstore-mysql environment: MYSQL_DATABASE: bookstore MYSQL_USER: bookstore MYSQL_PASSWORD: bookstore MYSQL_ROOT_PASSWORD: root ports: - 3306:3306 volumes: - ./mysql/init:/docker-entrypoint-initdb.d healthcheck: test: [CMD, mysqladmin, ping, -h, localhost] interval: 5s timeout: 3s retries: 10 redis: image: redis:7 container_name: bookstore-redis ports: - 6379:6379 server: build: ../server container_name: bookstore-server depends_on: mysql: condition: service_healthy redis: condition: service_started ports: - 8081:8081这里必须说明depends_on的一个坑它默认只保证启动顺序不保证依赖服务已经可用。Spring Boot 启动时要连数据库如果 MySQL 还没就绪就启动应用会直接失败。所以我在 mysql 服务上加了 healthcheck并在 server 的 depends_on 里用 condition: service_healthy 强制等待 MySQL 健康检查通过。启动命令就是docker compose up -d --build日志用docker compose logs -f server跟踪看「Started BookstoreApplication」就算起来成功。6.2 验证清单跑通主链路比看到首页更重要能在浏览器里看到商城首页只是「项目能启动」离「整个项目复现成功」还差得远。真正对这套代码有信心的标志是下面这张清单全部通过。验证项预期结果失败时排查方向注册并登录返回 token带 token 访问 /api/cart 返回空购物车数据库 user 表是否有记录密码是否为 BCrypt 哈希首页商品列表显示 3 本种子图书检查 book 表数据确认 status1加入购物车并下单订单状态变为待支付库存减少对应数量看 server 日志里有没有库存不足异常模拟支付回调订单状态变为已支付库存不再变化检查回调接口是否走幂等分支等待 30 分钟订单自动关闭库存回补检查定时任务是否执行扫描批次是否正确管理后台调库存商品详情页库存同步更新确认管理端接口走的是同一套后端服务最后补一个实战技巧本地没有真实支付渠道模拟支付回调可以做成一个本地脚本每次测试直接跑不用手点页面。# 模拟支付成功回调orderNo 换成实际订单号 curl -X POST http://localhost:8081/api/payments/notify \ -H Content-Type: application/json \ -d {orderNo: 202501011200001234, amount: 4900}把这条 curl 存成一个mock-pay.sh配合定时关单的等待时间整个支付链路可以在两分钟内完整走完一次。「整个项目源代码」不应该是一个只能看不能跑的压缩包把它还原成能下单、能支付、能关单、能回补库存的闭环才算真正掌握这套电商项目。我也曾经以为代码越多越完整后来发现主链路跑通加上幂等、并发、超时三个边界都处理干净比堆几十个用不到的模块实在得多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑