尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

Spring Boot实现外卖配送车辆与骑手调度管理系统

Spring Boot实现外卖配送车辆与骑手调度管理系统 外卖配送发展到今天早已不只是“商家出餐、骑手取餐、用户收餐”这么简单。很多配送团队会批量采购或租赁车辆给骑手使用甚至出现“免租金”“开学季选车”“冲单王奖励”等运营玩法。但这里有一个经常被忽略的问题当车辆、骑手、订单混在一起如果没有一套状态清晰的管理系统很容易出现车辆被重复分配、骑手接了单却没有可用车、订单完成后车辆没有及时释放等情况。本文就从配送业务中的“车、人、单”三个核心对象出发完整实现一套外卖配送车辆与骑手调度管理系统。内容包括数据库设计、Spring Boot 后端接口、车辆分配逻辑、骑手单量统计、GPS 位置上报以及生产环境中常见的坑和排查思路。无论你是刚开始学习 Spring Boot 的读者还是已经在做配送类业务后端开发都能从中看到一套比较完整的最小实现。1. 配送管理系统到底要解决什么问题先理解一个业务模型一个配送站点通常有若干车辆车辆不一定属于骑手个人可能是站点统一调度。骑手从早高峰开始接单每接一单需要占用一辆车订单完成后车辆释放再继续接下一单。这背后的业务规则看起来简单但落实到系统层至少涉及四个问题车辆状态不可见哪辆车在用、哪辆车空闲、哪辆车在维修如果靠人记基本都会乱。骑手与车辆绑定关系不清晰一个骑手可能骑过好几辆车车辆出问题后无法追踪历史使用人。订单状态流转不严谨订单从待接单、配送中、已完成到取消任何一个环节漏更新都会导致统计出错。位置数据无法追溯用户投诉“餐没送到”时如果拿不出骑手轨迹就很难界定责任。所以一个真正能落地的配送管理系统核心不是页面多好看而是把车辆状态、骑手状态、订单状态和位置轨迹之间的流转关系设计清楚。本文的演示项目不会做复杂的微服务或高并发架构而是把重点放在 Spring Boot 单机应用上先把核心状态流转跑通再扩展成生产级方案。2. 核心概念与系统边界2.1 车辆生命周期车辆不是只有“空闲”和“占用”两种状态实际业务里还应该有维修、报废等状态。状态值含义说明0空闲车辆可被分配1已占用车辆已被骑手使用2维修中车辆不可分配3已报废车辆彻底退出运营这里最容易踩的坑是查询可用车辆时只查了“不是维修状态”结果把报废车辆也算进去了。更稳妥的做法是维护一个显式的状态字段并限制状态流转顺序例如“维修中”的车辆不能直接变成“已占用”必须先恢复为“空闲”。2.2 骑手状态与订单状态骑手状态比较简单可以定义为状态值含义0休息1接单中2离线订单状态是配送系统里最需要细心设计的字段建议至少包含状态值含义0待分配1配送中2已完成3已取消注意订单一旦从“待分配”变成“配送中”就需要同时绑定骑手和车辆。如果先绑定骑手、再绑定车辆中间任何一步失败都会产生脏数据。所以这个操作必须放在同一个事务里。2.3 调度基本流程先来看一次完整调度流程涉及的步骤这是后面写代码的提纲用户下单生成待分配订单。系统挑选一个空闲骑手。系统挑选一辆空闲车辆。将骑手和车辆绑定到订单上。订单状态切换为“配送中”。骑手上报位置写入轨迹表。骑手送达订单状态切换为“已完成”。车辆释放为空闲骑手变为可接单状态。这个流程里最关键的步骤是 2、3、4因为存在并发风险两个调度任务可能同时选到同一辆空闲车或同一个空闲骑手。3. 环境准备与项目结构3.1 开发环境版本本文示例采用以下技术组合版本可根据你的实际项目调整JDK 8 或 JDK 17Spring Boot 2.7.xMyBatis-Plus 3.5.xMySQL 8.0Maven 3.6IDEIDEA 或 Eclipse如果你使用的是 Spring Boot 3.x需要把 JDK 升级到 17并注意 MySQL 驱动依赖坐标会变化例如mysql:mysql-connector-java在 Spring Boot 3 中改为com.mysql:mysql-connector-j。3.2 Maven 依赖在pom.xml中引入以下依赖dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesSpring Boot 父级依赖可以通过spring-boot-starter-parent统一管理具体版本号建议参考当前 Spring Boot 对应版本避免强制指定子依赖版本导致的兼容问题。3.3 项目目录结构src/main/java/com/example/dispatch ├── DispatchApplication.java ├── config ├── controller │ ├── DispatchController.java │ ├── LocationController.java │ └── VehicleController.java ├── service │ └── DispatchService.java ├── mapper │ ├── VehicleMapper.java │ ├── RiderMapper.java │ ├── DispatchOrderMapper.java │ └── LocationRecordMapper.java ├── entity │ ├── Vehicle.java │ ├── Rider.java │ ├── DispatchOrder.java │ └── LocationRecord.java └── common ├── Result.java └── StatusConstant.java这里推荐按 Controller、Service、Mapper、Entity 分层。尽管 MyBatis-Plus 能省掉很多手写 SQL但分层仍然能保证后续引入 Redis、MQ 时改动范围最小。4. 数据库设计与初始化4.1 表结构设计先看四张核心表的字段设计CREATE TABLE vehicle ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, vehicle_no VARCHAR(32) NOT NULL COMMENT 车辆编号, vehicle_name VARCHAR(64) DEFAULT COMMENT 车辆名称, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0空闲 1已占用 2维修中 3已报废, current_rider_id BIGINT DEFAULT NULL COMMENT 当前骑手ID, rent_free_flag TINYINT NOT NULL DEFAULT 0 COMMENT 是否免租金 0否 1是, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_status (status), KEY idx_current_rider (current_rider_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT车辆表;CREATE TABLE rider ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, rider_name VARCHAR(64) NOT NULL COMMENT 骑手姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0休息 1接单中 2离线, total_order_count INT NOT NULL DEFAULT 0 COMMENT 累计完成单量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT骑手表;CREATE TABLE dispatch_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, order_no VARCHAR(64) NOT NULL COMMENT 业务订单号, rider_id BIGINT DEFAULT NULL COMMENT 骑手ID, vehicle_id BIGINT DEFAULT NULL COMMENT 车辆ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态 0待分配 1配送中 2已完成 3已取消, pickup_address VARCHAR(255) DEFAULT COMMENT 取餐地址, delivery_address VARCHAR(255) DEFAULT COMMENT 配送地址, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, KEY idx_rider_id (rider_id), KEY idx_vehicle_id (vehicle_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT配送订单表;CREATE TABLE location_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, rider_id BIGINT NOT NULL COMMENT 骑手ID, vehicle_id BIGINT NOT NULL COMMENT 车辆ID, lng DECIMAL(10, 6) NOT NULL COMMENT 经度, lat DECIMAL(10, 6) NOT NULL COMMENT 纬度, report_time DATETIME NOT NULL COMMENT 上报时间, KEY idx_rider_time (rider_id, report_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT位置轨迹表;这里补充两个设计时的考虑rent_free_flag字段对应“外卖车免租金”这类运营活动。实际项目中这类运营属性可能变化不建议直接把运营规则写死在主业务表里但作为演示字段足够表达“车辆是否有活动属性”的含义。location_record属于高频写入表后续一定要做分区或归档否则单表数据量会涨得很快。4.2 为什么要单独建订单表车辆和骑手是多对多关系一位骑手会在不同时间段使用不同车辆如果不建关联表直接在车辆表里记录骑手 ID 会丢失历史轨迹。所以这里通过dispatch_order把骑手、车辆、订单关联起来vehicle.current_rider_id只保留“当前使用人”作为快速查询的冗余字段历史关联关系全部以订单表为准。这种设计在业务上是更合理的。5. 后端核心代码实现5.1 启动类与基础配置主启动类package com.example.dispatch; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; SpringBootApplication MapperScan(com.example.dispatch.mapper) public class DispatchApplication { public static void main(String[] args) { SpringApplication.run(DispatchApplication.class, args); } }application.yml配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/delivery_vehicle_demo?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto注意map-underscore-to-camel-case必须开启否则数据库字段vehicle_no无法自动映射到实体属性vehicleNo。5.2 状态常量类把状态值用常量类管理避免业务代码里到处出现魔法数字package com.example.dispatch.common; public final class StatusConstant { private StatusConstant() { } public static final int VEHICLE_IDLE 0; public static final int VEHICLE_OCCUPIED 1; public static final int VEHICLE_REPAIR 2; public static final int VEHICLE_RETIRED 3; public static final int RIDER_REST 0; public static final int RIDER_BUSY 1; public static final int RIDER_OFFLINE 2; public static final int ORDER_PENDING 0; public static final int ORDER_DELIVERING 1; public static final int ORDER_FINISHED 2; public static final int ORDER_CANCELED 3; }5.3 实体类这里只展示车辆实体其他地方代码结构相同。为了减少样板代码我使用 Lombok 的Data。package com.example.dispatch.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.time.LocalDateTime; Data TableName(vehicle) public class Vehicle { TableId(type IdType.AUTO) private Long id; private String vehicleNo; private String vehicleName; private Integer status; private Long currentRiderId; private Integer rentFreeFlag; private LocalDateTime createTime; private LocalDateTime updateTime; }Mapper 接口更简单继承BaseMapper即可package com.example.dispatch.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.dispatch.entity.Vehicle; public interface VehicleMapper extends BaseMapperVehicle { }5.4 统一返回结构定义一套统一的接口返回格式方便前端处理package com.example.dispatch.common; import lombok.Data; Data public class ResultT { private int code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 0; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 1; result.message message; return result; } }5.5 车辆分配调度核心逻辑这是本文最核心的代码直接体现“车辆 骑手 订单”的状态流转。package com.example.dispatch.service; import com.example.dispatch.common.Result; import com.example.dispatch.common.StatusConstant; import com.example.dispatch.entity.DispatchOrder; import com.example.dispatch.entity.Rider; import com.example.dispatch.entity.Vehicle; import com.example.dispatch.mapper.DispatchOrderMapper; import com.example.dispatch.mapper.RiderMapper; import com.example.dispatch.mapper.VehicleMapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import javax.annotation.Resource; import java.time.LocalDateTime; Service public class DispatchService { Resource private VehicleMapper vehicleMapper; Resource private RiderMapper riderMapper; Resource private DispatchOrderMapper dispatchOrderMapper; Transactional(rollbackFor Exception.class) public Long assignOrder(String orderNo, Long riderId, Long vehicleId) { Rider rider riderMapper.selectById(riderId); if (rider null) { throw new RuntimeException(骑手不存在); } Vehicle vehicle vehicleMapper.selectById(vehicleId); if (vehicle null) { throw new RuntimeException(车辆不存在); } if (vehicle.getStatus() ! StatusConstant.VEHICLE_IDLE) { throw new RuntimeException(车辆当前不可分配status vehicle.getStatus()); } if (rider.getStatus() ! StatusConstant.RIDER_REST) { throw new RuntimeException(骑手当前不可接单status rider.getStatus()); } vehicle.setStatus(StatusConstant.VEHICLE_OCCUPIED); vehicle.setCurrentRiderId(riderId); vehicle.setUpdateTime(LocalDateTime.now()); vehicleMapper.updateById(vehicle); rider.setStatus(StatusConstant.RIDER_BUSY); rider.setUpdateTime(LocalDateTime.now()); riderMapper.updateById(rider); DispatchOrder order new DispatchOrder(); order.setOrderNo(orderNo); order.setRiderId(riderId); order.setVehicleId(vehicleId); order.setStatus(StatusConstant.ORDER_DELIVERING); order.setCreateTime(LocalDateTime.now()); dispatchOrderMapper.insert(order); return order.getId(); } }这里有几个细节值得展开Transactional(rollbackFor Exception.class)保证车辆状态、骑手状态、订单插入三个操作要么全部成功要么全部回滚避免出现“订单已创建但车辆没有绑定”的脏数据。分配前必须检查车辆状态和骑手状态不能上来就直接更新。当前演示代码是事务内“先查询再更新”在高并发下仍可能出现两个请求同时读到同一辆车空闲的情况后面的并发控制会专门说明。5.6 订单完成与车辆释放订单完成时要同时把车辆释放为空闲并更新骑手状态和累计单量Transactional(rollbackFor Exception.class) public void finishOrder(Long orderId) { DispatchOrder order dispatchOrderMapper.selectById(orderId); if (order null) { throw new RuntimeException(订单不存在); } if (order.getStatus() ! StatusConstant.ORDER_DELIVERING) { throw new RuntimeException(订单当前状态不允许完成status order.getStatus()); } order.setStatus(StatusConstant.ORDER_FINISHED); order.setFinishTime(LocalDateTime.now()); dispatchOrderMapper.updateById(order); Long vehicleId order.getVehicleId(); Vehicle vehicle vehicleMapper.selectById(vehicleId); if (vehicle ! null) { vehicle.setStatus(StatusConstant.VEHICLE_IDLE); vehicle.setCurrentRiderId(null); vehicle.setUpdateTime(LocalDateTime.now()); vehicleMapper.updateById(vehicle); } Rider rider riderMapper.selectById(order.getRiderId()); if (rider ! null) { rider.setStatus(StatusConstant.RIDER_REST); rider.setTotalOrderCount(rider.getTotalOrderCount() 1); riderMapper.updateById(rider); } }5.7 高并发分配优化上面的代码在单机演示环境没问题但在真实配送场景中需要防止两个调度任务同时把同一辆车分配出去。常见做法是查询时使用数据库行锁也就是SELECT FOR UPDATE。在 MyBatis-Plus 中可以通过自定义 SQL 实现Select(SELECT * FROM vehicle WHERE id #{id} AND status 0 FOR UPDATE) Vehicle selectIdleVehicleByIdForUpdate(Param(id) Long id);然后修改分配逻辑先锁住车辆记录再更新状态Vehicle vehicle vehicleMapper.selectIdleVehicleByIdForUpdate(vehicleId); if (vehicle null) { throw new RuntimeException(车辆不可分配); }这样第二个并发请求会阻塞在SELECT FOR UPDATE上直到第一个事务提交锁释放后重新读取此时车辆状态已经是“已占用”第二个请求就会进入“车辆不可分配”的分支。使用行锁时务必将整个操作包在事务里否则锁会在 SQL 执行完立刻释放失去保护意义。5.8 骑手单量统计“做单王”这类运营活动在统计上通常需要按骑手维度统计完成单量。简单演示可以先把所有已完成订单查出来再在内存中聚合但只适合小数据量。public ListRiderRankVo listRiderRank() { ListDispatchOrder finishedOrders dispatchOrderMapper.selectList( new QueryWrapperDispatchOrder() .eq(status, StatusConstant.ORDER_FINISHED) ); MapLong, Long countMap finishedOrders.stream() .collect(Collectors.groupingBy(DispatchOrder::getRiderId, Collectors.counting())); ListRider riders riderMapper.selectList(null); ListRiderRankVo rankList new ArrayList(); for (Rider rider : riders) { RiderRankVo vo new RiderRankVo(); vo.setRiderId(rider.getId()); vo.setRiderName(rider.getRiderName()); vo.setTotalOrderCount(countMap.getOrDefault(rider.getId(), 0L)); rankList.add(vo); } rankList.sort((a, b) - Long.compare(b.getTotalOrderCount(), a.getTotalOrderCount())); return rankList; }实际生产环境建议直接用 SQLGROUP BY统计或者维护一张骑手日结统计表避免每次排行都扫描全部订单明细。5.9 位置上报接口位置数据属于高频写入接口设计要尽量轻量。package com.example.dispatch.controller; import com.example.dispatch.common.Result; import com.example.dispatch.entity.LocationRecord; import com.example.dispatch.mapper.LocationRecordMapper; 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; import javax.annotation.Resource; import java.time.LocalDateTime; RestController RequestMapping(/api/location) public class LocationController { Resource private LocationRecordMapper locationRecordMapper; PostMapping(/report) public ResultVoid report(RequestBody LocationReportRequest request) { LocationRecord record new LocationRecord(); record.setRiderId(request.getRiderId()); record.setVehicleId(request.getVehicleId()); record.setLng(request.getLng()); record.setLat(request.getLat()); record.setReportTime(LocalDateTime.now()); locationRecordMapper.insert(record); return Result.success(null); } }请求体 DTOpackage com.example.dispatch.controller; import lombok.Data; Data public class LocationReportRequest { private Long riderId; private Long vehicleId; private Double lng; private Double lat; }这里只演示单条插入。真实场景下骑手 App 可能每 3 到 5 秒上报一次单条插入会对数据库造成较大压力通常需要收集一批后批量入库或者先写入消息队列再由消费者批量落库。6. 运行与验证6.1 初始化数据启动应用前先准备几条基础数据INSERT INTO vehicle (vehicle_no, vehicle_name, status, rent_free_flag) VALUES (SH-A1001, 外卖专用车-001, 0, 1); INSERT INTO vehicle (vehicle_no, vehicle_name, status, rent_free_flag) VALUES (SH-A1002, 外卖专用车-002, 0, 0); INSERT INTO rider (rider_name, phone, status) VALUES (张三, 13800001111, 0);6.2 启动应用在项目根目录执行mvn spring-boot:run看到类似Tomcat started on port(s): 8080的日志说明启动成功。6.3 调用车辆分配接口使用 curl 测试curl -X POST http://localhost:8080/api/dispatch/assign \ -H Content-Type: application/json \ -d {orderNo:ORD20250601001,riderId:1,vehicleId:1}预期返回{ code: 0, message: success, data: 1 }data对应的是新建订单记录的主键 ID。6.4 调用位置上报接口curl -X POST http://localhost:8080/api/location/report \ -H Content-Type: application/json \ -d {riderId:1,vehicleId:1,lng:121.473701,lat:31.230416}返回code0说明位置写入成功。6.5 查询车辆列表curl http://localhost:8080/api/vehicle/list如果分配成功车辆SH-A1001的status应该为 1且currentRiderId为 1。整个流程可以归纳为创建车辆和骑手 → 分配车辆 → 上报位置 → 订单完成 → 车辆释放。跑通这个闭环核心业务逻辑就基本成型了。7. 常见问题与排查思路问题现象常见原因解决思路车辆被重复分配查询和更新之间缺少并发控制使用SELECT FOR UPDATE或乐观锁确保状态更新原子性订单已完成但车辆仍显示占用完成订单和释放车辆不在同一事务将状态更新放在一个事务里注意事务边界骑手已接单但状态仍为休息只更新了订单没更新骑手状态统一维护订单和骑手状态的流转逻辑不要分散更新位置数据量增长过快每几秒插入一条没有批量处理使用消息队列 批量插入按天归档历史轨迹单量统计不准多实例并发更新total_order_count改为订单表 count 统计或使用 Redis 原子自增接口报错但没有回滚异常没有被 Spring 捕获为 RuntimeException明确Transactional(rollbackFor Exception.class)下面重点讲两个高频问题。7.1 事务不生效的排查顺序很多同学写完Transactional后发现数据没回滚通常原因有几种方法被同类内部调用事务代理没有生效。异常被 try-catch 吞掉事务感知不到异常。数据库表使用了不支持事务的存储引擎比如旧版本 MyISAM。事务没有抛出RuntimeException默认回滚策略不包含普通异常。排查时可以按“先看异常有没有抛出再看调用方式是否通过 Spring Bean 代理最后看数据库引擎”的顺序来。7.2 位置上报压力过大如果骑手规模达到几千人每 5 秒上报一次每秒的写入压力就可能达到千级。单条insert很难扛住。建议方案在应用层把 30 秒内的位置记录聚合成一批调用 MyBatis-Plus 的批量插入方法。使用 Redis Geo 暂存最近轨迹定时同步到数据库。对location_record按时间做分区按月或按周创建分区避免单表数据过大。这些优化不用一开始就做但要在系统架构上预留扩展点。8. 最佳实践与工程建议8.1 用状态机约束状态流转项目里如果直接写order.setStatus(2)时间久了很容易出现非法流转比如“已完成”的订单被改成“取消”。建议把状态流转封装成独立方法只暴露合法动作。public void cancelOrder(Long orderId) { DispatchOrder order dispatchOrderMapper.selectById(orderId); if (order null) { throw new RuntimeException(订单不存在); } if (order.getStatus() ! StatusConstant.ORDER_PENDING) { throw new RuntimeException(仅待分配订单可取消); } order.setStatus(StatusConstant.ORDER_CANCELED); dispatchOrderMapper.updateById(order); }这种方式比直接改状态字段更安全代码可读性也更高。8.2 数据库索引与 SQL 检查四张核心表里需要重点关注的索引有vehicle.status查询空闲车辆时使用。dispatch_order.rider_id统计骑手订单时使用。dispatch_order.status筛选已完成订单时使用。location_record(rider_id, report_time)查询骑手轨迹时使用。订单量上来后你可以用 MySQL 的EXPLAIN检查查询是否走索引。如果发现typeALL说明出现了全表扫描需要考虑索引是否命中。8.3 敏感字段安全处理骑手手机号属于敏感信息数据库存储时建议加密接口返回时脱敏。常见做法入库前使用 AES 或国密算法加密。查询列表时手机号中间四位用*代替。涉及手机号解密操作必须走服务端接口不能下放到前端。还需要注意接口鉴权至少要有登录校验和接口权限控制。比如车辆分配、订单完成这类管理操作只允许管理员角色调用骑手端只能上报位置和查询自己的订单。8.4 日志与操作审计车辆分配、订单完成这类变更操作建议记录操作日志。最简单的做法是每次变更时向operation_log表插入一条记录包含操作人、操作类型、操作前后状态、变更时间。线上排查问题时这些日志的价值往往比业务代码本身还高。8.5 生产环境上线建议上线前在测试环境完整模拟一次“车辆分配 → 位置上报 → 订单完成 → 车辆释放”流程。涉及数据库结构变更时先备份确认 SQL 可回滚。新接口发布时先做压测重点关注调度接口和位置上报接口的 TPS。如果业务量增长很快优先引入消息队列削峰再考虑缓存热点数据。9. 下一步可以怎么学本文这套系统把“车、人、单、位”四个核心对象串了起来属于一个非常典型的后端业务系统雏形。你可以继续在几个方向上做扩展接入 WebSocket把骑手位置实时推送到用户端。使用 Redis 维护空闲车辆列表提高分配效率。引入 XXL-Job 或 Spring 定时任务做每日骑手单量结算。把订单状态流转改造成 Spring StateMachine。用 MyBatis-Plus 的Version乐观锁替换数据库行锁比较两种方案在高并发下的表现。配送类系统的难点不在单点功能而在于状态的一致性、数据的高频写入和业务规则的边界控制。建议先跑通本文的单机版再逐步引入中间件。只要把车辆分配、订单完成、车辆释放这一条核心链路想清楚后续很多扩展都会顺畅很多。
返回列表