SpringBoot3 + Vue 订单库存物流项目完整案例

目录

1. 项目概述

1.1 业务需求分析

1.2 核心业务流程

1.3 技术选型

1.3.1 后端技术栈

1.3.2 前端技术栈

1.3.3 开发环境与工具

2. 数据库设计

2.1 核心 ER 实体关系图

2.2 数据库表设计

2.2.1 商品表(products)

2.2.2 库存表(inventory)

2.2.3 订单表(orders)

2.2.4 订单明细表(order_detail)

2.2.5 物流订单表(logistics_order)

2.2.6 物流轨迹表(logistics_trace)

2.3 核心业务关联关系

2.4 初始化 SQL 脚本

3. 开发环境搭建

3.1 后端 Spring Boot 工程搭建

3.1.1 工程初始化

3.1.2 配置文件调整

3.1.3 引入必要依赖

3.1.4 工程包结构规划

3.2 前端 Vue 工程搭建

3.2.1 工程初始化

3.2.2 项目结构规划

3.2.3 跨域配置

3.3 前后端联调验证

4. 后端核心业务开发

4.1 公共层基础开发

4.1.1 统一返回结果封装

4.1.2 全局异常处理

4.1.3 工具类开发

4.2 商品模块开发

4.2.1 实体类(Entity)

4.2.2 Mapper 层

4.2.3 Service 层

4.2.4 Controller 层

4.3 库存模块开发

4.3.1 实体类(Entity)

4.3.2 Mapper 层

4.3.3 Service 层

4.3.4 Controller 层

4.4 订单模块开发

4.4.1 实体类(Entity)

4.4.2 Mapper 层

4.4.3 Service 层

4.4.4 Controller 层

4.5 物流模块开发

4.5.1 实体类(Entity)

4.5.2 Mapper 层

4.5.3 Service 层

4.5.4 Controller 层

5. 前端核心业务开发

5.1 公共层封装

5.1.1 后端 API 接口统一封装

5.1.2 路由配置

5.2 订单列表页开发

5.2.1 页面结构

5.2.2 交互逻辑

5.3 物流跟踪页开发

5.3.1 页面结构

5.3.2 交互逻辑

5.4 前后端联调验证

6. 测试用例编写

6.1 测试环境配置

6.2  Service 层单元测试

6.2.1 库存扣减测试(InventoryService)

6.2.2 订单流程测试(OrderService)

6.2.3 物流状态流转测试(LogisticsService)

6.3 Controller 层集成测试

6.3.1 订单接口测试(OrderController)

6.3.2 物流接口测试(LogisticsController)

6.3.3 库存接口测试(InventoryController)

6.4 测试执行与报告生成

7. 总结与扩展优化

7.1 项目总结

7.2 扩展优化方向

7.2.1 高并发场景优化

7.2.2 物流轨迹实时更新优化

7.2.3 状态流转优化

7.2.4 基础设施优化

7.2.5 业务场景优化


从设计文档、工程搭建到业务实现、测试验证的全链路开发指南

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_namespu_valuespu_price字段的业务定义与存储约束,和此前的电商类项目设计保持一致,保证了业务逻辑的连贯性;同时,表中针对spu_namestate字段创建了联合索引,优化商品列表页的查询、过滤性能(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_idwarehouse_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_idproduct_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)、NameDescription,包名、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,分别与ordersorder_detail表结构一一对应。

4.4.2 Mapper 层

mapper包下创建订单数据访问层接口OrderMapper、订单明细数据访问层接口OrderDetailMapper,均需继承 MyBatis-Plus 的BaseMapper接口,OrderMapper提供基于用户 ID、订单状态、创建时间的分页查询方法,OrderDetailMapper提供基于订单 ID 查询订单明细的方法。

4.4.3 Service 层

service包下创建订单业务层接口OrderService,定义创建订单、查询订单列表、查询订单详情、取消订单四个核心方法;在service/impl包下创建接口实现类OrderServiceImpl,注入OrderMapperOrderDetailMapperInventoryService,实现订单业务逻辑。

核心业务逻辑说明:

创建订单:该方法必须添加@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_orderlogistics_trace表结构一一对应。

4.5.2 Mapper 层

mapper包下创建物流订单数据访问层接口LogisticsOrderMapper、物流轨迹数据访问层接口LogisticsTraceMapper,均需继承 MyBatis-Plus 的BaseMapper接口,自定义更新物流单状态、根据物流单 ID 查询轨迹、根据订单 ID 查询物流单的方法。

4.5.3 Service 层

service包下创建物流业务层接口LogisticsService,定义生成物流单、更新物流状态、查询物流轨迹三个核心方法;在service/impl包下创建接口实现类LogisticsServiceImpl,注入LogisticsOrderMapperLogisticsTraceMapperOrderServiceInventoryService,实现物流业务逻辑。

核心业务逻辑说明:

生成物流单:该方法必须添加@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.jsinventory.jslogistics.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.vuescript 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.vuescript 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 请求 / 响应的完整性、权限校验的正确性,采用@SpringBootTestAutoConfigureMockMvc注解实现接口集成测试,模拟前端请求,验证接口的响应数据、状态码是否符合预期。

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 技术栈的全栈开发能力,理解电商场景下核心业务的设计思路与技术实现方案,具备支撑中小型业务系统的技术架构能力。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值