简介同城物流派送是一套基于Android平台的完整项目源码面向移动开发初学者、毕业设计学生以及想了解SQLite实际应用的开发者。项目围绕城市内快速配送场景实现从用户下单、司机抢单、状态更新到货物送达的完整业务闭环并重点演示SQLite数据库在用户信息、订单数据、司机数据等持久化存储中的增删改查操作。资源包共275个文件大小约53.42MB包含可直接运行的APK安装包、31个Java源码文件、94张界面切图、39个XML布局与配置、9个jar依赖库以及so动态库等目录结构清晰便于按模块研读。目前已有323人学习/下载适合作为课程设计或入职实战练习的参考资料。从内容预览可见项目包含DBManager数据库管理、FahuoFragment发货、WodeFragment我的、Chongzhi充值等功能模块覆盖了账号注册登录、自定义Adapter订单列表、抢单派送流程、GPS定位与路线规划等关键技术。学习这套代码可快速掌握SQLite与ListView/RecyclerView结合的开发范式理解状态机式的订单流转逻辑并能复用注册登录、订单展示等成熟模块对构建同类同城服务App有直接参考价值。1. 同城物流派送不只是“把订单派给司机”那么简单同城物流派送看着简单——把货从商家送到客户手上。真正动手做系统才会发现难点根本不在骑车的人而在于怎么把几百个订单在几分钟内分给几十个司机让每个司机少跑冤枉路同时每个客户都觉得自己的单被优先照顾。它本质上是一个带时间窗约束的动态分配问题既要算路径更要算排队和组合。这篇内容写给正在搭建同城派送平台、或者在现有电商体系里补配送能力的团队围绕调度引擎、路径计算、数据建模和实战踩坑展开。新手能按章节从零搭出可运行的调度原型熟手可以直接跳到参数调优和故障排查部分对齐自己的实现。同城配送是门手艺活调度算法撑起一半另一半靠对运营细节的敬畏。2. 拆解同城派送系统的核心模块与职责边界在动手写代码之前先把同城派送系统拆成四个大块订单接入、运力管理、调度中心、状态追踪。四块之间通过事件驱动连接而不是靠共享数据库表互相查询。为什么这样分因为同城派送的并发量虽然不比电商大促但订单状态变化极快一个订单在十几分钟内要经过待分配、待取货、配送中、已送达四个状态如果每个模块都去轮询同一张订单表接口就会乱套排查问题时分不清是谁改坏了状态。2.1 订单接入层先统一订单模型地址解析与可配送性检查前置订单接入层定义订单的数据结构是整个系统的地基。一个订单至少要有三组字段业务信息订单号、商家地址、客户地址、订单创建时间、履约信息期望送达时间、备货时长、商品类型、地理信息经纬度订单进入系统时完成地址解析和地理编码。我习惯在接入时就调用地理编码服务把商家的仓库坐标和客户的收货坐标解析成经纬度落到订单表里而不是等调度阶段现算。这个习惯源于一次真实的排查经历。某开发者一开始把地理编码放在调度前做高峰期订单涌入时调度引擎为了解析地址白白多花了400毫秒订单池越堆越多最终连锁反应导致整个调度链路变慢。把地址解析前置到接入层之后调度阶段只需要做纯几何计算不再做字符串匹配性能问题直接消失。接入层还要顺手做可配送性检查超出配送范围、货品尺寸超限、极端天气下需改约的订单在进入调度池之前就打上标记。这个规则前置非常重要否则调度引擎就要承担大量它不该承担的业务判断。接入层的最后一个职责是订单去重和防重。同城场景下用户误提交、渠道重复推送都很常见同一笔订单如果重复进入调度池会造成重复派单的严重事故。我建议在接入层用“商家ID 用户ID 创建时间窗口 商品摘要”做联合去重键命中重复就丢弃后到的一条并记录事件日志。2.2 运力管理司机状态机、定位上报与异常判定运力管理的核心是维护一份可靠的司机实时台账。很多团队只做一张在线/离线表这是远远不够的。司机在线但车坏了、司机在线但在等电梯、司机在线但停在无法接单的位置——这些都不应该被当作“可用运力”。状态机设计得不好调度引擎会把任务派给一个实际上无法履约的司机配送超时立刻出现。我建议把司机状态收敛为四个枚举值可用、忙碌、休息、异常。可用表示当前可以接受新任务忙碌表示已分配任务且正在执行休息表示主动下线系统不再分配异常由风控定时任务自动扫描产生触发条件包括定位长时间不动、连续拒绝订单超过两次、车速异常等。异常状态要独立于司机手动状态之外调度引擎只信任运力层给出的可用司机集合这样才能让“系统误判”和“司机故意离线”两个问题分离开来。司机端定位上报的频率也要单独设计。全城范围可用5秒一次后台入库可以降到15秒一次进入取货或配送阶段要升到2秒一次并开启前台持续定位。高频定位在上报链路里只做位置更新不直接触发调度动作调度由事件驱动。这样可以避免定位上报风暴打爆调度中心。2.3 调度中心与履约层的接口边界任务表优先订单表不可越权调度中心是大家最关注的模块但我反而要先讲清楚它的边界。调度中心只做一件事接收待分配订单集合和可用司机集合产出“司机—订单”的分配结果并把结果写成任务记录。真正去修改订单状态的是履约模块——骑士点击“取货”“送达”时调用履约接口来推进订单状态。这个边界设计有明确的好处。调度中心可能会因为订单取消、司机改派、超时重算而反复修改分配结果如果每改一次都直接动订单表订单表会被频繁改写审计困难。任务表记录每一次分配、撤单、重派的历史订单表只记录当前最权威的状态。两个模块通过任务ID关联互相解耦。出现问题的时候把任务表整个时间线拉出来就能还原当时发生了什么这种还原能力在生产环境里非常救命。调度中心内部维护两个队列订单池和司机池。订单池存待分配的订单司机池存当前可接单的运力。每次调度周期启动时先从订单池抽出时间窗紧迫的订单再从司机池筛选出位置合适的司机进入第3章要展开的分配算法。两个队列的更新逻辑要独立不然其中一个队列的抖动会影响另一个。3. 调度与路径的核心算法批量建模、VRP变体、参数校准同城派送的调度问题在学术界和工程界都能建模成带时间窗的车辆路径问题VRP with Time Windows简称 VRPTW。目标函数是让所有订单的总配送时长最短同时满足每个订单的时间窗口约束、每辆车的载货能力约束、以及每个司机的连续工作时长约束。但工程落地与学术论文有很远的距离难点不在建模本身而在约束条件的取舍和参数的校准。3.1 从“一单一派”到“批量组合”调度周期与积压阈值的双触发最朴素的调度是来一单派一个司机就近分配。这在订单稀疏时段勉强能用一旦订单密度上来一单一派的代价是灾难性的两个顺路订单被分给两个方向相反的司机运力浪费一倍司机刚接单出发系统又来一个同方向的订单只能再派一个司机整体路径乱成一团。批量的价值在于把时间窗口内到达的订单攒起来统一放进一个分配周期里做组合优化。我一般用“调度周期 积压阈值”双触发机制。距离上次调度超过两分钟或者订单池里积压超过20单满足任一条件就触发一轮批量调度。两分钟的周期保证低峰期订单不会等太久20单的阈值保证高峰期有足够的组合空间让排序算法发挥作用。这两个参数的取值要按城市单量和运力规模调整一线城市密集商圈可以把周期缩短到60秒、积压阈值降低到15单运力不足的三四线城市反而要把周期拉长到3分钟避免频繁调度产生碎片化路线。批量调度的核心是构建“组合图”把每个司机当作一条路径的起点每个待分配订单当作路径上的节点。如果司机出发点与订单取货点的距离在一个阈值内我常用2公里就在司机和订单之间画一条可行边两个订单之间如果取货点相近且送货方向一致也画一条可行边。组合图上的结果是一条条“司机—订单—订单”的链求解的目标是让总成本最低的一组链覆盖尽可能多的订单。这里说的总成本不只是距离还包括时间窗违例惩罚和司机绕路惩罚。3.2 路径排序的决策优先级效率优先、顺路优先、时效兜底进入“司机—订单”匹配阶段排序逻辑建议分三层。第一层按紧急程度排序最晚送达时间临近的订单先被考虑已经超过最晚送达时间但还没分配的订单插到最前面。第二层按配送成本排序对同一个司机计算把每个订单放入当前路径后的增量成本选择增量最小的订单先去取。第三层做冲突消解当两个高优先级订单同时看中同一个司机时比较哪个订单的备用可选司机更多备用多的先让一步备用少的一方优先分配。三层排序逻辑实现起来不复杂在大多数生产场景下比复杂模型更可靠。关键是可解释性——运营人员问你为什么这单先送那单后送时你能用“时间窗口临近、顺路优先”一句话讲清楚。模型打分是个黑匣子一旦一线团队不信任系统的分配结果他们就会开始人工改单改单越来越多系统数据越来越脏形成恶性循环。在算法选型上我坚定地站“简单可解释优先复杂模型谨慎引入”。增量成本的计算公式值得展开一下。对一个正在执行任务的司机假设它当前路径的末端点是 A新订单的取货点是 B送货点是 C。把新订单插入到路径末尾的额外距离是 dist(A, B) dist(B, C)。如果司机下一程本来要从 A 去往另一个点 D那条本来的路径距离是 dist(A, D)。增量成本等于 dist(A, B) dist(B, C) - dist(A, D)。这个差值越小说明新订单越顺路。3.3 三个必须校准的核心参数合并半径、最大订单数、缓冲时间同城派送的参数多如牛毛但下面三个是无论如何都要认真校准的直接影响履约质量和运力效率。我做了一张参数速查表供你对照自己的城市和运力结构来做第一组校准。参数含义电动自行车场景建议汽车场景建议调参信号取货点合并半径两个订单取货点距离小于该值且送货方向一致时合并取货500米1公里如果司机空跑取货次数多适当放大单路线最大订单数一个司机一趟最多同时携带的订单数6~8单12~15单如果司机频繁超时减少订单数取货缓冲时间每单在商家处等待出货的预留时间3~5分钟3~5分钟高峰时段加30%缓冲避免司机空等取货点合并半径这个参数尤其值得细调。城市中心地带商家密集同一个商场里可能有五六家门店都有订单如果系统每次都让司机单独跑一趟取货电梯和门禁的等待时间会被叠加上去一趟下来光取货就花掉30分钟。把合并半径从200米放到500米后司机可以一次取完商圈内多个订单履约效率提升很明显。但半径不能盲目放大超过某个阈值后司机为了凑单会在商圈里绕圈反而拖慢时效。单路线最大订单数直接影响司机端的承载体验。电动自行车载货空间有限订单太多放不下取货顺序错乱也容易拿错货。我见过某个团队把最大订单数设到15单结果司机一趟下来丢失了3个订单赔偿加投诉损失远大于多赚的配送费。汽车配送可以放宽到12到15单但要在司机端做一个简单的装载顺序提示按送货顺序从后往前装避免送货时反复翻找。4. 跑通最小可用的同城派送方案数据表、路由计算与状态流转第3章讲的是算法思路这一章落到能跑起来的最小工程。按照“一张订单表、一张司机实时表、一张任务表、一个调度定时器”这样最朴素的模型搭起来后续需要扩展时再加索引和分布式组件。先跑通闭环远比一开始就追求完美架构重要因为同城派送的真实约束只有跑起来才能看到。4.1 订单表与司机表的字段设计订单表的最小集字段要覆盖业务、履约、地理三大组。下面这段建表语句是我常用的结构去掉了与业务无关的冗余字段可以直接参考。CREATE TABLE delivery_order ( order_id BIGINT PRIMARY KEY, merchant_lat DOUBLE NOT NULL, -- 商家纬度 merchant_lng DOUBLE NOT NULL, -- 商家经度 customer_lat DOUBLE NOT NULL, -- 客户纬度 customer_lng DOUBLE NOT NULL, -- 客户经度 created_at DATETIME NOT NULL, expect_pickup_time DATETIME NOT NULL, -- 期望取货时间 expect_delivery_time DATETIME NOT NULL, -- 期望送达时间时间窗 order_status TINYINT NOT NULL DEFAULT 0, -- 0待分配 1已分配 2已取货 3已送达 4已取消 assigned_driver_id BIGINT DEFAULT NULL, -- 当前分配的司机ID batch_no VARCHAR(32) DEFAULT NULL -- 调度批次号 ); CREATE TABLE driver_rt ( driver_id BIGINT PRIMARY KEY, lat DOUBLE NOT NULL, -- 当前纬度 lng DOUBLE NOT NULL, -- 当前经度 status TINYINT NOT NULL, -- 1可用 2忙碌 3休息 4异常 current_task_chain VARCHAR(1024) DEFAULT NULL, -- 当前任务链订单ID序列 last_report_time DATETIME NOT NULL );参数说明order_status 用数字枚举比字符串更省空间也方便索引driver_rt 表中 current_task_chain 字段存储的是司机当前已分配订单 ID 的数组序列格式简单如 JsonArray调度引擎读取后就是待处理的路径点列表。last_report_time 是定位上报时间用于异常状态判定——超过 30 秒未上报可以考虑把司机标记为可疑状态。注意driver_rt 表是典型的写多读少表定位上报每 5 秒一次高峰时几百个司机同时写入一定要把读写分离出来或者用内存缓存持有一份司机位置快照调度引擎只读快照避免每次调度都打几万次数据库查询。4.2 批量调度的贪心实现增量成本计算与组合分配把第3章的批量调度简化成一段可读的 Python 伪代码核心是两层循环第一层遍历可用司机第二层遍历待分配订单计算增量成本后贪心选取。数据量大时可以用并发版本但逻辑骨架保持一致。# 批量调度主流程伪代码已精简 def batch_assign(orders, drivers): assigned [] # 分配结果 remain_orders orders.copy() # 待分配订单池 # 优先处理最晚送达时间最紧的订单 remain_orders.sort(keylambda o: o.expect_delivery_time) for d in drivers: if not remain_orders: break # 当前司机路径末端作为锚点 anchor get_path_tail(d) # 任务链末尾的送货点无任务则取当前位置 best_order, best_inc_cost None, float(inf) for o in remain_orders: if not is_assignable(d, o): # 检查单路线订单数上限、司机状态、时间窗可行性 continue inc extra_distance(anchor, o.merchant_lat, o.merchant_lng, o.customer_lat, o.customer_lng) if inc best_inc_cost: best_inc_cost inc best_order o if best_order: assigned.append((d.driver_id, best_order.order_id)) remain_orders.remove(best_order) append_task(d, best_order) # 更新司机任务链 return assigned逻辑说明主流程先把订单按最晚送达时间排序保证时间窗最紧的订单优先拿到分配机会。然后逐个司机扫描订单池用“当前司机锚点 增量成本最小”作为贪心策略。is_assignable 函数负责检查司机状态、订单数上限、时间窗可行性extra_distance 负责计算插入新订单后的绕路增量。这段代码能跑通最小闭环但缺少“两个订单组合让司机一趟带完”的全局优化生产环境要在此基础上加上批次重排。参数说明anchor 是司机当前路径末端的位置点。extra_distance 的计算方式是从 anchor 出发到家新订单的取货点再送到送货点减掉 anchor 本来要去下一个点的距离。差值越小代表越顺路。如果订单池很大这个双重循环是 O(司机数 × 订单数)建议用司机位置的网格索引先过滤掉不相干的候选把复杂度降下来。4.3 状态流转与事件通知调度不直接改订单状态调度结束后引擎把分配结果写入任务表然后发送一个“任务已分配”事件。订单状态由履约模块根据司机端上报来推进司机点击“取货”订单进入已取货点击“送达”订单进入已送达。为什么调度引擎不去直接改订单状态因为一个订单可能经历分配、撤单、重派多次如果每改一次都要直接动订单表订单状态字段就会反复震荡无法反映真实履约进度。任务表的设计就像一个审计日志每次分配、撤单、重派都追加一条记录。某个订单最终状态异常时把任务表里该订单的所有记录拉出来就能看到完整的调度轨迹。可追溯性在同城派送里非常重要因为它涉及三方协作商家、司机、客户任何一方说“我没收到货”时都需要用数据来还原真实情况。事件通知走最小闭环就行。最常见做法是建一张事件表由事件发布组件轮询该表推送给用户端、司机端、商家端。没有消息队列基础设施时轮询接口加一个 last_event_id 就能支撑小体量运转。等到订单量到每天几千单以上时再考虑引入消息中间件前期不必为此增加部署复杂度。5. 同城派送实战中的常见问题与排查三个反复出现的翻车场景这一章集中写实战里踩过、见过、帮别人排查过的真问题全是那种不做不知道、做了才知道的重点。每一条都按“现象 → 原因 → 解决”的结构展开方便你直接对照自己的系统状态来诊断。5.1 午高峰前调度引擎静默失败司机大面积空等现象每天 11:30 到 12:00 之间订单池积压严重但司机侧几乎收不到新派单。后台看运力状态全部正常司机也没有离线像集体“罢工”。第一反应往往是司机偷懒翻派单流水才发现调度引擎根本没有产出分配结果。原因调度主流程里调用了外部接口获取天气数据接口超时导致整个调度循环抛出异常异常被上层框架吞掉后只有日志默默记录订单池卡在入口处没有进入分配逻辑。外部依赖的一次超时让整个调度中心停摆而且因为是静默失败告警也没触发。解决外部依赖调用移出调度主流程主流程改为读取最近一次天气缓存外部调用加超时兜底最多等待 300 毫秒失败就使用默认值。调度引擎的主循环整体用 try-except 包裹任何异常都必须触发告警和人工值班介入不允许静默失败。同城派送对实时性要求很高调度链路里不允许存在无超时保护的外部调用。5.2 司机按系统路线行驶一天内同一个取货点跑三趟现象司机反馈路线规划不合理同一个商家一天跑三次每次只取一单。后台看派单记录发现该司机的任务链确实被拆成了三条不相邻的路线中间穿插了大量其他订单。原因调度引擎没有把“该司机已在该商家取过一次货”计入组合约束。后续新订单进来时只做单独匹配没有检查司机当前任务链中是否已有这个商家的取货任务导致可以合并的取货动作被硬生生拆散。解决在分配条件里增加前置检查。调度前把待分配订单按取货商家的地理编码聚簇同一簇内的订单优先分配给已在该商家有取货任务的司机并在批量分配时合并取货动作。司机端也要增设“到达取货点自动打卡”逻辑让系统感知司机是否已经在商家附近作为组合判断的辅助信号。这个修复后同一个司机在同一个商家的取货次数从平均三次降为一点五次。5.3 两个订单收货地址相同却分给了两个司机现象两个订单的收货地址是同一个写字楼系统却把两单分给了不同司机。客户接到两个电话、跑两趟去取件体验非常糟糕。后台对比两个订单的收货坐标发现数值确实不同。原因分配算法只用了经纬度严格相等判定没有做模糊空间匹配。同一个写字楼在不同地图数据源里经纬度存在微小偏差比如一个拿到的是主楼坐标另一个拿到的是东门坐标严格比较等于零才认为是同一地址这种偏差就成了漏网之鱼。解决判断收货地址是否相近时用两倍于 GPS 漂移误差的阈值做近似匹配。我采用的方法是计算两个收货点的球面距离低于 80 米且订单接入层的地址别名表命中相同关键词即视为同一收货点。同时调整分配规则同收货点的订单定向路由给同一个司机且取货完成时间尽量对齐让司机一趟送完。5.4 司机按系统导航“已送达”客户却投诉没收到货现象司机端显示距离客户地址 50 米系统判定送达客户却投诉没收到。后台查看订单坐标和司机轨迹两个点的距离只有三十几米看起来一切正常。原因写字楼和小区内部道路的 GPS 漂移严重地图匹配会把司机坐标强行拉到最近的路上司机实际停在小区门口或写字楼大堂而不是客户指定的楼栋房间。司机端直接用“距离小于 50 米可点击送达”的判定逻辑又没有二次确认司机赶时间就直接点了。解决把送达操作控制得更严。司机点击“送达”前弹出小窗确认“本人已联系客户或已放置在指定地点”确认动作与订单状态变更绑定同时把坐标判定只对开放道路生效小区内部、写字楼内部加强人工确认流程。对高频送件的场景可以考虑在客户端添加电子签名或开箱照片作为送达凭证减少争议。5.5 运营人工改单后系统下一轮调度又把单派回原司机现象运营人员每天花大量时间手工调整系统分派结果改完之后下一轮调度触发订单又被系统按原逻辑派回原来的司机两边反复打架司机也被折腾得不轻。原因调度引擎没有“尊重人工干预”的机制。人工改单只改了任务表但没有给订单打上“人工干预”标记调度引擎把订单当作未处理订单重新进入待分配池于是被老算法再次派给原司机。解决订单表增加一个人工分配标记字段一旦被人工改派该订单在本调度周期内自动跳过自动分配。等这个订单完成后再清除标记。这个改动虽小却让运营人员对系统的信任度大幅上升。系统可以笨一点但不能“固执”人工干预是运营兜底的权利系统必须让步。6. 进阶技巧用历史数据反推运力缺口把调度的账算明白调度算法把单分出去只是第一步真正的运营问题是“今天需要多少司机、明天每个小时需要几个人”。这两个数字加上自动设定动态配送费率和异常预警区间能反推出一个同城派送平台的成本模型是最值得投入的方向。6.1 用订单量与单均完成时长测算每个小时的运力缺口把历史三个月每小时的订单量除以该时段内司机平均有效完成单数就得到“理论最低司机数”。对比实际在岗司机数就能发现某个城市、某个时段长期处于运力缺口。这个缺口靠调度修不了只能靠排班表补。具体做法是按小时粒度做一张对比表订单量取 90 分位而不是平均值否则赶上高峰就会被平均值掩盖缺口的严重程度。6.2 把需求预测接到司机引导提前30分钟预备运力更进一步的做法是把全天预测线叠加到实时调度上。预测未来 30 分钟订单量骤增时提前给司机推送“别走远就在商圈附近待命”预测订单量骤降时把司机引导到下一个热点区域。我用的逻辑很朴素最近 15 分钟新增订单量与完成订单量的比值连续两个周期超过 1.3就触发预备运力提醒。做这种预测最重要的一点是保持模型简单可维护。同城派送场景变量非常多天气、商圈活动、平台补贴都会改变订单量。模型太复杂一个参数调错就会影响全盘。先把“每小时订单量预测”这一个小目标做好覆盖掉运力排班的大头就已经能为平台省下很可观的成本。最后说一个习惯每次排班表上线前我会在心里模拟一遍最差情况——某个商圈的几个司机同时集体下线怎么办。这个习惯帮我躲过好几次午夜高峰的大面积延迟。算法再厉害也要留一手人工干预的活口排班方案要能临时替换调度引擎要有手动改派的逃生通道。希望这篇内容能帮你在同城派送这个方向上少走几个弯路。本文还有配套的精品资源点击获取