恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
用SQLite构建同城物流派单系统:表结构、贪心算法与避坑指南
首页
资讯中心
/
用SQLite构建同城物流派单系统:表结构、贪心算法与避坑指南
用SQLite构建同城物流派单系统:表结构、贪心算法与避坑指南
发布时间:2026/10/14 7:13:20
简介一份基于SQLite的同城物流派送系统完整Android项目覆盖订单发起、司机抢单、配送状态跟踪等核心业务适合Android开发学习者作为课设、毕设或就业项目参考。项目基于SQLite构建数据存储层包含用户注册登录、自定义适配器列表展示、订单流程控制、GPS定位与路线规划等模块代码结构清晰。其中注册登录模块涉及密码加密保存与登录校验订单模块包含抢单和状态同步逻辑适配器用于高效展示订单与司机信息。资源共275个文件压缩包约53.42MB以Java源码、XML布局、class编译文件为主另有APK安装包、GIF演示图和界面截图方便直接运行和对照调试。已有323人学习适合作为课程设计参考项目。通过源码可掌握SQLite数据库增删改查与封装、自定义Adapter绑定数据、订单并发状态更新、定位与路线规划接入等实用技术同时理解Android项目多模块协作方式。1. 同城物流派送系统一张 SQLite 文件扛下订单、骑手与派单同城配送的订单没有固定路由派给谁、从哪取、送到哪全靠动态决策这类系统的难点从来不在界面而在派单和数据一致性。这套同城物流派送系统把订单、骑手、派单记录和状态日志全部收进一张 SQLite 文件里单机直接跑不需要安装数据库服务拷走文件夹就能交付。适合用来改课设、做毕设起点或者小团队内部快速验证派单思路。关键词 sqlite 是核心所有业务读写都落在同一个 .db 文件后面讲的表结构、派单 SQL 和避坑路径也全部围绕它展开。2. 数据层先立住五张表、索引和连接管理的两种姿势2.1 为什么这个场景选 SQLite 而不是 MySQL同城派单一天的订单量就是几百到一两千行骑手几十人这种规模上 MySQL 属于给课设找麻烦。身边翻车最多的不是业务逻辑而是 MySQL 没启动、账号密码记不住、端口被占。SQLite 是嵌入式数据库整库就是一个 .db 文件Python 自带 sqlite3 模块直接读写没有服务进程、没有账号权限这一层拷走文件夹就是完整交付。ACID 事务它一样支持写操作串行化读操作可以并行对单机派单系统绰绰有余。我跑通这套资源后最直观的感受是做演示前不用先启动任何服务双击运行脚本就能看到订单列表。边界也划清楚如果以后要多门店、多终端同时高频写每秒几十次下单这种SQLite 的写锁会成为瓶颈那时候再迁 MySQL。表结构几乎不用大改迁移成本主要在连接层。选型不是越重越好量级决定工具。2.2 建库脚本骑手、订单、日志、参数、派单记录资源包里的建库脚本是这么设计的我在原版基础上加了字段注释-- 骑手表运力主体status 决定能不能接单active_orders 参与派单评分 CREATE TABLE riders ( rider_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, phone TEXT UNIQUE NOT NULL, status TEXT DEFAULT 空闲, -- 空闲 / 忙碌 / 休息 current_x REAL DEFAULT 0, -- 当前经度地图坐标系近似 current_y REAL DEFAULT 0, -- 当前纬度 max_orders INTEGER DEFAULT 5, -- 最大并行配送单数 active_orders INTEGER DEFAULT 0 -- 正在配送的单数 ); -- 订单表一单两址前端展示和派单评分都用它 CREATE TABLE orders ( order_id INTEGER PRIMARY KEY AUTOINCREMENT, order_no TEXT UNIQUE NOT NULL, customer_name TEXT NOT NULL, customer_phone TEXT NOT NULL, pickup_addr TEXT NOT NULL, delivery_addr TEXT NOT NULL, pickup_x REAL NOT NULL, pickup_y REAL NOT NULL, delivery_x REAL NOT NULL, delivery_y REAL NOT NULL, status TEXT DEFAULT 待分配, -- 待分配/已分配/配送中/已完成/已取消 rider_id INTEGER, create_time TEXT DEFAULT (datetime(now,localtime)), assign_time TEXT, finish_time TEXT, pay_amount REAL DEFAULT 0, FOREIGN KEY (rider_id) REFERENCES riders(rider_id) ); -- 状态日志表每次流转留痕核验阶段全靠它 CREATE TABLE order_status_log ( log_id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, from_status TEXT, to_status TEXT NOT NULL, change_time TEXT DEFAULT (datetime(now,localtime)), remark TEXT ); -- 派单记录表系统派单和人工改派都写一条方便追溯 CREATE TABLE dispatch_log ( dispatch_id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL, rider_id INTEGER NOT NULL, dispatch_by TEXT DEFAULT system, -- system / manual score REAL, dispatch_time TEXT DEFAULT (datetime(now,localtime)) ); -- 参数表权重和半径放配置调参不碰代码 CREATE TABLE config ( key TEXT PRIMARY KEY, value REAL ); INSERT INTO config(key, value) VALUES (dist_weight, 0.8); INSERT INTO config(key, value) VALUES (load_weight, 0.2); INSERT INTO config(key, value) VALUES (search_radius_km, 10); -- 高频查询索引 CREATE INDEX idx_orders_status ON orders(status); CREATE INDEX idx_orders_rider ON orders(rider_id); CREATE INDEX idx_orders_time ON orders(create_time);字段设计上有几个容易忽略的点。骑手表的 current_x / current_y 是派单评分的输入演示环境里用定时任务模拟骑手移动真实环境换成骑手 App 上报即可。订单表把取货点和送货点的坐标都存成 REAL评分时不用再调接口。status 用中文 TEXT 而不是数字枚举代价是多占几个字节换来的排查体验是界面直接显示、不靠查表课设场景值得。状态日志表是数据核验的底牌。原版资源没有这张表我跑通后补上的——没有日志订单状态出问题时只能瞎猜。三个索引里idx_orders_status 支撑待分配队列和状态统计idx_orders_rider 支撑骑手负载统计idx_orders_time 支撑时间排序和批量派单取前 N 单。不要在 status rider_id 上建联合索引状态变化频繁索引维护成本大于收益单列已经够用。2.3 连接管理短连接 统一入口同城派单这类系统最常见的隐患是连接被多个窗口共用。资源包的做法是统一走一个入口函数我在原版基础上加了 WAL 和超时配置# db.py所有模块都从这里拿连接 import sqlite3 from contextlib import contextmanager DB_PATH same_city.db # 与建库脚本保持同一路径 contextmanager def get_db(pathDB_PATH): conn sqlite3.connect(path, timeout5.0) # 等锁最多 5 秒 conn.row_factory sqlite3.Row # 列名访问结果 conn.execute(PRAGMA journal_modeWAL) # 读写并行 conn.execute(PRAGMA busy_timeout3000) # 锁等待 3 秒 conn.execute(PRAGMA foreign_keysON) # 外键约束生效 try: yield conn conn.commit() # 正常结束提交事务 except Exception: conn.rollback() # 异常时全部回滚 raise finally: conn.close() # 用完即关不给长连接留机会每个模块用with get_db() as conn:拿连接函数退出即提交并关闭天然避开两件事事务没提交导致锁悬挂、连接被另一个线程误用。PRAGMA foreign_keysON 需要每条连接都执行所以放在入口里而不是建库脚本里——这一点原版资源容易漏漏掉之后删除骑手会静默失败界面上看起来像 bug。另一种姿势是长连接 threading.local适合界面常驻、频繁下拉刷新的场景在 get_db 里按线程缓存连接、线程结束时关闭。课设阶段短连接足够别两头都上越简单越不容易翻车。3. 派单核心逻辑贪心评分、状态机与事务边界3.1 为什么要写成贪心而不是全局最优订单和骑手是多对多的指派问题教科书上是匈牙利算法找全局最优。但同城派单是动态的订单随机进来骑手位置一直在变全局最优算完现场条件已经变了。资源包选的是贪心每进来一单从空闲且没满载的骑手里挑评分最低的。单量在两三百单以内贪心和全局最优的差距通常只有几个百分点但实现和维护成本低一个量级。评分公式是核心资源包里这样定义score 距离 × 0.8 负载 × 0.2两个细节决定评分是否靠谱。一是负载必须归一化再乘权重active_orders / max_orders 得到一个 0 到 1 的小数再乘 0.2如果直接把 active_orders 乘 0.2负载权重就淹没在距离的公里级数值里等于没起作用。二是距离用欧氏距离近似def calc_distance(x1, y1, x2, y2): 城市尺度下的欧氏距离近似单位为公里量级 return ((x1 - x2) ** 2 (y1 - y2) ** 2) ** 0.5城市内几公里的范围经纬度欧氏距离和真实球面距离的误差可以忽略省去 haversine 的开销。注意第三地图服务商返回的经纬度坐标系不一致评分前先统一坐标系别混着用。3.2 单笔派单模块一个事务包住三步写操作def dispatch_order(conn, order_id): 为单笔订单指派骑手返回 (骑手, 提示) row conn.execute( SELECT * FROM orders WHERE order_id? AND status待分配, (order_id,) ).fetchone() if not row: return None, 订单不存在或已被分配 riders conn.execute( SELECT * FROM riders WHERE status空闲 AND active_orders max_orders ORDER BY current_x, current_y ).fetchall() if not riders: return None, 当前没有可用骑手 # 取配置权重参数表里的值优先于代码默认值 cfg {k: v for k, v in conn.execute(SELECT key, value FROM config)} dist_w cfg.get(dist_weight, 0.8) load_w cfg.get(load_weight, 0.2) best, best_score None, None for r in riders: d calc_distance(row[pickup_x], row[pickup_y], r[current_x], r[current_y]) load r[active_orders] / max(r[max_orders], 1) # 归一化 score d * dist_w load * load_w if best_score is None or score best_score: best, best_score r, score conn.execute( UPDATE orders SET rider_id?, status已分配, assign_timedatetime(now,localtime) WHERE order_id?, (best[rider_id], order_id) ) conn.execute( UPDATE riders SET active_ordersactive_orders1 WHERE rider_id?, (best[rider_id],) ) conn.execute( INSERT INTO order_status_log(order_id, from_status, to_status, remark) VALUES (?, ?, ?, ?), (order_id, 待分配, 已分配, 系统派单) ) return best, OK这段代码有三个边界要点。一是查询可用骑手时就过滤 status 和 active_orders不让评分函数处理无效候选。二是三条 UPDATE/INSERT 必须在同一个事务里提交保证订单状态、骑手负载、日志一起生效函数外面用 get_db 包一层异常时全部回滚不会出现订单已分配但骑手负载没加的半截状态。三是 dispatch_log 可以在同一事务里追加一条记录骑手和评分事后追溯这单是谁派出去的、按什么分数派的人工改派功能里也应该写它。3.3 状态机把流转钉死防止脏数据界面按钮一多订单状态最容易被写乱。资源包的做法是集中管流转ALLOWED_TRANSITIONS { 待分配: [已分配, 已取消], 已分配: [配送中, 已取消], 配送中: [已完成, 已取消], 已完成: [], 已取消: [], } def change_status(conn, order_id, to_status, remark): cur conn.execute( SELECT status FROM orders WHERE order_id?, (order_id,)) row cur.fetchone() if not row: raise ValueError(订单不存在) if to_status not in ALLOWED_TRANSITIONS[row[status]]: raise ValueError(f非法流转: {row[status]} → {to_status}) extra_sql params [to_status, order_id] if to_status 已完成: extra_sql , finish_timedatetime(now,localtime) conn.execute( UPDATE orders SET status? extra_sql WHERE order_id?, params ) conn.execute( INSERT INTO order_status_log(order_id, from_status, to_status, remark) VALUES (?, ?, ?, ?), (order_id, row[status], to_status, remark) )所有界面按钮都只调 change_status不直接写 UPDATE。非法流转直接抛异常演示时最怕的就是已取消订单又进入配送中这种尴尬。注意已完成分支里顺带写 finish_time别留在别处补补的那次往往就忘了。4. 运行期避坑记录锁库、跨线程和中文乱码的排查路径4.1 database is locked 频繁出现单机也能看到并发现象批量派单按钮一点界面抛 sqlite3.OperationalError: database is locked有时候重试一下又好了有时候一直卡死。原因默认 journal 模式下读写不能并行某个连接开了事务没提交别的连接写就排队超时。最常见的是长连接挂着不关事务悬着不放。解决三件事一起做。连接里执行 PRAGMA journal_modeWAL 和 PRAGMA busy_timeout3000合并到 get_db 入口所有写操作走短连接、用完即关批量派单不要在一个事务里循环几百个订单拆成每 50 单一批。提示WAL 模式下仍然只有一个写者busy_timeout 只是让写操作排队等待不是并发写。真到了高频并发写现在就该考虑迁移 MySQL。4.2 跨线程共享连接直接报线程错误现象界面里起线程刷新订单列表把主线程建立的 conn 传进子线程抛 ProgrammingError: SQLite objects created in a thread can only be used in that thread。原因sqlite3 连接默认绑定创建它的线程跨线程使用属于未定义行为库直接拦截不给你出错的机会。解决每个线程独立开连接。子线程里重新调用 get_db()用完关闭数据量小短连接开销可忽略。不要偷懒共享连接省下的代码量会在排错时加倍还回去。4.3 导出的 CSV 在表格软件里全是乱码现象开发工具打开 CSV 正常发给别人用表格软件打开中文全变成乱码第一行尤其明显。原因Python open 默认 utf-8 无 BOM 写文件某些表格软件按本地编码解码中文自然全翻。解决写文件时加 encodingutf-8-sig带上 BOM 头表格软件就能正确识别。这不只是派单系统的问题所有导出功能都建议这么写with open(orders.csv, w, encodingutf-8-sig, newline) as f: writer csv.writer(f) writer.writerow([订单号, 状态, 骑手]) # 按查询结果逐行写入4.4 打包后报 no such table现象源码跑通用 PyInstaller 打包成 exe 后启动报 no such table: orders同目录的 .db 文件明明就在。原因打包时把仓库里的 .db 也收进去了程序运行时解包到临时目录每次启动路径都会变数据库被当成静态模板写操作写到了另一个地方。解决把数据库路径改成程序根目录用 sys.executable 的所在目录判断启动时检查文件不存在就从模板复制一份。千万别用file判断打包后的路径临时目录靠不住。4.5 按时间排序的订单顺序乱了现象订单列表按 create_time 排序9:50 的单排在 10:05 的单后面看着像排序 bug。原因时间存成了 2025/6/9 9:50 这类没补零的字符串字典序和自然时间序不一致字符串排序结果就错位。解决统一存储 YYYY-MM-DD HH:MM:SS插入时用 datetime(now,localtime) 保证格式。存量脏数据写一条 UPDATE 重新格式化别在 SELECT 里做字符串替换那是治标不治本。5. 派单调优与数据核验批量派单和三组校验 SQL5.1 批量派单一个事务包住一批def batch_dispatch(conn, limit50): pending conn.execute( SELECT order_id FROM orders WHERE status待分配 ORDER BY create_time LIMIT ?, (limit,) ).fetchall() for p in pending: dispatch_order(conn, p[order_id])批次由外层 with get_db() 包同一个事务中途出错全部回滚避免派了一半的死状态。limit 参数控制批次大小配合前面锁的问题50 单一批比较稳。ORDER BY create_time 保证先来的订单先被派配合订单表上的时间索引取前 N 单不会全表扫。5.2 三组校验 SQL把数据查穿-- 1. 状态与骑手不一致的脏单 SELECT order_id, status, rider_id FROM orders WHERE status ! 待分配 AND rider_id IS NULL; -- 2. 骑手负载分布找出过载骑手 SELECT rider_id, COUNT(*) AS cnt FROM orders WHERE status IN (已分配, 配送中) GROUP BY rider_id ORDER BY cnt DESC; -- 3. 状态日志缺失的订单说明有人绕过状态机直接改库 SELECT o.order_id, o.status, COUNT(l.log_id) AS log_cnt FROM orders o LEFT JOIN order_status_log l ON o.order_id l.order_id WHERE o.status NOT IN (已完成, 已取消) GROUP BY o.order_id HAVING log_cnt 0;第一组查脏单状态非待分配却没有骑手基本是改派逻辑没写干净。第二组看负载骑手手里单数长期顶满评分权重需要调。第三组最关键日志缺失意味着某处代码直接 UPDATE 了 orders 表绕过了状态机这类问题不查出来后面所有统计都是黑匣子。5.3 接手任何源码包先核验再改代码我最初拿到这套资源时第一件事就是跑上面三组 SQL结果发现自带数据里真有两条状态与骑手不一致的脏单——不是程序运行出来的是建库脚本里某个状态流转漏写了日志。从那以后我每次接手别人的课设源码包都强制先跑一遍校验 SQL确认状态机和数据对得上才开始动界面代码省下的排错时间比什么都值。这套包里的建库脚本、派单模块和校验 SQL 是一体的表名和字段按自己的演示数据改一改就能用评分权重放进 config 表后调参连代码都不用碰。希望帮到你。本文还有配套的精品资源点击获取