简介:一套基于Java开发的派单系统完整项目,包含Android客户端源码与说明文档,面向服务行业订单分配场景(如外卖、家政),适合希望系统学习Java服务端与Android移动端全栈开发的初中级开发者。资源包共11019个文件,大小约65.36MB,其中以Java、JS、CSS、HTML等代码文件为主,辅以大量PNG/SVG图标、XML配置及Git忽略文件,覆盖后端逻辑、前端页面与移动端交互所需的完整素材。项目借鉴类似upWork的工作流管理机制,实现了任务发布、接单与进度跟踪等核心模块,并通过SSL/TLS加密保证前后端通信安全;附带“源码必读.txt”对源码结构、关键组件和使用注意事项作了详细说明,方便快速上手。源码按功能模块组织,后端、管理端与Android端层次清晰,已有311人学习。作为全栈项目实战参考,有助于读者掌握订单派发、用户认证、RESTful API交互及移动端网络通信等关键实现。
1. 派单系统到底是什么:从订单到履约的闭环,以及这套源码的边界
一位做同城维修平台的朋友跟我吐槽:单量上来后,每天在微信群里人工派单,客服从早忙到晚,抢单靠手速,投诉靠运气。他到处找“能直接用”的 java 派单系统,最好连 Android 端一起交付。这套标题指向的源码,通常就是一颗完整的技术树:Spring Boot 写的 Java 后端,负责订单、派单、推送和结算;Android 客户端负责收单、抢单、展示进度;再加上一份项目说明,把建表、配置、接口时序交代清楚。它的价值不在算法多高深,而在于把“用户下单→系统派单→师傅接单→完成履约”这条链路从代码层面完整打通。适合三类人:想快速搭配送、维修、家政类 MVP 的团队,需要一套有业务深度的练手项目的 Java/Android 工程师,以及准备做二次开发又不想从零设计状态机的人。
2. Java 后端核心架构:工程骨架、数据表设计与派单策略抽象
2.1 后端工程结构:业务模块如何划分,才能方便接手的人二次开发
我接手过的派单系统一般不是单文件工程,而是按业务域拆模块。常见的多模块结构长这样:
dispatch-server ├── dispatch-admin # 管理后台接口:订单管理、人员管理、派单操作 ├── dispatch-api # 对 Android 端暴露的 REST 接口 ├── dispatch-core # 核心业务:订单、派单策略、状态机 ├── dispatch-mapper # MyBatis 数据访问层 └── dispatch-common # 通用工具、异常、常量也有团队把 core 和 mapper 合成一个模块,这都行。关键点在于:派单逻辑必须单独占一个模块或至少一个独立的 service 包,不能和订单 CRUD 混在一起。因为派单是一个会被替换的需求点——你今天用自动派单,明天可能想改成抢单加自动兜底,如果派单代码散落在 controller 和 service 里,改一次就要翻遍全项目。我在dispatch-core里会固定放四个包:order、dispatch、push、statistics。订单只管状态流转,派单管策略选择,推送管消息触达,统计管结算和运营报表。Android 端要接的接口全在dispatch-api里,这样前后端联调时只需要盯这一个模块的 REST 路径。
这里还要提一句“项目说明”怎么用:拿到源码后别急着启动,先看项目说明里的“目录结构”和“启动步骤”两节。如果项目说明没有这两节,就说明这份源码的配套文档不够完整,后面启动时大概率要自己踩环境依赖的坑。一份正经的派单系统项目说明,至少应该交代:JDK 版本、MySQL 初始化脚本的位置、Redis 是否需要、Android 端需要修改的服务器地址在哪几个文件。
提示:判断源码是否值得跑通的第一个标准,不是代码多漂亮,而是项目说明里有没有一条明确到“命令级”的启动路径。缺了这一步,后面所有时间都会耗在环境上。
2.2 订单、骑手与派单记录的表设计:状态机才是派单系统的灵魂
派单系统里最容易看走眼的不是表多,而是状态没设计好。核心表其实就三张:订单表、骑手/服务人员表、派单记录表。下面是我整理过的最小字段集合。
表:订单表关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_no | varchar(32) | 订单唯一编号,对 Android 端展示用 |
| service_type | int | 服务类型:1维修 2保洁 3配送 |
| address | varchar(255) | 服务地址 |
| lng / lat | decimal(10,6) | 订单坐标,GCJ-02 坐标系 |
| status | tinyint | 0待派单 1待接单 2进行中 3已完成 4已取消 |
| create_time / update_time | datetime | 审计字段 |
表:骑手/服务人员表关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| name / phone | varchar | 联系人 |
| status | tinyint | 0离线 1空闲 2忙碌 |
| cur_lng / cur_lat | decimal(10,6) | 实时定位 |
| order_count | int | 今日已完成单数,负载均衡会用到 |
| score | decimal(3,1) | 服务评分,派单排序会用到 |
表:派单记录表关键字段
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| order_id | bigint | 关联订单 |
| worker_id | bigint | 关联骑手/师傅 |
| dispatch_type | tinyint | 1手动派单 2自动派单 3抢单 |
| status | tinyint | 0待接 1已接 2已拒绝 3超时 |
| dispatch_time / accept_time | datetime | 派单与接单时间 |
这套表里最关键的是订单的status。很多新手的做法是把状态当成普通字段随手update,结果就是状态被乱覆盖——A 请求把订单改成进行中,B 请求又把它改成已完成,前后端展示全靠猜。正确做法是给状态迁移定一个最小状态机:
- 待派单(0) → 待接单(1):系统派单或管理员手动派单后
- 待接单(1) → 进行中(2):骑手/师傅接单
- 待接单(1) → 待派单(0):无人接单超时回退
- 进行中(2) → 已完成(3):服务完成
- 待派单(0)/待接单(1)/进行中(2) → 已取消(4):用户取消或超时取消
每次迁移都用带条件的更新语句,防止并发把事情搞乱。用 MyBatis 时我会在 mapper 里这样写:
UPDATE t_order SET status = 2, update_time = NOW() WHERE id = #{orderId} AND status = 1这行的意思是:只有当前状态是“待接单”时才允许改成“进行中”。UPDATE返回的影响行数如果为 0,说明状态已经被人改过,调用方要按“抢单失败”处理。这也是 java 面试题里偏并发方向的一个高频考点:乐观锁不以锁表为代价,而是把状态条件写进WHERE。
如果用 MyBatis-Plus,可以给实体加@Version注解实现同样的效果,但我个人更喜欢写原生 SQL,因为派单系统的状态机分支多,写进 SQL 更直观,也方便在项目说明里给后来者画状态流转图。需要行级权限的场景,比如部分区域只能由指定组长派单,可以再加一张区域和人员的关联表,配合 SQL 里的region_id条件做行级数据过滤。
2.3 派单策略抽象:手动、自动、抢单三种模式的设计取舍
派单系统最常见的三种模式分别是:手动派单、自动派单、骑手抢单。它们不是互斥的,生产环境通常会把三者组合成一个“先自动,超时转抢单,再没人就人工兜底”的流程。所以代码上要用策略模式把“选人的规则”抽象出来,而不是用一长串 if-else 写在 controller 里。先定义接口:
public interface DispatchStrategy { // 返回可接单的候选人员,按推荐程度排序 List<Worker> selectCandidates(Order order, DispatchConfig config); // 策略编码,用于日志和统计 int type(); }手动派单是策略里最简单的一种:管理员在后台选人,接口直接调用dispatchCore.assign(orderId, workerId),不走候选计算。有团队会把调度台做成 Android 平板或 Android TV 大屏挂在墙上,手动派单接口就是给这类管理端用的。自动派单则要执行“圈人-排序-推送”三步,是下一章的重点。抢单模式的特别之处在于它不主动选人,而是把订单广播给一批空闲人员,先到先得。
实现上要注意:策略的选择本身要可配置。常见做法是用 Spring 把三个实现类注入到一个 Map 里,根据订单里的配置项路由:
@Service public class DispatchRouter { private final Map<Integer, DispatchStrategy> strategyMap; public DispatchRouter(List<DispatchStrategy> strategies) { this.strategyMap = strategies.stream() .collect(Collectors.toMap(DispatchStrategy::type, s -> s)); } public List<Worker> route(Order order, DispatchConfig config) { DispatchStrategy strategy = strategyMap.get(order.getDispatchType()); if (strategy == null) { throw new IllegalArgumentException("unsupported dispatch type: " + order.getDispatchType()); } return strategy.selectCandidates(order, config); } }这样的好处是:新增一种派单模式,只需要加一个实现类,不用动路由代码。我见过不少团队在二期要加“预约单提前 15 分钟派单”时,因为当时用了 if-else 只好重写整个方法;而用策略模式的工程,只需要加一个ReserveDispatchStrategy。
配置项DispatchConfig里常用的参数有:初始派单半径(默认 3 公里)、候选人数上限(默认 5 个)、最大并发在途单数(默认 3)、超时回退次数(默认 2 次)。这些参数要落到配置中心或数据库配置表,因为上线后一定会反复调,写死在代码里等于给运营挖坑。
3. 把自动派单跑通:候选池计算、抢单幂等化与推送链路
3.1 自动派单算法:距离、负载与评分的加权排序
自动派单最容易犯的错是“谁近派给谁”。如果不加负载和评分因素,会形成马太效应:距离最近的师傅永远在接单,其他师傅闲着,整体时效反而越来越差。所以候选算法要分三步。
第一步是圈池子:以上个单位时间上报的经纬度为原点,画一个半径圆,SQL 里用球面距离公式过滤出空闲人员。第二步是算综合分,第三步是取前 N 个候选。综合分公式我一般这样设计:
score = w1 * 距离分 + w2 * 负载分 + w3 * 评分分三个维度都要归一化到 0~1 之间。距离分是1 - distance / maxRadius,距离越近分越高;负载分是1 - orderCount / maxLoad,单越少分越高;评分分直接score / 5.0。权重w1、w2、w3的初始值常见是 0.5、0.3、0.2,三者加起来等于 1。排序时用 Java Stream 的comparing链,而不是手写冒泡排序——虽然这类业务数据量不大,但代码可读性差很多,后续加权重也麻烦。
public List<Worker> selectCandidates(Order order, DispatchConfig config) { double maxRadius = config.getRadiusKm(); List<Worker> workers = workerMapper.findAvailableWithinRadius( order.getLat(), order.getLng(), maxRadius); return workers.stream() .filter(w -> w.getOrderCount() < config.getMaxLoad()) .map(w -> { double normalized = normalize(w, order, maxRadius, config.getMaxLoad()); w.setScore(normalized); return w; }) .sorted(Comparator.comparingDouble(Worker::getScore).reversed()) .limit(config.getCandidateLimit()) .collect(Collectors.toList()); }代码逻辑:先从数据库筛出半径内的空闲人员,过滤掉已经满负载的人,再计算每个候选人的综合分并按分数降序排序,最后截取前 N 个。这里的findAvailableWithinRadius在数据库里做的事是:
SELECT *, (6371 * acos(cos(radians(#{lat})) * cos(radians(lat)) * cos(radians(lng) - radians(#{lng})) + sin(radians(#{lat})) * sin(radians(lat)))) AS distance FROM t_worker WHERE status = 1 AND distance <= #{radius} ORDER BY distance注意 MySQL 里不能在 WHERE 直接引用 SELECT 里的别名,要包一层子查询,不同版本行为还不一致;工程上我更推荐先按一个带冗余max_lat/min_lat/max_lng/min_lng的矩形框粗筛,再在 Java 侧做精确的球面距离计算。这样能避免全表扫描,也能减少数据库压力。
调参经验:半径先给 3 公里,候选取 5 个,最大负载取“当前在线人数 / 今日订单峰值”的 10%~20%。如果高峰期经常出现“收到派单但无人接”,优先调大半径和候选数,而不是调小负载门槛。
3.2 用 Redis+Lua 把抢单变成原子操作:一单多接的血泪教训
抢单模式的并发问题是最典型的。Android 端同时有几十个师傅在刷待接单列表,订单推送出去后,一瞬间可能有多个人点了“接单”。如果后端是“先查状态再更新”,在并发下几乎必现一单多接。我之前在一个项目里就翻过车:两个师傅同时进来,两个请求都查到了 status=1(待接单),然后各自执行UPDATE ... SET status=2,其中一个把另一个覆盖了,结果订单实际完成时报了双人签到,结算直接乱了。
解决办法是让“检查状态 + 更新状态”变成一个原子操作。常见做法有两种:第一种是数据库乐观锁,就是第 2 章那条带WHERE status = 1的 UPDATE,影响行数为 1 的才算抢单成功;第二种是 Redis Lua 脚本,适合在推送前做预占位。
-- KEYS[1]: 订单号 -- ARGV[1]: 期望的当前状态 -- ARGV[2]: 目标状态 local current = redis.call('GET', KEYS[1]) if current == ARGV[1] then redis.call('SET', KEYS[1], ARGV[2], 'PX', 30000) return 1 end return 0这段 Lua 的意思是:只有当 Redis 里这个订单的状态还是“待接单”时,才把它改成“进行中”并设置 30 秒过期。过期时间是一个兜底,防止业务逻辑异常时这个 key 永远卡死。用 Redis 做状态预占,本质是把热点写在内存里,扛并发比直接打 MySQL 稳定。不过要记住:Redis 不是最终真相,数据库订单状态才是。所以正确顺序是:先用 Lua 预占成功,再去执行数据库乐观锁更新;数据库更新成功后再删 Redis 里的 key。
注意:不要把 Redis 里的值当作唯一的订单状态源。Redis 宕机或 key 过期都会让状态失真,数据库的状态迁移必须始终存在,Redis 只承担抢单入口的并发过滤。
还要防一个隐蔽问题:网络重试。Android 端点击接单后如果超时,会走“重试”逻辑,同一个订单同一个师傅可能连发两次接单请求。后端幂等处理要在接口层做:以(orderId, workerId)为维度查接单记录,已存在就直接返回成功,而不是再走一次抢占逻辑。
3.3 推送链路:WebSocket 与厂商推送怎么选,Android 端如何配合
派单系统里“让人知道有单”和“让人能接到单”同等重要。常见的推送方案有两类:自有 WebSocket 长连接和第三方厂商推送。WebSocket 的优点是实时性高、免费、前后端都能控制,缺点是 App 一旦被系统杀后台,连接就断了,消息就推不到。厂商推送(小米、华为、OPPO、vivo 各自的推送服务)能保活,但要挨个厂商申请、集成 SDK,维护成本高。
我一般的做法是“双通道”:App 在前台或活跃状态时用 WebSocket 实时推;检测到连接断开或消息未被 ack 时,降级走厂商推送。对于只做 demo 或内部使用的派单系统,WebSocket 就够用了。WebSocket 服务端用 Spring 的TextWebSocketHandler就能做,消息体建议统一成这样:
{ "type": 1, "orderId": 10086, "title": "新订单待接单", "content": "维修订单,距您 800 米", "expireAt": 1699999999999 }type用于 Android 端区分消息类型:1 新订单,2 订单被抢,3 订单取消,4 订单完成。expireAt是接单截止时间戳,Android 端收到后要做倒计时,显示成一个进度条或者数字,超时后这单从列表消失。Android 端收到原始推送后不能直接把服务端的数据渲染到 UI,而是要带着orderId去拉一次最新的订单详情,防止推送内容和数据库不一致。
心跳参数也值得记一下:WebSocket 的心跳间隔设为 60 秒,服务端超过 90 秒没收到心跳就判定离线;客户端重连要带指数退避,第一次 3 秒、第二次 6 秒、第三次 12 秒,最多 5 次,避免服务端挂掉时客户端疯狂重连把入口打爆。
4. Android 端源码拆解:MVVM 分层、网络层拦截与订单状态机
4.1 客户端分层:为什么用 MVVM 而不是把接口写在 Activity 里
拿到 Android 端源码,第一步先看它的包结构。好的派单客户端一般长这样:
app/ ├── ui/ # Activity、Fragment、Adapter ├── viewmodel/ # 订单列表、订单详情、接单、登录 ├── repository/ # 数据仓库,对接网络与本地缓存 ├── network/ # Retrofit 接口定义、拦截器 ├── model/ # Order、Worker、ApiResult └── push/ # WebSocket、通知栏工具派单 App 的特点是异步消息多,一个订单的状态会被多条推送反复修改。如果按 MVC 的老写法,把逻辑全塞进 Activity,Activity 在屏幕旋转时重建,异步回调就可能会操作已销毁的 View,导致空指针和界面闪烁。MVVM 把界面状态放在ViewModel里持有,Activity 只负责订阅状态变化,生命周期结束后订阅自动解除,这是处理这类高频更新场景最省心的方案。Kotlin 下常用ViewModel + StateFlow,在Android Studio里新建工程选 Kotlin 和 ViewModel 就行。
class OrderListViewModel(private val repo: OrderRepository) : ViewModel() { private val _orderList = MutableStateFlow<List<OrderUiModel>>(emptyList()) val orderList: StateFlow<List<OrderUiModel>> = _orderList.asStateFlow() fun refresh(status: Int) { viewModelScope.launch { val list = repo.fetchOrders(status) _orderList.value = list } } fun onPushArrived(orderId: String) { viewModelScope.launch { val updated = repo.fetchOrderDetail(orderId) _orderList.update { items -> items.map { if (it.orderId == orderId) updated else it } } } } }逻辑说明:订单列表页面通过refresh拉取列表,收到推送时调用onPushArrived只替换对应订单的数据,而不是整页刷新。这个做法的价值在于省流量、不闪屏,而且处理“待接单列表里有 20 个订单、其中 1 个被别人抢了”这种场景时,只改动受影响的那一行,用户体验最好。
4.2 Retrofit+OkHttp 网络层配置:超时、Token 刷新与明文 HTTP 限制
Android 端的所有接口调用都走network包。用 Retrofit 定义接口时,我会把和派单相关的接口收敛到一个OrderApi里,方便对照后端dispatch-api模块。
interface OrderApi { @POST("order/{orderId}/grab") suspend fun grabOrder(@Path("orderId") String orderId): ApiResult<GrabResp> @GET("order/list") suspend fun fetchOrderList( @Query("status") status: Int, @Query("page") page: Int ): ApiResult<OrderPage> }配合 OkHttp 的拦截器,统一处理三件事:加 token、刷新 token、统一异常。示例:
class AuthInterceptor(private val tokenRepository: TokenRepository) : Interceptor { override fun intercept(chain: Interceptor.Chain): Response { val request = chain.request().newBuilder() .header("Authorization", "Bearer ${tokenRepository.getToken()}") .build() val response = chain.proceed(request) if (response.code == 401) { // 同步刷新 token,然后重放一次原始请求 } return response } }参数上我一般这样设:connectTimeout10 秒,readTimeout30 秒,writeTimeout30 秒。派单场景里有大图上传(完工照片),所以读超时给长一点;抢单接口不建议设太长的 readTimeout,因为等 30 秒拿一个“已被抢”的结果,对用户来说太煎熬,10 秒没返回就该提示“网络繁忙,请重试”。
一个特别容易忘的事:Android 9 及以上默认禁止明文 HTTP 流量。如果你的后端接口地址是http://ip:8080这种内网地址,调试时要在AndroidManifest.xml里配置:
<application android:networkSecurityConfig="@xml/network_security_config"> </application><!-- res/xml/network_security_config.xml --> <network-security-config> <domain-config cleartextTrafficPermitted="true"> <domain includeSubdomains="false">192.168.1.100</domain> </domain-config> </network-security-config>生产环境务必换成 HTTPS,并在项目说明里写清楚服务器地址是改在BuildConfig里还是gradle.properties里。我见过太多源码下载者把地址直接写死在拦截器里,换环境还要重新编译,这个设计就不好。
4.3 订单状态机在客户端的三处实现细节
Android 端不能只信接口的一次性返回值,它必须维护一个最小状态的“客户端状态机”,否则推送和手动刷新会互相打架。这里有三处细节值得注意。
第一处:乐观 UI。用户点击“接单”按钮后,不应等接口返回才变 UI,而应先把订单从“待接单”置为“进行中”,同时显示一个“提交中”的进度条;接口失败再回滚回“待接单”。这个过程在代码里要标记上“本地状态”和“服务端确认状态”两个标志位,不能把展示状态和事实状态混成一个字段。
object OrderStatus { const val WAIT_DISPATCH = 0 const val WAIT_ACCEPT = 1 const val DOING = 2 const val DONE = 3 const val CANCELED = 4 }第二处:幂等处理。同一个订单的推送可能收到两遍,比如 WebSocket 重连后服务端补发。客户端要根据orderId去重,最简单的做法是在接收推送时判断本地是否已有同orderId且同状态的数据,有则跳过更新。
第三处:退出重置。师傅退出登录后,本地缓存的待接单列表、WebSocket 连接、通知栏消息必须全部清掉,否则下一个登录的人会看到上一个账号的订单。常见的错误是在logout()里只清 token 不清缓存,换账号后订单列表闪一下旧数据。
5. 常见踩坑与排查:从收不到通知到坐标偏移的 5 条真实记录
5.1 现象:Android 端收不到派单消息,WebSocket 显示已连接
原因有三个层级:第一,App 在后台被系统回收,WebSocket 断开,服务端还在往旧连接推;第二,服务端推送时根据“是否在线”判断候选池,但师傅的在线状态是启动 App 时上报的一次性值,没有续期;第三,Android 厂商省电策略把后台连接切断了,而这种切断在系统层不报错。
解决:WebSocket 断线重连要做,这是必须的;在线状态要改成“每 5 分钟上报一次活跃心跳”而不是“登录时报一次”;如果 App 被系统杀死属于真后台,要保活只能接厂商推送通道。最实在的兜底是:师傅重新打开 App 时,主动拉取一遍“待接单”列表,把离线期间漏掉的消息补回来。
5.2 现象:两个师傅同时抢到同一笔订单,结算对不上
原因就是上一章写的并发问题:检查状态和更新状态不是原子操作,两个请求都读到“待接单”,两个更新都成功。注意 MySQL 的UPDATE ... WHERE status = 1能解决数据库层面的竞态,但如果前面加了“从 Redis 读状态”这一步,Redis 和数据库的状态不是同一个数据源,还会出问题。
解决:统一用“数据库条件更新影响行数”作为唯一判定标准。Redis 可以做前置预占,但最终结果以数据库返回的int rows为准。接口层要用(orderId, workerId)做幂等表,同一师傅重复点击也只算一次。
5.3 现象:自动派单后长时间无人接单,订单卡在“待接单”
原因通常是:候选池算出来全是满负载,或者半径太小身边没人,或者推送通道静默失败。表面上看是没人抢,实际上可能是系统根本没有把订单推到任何人的手机上。
解决:给自动派单加重试和回退机制。3 分钟后把半径扩大 1.5 倍重推一次;5 分钟后自动转抢单广播;10 分钟后进入人工待处理队列并在后台醒目提醒。同时,每次派单要写派单日志,字段至少包含:订单号、派单模式、候选人数、被推送人、推送时间、回执状态。没有日志,这类问题每次都要靠猜。
5.4 现象:上报的定位坐标与订单地址相距几百米,派单距离完全失真
原因:Android 获取定位返回的坐标系和高德/百度地图使用的坐标系不一致。真实设备上你拿到的经纬度可能是 WGS-84 裸坐标,而后端如果按高德地图的 GCJ-02 坐标来计算距离,结果会偏移几百米到几公里,自动派单的“半径 3 公里”就会圈错人。
解决:整个项目统一坐标系。Android 端如果用的高德 SDK,就把coordinateType设为 GCJ-02,后端存储和计算全部按同一坐标系;如果后端要和 GPS 设备数据混算,就在接入层做一次坐标转换。项目说明里必须写清楚“本仓库所有坐标均为 GCJ-02”,否则二次开发的人拿着 GPS 数据一接就偏。
5.5 现象:项目说明文档和源码对不上,按文档跑不起来
原因:源码下载前的文档可能是早期版本,或者文档作者复制了另一个项目的启动步骤。派单系统牵涉 MySQL、Redis、WebSocket、Android 工程四个环境,文档只要有一处不一致,新手就会卡在启动阶段。
解决:动手之前先做一次“最小闭环核对”。按文档说的 JDK、MySQL、Redis 版本起环境,然后只跑一个流程:管理员创建订单 → 自动派单 → Android 接单 → 订单完成。这个流程能跑通,再去看其他高级功能。如果最小闭环都跑不通,对照日志和代码找版本差异,优先相信代码和 POM 文件里的依赖版本,而不是文档描述。
6. 上生产前的最后一战:并发压测、状态流转验证和派单调参
6.1 先杀死一个订单:用脚本跑通最小闭环
上生产前我最先做的一件事,是写一个 shell 脚本把“创建订单→自动派单→师傅接单→完成→结算”完整打一遍,脚本里每步都校验接口返回码和订单状态。这比任何文档都可靠,因为它在验证代码本身。
# 创建订单 ORDER_ID=$(curl -s -X POST http://localhost:8080/api/order \ -H "Content-Type: application/json" \ -d '{"serviceType":1,"address":"测试地址","lng":120.1,"lat":30.2}' | jq -r '.data.orderId') # 自动派单 curl -s -X POST http://localhost:8080/api/dispatch/auto -d "orderId=$ORDER_ID" # 模拟师傅接单 curl -s -X POST http://localhost:8080/order/$ORDER_ID/grab -H "Authorization: Bearer $TOKEN"注意脚本里要校验每一步返回的状态码,失败立即退出。第一次跑不要用真实账号,先在测试环境把状态流转日志打全。
6.2 并发压测先打“抢单”接口,而不是登录接口
派单系统最大的并发压力在抢单接口,所以压测时用 JMeter 或 wrk 模拟 50 个线程同时抢同一个订单,看返回结果里成功数是否为 1。这一步能一次性暴露三类问题:Redis Lua 是否真的原子、数据库乐观锁是否生效、接口幂等是否挡住重复请求。如果压测结果显示成功数大于 1,说明上面三处至少有一处没做对。之后再压“订单列表”和“推送接收”,这两个接口的关注点是响应时间而不是一致性。
6.3 调参顺序:半径 → 候选数 → 负载上限 → 权重
我是按这个顺序调自动派单的:先调半径,看“无人接单率”的曲线;再调候选数,看“被推送人的接单转化率”;接着调最大负载,让订单分布尽量均匀;最后才动三个权重。不建议一上来就调权重,因为权重的效果依赖前面三个参数的数据基础,基础没稳住,调出来的权重只是自欺欺人。每次调参只改一个变量,记录当日数据再对比,这是我这几年在派单系统上最值钱的一条经验。
这样一套跑下来,源码里再深的坑也会暴露得差不多。派单系统的价值不在于某一次抢单跑得多快,而在于状态是否永远正确、日志是否足够完整、文档是否对得上代码。希望帮到你。
本文还有配套的精品资源,点击获取