目录
6.2.1 库存扣减测试(InventoryService)
6.2.3 物流状态流转测试(LogisticsService)
6.3.2 物流接口测试(LogisticsController)
6.3.3 库存接口测试(InventoryController)
从设计文档、工程搭建到业务实现、测试验证的全链路开发指南
1. 项目概述
本文将基于 Spring Boot 3 与 Vue 3 技术栈,开发一套包含订单下单、商品库存扣减、物流状态变更的完整业务案例。该案例聚焦核心业务流,不引入复杂权限体系,兼顾功能可扩展性与高并发场景下的业务稳定性,适合作为全栈开发技术验证、中小型业务系统基础脚手架,或在校学生课程设计 / 毕业设计的参考工程。
1.1 业务需求分析
本案例核心业务围绕 “用户下单→库存扣减→物流更新→订单状态同步” 的闭环流程展开,不涉及权限管理、支付对接等附加功能,核心需求覆盖订单、库存、物流三个模块的基础业务逻辑:
订单模块:用户可提交包含多商品的订单,提交后系统将自动生成订单记录,同时支持查询订单列表、查看订单详情;在库存不足、物流异常等场景下,系统需支持手动或自动取消订单,取消后同步恢复对应库存数量。
库存模块:需严格保证扣减操作的准确性,避免高并发场景下出现超卖、库存为负等异常情况;库存扣减需与订单创建、物流发货流程实现协同一致性,即订单创建时预扣库存、物流发货时确认扣减,若订单取消则自动恢复预扣库存。
物流模块:为已支付 / 已确认发货的订单分配物流单号,支持录入或同步物流运输轨迹信息,物流状态需覆盖 “待发货→发货中→运输中→已签收” 的完整生命周期;同时提供物流单号与订单号的双向关联查询,支持通过订单号获取最新物流状态。
状态联动约束:订单、库存、物流的状态变更需遵循强制业务联动规则,且所有变更操作需保留可溯源的操作记录。
1.2 核心业务流程
本案例的核心业务流程以 “用户下单” 为起点,以 “订单完成 / 关闭” 为终点,串联订单、库存、物流三大模块的核心操作,完整流程的关键节点如下:
用户下单:用户在前端确认商品、数量、收货地址等信息后,提交订单请求;后端接收请求后,首先校验商品的基础库存是否充足,若库存不足则直接返回 “商品库存不足” 的异常提示。
创建订单:若前置库存校验通过,后端将生成状态为 “待发货” 的订单初始记录;此时系统仅锁定对应商品的预扣库存,不会直接扣减商品的实际库存,确保用户有足够的时间完成后续确认操作。
库存扣减:订单确认生效后,系统将执行实际库存扣减操作;为保证高并发场景下的扣减准确性,扣减过程需采用 “Redis 预扣 + 数据库最终校验” 的方案,同时通过数据库乐观锁避免同一库存被重复扣减。
物流发起:库存扣减完成后,系统自动将订单状态流转为 “待发货”,并触发物流业务流程;此时可通过人工录入或调用第三方物流平台接口的方式,为该订单分配唯一的物流单号。
物流状态更新:在货物运输的全流程中,物流节点的操作人员(如揽件员、中转站分拣员、派件员)会通过移动端或后台系统更新物流节点状态;所有状态变更将同步到订单系统,保持订单与物流状态的实时统一。
订单完成:当物流状态更新为 “已签收” 后,系统将自动同步订单状态为 “已完成”,并记录订单完成时间;若在物流运输过程中发生异常(如包裹丢失、用户拒收),员工可录入异常物流状态,系统将根据状态类型触发对应的逆向流程(如库存回流、订单关闭)。
1.3 技术选型
本案例采用业界主流的前后端分离架构,技术选型以 “稳定、通用、易落地” 为核心原则,所有技术栈均是企业级项目中广泛使用的成熟方案。
1.3.1 后端技术栈
后端技术栈以 Spring Boot 3 为核心,覆盖 Web 服务、数据访问、缓存、事务管理等全链路能力,具体选型及说明如下:
核心框架:Spring Boot 3.2.x 作为项目核心框架,提供自动化配置、嵌入式 Web 容器、依赖注入等核心能力,简化企业级应用的初始搭建与后续维护;该版本要求 JDK 版本不低于 17,以支撑其虚拟线程、核心 API 优化等新特性的落地。
数据库:选择 MySQL 8.0 作为关系型数据库,存储订单、商品、库存、物流等所有业务数据;MySQL 8.0 的新特性(如增强的事务隔离级别、JSON 类型字段支持)可更好支撑物流轨迹、订单扩展属性等非结构化数据的存储需求。
ORM 框架:选用 MyBatis-Plus 3.5.3.1 作为数据访问层框架,它在 MyBatis 的基础上封装了单表 CRUD、分页、条件构造器等通用操作,同时兼容原生 MyBatis 的 SQL 编写能力,既能减少重复代码开发量,又能灵活支撑复杂关联查询的 SQL 优化需求(61)。
缓存:采用 Redis 7.2 作为缓存中间件,主要承担两个核心职责:一是在高并发下单场景中,通过原子化操作预扣库存,减少对数据库的直接压力;二是缓存热点数据(如商品基础信息、用户常用地址),提升系统整体响应速度(37)。
工具类:项目中引入 Lombok 简化实体类的 getter/setter、构造方法等重复代码的编写,同时引入 Spring Boot Starter Validation 作为参数校验工具,对所有前端传入的请求参数进行合法性校验,避免非法参数进入业务流程。
接口文档:整合 Knife4j-OpenAPI 3.0 框架,自动生成符合 RESTful 规范的接口文档;文档支持在线调试、参数 Mock、响应示例查看等功能,可有效降低前后端联调、后期接口维护的沟通成本(100)。
1.3.2 前端技术栈
前端技术栈以 Vue 3 为核心,搭配现代化的构建工具、UI 组件库,开发体验与页面性能更适配企业级业务需求,具体选型及说明如下:
核心框架:Vue 3.4.x 作为核心视图框架,其组合式 API(Composition API)可实现更灵活的业务逻辑复用,在大型业务页面开发时能更精准地控制组件更新粒度,提升页面渲染性能(1)。
构建工具:选用 Vite 4.x 作为前端构建工具,相比传统 Webpack 构建工具,Vite 基于 ESBuild 预构建依赖、利用浏览器原生 ES 模块发起请求,在开发环境下的冷启动、热更新效率有量级级提升,可显著缩短本地开发调试的等待时间(85)。
UI 组件库:采用 Element Plus 3.x 作为基础 UI 组件库,它提供了表格、分页、表单、对话框、面包屑等企业级业务开发常用的高质量组件,且所有组件都遵循 WCAG 无障碍访问规范,开箱即用的特性可大幅降低页面开发量(63)。
状态管理:选用 Pinia 2.x 作为 Vue 应用的状态管理工具,它是 Vue 官方推荐的状态管理方案,相比传统 Vuex,Pinia 提供了更简洁的 API、更完善的 TypeScript 类型推断支持,且代码分割逻辑更轻量化,便于在订单、物流等跨模块业务场景下共享状态。
路由管理:采用 Vue Router 4.x 作为前端路由管理器,实现订单列表、物流跟踪、详情页等业务页面的无刷新切换;配合 Vue 3 的 keep-alive 组件,可实现业务页面的缓存功能,提升用户交互体验(85)。
HTTP 客户端:选用 Axios 1.6.x 作为 HTTP 请求工具,封装前端与后端的所有 API 交互;通过请求 / 响应拦截器统一预处理请求参数、格式化响应数据、处理全局异常,还可在请求端启用代理配置,解决前后端联调时的跨域问题(85)。
地图组件:为了更直观地展示物流运输轨迹,项目中引入了高德地图 JSAPI 的 Vue 封装组件,可在物流跟踪页面上以可视化路线图形式渲染运输轨迹、标记当前包裹位置。
1.3.3 开发环境与工具
为保证项目在开发、联调、部署阶段的环境一致性,规避因工具版本差异导致的兼容性问题,团队对开发工具的版本选型进行了明确约束:
JDK 版本:选择与 Spring Boot 3.2.x 兼容的 JDK 17,这是 Spring Boot 3.x 系列的最低要求版本,无论是 Oracle JDK 还是 OpenJDK,只要版本号不低于 17,均可支撑项目开发;
Node.js 版本:选择 LTS 18.16.0 及以上版本,这是 Vite 4.x 的要求版本,可保证前端项目的构建、热更新等功能正常运行;
开发 IDE:后端推荐使用 IntelliJ IDEA Ultimate 版,该工具对 Spring Boot、MyBatis-Plus 提供了开箱即用的支持,如配置文件自动提示、Bean 依赖关系查看、Mapper 方法与 SQL 的跳转关联;前端推荐使用 VS Code,配合 Vue Official、Vite、Element UI Snippets 等官方插件,可大幅提升编码效率、降低编码错误率(83);
其他工具:项目中采用 Maven 3.8.x 作为后端依赖管理工具,使用 Git 进行版本控制,同时采用了更轻量化的 Postman 或 Apifox 工具进行后端接口调试,确保接口功能符合业务需求(85)。
2. 数据库设计
本案例采用 MySQL 8.0 作为数据库,严格遵循第三范式(3NF)设计核心业务表,通过合理的字段冗余、索引设计,在满足数据一致性的前提下,优先保障订单查询、物流跟踪等高频业务场景的性能表现。
2.1 核心 ER 实体关系图
本案例的核心业务实体包含商品、订单、订单详情、库存、物流订单、物流轨迹,实体间的关联关系为:一个订单可包含多个订单详情(行项),每个订单详情对应一件商品;一件商品对应一条库存记录,存储当前仓库的实际库存数量;一个订单关联唯一一条物流订单记录,一条物流订单对应多个物流轨迹节点,从而完整覆盖 “商品 - 订单 - 库存 - 物流” 的全链路业务关联逻辑(12)。
2.2 数据库表设计
根据上述实体关系,设计出符合业务约束的核心业务表结构。为保证所有业务表的字段一致性、后期可维护性,设计时对部分字段进行了统一约束:所有业务表均使用id作为主键,所有表的create_time字段默认取当前系统时间,update_time字段设置为 “在数据更新时自动更新为当前时间”;同时,为了实现数据的柔性删除、避免物理删除导致的历史数据丢失,所有表均增加了is_deleted逻辑删除字段。
2.2.1 商品表(products)
商品表存储所有上架销售的商品基础信息,是订单、库存、物流业务的核心关联基础表,唯一约束为 “商品编号唯一”,其表结构设计如下:
| 字段名 | 类型 | 含义 | 约束 |
| id | BIGINT | 商品 ID | 主键、自增 |
| spu_name | VARCHAR(100) | 商品 SPU 名称 | 非空 |
| spu_value | VARCHAR(100) | 商品规格(如尺码、颜色) | 非空 |
| spu_price | DECIMAL(10,2) | 商品售价 | 非空 |
| product_sales | INT | 商品销量 | 默认 0 |
| state | TINYINT | 商品上下架状态(0 - 上架,1 - 下架) | 默认 0 |
| create_time | DATETIME | 创建时间 | 默认当前时间 |
| update_time | DATETIME | 更新时间 | 默认当前时间 |
| is_deleted | TINYINT | 逻辑删除标识(0 - 未删除,1 - 已删除) | 默认 0 |
其中,spu_name、spu_value、spu_price字段的业务定义与存储约束,和此前的电商类项目设计保持一致,保证了业务逻辑的连贯性;同时,表中针对spu_name、state字段创建了联合索引,优化商品列表页的查询、过滤性能(95)。
2.2.2 库存表(inventory)
库存表存储商品的实际库存、预扣库存数据,是保证库存精准扣减、避免超卖问题的核心业务表,其表结构设计如下:
| 字段名 | 类型 | 含义 | 约束 |
| id | BIGINT | 库存记录 ID | 主键、自增 |
| product_id | BIGINT | 关联商品 ID | 非空、唯一 |
| warehouse_id | BIGINT | 仓库 ID | 非空,默认 1 |
| stock_num | INT | 实际库存数量 | 非空,默认 0 |
| locked_stock | INT | 预扣 / 锁定库存数量 | 非空,默认 0 |
| version | INT | 乐观锁版本号 | 非空,默认 0 |
| create_time | DATETIME | 创建时间 | 默认当前时间 |
| update_time | DATETIME | 更新时间 | 默认当前时间 |
| is_deleted | TINYINT | 逻辑删除标识(0 - 未删除,1 - 已删除) | 默认 0 |
该表的核心设计细节如下:
为了支持多仓库业务扩展,表中增加了warehouse_id字段,作为后续扩展多仓库库存隔离的基础标识;
通过stock_num(实际库存)与locked_stock(锁定库存)两个字段,将 “可售库存” 拆分为 “待扣减库存” 与 “已预扣库存” 两部分,只有支付或发货确认后,锁定库存才会被实际扣减,有效避免超卖、库存不一致的问题(32);
引入version字段作为乐观锁标识,每次执行库存扣减前,会先校验数据库中的版本号与业务逻辑中取出的版本号是否一致,只有一致才会执行扣减操作,避免同一库存被多个请求重复扣减(36);
为了保证查询性能,表中针对product_id、warehouse_id字段创建了联合唯一索引,提升商品库存的关联查询、更新速度(32)。
2.2.3 订单表(orders)
订单表存储用户提交订单的核心基础信息,是整个订单库存物流业务的核心主表,其表结构设计如下:
| 字段名 | 类型 | 含义 | 约束 |
| id | BIGINT | 订单 ID | 主键、自增 |
| order_no | VARCHAR(32) | 订单编号 | 非空、唯一 |
| user_id | BIGINT | 下单用户 ID | 非空 |
| total_amount | DECIMAL(10,2) | 订单总金额 | 非空 |
| status | VARCHAR(20) | 订单状态 | 非空 |
| payment_method | VARCHAR(50) | 支付方式 | 可选 |
| delivery_address | TEXT | 收货地址 | 非空 |
| create_time | DATETIME | 创建时间 | 默认当前时间 |
| update_time | DATETIME | 更新时间 | 默认当前时间 |
| is_deleted | TINYINT | 逻辑删除标识(0 - 未删除,1 - 已删除) | 默认 0 |
其中,order_no为对外业务标识,采用自定义规则生成(如时间戳 + 用户 ID 后缀),保证全局唯一,避免使用可遍历的数字主键,提升订单安全性;status字段的业务定义,与此前设计的订单状态机流转规则保持一致,其取值范围包括CREATED(已创建)、PAID(已支付)、SHIPPED(已发货)、COMPLETED(已完成)、CANCELLED(已取消),是订单、物流状态联动的核心依据(10)。
2.2.4 订单明细表(order_detail)
订单明细表存储用户提交订单的行项商品信息,是 “订单 - 商品” 关联的中间表,一个订单可包含多个订单明细,其表结构设计如下:
| 字段名 | 类型 | 含义 | 约束 |
| id | BIGINT | 订单明细 ID | 主键、自增 |
| order_id | BIGINT | 关联订单 ID | 非空 |
| product_id | BIGINT | 关联商品 ID | 非空 |
| quantity | INT | 购买商品数量 | 非空 |
| price | DECIMAL(10,2) | 商品下单时单价 | 非空 |
| total_price | DECIMAL(10,2) | 该行项商品总金额 | 非空 |
| create_time | DATETIME | 创建时间 | 默认当前时间 |
| update_time | DATETIME | 更新时间 | 默认当前时间 |
| is_deleted | TINYINT | 逻辑删除标识(0 - 未删除,1 - 已删除) | 默认 0 |
该表中,order_id与product_id的联合唯一约束,可避免同一订单重复添加同一个商品;同时,表中记录的price(商品下单时单价)是商品的快照数据,不可复用商品表中的spu_price,保证后续商品基础价格被调整时,订单的历史行项金额不会被影响,保障订单数据的可溯源性。
2.2.5 物流订单表(logistics_order)
物流订单表存储订单对应的物流单核心信息,是连接订单业务与物流运输业务的主表,一个订单对应唯一一条物流订单记录,其表结构设计如下:
| 字段名 | 类型 | 含义 | 约束 |
| id | BIGINT | 物流单 ID | 主键、自增 |
| order_id | BIGINT | 关联订单 ID | 非空、唯一 |
| tracking_no | VARCHAR(50) | 物流单号 | 非空、唯一 |
| carrier | VARCHAR(20) | 承运商 | 可选 |
| status | VARCHAR(20) | 物流单当前状态 | 非空 |
| estimated_delivery_time | DATETIME | 预计送达时间 | 可选 |
| created_at | DATETIME | 创建时间 | 默认当前时间 |
| updated_at | DATETIME | 更新时间 | 默认当前时间 |
| is_deleted | TINYINT | 逻辑删除标识(0 - 未删除,1 - 已删除) | 默认 0 |
其中,tracking_no为物流商提供的唯一单号,status字段的取值范围包括WAITING_PICKUP(待揽件)、PICKED_UP(已揽件)、IN_TRANSIT(运输中)、DELIVERED(已送达)、SIGNED(已签收),是物流状态同步给订单的核心依据(99)。
2.2.6 物流轨迹表(logistics_trace)
物流轨迹表存储物流订单的全流程运输节点信息,是物流跟踪功能的核心支撑表,一条物流订单对应多个物流轨迹节点,其表结构设计如下:
| 字段名 | 类型 | 含义 | 约束 |
| id | BIGINT | 轨迹 ID | 主键、自增 |
| logistics_order_id | BIGINT | 关联物流单 ID | 非空 |
| trace_time | DATETIME | 节点发生时间 | 非空 |
| status | VARCHAR(20) | 节点物流状态 | 非空 |
| description | VARCHAR(255) | 物流节点详情描述 | 可选 |
| location | VARCHAR(255) | 节点经纬度位置 | 可选 |
| created_at | DATETIME | 创建时间 | 默认当前时间 |
| updated_at | DATETIME | 更新时间 | 默认当前时间 |
| is_deleted | TINYINT | 逻辑删除标识(0 - 未删除,1 - 已删除) | 默认 0 |
该表中,location字段存储物流节点的经纬度信息,用于在前端地图上渲染运输轨迹;description字段存储物流节点的详细描述(如 “【广州市】广州白云区公司收件员已揽件”),对用户展示物流进展的详细说明;同时,表中针对logistics_order_id字段创建了二级索引,提升物流轨迹的多节点查询性能(26)。
2.3 核心业务关联关系
为了保证订单、库存、物流数据的一致性,项目通过外键约束与业务逻辑的组合,建立了三张核心业务表之间的强关联关系,具体关联规则为:
订单明细表的order_id字段关联订单表的id主键,product_id字段关联商品表的id主键,确保订单行项的商品、用户收货地址等基础数据不会被物理删除;
库存表的product_id字段与商品表的id主键建立唯一关联,确保每一件上架商品都有独立的库存记录;
物流订单表的order_id字段关联订单表的id主键,且两者的关联关系为一对一,确保一个订单只能对应一个物流单;
物流轨迹表的logistics_order_id字段关联物流订单表的id主键,确保所有物流轨迹节点都能溯源到对应的订单。
在后续的高并发优化中,为了降低数据库的压力,可将部分外键约束(如物流轨迹表与物流订单表的外键)从数据库层面调整到业务逻辑层,由业务代码在更新数据时保证一致性。
2.4 初始化 SQL 脚本
本案例的所有数据库表初始化 SQL 脚本,均严格遵循 MySQL 8.0 的语法规范,核心业务表的 SQL 建表示意脚本如下:
| -- 创建数据库(如果不存在) CREATE DATABASE IF NOT EXISTS `order_demo` DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; -- 使用数据库 USE `order_demo`; -- 商品表建表语句 CREATE TABLE IF NOT EXISTS `products` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品ID', `spu_name` VARCHAR(100) NOT NULL COMMENT '商品SPU名称', `spu_value` VARCHAR(100) NOT NULL COMMENT '商品规格', `spu_price` DECIMAL(10,2) NOT NULL COMMENT '商品售价', `product_sales` INT NOT NULL DEFAULT 0 COMMENT '销量', `state` TINYINT NOT NULL DEFAULT 0 COMMENT '上下架状态', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标识', PRIMARY KEY (`id`), INDEX `idx_spu_name` (`spu_name`), INDEX `idx_state` (`state`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; -- 库存表建表语句 CREATE TABLE IF NOT EXISTS `inventory` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '库存记录ID', `product_id` BIGINT NOT NULL COMMENT '商品ID', `warehouse_id` BIGINT NOT NULL DEFAULT 1 COMMENT '仓库ID', `stock_num` INT NOT NULL DEFAULT 0 COMMENT '实际库存数量', `locked_stock` INT NOT NULL DEFAULT 0 COMMENT '锁定库存数量', `version` INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标识', PRIMARY KEY (`id`), UNIQUE INDEX `idx_product_warehouse` (`product_id`, `warehouse_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='库存表'; -- 订单表建表语句 CREATE TABLE IF NOT EXISTS `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` VARCHAR(32) NOT NULL COMMENT '订单编号', `user_id` BIGINT NOT NULL COMMENT '用户ID', `total_amount` DECIMAL(10,2) NOT NULL COMMENT '订单总金额', `status` VARCHAR(20) NOT NULL COMMENT '订单状态', `payment_method` VARCHAR(50) DEFAULT NULL COMMENT '支付方式', `delivery_address` TEXT NOT NULL COMMENT '收货地址', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标识', PRIMARY KEY (`id`), UNIQUE INDEX `idx_order_no` (`order_no`), INDEX `idx_user_id` (`user_id`), INDEX `idx_create_time` (`create_time`), INDEX `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表'; -- 订单明细表建表语句 CREATE TABLE IF NOT EXISTS `order_detail` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '订单明细ID', `order_id` BIGINT NOT NULL COMMENT '订单ID', `product_id` BIGINT NOT NULL COMMENT '商品ID', `quantity` INT NOT NULL COMMENT '购买数量', `price` DECIMAL(10,2) NOT NULL COMMENT '商品下单单价', `total_price` DECIMAL(10,2) NOT NULL COMMENT '该行项总金额', `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标识', PRIMARY KEY (`id`), INDEX `idx_order_id` (`order_id`), INDEX `idx_product_id` (`product_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'; -- 物流订单表建表语句 CREATE TABLE IF NOT EXISTS `logistics_order` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '物流单ID', `order_id` BIGINT NOT NULL COMMENT '订单ID', `tracking_no` VARCHAR(50) NOT NULL COMMENT '物流单号', `carrier` VARCHAR(20) DEFAULT NULL COMMENT '承运商', `status` VARCHAR(20) NOT NULL COMMENT '物流单状态', `estimated_delivery_time` DATETIME DEFAULT NULL COMMENT '预计送达时间', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标识', PRIMARY KEY (`id`), UNIQUE INDEX `idx_order_id` (`order_id`), UNIQUE INDEX `idx_tracking_no` (`tracking_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流订单表'; -- 物流轨迹表建表语句 CREATE TABLE IF NOT EXISTS `logistics_trace` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '轨迹ID', `logistics_order_id` BIGINT NOT NULL COMMENT '物流单ID', `trace_time` DATETIME NOT NULL COMMENT '节点时间', `status` VARCHAR(20) NOT NULL COMMENT '节点物流状态', `description` VARCHAR(255) DEFAULT NULL COMMENT '节点详情描述', `location` VARCHAR(255) DEFAULT NULL COMMENT '节点位置', `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除标识', PRIMARY KEY (`id`), INDEX `idx_logistics_order_id` (`logistics_order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='物流轨迹表'; |
实际开发时,可将上述脚本保存为database.sql文件,在 MySQL 客户端或 Navicat、DBeaver 等可视化工具中执行,完成初始化数据库与表结构的准备工作。
3. 开发环境搭建
本节将详细介绍从零基础搭建前后端分离项目的步骤,包含后端 Maven 工程创建、前端 Vue 工程初始化,以及工程配置、依赖整合的完整过程。
3.1 后端 Spring Boot 工程搭建
后端采用技术栈组合为 Spring Boot 3.2.x + MyBatis-Plus + MySQL 8.0 + Redis,工程构建工具为 Maven,可通过 Spring Initializr 或 IDE 可视化两种方式快速搭建基础工程。
3.1.1 工程初始化
本案例提供两种搭建后端基础工程的方式,开发者可根据自身习惯选择其中一种。
方式一:通过 Spring Initializr 创建
这是 Spring 官方提供的项目初始化工具,可通过浏览器访问 Spring Initializr 页面,快速生成基础工程骨架,具体操作步骤为:
选择项目构建工具为Maven,开发语言为Java,Spring Boot 版本选择3.2.x(如 3.2.5);
填写项目元信息:Group(如com.example)、Artifact(如order-demo)、Name、Description,包名、Java 版本选择17;
选择项目依赖,核心依赖包括Spring Web(提供 Web 服务能力)、MySQL Driver(提供 MySQL 数据库连接能力)、Lombok(简化实体类代码编写)、MyBatis Framework(数据库 ORM 框架)、Spring Data Redis(提供 Redis 缓存操作能力)、Validation(提供参数校验能力);
点击 “Generate” 按钮,自动下载生成的项目压缩包,解压后导入到开发 IDE 中(如 IntelliJ IDEA)。
方式二:通过 IntelliJ IDEA 可视化创建
如果使用 IntelliJ IDEA 作为开发 IDE,可直接通过 IDE 集成的 Spring Initializr 创建工程,具体操作步骤为:
打开 IDE 后,点击File → New → Project,选择Spring Initializr选项,设置项目元信息(Group、Artifact、Java 版本等);
选择 Spring Boot 版本为3.2.x,在依赖列表中勾选上述核心依赖;
确认项目名称、存储磁盘路径后,点击 “Finish” 按钮,完成项目初始化;IDE 将自动下载配置的依赖,等待下载完成即可进行后续开发。
3.1.2 配置文件调整
工程初始化完成后,需在application.properties(或application.yml)配置文件中添加数据库、Redis、MyBatis-Plus、项目端口等核心配置,确保项目能正常连接依赖资源、启动 Web 服务。本案例采用application.yml格式作为项目配置文件,核心配置内容示意如下:
| # 应用服务配置 server: port: 9090 # 后端服务端口号 servlet: context-path: /api # 项目统一请求前缀 # 数据源配置 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root # 数据库用户名 password: 123456 # 数据库密码 druid: initial-size: 5 max-active: 20 min-idle: 5 max-wait: 60000 # Redis配置 redis: host: localhost # Redis服务器地址 port: 6379 # Redis端口号 password: # Redis密码,默认为空 database: 0 # 选择Redis库 timeout: 3000ms lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 # MyBatis-Plus配置 mybatis-plus: mapper-locations: classpath:mapper/*.xml # 加载Mapper XML文件 type-aliases-package: com.example.orderdemo.entity # 配置实体类包别名 configuration: map-underscore-to-camel-case: true # 开启驼峰命名与下划线转换 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto # 数据库主键自增 logic-delete-field: is_deleted # 逻辑删除字段 logic-delete-value: 1 # 逻辑删除已删除值 logic-not-delete-value: 0 # 逻辑删除未删除值 # 日志配置 logging: level: com.example.orderdemo.mapper: debug # 开启SQL日志打印 |
配置完成后,需在工程的src/main/java/com/example/orderdemo目录下创建启动类OrderDemoApplication.java,添加@SpringBootApplication注解,标识这是一个 Spring Boot 启动类,代码示例如下:
| package com.example.orderdemo; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication @MapperScan("com.example.orderdemo.mapper") // 扫描Mapper接口 public class OrderDemoApplication { public static void main(String[] args) { SpringApplication.run(OrderDemoApplication.class, args); } } |
执行启动类的 main 方法,若控制台无异常日志、输出 “Started OrderDemoApplication” 标识,说明后端基础工程搭建成功。
3.1.3 引入必要依赖
在生成的pom.xml文件中,除了 Spring Initializr 自动引入的依赖外,还需手动补充 MyBatis-Plus、Druid 连接池、Knife4j 接口文档、Jackson 等核心依赖的配置,确保后续业务开发有完整的技术支撑,完整的pom.xml依赖配置示例如下:
| <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.example</groupId> <artifactId>order-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>order-demo</name> <description>订单库存物流示例项目</description> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>17</java.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version> <druid.version>1.2.16</druid.version> <knife4j.version>4.3.0</knife4j.version> </properties> <dependencies> <!-- Spring Boot Web Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Spring Boot Validation Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <!-- Spring Boot Data Redis Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- MyBatis-Plus Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- MySQL Driver --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- Druid Connection Pool --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>${druid.version}</version> </dependency> <!-- Lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <!-- Knife4j OpenAPI --> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <version>${knife4j.version}</version> </dependency> <!-- Spring Boot Test Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Spring Boot Maven Plugin --> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project> |
补充依赖配置后,需执行 Maven 加载命令,下载 / 加载所有依赖,确保项目编译正常。
3.1.4 工程包结构规划
为了保证后续业务代码的可维护性,需采用分层架构设计后端工程的包结构,将业务逻辑、数据访问、控制器、实体类进行清晰隔离,标准的包结构规划如下:
| src/main/java/com/example/orderdemo/ ├── OrderDemoApplication.java // 项目启动类 ├── config/ // 配置类 │ ├── RedisConfig.java // Redis配置类 │ ├── MyBatisPlusConfig.java // MyBatis-Plus配置类 │ ├── CorsConfig.java // 跨域配置类 │ └── Knife4jConfig.java // Knife4j接口文档配置类 ├── controller/ // 控制器层,处理HTTP请求响应 │ ├── OrderController.java // 订单相关控制器 │ ├── ProductController.java // 商品相关控制器 │ ├── InventoryController.java // 库存相关控制器 │ └── LogisticsController.java // 物流相关控制器 ├── dto/ // 数据传输对象,接收/响应前端请求 │ ├── order/ // 订单相关DTO │ ├── inventory/ // 库存相关DTO │ └── logistics/ // 物流相关DTO ├── entity/ // 数据库实体类,与表结构一一对应 │ ├── Order.java // 订单实体类 │ ├── OrderDetail.java // 订单明细实体类 │ ├── Product.java // 商品实体类 │ ├── Inventory.java // 库存实体类 │ ├── LogisticsOrder.java // 物流订单实体类 │ └── LogisticsTrace.java // 物流轨迹实体类 ├── mapper/ // 数据访问层,数据库操作接口 │ ├── OrderMapper.java // 订单Mapper接口 │ ├── OrderDetail.java // 订单明细Mapper接口 │ ├── ProductMapper.java // 商品Mapper接口 │ ├── InventoryMapper.java // 库存Mapper接口 │ ├── LogisticsOrderMapper.java // 物流订单Mapper接口 │ └── LogisticsTraceMapper.java // 物流轨迹Mapper接口 ├── service/ // 业务服务层,封装核心业务逻辑 │ ├── OrderService.java // 订单业务接口 │ ├── InventoryService.java // 库存业务接口 │ ├── LogisticsService.java // 物流业务接口 │ └── impl/ // 接口实现类 │ ├── OrderServiceImpl.java // 订单业务实现类 │ ├── InventoryServiceImpl.java // 库存业务实现类 │ └── LogisticsServiceImpl.java // 物流业务实现类 ├── utils/ // 工具类 │ ├── OrderNoUtils.java // 订单编号生成工具类 │ ├── RedisLockUtils.java // Redis分布式锁工具类 │ └── ValidationUtils.java // 参数校验工具类 └── exception/ // 自定义异常类 ├── BusinessException.java // 业务异常类 └── GlobalExceptionHandler.java // 全局异常处理器 |
其中,service层是核心业务逻辑的封装层,controller层仅做请求参数接收、响应数据格式化、权限校验,不承载任何业务逻辑,保证后续业务扩展、维护的便利性。
3.2 前端 Vue 工程搭建
前端采用 Vue 3 + Vite + Element Plus + Pinia 技术栈,通过 npm 包管理工具初始化工程。
3.2.1 工程初始化
本案例采用 Vite 构建工具初始化前端基础工程,需提前安装好 LTS 18.16.0 及以上版本的 Node.js 环境,具体操作步骤为:
打开终端,执行如下命令,使用 Vite 初始化 Vue 3 基础工程:
| npm create vite@latest order-frontend -- --template vue-ts |
该命令将创建一个名为order-frontend的前端工程,基础模板为 Vue 3+JavaScript。
进入工程目录,执行如下命令,安装项目的基础依赖:
| cd order-frontend npm install |
安装后续业务开发必备的核心依赖,包括 Element Plus UI 组件库、Axios 请求工具、Vue Router 路由管理器、Pinia 状态管理库、@vueuse/core 工具库、sass CSS 预处理器,命令如下:
| npm install element-plus axios vue-router@4 pinia @vueuse/core sass |
3.2.2 项目结构规划
为了和后端工程的结构保持代码逻辑隔离,前端采用 “功能 + 类型” 的规范进行结构规划,标准的前端工程结构如下:
| src/ ├── main.js // 项目入口文件 ├── App.vue // 根组件 ├── index.css // 全局样式 ├── router/ // 路由配置 │ └── index.js // 路由规则文件 ├── store/ // 状态管理 │ ├── useOrderStore.js // 订单状态管理 │ └── useLogisticsStore.js // 物流状态管理 ├── components/ // 公共复用组件 │ ├── OrderList.vue // 订单列表组件 │ ├── OrderDetail.vue // 订单详情组件 │ └── LogisticsTracker.vue // 物流跟踪组件 ├── views/ // 业务页面 │ ├── OrderView.vue // 订单业务页面 │ ├── LogisticsView.vue // 物流业务页面 │ └── ProductView.vue // 商品业务页面 ├── api/ // 后端接口请求 │ ├── order.js // 订单相关接口 │ ├── inventory.js // 库存相关接口 │ └── logistics.js // 物流相关接口 ├── utils/ // 工具类 │ ├── request.js // Axios请求封装 │ └── format.js // 格式化工具 └── assets/ // 静态资源 ├── images/ // 图片资源 └── styles/ // 样式资源 |
3.2.3 跨域配置
由于前后端分离部署时,前端服务与后端服务必然存在域名 / 端口差异,会产生浏览器跨域问题,本案例通过在前端工程中配置服务端代理,解决跨域请求问题。具体操作为:修改工程根目录下的vite.config.js配置文件,添加服务端代理配置,将前端的 API 请求代理到后端服务,示例代码如下:
| import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' // https://vitejs.dev/config/ export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, './src') // 配置路径别名@,指向src目录 } }, server: { port: 8080, // 前端服务端口 proxy: { // 代理所有以 /api 开头的请求 '/api': { target: 'http://localhost:9090', // 后端服务地址 changeOrigin: true, // 强制修改请求头Origin,解决跨域 rewrite: (path) => path.replace(/^\/api/, '') // 重写请求路径,去掉/api前缀 } } } }) |
为了统一处理请求 / 响应、简化业务层 HTTP 调用,还需要对 Axios 进行二次封装,在src/utils/request.js文件中创建封装后的请求工具,示例代码如下:
| import axios from 'axios' import { ElMessage } from 'element-plus' // 创建Axios实例 const request = axios.create({ baseURL: '/api', // 统一请求基础路径 timeout: 10000, // 请求超时时间 headers: { 'Content-Type': 'application/json;charset=utf-8' } }) // 请求拦截器,统一处理请求参数 request.interceptors.request.use( config => { // 可在请求头中加入token等认证信息 return config }, error => { // 处理请求异常 return Promise.reject(error) } ) // 响应拦截器,统一处理响应数据 request.interceptors.response.use( response => { const res = response.data // 手动处理业务级异常 if (res.code !== 200) { ElMessage.error(res.message || '业务操作失败') return Promise.reject(new Error(res.message || '业务操作失败')) } return res }, error => { // 处理HTTP级异常 let message = error.message if (error.response && error.response.data) { message = error.response.data.message || `HTTP错误:${error.response.status}` } ElMessage.error(message) return Promise.reject(error) } ) export default request |
3.3 前后端联调验证
完成上述配置后,需验证前后端工程是否能正常启动、联调。
启动后端工程:执行OrderDemoApplication类的 main 方法,控制台输出 “Started OrderDemoApplication” 标识,说明后端服务正常启动,端口为 9090。
启动前端工程:在终端执行npm run dev命令,启动前端服务,控制台输出本地访问地址(如http://localhost:8080),说明前端工程正常启动。
验证联调:在后端OrderController类中定义一个测试接口/api/order/test,在前端OrderView.vue页面的onMounted生命周期钩子中,调用该测试接口;若前端能正常访问后端接口、返回预期结果,说明前后端联调环境正常,工程搭建完成。
4. 后端核心业务开发
本节将实现订单、库存、物流三大模块的后端核心业务逻辑,包含实体类、Mapper 层、Service 层、Controller 层的完整代码开发。
4.1 公共层基础开发
在开发具体业务模块前,需先完成公共基础层的代码开发,为后续业务提供通用能力支撑。
4.1.1 统一返回结果封装
为了保证前后端交互的响应数据格式一致性,便于前端统一处理响应,在后端工程中封装统一的响应结果类R,包含响应码、响应消息、响应数据三个核心属性,示例代码如下:
| package com.example.orderdemo.dto; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Data @Schema(description = "统一响应结果") public class R<T> { @Schema(description = "响应码:200-成功,500-业务异常,400-请求参数异常") private Integer code; @Schema(description = "响应消息提示") private String message; @Schema(description = "响应数据") private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.setCode(200); r.setMessage("操作成功"); r.setData(data); return r; } public static <T> R<T> ok() { return ok(null); } public static <T> R<T> fail(Integer code, String message) { R<T> r = new R<>(); r.setCode(code); r.setMessage(message); return r; } public static <T> R<T> fail(String message) { return fail(500, message); } } |
4.1.2 全局异常处理
为了避免业务异常直接返回给前端、保证异常处理的统一性,后端工程中采用@RestControllerAdvice注解实现全局异常处理器,拦截并处理所有 Controller 层抛出的异常,示例代码如下:
| package com.example.orderdemo.exception; import com.example.orderdemo.dto.R; import lombok.extern.slf4j.Slf4j; import org.springframework.validation.BindException; import org.springframework.validation.BindingResult; import org.springframework.validation.FieldError; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @Slf4j @RestControllerAdvice public class GlobalExceptionHandler { // 处理自定义业务异常 @ExceptionHandler(BusinessException.class) public R<Void> handleBusinessException(BusinessException e) { log.error("业务异常:{}", e.getMessage(), e); return R.fail(e.getCode(), e.getMessage()); } // 处理请求参数校验异常 @ExceptionHandler(BindException.class) public R<Void> handleBindException(BindException e) { BindingResult bindingResult = e.getBindingResult(); FieldError fieldError = bindingResult.getFieldError(); String message = fieldError.getField() + fieldError.getDefaultMessage(); log.error("参数校验异常:{}", message, e); return R.fail(message); } // 处理系统异常 @ExceptionHandler(Exception.class) public R<Void> handleException(Exception e) { log.error("系统异常:{}", e.getMessage(), e); return R.fail("系统繁忙,请稍后再试"); } } |
4.1.3 工具类开发
为了保证后续业务开发的便利性,需提前开发三个核心通用工具类:
OrderNoUtils:基于 “时间戳 + 雪花算法” 的规则生成全局唯一订单号,避免使用数据库自增主键,提升订单安全性;
RedisLockUtils:封装 Redis 分布式锁的获取、释放逻辑,避免库存扣减、订单创建等并发场景下发生资源竞争;
ValidationUtils:封装通用参数校验逻辑,对前端传入的业务参数进行合法性校验,避免非法参数进入业务流程。
4.2 商品模块开发
商品模块是订单、库存、物流业务的基础支撑,本案例仅提供基础的商品查询接口,不涉及商品新增、编辑业务逻辑。
4.2.1 实体类(Entity)
在后端工程的entity包下创建商品实体类Product,与数据库的products表结构一一对应,示例代码如下:
| package com.example.orderdemo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; @Data @TableName("products") public class Product { @TableId(type = IdType.AUTO) private Long id; private String spuName; private String spuValue; private BigDecimal spuPrice; private Integer productSales; private Integer state; private LocalDateTime createTime; private LocalDateTime updateTime; @TableLogic private Integer isDeleted; } |
4.2.2 Mapper 层
在mapper包下创建商品数据访问层接口ProductMapper,需继承 MyBatis-Plus 的BaseMapper接口,获得通用 CRUD 能力,示例代码如下:
| package com.example.orderdemo.mapper; import com.example.orderdemo.entity.Product; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; @Mapper public interface ProductMapper extends BaseMapper<Product> { } |
4.2.3 Service 层
在service包下创建商品业务层接口ProductService,定义查询商品列表、查询商品详情的方法;在service/impl包下创建接口实现类ProductServiceImpl,继承 MyBatis-Plus 的ServiceImpl基础实现类,注入ProductMapper,实现商品查询业务逻辑。
4.2.4 Controller 层
在controller包下创建商品控制器ProductController,提供商品查询相关的 RESTful 接口,示例代码如下:
| package com.example.orderdemo.controller; import com.example.orderdemo.dto.R; import com.example.orderdemo.entity.Product; import com.example.orderdemo.service.ProductService; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/product") @RequiredArgsConstructor @Tag(name = "商品管理", description = "商品相关接口") @Validated public class ProductController { private final ProductService productService; @GetMapping("/list") @Operation(summary = "分页查询商品列表", description = "根据分页参数查询商品列表") public R<IPage<Product>> list( @Parameter(description = "页码", required = true) @RequestParam Integer pageNum, @Parameter(description = "每页条数", required = true) @RequestParam Integer pageSize) { return R.ok(productService.page(new Page<>(pageNum, pageSize))); } @GetMapping("/{id}") @Operation(summary = "查询商品详情", description = "根据商品ID查询商品详情") public R<Product> getById(@PathVariable Long id) { return R.ok(productService.getById(id)); } } |
4.3 库存模块开发
库存模块是保证订单业务准确性、避免超卖问题的核心支撑模块,需实现库存预扣、库存确认扣减的核心业务逻辑。
4.3.1 实体类(Entity)
在entity包下创建库存实体类Inventory,与数据库的inventory表结构一一对应,示例代码如下:
| package com.example.orderdemo.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableLogic; import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.Version; import lombok.Data; import java.time.LocalDateTime; @Data @TableName("inventory") public class Inventory { @TableId(type = IdType.AUTO) private Long id; private Long productId; private Long warehouseId; private Integer stockNum; private Integer lockedStock; @Version private Integer version; private LocalDateTime createTime; private LocalDateTime updateTime; @TableLogic private Integer isDeleted; } |
4.3.2 Mapper 层
在mapper包下创建库存数据访问层接口InventoryMapper,继承 MyBatis-Plus 的BaseMapper接口,定义库存扣减的自定义 SQL 方法,示例代码如下:
| package com.example.orderdemo.mapper; import com.example.orderdemo.entity.Inventory; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Update; @Mapper public interface InventoryMapper extends BaseMapper<Inventory> { @Update("UPDATE inventory SET locked_stock = locked_stock + #{quantity}, version = version + 1 " + "WHERE product_id = #{productId} AND version = #{version} AND stock_num >= #{quantity}") int lockStock(@Param("productId") Long productId, @Param("quantity") Integer quantity, @Param("version") Integer version); @Update("UPDATE inventory SET locked_stock = locked_stock - #{quantity}, stock_num = stock_num - #{quantity} " + "WHERE product_id = #{productId} AND locked_stock >= #{quantity}") int confirmDeductStock(@Param("productId") Long productId, @Param("quantity") Integer quantity); } |
4.3.3 Service 层
在service包下创建库存业务层接口InventoryService,定义预扣库存、确认扣减库存、回滚库存三个核心方法;在service/impl包下创建接口实现类InventoryServiceImpl,注入InventoryMapper,实现库存扣减业务逻辑。
其中,预扣库存的核心逻辑为:先校验商品的实际库存是否充足,若库存充足则将对应数量的商品从 “可售库存” 转移到 “锁定库存”;确认扣减库存是在订单生效、物流发货时,将锁定库存中的商品数量正式扣减;回滚库存是在订单取消时,将锁定库存的商品数量恢复到可售库存。
为了保证高并发场景下的扣减准确性,业务层同时采用 “Redis 预扣 + 数据库乐观锁” 的方案:先在 Redis 中预扣库存,再基于数据库的行级锁 + 乐观锁,保证多个请求扣减同一商品库存时的原子性。
4.3.4 Controller 层
在controller包下创建库存控制器InventoryController,仅提供内部调用接口,不对外暴露,示例代码如下:
| package com.example.orderdemo.controller; import com.example.orderdemo.dto.R; import com.example.orderdemo.service.InventoryService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/inventory") @RequiredArgsConstructor @Tag(name = "库存管理", description = "库存相关接口") @Validated public class InventoryController { private final InventoryService inventoryService; @PostMapping("/lock/{productId}") @Operation(summary = "预扣库存", description = "订单创建时调用,预扣库存") public R<Boolean> lockStock( @Parameter(description = "商品ID", required = true) @PathVariable Long productId, @Parameter(description = "购买数量", required = true) @RequestParam Integer quantity) { return R.ok(inventoryService.lockStock(productId, quantity)); } @PostMapping("/confirm/{productId}") @Operation(summary = "确认扣减库存", description = "物流发货时调用,确认扣减库存") public R<Boolean> confirmDeductStock( @Parameter(description = "商品ID", required = true) @PathVariable Long productId, @Parameter(description = "购买数量", required = true) @RequestParam Integer quantity) { return R.ok(inventoryService.confirmDeductStock(productId, quantity)); } @PostMapping("/rollback/{productId}") @Operation(summary = "回滚库存", description = "订单取消时调用,恢复预扣库存") public R<Boolean> rollbackStock( @Parameter(description = "商品ID", required = true) @PathVariable Long productId, @Parameter(description = "购买数量", required = true) @RequestParam Integer quantity) { return R.ok(inventoryService.rollbackStock(productId, quantity)); } } |
4.4 订单模块开发
订单模块是整个业务流程的核心入口,需实现创建订单、查询订单列表、查询订单详情、取消订单四个核心功能,订单模块的开发需与库存模块联动完成数据操作。
4.4.1 实体类(Entity)
根据之前设计的数据库表结构,在entity包下创建订单实体类Order、订单明细实体类OrderDetail,分别与orders、order_detail表结构一一对应。
4.4.2 Mapper 层
在mapper包下创建订单数据访问层接口OrderMapper、订单明细数据访问层接口OrderDetailMapper,均需继承 MyBatis-Plus 的BaseMapper接口,OrderMapper提供基于用户 ID、订单状态、创建时间的分页查询方法,OrderDetailMapper提供基于订单 ID 查询订单明细的方法。
4.4.3 Service 层
在service包下创建订单业务层接口OrderService,定义创建订单、查询订单列表、查询订单详情、取消订单四个核心方法;在service/impl包下创建接口实现类OrderServiceImpl,注入OrderMapper、OrderDetailMapper、InventoryService,实现订单业务逻辑。
核心业务逻辑说明:
创建订单:该方法必须添加@Transactional注解,开启本地事务保证原子性;先生成订单基础记录,再调用库存模块的预扣库存方法,若库存预扣成功则返回订单编号,若预扣失败则抛出业务异常,触发事务回滚。
取消订单:需先校验订单状态是否为待发货,再更新订单状态为已取消,最后调用库存模块的回滚库存方法;若物流单已生成,则同步更新物流单状态为已取消。
查询订单列表:支持按订单状态、创建时间范围筛选,关联查询订单明细的行项商品数据。
4.4.4 Controller 层
在controller包下创建订单控制器OrderController,提供订单相关的 RESTful 接口,示例代码如下:
| package com.example.orderdemo.controller; import com.example.orderdemo.dto.R; import com.example.orderdemo.dto.order.OrderCreateDTO; import com.example.orderdemo.dto.order.OrderQueryDTO; import com.example.orderdemo.entity.Order; import com.example.orderdemo.service.OrderService; import com.baomidou.mybatisplus.core.metadata.IPage; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/order") @RequiredArgsConstructor @Tag(name = "订单管理", description = "订单相关接口") @Validated public class OrderController { private final OrderService orderService; @PostMapping("/create") @Operation(summary = "创建订单", description = "提交订单时调用,创建订单并预扣库存") public R<String> create(@RequestBody @Validated OrderCreateDTO dto) { return R.ok(orderService.create(dto)); } @GetMapping("/list") @Operation(summary = "分页查询订单列表", description = "根据条件分页查询订单列表") public R<IPage<Order>> list(@Validated OrderQueryDTO queryDTO) { return R.ok(orderService.pageList(queryDTO)); } @GetMapping("/{id}") @Operation(summary = "查询订单详情", description = "根据订单ID查询订单详情") public R<Order> getById(@PathVariable Long id) { return R.ok(orderService.getById(id)); } @PostMapping("/cancel/{id}") @Operation(summary = "取消订单", description = "取消待发货订单,回滚库存") public R<Boolean> cancel(@PathVariable Long id) { return R.ok(orderService.cancel(id)); } } |
4.5 物流模块开发
物流模块是本案例业务流程的收尾模块,需实现生成物流单、更新物流状态、查询物流轨迹三个核心功能,物流模块的开发需与订单模块联动完成数据操作。
4.5.1 实体类(Entity)
根据之前设计的数据库表结构,在entity包下创建物流订单实体类LogisticsOrder、物流轨迹实体类LogisticsTrace,分别与logistics_order、logistics_trace表结构一一对应。
4.5.2 Mapper 层
在mapper包下创建物流订单数据访问层接口LogisticsOrderMapper、物流轨迹数据访问层接口LogisticsTraceMapper,均需继承 MyBatis-Plus 的BaseMapper接口,自定义更新物流单状态、根据物流单 ID 查询轨迹、根据订单 ID 查询物流单的方法。
4.5.3 Service 层
在service包下创建物流业务层接口LogisticsService,定义生成物流单、更新物流状态、查询物流轨迹三个核心方法;在service/impl包下创建接口实现类LogisticsServiceImpl,注入LogisticsOrderMapper、LogisticsTraceMapper、OrderService、InventoryService,实现物流业务逻辑。
核心业务逻辑说明:
生成物流单:该方法必须添加@Transactional注解,开启本地事务保证原子性;根据订单 ID 生成物流单记录,调用库存模块的确认扣减库存方法,将锁定库存正式扣减,再调用订单模块的更新状态方法,将订单状态变更为已发货。
更新物流状态:更新物流单的当前状态,新增物流轨迹节点记录;若更新后的状态为已签收,则同步更新订单状态为已完成;若状态为异常、拒收,则触发逆向流程,回滚库存、更新订单状态为已取消。
查询物流轨迹:根据订单 ID 或物流单号,返回包含轨迹节点列表的物流详情记录。
4.5.4 Controller 层
在controller包下创建物流控制器LogisticsController,提供物流相关的 RESTful 接口,示例代码如下:
| package com.example.orderdemo.controller; import com.example.orderdemo.dto.R; import com.example.orderdemo.dto.logistics.LogisticsCreateDTO; import com.example.orderdemo.dto.logistics.LogisticsTraceQueryDTO; import com.example.orderdemo.entity.LogisticsOrder; import com.example.orderdemo.service.LogisticsService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/logistics") @RequiredArgsConstructor @Tag(name = "物流管理", description = "物流相关接口") @Validated public class LogisticsController { private final LogisticsService logisticsService; @PostMapping("/create") @Operation(summary = "生成物流单", description = "订单发货时调用,创建物流单") public R<Boolean> create(@RequestBody @Validated LogisticsCreateDTO dto) { return R.ok(logisticsService.create(dto)); } @PostMapping("/updateStatus") @Operation(summary = "更新物流状态", description = "更新物流单状态,记录物流轨迹") public R<Boolean> updateStatus(@RequestBody @Validated LogisticsTraceQueryDTO dto) { return R.ok(logisticsService.updateStatus(dto)); } @GetMapping("/{orderId}") @Operation(summary = "查询物流轨迹", description = "根据订单ID查询物流轨迹") public R<LogisticsOrder> getByOrderId(@PathVariable Long orderId) { return R.ok(logisticsService.getByOrderId(orderId)); } } |
5. 前端核心业务开发
本节将实现订单列表、物流跟踪两个核心业务页面,以及前后端联调验证,完成用户端的业务交互流程。
5.1 公共层封装
在开发具体业务页面前,需先封装后端 API 调用接口,便于后续业务开发的统一维护,减少重复代码。
5.1.1 后端 API 接口统一封装
在前端工程的src/api目录下,按业务维度分别创建order.js、inventory.js、logistics.js三个文件,封装对应模块的后端接口调用,示例代码如下:
| // src/api/order.js import request from '@/utils/request' // 创建订单 export function createOrder(data) { return request({ url: '/order/create', method: 'post', data }) } // 分页查询订单列表 export function getOrderList(params) { return request({ url: '/order/list', method: 'get', params }) } // 查询订单详情 export function getOrderDetail(id) { return request({ url: `/order/${id}`, method: 'get' }) } // 取消订单 export function cancelOrder(id) { return request({ url: `/order/cancel/${id}`, method: 'post' }) } // src/api/logistics.js import request from '@/utils/request' // 创建物流单 export function createLogistics(data) { return request({ url: '/logistics/create', method: 'post', data }) } // 更新物流状态 export function updateLogisticsStatus(data) { return request({ url: '/logistics/updateStatus', method: 'post', data }) } // 查询物流轨迹 export function getLogisticsByOrderId(orderId) { return request({ url: `/logistics/${orderId}`, method: 'get' }) } |
5.1.2 路由配置
在src/router/index.js文件中配置订单、物流页面的路由规则,实现无刷新页面切换,示例代码如下:
| import { createRouter, createWebHistory } from 'vue-router' import OrderView from '@/views/OrderView.vue' import LogisticsView from '@/views/LogisticsView.vue' const routes = [ { path: '/', redirect: '/order' }, { path: '/order', name: 'Order', component: OrderView, meta: { title: '订单管理' } }, { path: '/logistics/:orderId', name: 'Logistics', component: LogisticsView, meta: { title: '物流跟踪' }, props: true } ] const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes }) export default router |
5.2 订单列表页开发
订单列表页是用户访问订单信息、跳转物流跟踪页面的入口,具备查询订单、取消订单、跳转物流详情三个核心功能,采用 Vue 3 的 Composition API、Element Plus 组件库实现交互逻辑。
5.2.1 页面结构
在src/views目录下创建订单列表页面文件OrderView.vue,使用 Element Plus 的 Table 组件、Pagination 组件、Form 组件、Input 组件、Select 组件搭建页面结构,核心代码示例如下:
| <template> <div class="order-container"> <el-card class="order-card"> <template #header> <div class="order-header"> <span class="order-title">订单管理</span> </div> </template> <!-- 查询表单 --> <el-form :model="queryParams" inline class="search-form"> <el-form-item label="订单编号" prop="orderNo"> <el-input v-model="queryParams.orderNo" placeholder="请输入订单编号" clearable /> </el-form-item> <el-form-item label="订单状态" prop="status"> <el-select v-model="queryParams.status" placeholder="请选择订单状态" clearable> <el-option label="待付款" value="CREATED" /> <el-option label="待发货" value="PAID" /> <el-option label="已发货" value="SHIPPED" /> <el-option label="已完成" value="COMPLETED" /> <el-option label="已取消" value="CANCELLED" /> </el-select> </el-form-item> <el-form-item label="创建时间" prop="createTime"> <el-date-picker v-model="queryParams.createTime" type="daterange" range-separator="至" start-placeholder="开始日期" end-placeholder="结束日期" format="YYYY-MM-DD" value-format="YYYY-MM-DD" /> </el-form-item> <el-form-item> <el-button type="primary" @click="handleSearch">查询</el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> <!-- 订单列表 --> <el-table :data="orderList" v-loading="loading" stripe style="width: 100%"> <el-table-column label="订单编号" prop="orderNo" min-width="180" /> <el-table-column label="总金额" prop="totalAmount" min-width="100" /> <el-table-column label="订单状态" prop="status" min-width="100"> <template #default="scope"> <el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag> </template> </el-table-column> <el-table-column label="收货地址" prop="deliveryAddress" min-width="200" /> <el-table-column label="创建时间" prop="createTime" min-width="150" /> <el-table-column label="操作" min-width="200" fixed="right"> <template #default="scope"> <el-button type="primary" size="small" @click="goToLogistics(scope.row.id)" v-if="scope.row.status !== 'CANCELLED'"> 查看物流 </el-button> <el-button type="danger" size="small" @click="handleCancel(scope.row.id)" v-if="scope.row.status === 'PAID'"> 取消订单 </el-button> </template> </el-table-column> </el-table> <!-- 分页组件 --> <div class="pagination-container"> <el-pagination v-model:current-page="queryParams.pageNum" v-model:page-size="queryParams.pageSize" :total="total" :page-sizes="[10, 20, 50]" layout="total, sizes, prev, pager, next, jumper" @size-change="handleSearch" @current-change="handleSearch" /> </div> </el-card> </div> </template> |
5.2.2 交互逻辑
在OrderView.vue的script setup标签中,实现查询订单、取消订单、跳转物流详情的交互逻辑,核心代码示例如下:
| <script setup> import { ref, onMounted } from 'vue' import { useRouter } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { getOrderList, cancelOrder } from '@/api/order' const router = useRouter() const loading = ref(false) const orderList = ref([]) const total = ref(0) // 查询参数 const queryParams = ref({ pageNum: 1, pageSize: 10, orderNo: '', status: '', createTime: [] }) // 订单状态映射 const getStatusType = (status) => { const statusMap = { CREATED: 'info', PAID: 'warning', SHIPPED: 'primary', COMPLETED: 'success', CANCELLED: 'danger' } return statusMap[status] || 'info' } const getStatusText = (status) => { const statusMap = { CREATED: '待付款', PAID: '待发货', SHIPPED: '已发货', COMPLETED: '已完成', CANCELLED: '已取消' } return statusMap[status] || '未知状态' } // 查询订单列表 const handleSearch = async () => { loading.value = true try { const params = { ...queryParams.value } if (params.createTime && params.createTime.length) { params.beginTime = params.createTime[0] params.endTime = params.createTime[1] } const res = await getOrderList(params) orderList.value = res.data.records || [] total.value = res.data.total || 0 } catch (err) { ElMessage.error(err.message || '查询订单列表失败') } finally { loading.value = false } } // 重置查询 const handleReset = () => { queryParams.value = { pageNum: 1, pageSize: 10, orderNo: '', status: '', createTime: [] } handleSearch() } // 取消订单 const handleCancel = async (orderId) => { try { await ElMessageBox.confirm('确定要取消该订单吗?', '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' }) await cancelOrder(orderId) ElMessage.success('订单取消成功') handleSearch() } catch (err) { if (err !== 'cancel') { ElMessage.error(err.message || '订单取消失败') } } } // 跳转到物流跟踪页面 const goToLogistics = (orderId) => { router.push(`/logistics/${orderId}`) } // 页面加载完成后查询订单列表 onMounted(() => { handleSearch() }) </script> |
5.3 物流跟踪页开发
物流跟踪页是用户查看物流轨迹、同步物流状态的核心页面,具备查询物流轨迹、更新物流状态两个核心功能,采用 Vue 3 的 Composition API、Element Plus 组件库、高德地图组件实现交互逻辑。
5.3.1 页面结构
在src/views目录下创建物流跟踪页面文件LogisticsView.vue,使用 Element Plus 的 Card 组件、Timeline 组件、Button 组件搭建页面结构,核心代码示例如下:
| <template> <div class="logistics-container"> <el-card class="logistics-card"> <template #header> <div class="logistics-header"> <span class="logistics-title">物流跟踪详情</span> <el-button type="primary" size="small" @click="handleUpdateStatus" v-if="logisticsInfo.status !== 'SIGNED'"> 更新物流状态 </el-button> </div> </template> <!-- 物流基础信息 --> <div class="logistics-info" v-loading="loading"> <el-descriptions :column="2" border> <el-descriptions-item label="订单编号">{{ logisticsInfo.orderNo }}</el-descriptions-item> <el-descriptions-item label="物流单号">{{ logisticsInfo.trackingNo }}</el-descriptions-item> <el-descriptions-item label="承运商">{{ logisticsInfo.carrier }}</el-descriptions-item> <el-descriptions-item label="当前状态"> <el-tag :type="getStatusType(logisticsInfo.status)">{{ getStatusText(logisticsInfo.status) }}</el-tag> </el-descriptions-item> <el-descriptions-item label="收货地址" :span="2">{{ logisticsInfo.deliveryAddress }}</el-descriptions-item> <el-descriptions-item label="预计送达时间">{{ logisticsInfo.estimatedDeliveryTime || '未设置' }}</el-descriptions-item> </el-descriptions> </div> <!-- 物流轨迹 --> <div class="logistics-timeline"> <h3 class="timeline-title">物流轨迹</h3> <el-timeline v-if="logisticsInfo.traceList && logisticsInfo.traceList.length"> <el-timeline-item v-for="(trace, index) in logisticsInfo.traceList" :key="index" :timestamp="trace.traceTime" placement="top" :type="index === 0 ? 'primary' : 'info'" > <div class="trace-content"> <p class="trace-status">{{ trace.status }}</p> <p class="trace-description">{{ trace.description }}</p> <p class="trace-location">{{ trace.location }}</p> </div> </el-timeline-item> </el-timeline> <el-empty v-else description="暂无物流轨迹记录" /> </div> <!-- 运输地图轨迹 --> <div class="logistics-map" v-if="logisticsInfo.traceList && logisticsInfo.traceList.length"> <h3 class="map-title">运输轨迹</h3> <div id="map-container" style="width: 100%; height: 400px;"></div> </div> </el-card> </div> </template> |
5.3.2 交互逻辑
在LogisticsView.vue的script setup标签中,实现查询物流轨迹、更新物流状态的交互逻辑,核心代码示例如下:
| <script setup> import { ref, onMounted, nextTick } from 'vue' import { useRoute } from 'vue-router' import { ElMessage, ElMessageBox } from 'element-plus' import { getLogisticsByOrderId, updateLogisticsStatus } from '@/api/logistics' import { loadMap } from '@/utils/map' const route = useRoute() const loading = ref(false) const logisticsInfo = ref({ traceList: [] }) // 订单ID const orderId = route.params.orderId // 物流状态映射 const getStatusType = (status) => { const statusMap = { WAITING_PICKUP: 'info', PICKED_UP: 'primary', IN_TRANSIT: 'warning', DELIVERED: 'success', SIGNED: 'success' } return statusMap[status] || 'info' } const getStatusText = (status) => { const statusMap = { WAITING_PICKUP: '待揽件', PICKED_UP: '已揽件', IN_TRANSIT: '运输中', DELIVERED: '已送达', SIGNED: '已签收' } return statusMap[status] || '未知状态' } // 查询物流轨迹 const loadLogisticsInfo = async () => { loading.value = true try { const res = await getLogisticsByOrderId(orderId) logisticsInfo.value = res.data || {} // 加载地图轨迹 if (logisticsInfo.value.traceList && logisticsInfo.value.traceList.length) { await nextTick() loadMap('map-container', logisticsInfo.value.traceList) } } catch (err) { ElMessage.error(err.message || '查询物流轨迹失败') } finally { loading.value = false } } // 更新物流状态 const handleUpdateStatus = async () => { try { await ElMessageBox.prompt('请输入更新后的物流状态', '更新物流状态', { confirmButtonText: '确定', cancelButtonText: '取消', inputType: 'select', selectOptions: [ { label: '已揽件', value: 'PICKED_UP' }, { label: '运输中', value: 'IN_TRANSIT' }, { label: '已送达', value: 'DELIVERED' }, { label: '已签收', value: 'SIGNED' } ] }) // 实际场景中需传入完整的轨迹数据 const res = await updateLogisticsStatus({ orderId: orderId, status: 'IN_TRANSIT', description: '货物运输中,离开广州市白云区仓库', location: '广东省广州市白云区' }) if (res.code === 200) { ElMessage.success('物流状态更新成功') await loadLogisticsInfo() } else { ElMessage.error(res.message || '物流状态更新失败') } } catch (err) { if (err !== 'cancel') { ElMessage.error(err.message || '物流状态更新失败') } } } // 页面加载完成后查询物流轨迹 onMounted(() => { loadLogisticsInfo() }) </script> |
5.4 前后端联调验证
完成前端业务页面开发后,需重启前后端服务,验证完整业务交互链路,核心验证流程为:
浏览器访问http://localhost:8080/order,进入订单列表页,检查是否能正常加载订单列表、正常分页;
选择一个待发货的订单,点击 “查看物流” 按钮,跳转至物流跟踪页,检查物流基础信息、轨迹节点是否正常展示;
在物流跟踪页点击 “更新物流状态” 按钮,选择新的物流状态并提交,检查物流轨迹列表是否新增一条新节点,同时检查后端的订单表、库存表数据是否同步更新;
重复执行上述步骤,验证多场景下的状态同步是否正常,确保整个业务流程的所有环节都能正常联调。
6. 测试用例编写
为了保证项目的核心业务逻辑质量,本案例基于 JUnit 5、Mockito、Spring Boot Test 的技术组合,从保证业务正确性的角度出发,编写了核心业务层、接口层的自动化测试用例。
6.1 测试环境配置
在后端工程的src/test/resources目录下创建测试环境配置文件application-test.yml,配置测试用的数据库、Redis 资源,避免测试环境影响开发 / 生产环境数据,示例配置如下:
| # 测试环境配置 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_demo_test?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 redis: host: localhost port: 6379 password: database: 1 mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true |
6.2 Service 层单元测试
Service 层是核心业务逻辑的承载层,单元测试需覆盖所有核心业务场景,采用 “分层测试 + 边界条件验证” 的方案,测试方法的业务行为是否符合预期。
6.2.1 库存扣减测试(InventoryService)
测试用例需覆盖库存扣减的核心正常 / 异常场景,包含以下关键测试场景:
| package com.example.orderdemo.service; import com.example.orderdemo.entity.Inventory; import com.example.orderdemo.mapper.InventoryMapper; import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @DisplayName("库存业务层单元测试") class InventoryServiceTest { @Mock private InventoryMapper inventoryMapper; @Mock private RedisTemplate<String, Object> redisTemplate; @Mock private ValueOperations<String, Object> valueOperations; @InjectMocks private InventoryService inventoryService; private Inventory testInventory; @BeforeEach void setUp() { // 初始化测试库存数据 testInventory = new Inventory(); testInventory.setId(1L); testInventory.setProductId(1L); testInventory.setWarehouseId(1L); testInventory.setStockNum(100); testInventory.setLockedStock(0); testInventory.setVersion(1); } @Test @DisplayName("预扣库存成功 - 库存充足") void lockStock_Success_EnoughStock() { // 1. 模拟依赖行为 when(inventoryMapper.selectOne(any(UpdateWrapper.class))).thenReturn(testInventory); when(inventoryMapper.lockStock(anyLong(), anyInt(), anyInt())).thenReturn(1); when(redisTemplate.opsForValue()).thenReturn(valueOperations); when(valueOperations.increment(anyString(), anyInt())).thenReturn(-1); // 2. 执行测试方法 boolean result = inventoryService.lockStock(1L, 10); // 3. 断言结果 assertTrue(result); } @Test @DisplayName("预扣库存失败 - 库存不足") void lockStock_Failure_InsufficientStock() { // 1. 模拟依赖行为 testInventory.setStockNum(5); when(inventoryMapper.selectOne(any(UpdateWrapper.class))).thenReturn(testInventory); // 2. 执行测试方法,抛出异常 assertThrows(RuntimeException.class, () -> inventoryService.lockStock(1L, 10)); } @Test @DisplayName("确认扣减库存成功 - 锁定库存充足") void confirmDeductStock_Success_EnoughLockedStock() { // 1. 模拟依赖行为 testInventory.setLockedStock(10); when(inventoryMapper.selectOne(any(UpdateWrapper.class))).thenReturn(testInventory); when(inventoryMapper.confirmDeductStock(anyLong(), anyInt())).thenReturn(1); // 2. 执行测试方法 boolean result = inventoryService.confirmDeductStock(1L, 10); // 3. 断言结果 assertTrue(result); } @Test @DisplayName("回滚库存成功 - 恢复预扣库存") void rollbackStock_Success() { // 1. 模拟依赖行为 testInventory.setLockedStock(10); when(inventoryMapper.selectOne(any(UpdateWrapper.class))).thenReturn(testInventory); when(inventoryMapper.update(any(Inventory.class), any(UpdateWrapper.class))).thenReturn(1); // 2. 执行测试方法 boolean result = inventoryService.rollbackStock(1L, 10); // 3. 断言结果 assertTrue(result); } } |
6.2.2 订单流程测试(OrderService)
测试OrderService的核心业务逻辑,采用@MockBean注解注入模拟的InventoryService依赖,避免测试过程中实际调用库存资源,包含以下关键测试场景:
创建订单成功(库存充足,预扣库存成功);
创建订单失败(库存不足,预扣库存失败);
取消订单成功(订单状态为待发货,回滚库存成功);
取消订单失败(订单状态已为已发货,无法回滚库存)。
6.2.3 物流状态流转测试(LogisticsService)
测试LogisticsService的核心业务逻辑,验证物流状态变更、订单状态同步、库存扣减协同的正确性,包含以下关键测试场景:
生成物流单成功(订单状态更新为已发货,库存确认扣减成功);
更新物流状态成功(物流轨迹新增节点,订单状态同步为已完成);
更新物流状态为异常,触发库存回滚、订单状态更新为已取消。
6.3 Controller 层集成测试
Controller 层是接口的入口,需测试 HTTP 请求 / 响应的完整性、权限校验的正确性,采用@SpringBootTest、AutoConfigureMockMvc注解实现接口集成测试,模拟前端请求,验证接口的响应数据、状态码是否符合预期。
6.3.1 订单接口测试(OrderController)
测试订单相关的 RESTful 接口,包含以下关键测试场景:
| package com.example.orderdemo.controller; import com.example.orderdemo.dto.order.OrderCreateDTO; import com.example.orderdemo.service.OrderService; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @SpringBootTest @AutoConfigureMockMvc @DisplayName("订单接口集成测试") class OrderControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; @MockBean private OrderService orderService; @Test @DisplayName("创建订单接口 - 成功响应") void createOrder_Success() throws Exception { // 1. 模拟请求参数 OrderCreateDTO dto = new OrderCreateDTO(); dto.setUserId(1L); dto.setProductId(1L); dto.setQuantity(2); dto.setDeliveryAddress("北京市朝阳区"); // 2. 模拟Service层响应 when(orderService.create(any(OrderCreateDTO.class))).thenReturn("ORDER_NO_123456"); // 3. 执行接口请求,验证响应 mockMvc.perform(MockMvcRequestBuilders.post("/order/create") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(dto))) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").value("ORDER_NO_123456")); } @Test @DisplayName("查询订单列表接口 - 成功响应") void listOrder_Success() throws Exception { // 1. 执行接口请求 mockMvc.perform(MockMvcRequestBuilders.get("/order/list") .param("pageNum", "1") .param("pageSize", "10") .param("status", "PAID")) // 2. 验证响应 .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test @DisplayName("取消订单接口 - 成功响应") void cancelOrder_Success() throws Exception { // 1. 模拟Service层响应 when(orderService.cancel(1L)).thenReturn(true); // 2. 执行接口请求 mockMvc.perform(MockMvcRequestBuilders.post("/order/cancel/1")) // 3. 验证响应 .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").value(true)); } } |
6.3.2 物流接口测试(LogisticsController)
测试物流相关的 RESTful 接口,包含以下关键测试场景:
生成物流单接口,验证响应数据、状态码;
更新物流状态接口,验证响应数据、状态码;
查询物流轨迹接口,验证响应数据、状态码;
验证物流接口的参数校验是否生效,传入缺失必填参数的 JSON 请求体,验证接口返回的业务异常。
6.3.3 库存接口测试(InventoryController)
测试库存相关的内部 RESTful 接口,覆盖预扣库存、确认扣减库存、回滚库存的正常 / 异常场景。
6.4 测试执行与报告生成
本案例采用 Maven 的test命令执行所有测试用例,采用mvn surefire-report:report命令生成测试覆盖率报告,报告中需包含以下关键验证点:
所有测试用例的执行成功率;
核心业务逻辑的代码覆盖率,要求不低于 80%;
接口请求 / 响应的日志、异常栈信息;
物流轨迹、订单状态变更的数据库记录验证结果。
执行测试用例后,需确认所有核心业务场景的测试结果均符合预期,保证项目业务逻辑的正确性与稳定性。
7. 总结与扩展优化
本案例是一个功能完整、适配中小型业务场景的订单库存物流全栈项目,基于 Spring Boot 3、Vue 3、MySQL、Redis 等主流技术栈,覆盖了从需求分析、设计文档、工程搭建、前后端开发、测试验证的完整技术实现路径。
7.1 项目总结
本项目的技术实现要点与业务设计细节,总结为以下三个核心部分:
技术架构:采用成熟的前后端分离架构,后端采用 Spring Boot 3 + MyBatis-Plus + Redis 技术栈,利用虚拟线程、预扣库存、乐观锁等技术,解决了高并发场景下的超卖、库存一致性问题;前端采用 Vue 3 + Element Plus + Pinia 技术栈,基于组合式 API 实现了可复用的业务交互逻辑,同时通过 Axios 封装实现了请求统一处理。
业务设计:核心业务遵循 “订单预扣→库存锁定→物流确认扣减” 的标准流程,将库存状态与订单、物流状态强绑定,通过事务、分布式锁、乐观锁等多种技术手段,保证了数据在正常 / 异常场景下的一致性;物流模块通过状态机设计,保证了订单状态的流转可控,避免了非法状态变更。
工程落地:从零基础搭建前后端分离工程,提供了从建表、编码、联调到测试验证的完整落地流程;项目结构清晰,三层架构、业务模块的拆分符合业界主流规范,所有核心业务代码均添加了中文注释,便于理解和二次开发。
7.2 扩展优化方向
本案例作为基础 Demo 项目,仅包含了最核心的基础业务,若要将其升级为生产级业务系统,还需从以下四个方向进行扩展优化:
7.2.1 高并发场景优化
当前方案采用的是 “Redis 预扣 + 数据库乐观锁” 的基础扣减逻辑,可进一步优化为 “Redis 预扣 + 消息队列异步扣减 + 数据库最终校验” 的分层架构,将数据库的同步操作修改为异步操作,隔离前端高并发流量与后端数据库写流量,进一步支撑更大的并发量;同时,增加 Redis 集群部署、本地缓存、多级缓存策略,缓解 Redis 的压力。
7.2.2 物流轨迹实时更新优化
当前物流轨迹更新逻辑完全由人工手动录入,可扩展对接第三方物流商提供的电子面单 API、物流轨迹订阅接口(如快递鸟、快递 100),实现物流单号自动申请、运输轨迹实时回调的全流程自动化;在物流轨迹更新后,可通过 WebSocket 或 Server-Sent Events(SSE)技术,将物流状态变更实时推送至前端。
7.2.3 状态流转优化
当前的物流状态流转逻辑是在 Service 层通过硬代码流程控制实现的,可引入专业的轻量级状态机框架(如 Spring Statemachine、cola-statemachine),将状态流转逻辑从业务代码中拆分出来,实现配置化流转;同时,增加状态变更的拦截器、监听器,对所有状态变更操作进行日志记录、异常回调处理,增强业务的可扩展性。
7.2.4 基础设施优化
当前项目仅包含基础业务逻辑,缺少生产级必要的落地能力:
增加流量网关(如 Spring Cloud Gateway)、服务注册发现(如 Nacos)、配置中心等基础能力,将单体架构扩展为微服务架构;
对核心业务接口进行流控、熔断、降级配置,增加统一接口返回体、全局异常处理的增强逻辑;
增加完整的日志链路追踪能力,将业务操作日志、库存扣减记录、物流轨迹记录与用户操作 ID 关联,便于线上问题排查;
补充 Dockerfile、Kubernetes 部署文件、Jenkins 流水线配置,实现容器化部署、CI/CD 自动化构建;
增加接口安全校验、基础频率限制、用户操作审计、数据库备份等生产级能力。
7.2.5 业务场景优化
当前业务场景仅覆盖下单、扣减库存、发货的正向流程,可补充完整的逆向流程,比如用户取消订单、申请退货退款、物流拒收等场景,增加库存回流、逆向物流状态同步等业务逻辑;同时,完善订单与物流的消息通知机制,对接短信、邮件、站内信等通知渠道,在订单状态、物流状态变更时主动通知用户。
通过本案例的学习与实践,开发者可以掌握 Spring Boot 3 与 Vue 3 技术栈的全栈开发能力,理解电商场景下核心业务的设计思路与技术实现方案,具备支撑中小型业务系统的技术架构能力。
975

被折叠的 条评论
为什么被折叠?



