ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

PHP家政派单系统源码解析:订单调度与状态机实战

PHP家政派单系统源码解析:订单调度与状态机实战 简介基于PHP的得推家政派单系统源码是一套面向家政服务行业的完整项目代码适合PHP开发者、中小企业及希望掌握Web应用全流程的初学者。系统覆盖客户下单、工单分配、服务人员接单、订单追踪与后台管理等核心环节可直接用于二次开发或业务原型搭建。压缩包共2000个文件约22.65MB其中以1354个php文件为主承担业务逻辑与接口处理另有359个html页面模板、168个js脚本和52个css样式表完成前端展示与交互还包含JSON、TXT、MD等配置与说明文档。目录按MVC分层组织包含入口、配置、模型、视图、控制器等模块便于检索与二次开发开发者可借此研究PHP的MVC架构学习路由分发、数据库读写、用户认证与权限控制等常用技术。资源已在CSDN上被109人学习浏览适合想深入理解家政派单业务、提升PHP工程能力的开发者参考实践。1. 得推家政派单系统是什么一套PHP写的调度引擎而不是订单管理家政派单系统在O2O项目里是个典型品类用户下单、服务人员接单、上门服务、完工结算。表面看是订单管理实际核心是调度。得推家政派单系统这套PHP源码在从业者圈子里流传很广很多外包公司和家政创业团队拿它当二次开发底子。它的价值不在于订单表多花哨而在于把手动派单、抢单、自动指派、超时转派这一整套调度逻辑用PHP完整落地了。这篇文我按接手这套源码的视角来讲先看数据表结构判断技术栈再把派单距离计算和权重算法拆开接着处理订单状态机的非法跳转问题然后走一遍本地部署和排错最后落到代码审计和接口规范改造上。适合准备接手PHP家政项目、或者想理解派单系统实现的开发者。2. 看懂得推源码的第一步PHP技术栈判断与四张核心表2.1 先确认它跑在哪个PHP框架上拿到zip源码先解压看目录结构。常见做法是看根目录有没有think、application、vendor这些目录。公开流传的得推系统版本里多数基于ThinkPHP 5.x改造也有用原生PHP写的轻量分支。判断方法很直接ThinkPHP的入口在public/index.php原生PHP通常在根目录直接放index.php且没有命名空间。再用composer文件确认依赖grep -n think\\\|illuminate\|Slim -r vendor/composer/installed.json | head -20输出里有think\framework就是ThinkPHP系看到illuminate/database说明用Eloquent做ORM。这一步决定了后续改派单逻辑是动控制器加模型还是直接改SQL。我遇到过把ThinkPHP项目当原生PHP改的namespace没带上最后整个控制器加载失败白屏半天。2.2 四张核心表用户、服务人员、订单、派单日志派单系统的数据模型核心就四张表按常见结构写出来CREATE TABLE dt_user ( id int(11) NOT NULL AUTO_INCREMENT, mobile varchar(20) NOT NULL, openid varchar(64) DEFAULT NULL, nickname varchar(50) DEFAULT NULL, status tinyint(1) DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (id), KEY idx_mobile (mobile) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dt_worker ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL, mobile varchar(20) NOT NULL, lng decimal(10,6) NOT NULL DEFAULT 0.000000 COMMENT 经度, lat decimal(10,6) NOT NULL DEFAULT 0.000000 COMMENT 纬度, score decimal(3,1) DEFAULT 5.0 COMMENT 评分, busy_count int(11) DEFAULT 0 COMMENT 进行中订单数, status tinyint(1) DEFAULT 1 COMMENT 1空闲 0休假, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dt_order ( id int(11) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL, user_id int(11) NOT NULL, worker_id int(11) DEFAULT 0, service_type tinyint(1) DEFAULT 1, address varchar(255) NOT NULL, lng decimal(10,6) NOT NULL, lat decimal(10,6) NOT NULL, appoint_time datetime DEFAULT NULL COMMENT 预约上门时间, status tinyint(1) DEFAULT 0, remark varchar(500) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_status_time (status,appoint_time), KEY idx_worker (worker_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE dt_dispatch_log ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, worker_id int(11) NOT NULL, action varchar(20) NOT NULL COMMENT assign/grab/transfer/cancel, reason varchar(255) DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段选择上有几个点值得注意经纬度用decimal(10,6)不要用float不然距离计算会积累误差状态字段统一用tinyint别用varchar索引效率和比较开销差距明显dt_order上建了(status, appoint_time)复合索引因为后台最常见查询就是按状态加预约时间范围筛当天订单。dt_dispatch_log是排障关键很多人看代码看不懂“为什么派给了这个人”查这张表就知道是自动派单、手动改派还是抢单成功。2.3 状态字段的映射别在业务代码里写魔法数字拿到的版本如果状态散落在控制器里比如if ($order[status] 3)我一般会先抽一层常量。状态值参考值含义触发场景0待派单用户下单完成1已派单自动指派或抢单成功2服务中服务人员点击开始3待支付完工提交账单4已完成用户支付确认5已取消用户取消或超时取消6转派中原服务人员超时未接把这组映射放到config/order.php或者常量类里统一引用。好处是后面加状态只改一处而且结合dispatch_log能完整回溯某订单的生命周期。3. 派单核心逻辑手单、抢单与自动派单的距离与权重计算3.1 三种派单模式的差异与适用场景派单模块通常同时支持三种模式。手动派单最简单后台管理员看到新订单在地图上勾选一个空闲服务人员改worker_id和状态。抢单是并发问题重灾区新订单推给附近若干服务人员先到先得。自动派单是核心卖点系统按距离、评分、当前负载算综合分把订单派给最合适的人。抢单适合服务人员活跃度高的平台自动派单适合有稳定全职队伍的商家。得推这类源码默认配置通常是管理员在后台切换模式。切自动派单前要注意候选集必须只筛status1且busy_count小于阈值的人不然会出现“派了单但服务人员根本过不来”的情况。3.2 距离计算优先用Haversine公式而不是MySQL空间函数计算距离的第一步是筛选候选服务人员。MySQL 5.7以上支持ST_Distance_Sphere但本地开发环境经常是5.6或者没开空间扩展。为了让源码在更多环境里跑得动我一般直接在SQL里用Haversine公式算距离PHP侧再做二次决策SELECT w.id, w.name, w.lng, w.lat, w.score, w.busy_count, 6371 * 2 * ASIN(SQRT(POWER(SIN((39.9042 - w.lat) * PI() / 360), 2) COS(39.9042 * PI() / 180) * COS(w.lat * PI() / 180) * POWER(SIN((116.4074 - w.lng) * PI() / 360), 2))) AS distance FROM dt_worker w WHERE w.status 1 AND w.busy_count 3 ORDER BY distance ASC LIMIT 20;参数说明39.9042和116.4074是订单地址的纬度、经度实际替换成$order[lat]和$order[lng]6371是地球半径公里数busy_count 3表示同时进行中的订单最多两单第三个订单来的时候不参与候选。LIMIT 20控制候选集大小防止后续PHP内存里排序数据量过大。这套写法的好处是兼容旧版MySQL五万以内的服务人员表走(status, busy_count)索引后查询耗时基本在十几毫秒。服务人员过十万再考虑上MySQL空间索引或者直接换ES。3.3 综合得分距离加评分加负载的加权公式候选集取回来以后在PHP里做加权排序。常见权重分配是距离占大头评分次之负载最后因为家政服务上门成本远高于评分和等待成本function calcCandidateScore($distance, $score, $busyCount) { $distanceScore 60 / (1 $distance); $scoreScore 30 * ($score / 5.0); $loadScore 10 / (1 $busyCount); return $distanceScore $scoreScore $loadScore; } $candidates $pdo-query($sql)-fetchAll(PDO::FETCH_ASSOC); foreach ($candidates as $worker) { $worker[calc_score] calcCandidateScore( $worker[distance], $worker[score], $worker[busy_count] ); } usort($candidates, function($a, $b) { return $b[calc_score] $a[calc_score]; }); $bestWorker $candidates[0];逻辑说明60 / (1 distance)保证距离为0公里时满分605公里时得10分衰减曲线平滑30 * (score / 5.0)把评分5分制归一化到30分10 / (1 busyCount)让手上没单的人拿满分两单在身时只有3.3分。三个分相加总分高的人优先派单。这套公式是通用方案具体权重看业务调节。如果平台主要做月嫂这类高客单价服务评分权重应该提到50如果做日常保洁距离权重可以压到50以下。得推源码里如果只有距离排序没有评分参数二次开发时按这个公式补就行。3.4 抢单并发控制和超时转派php队列与Redis选型抢单流程里最怕两个服务人员同时抢同一单。后请求的人把前一个的worker_id覆盖掉数据就错了。常见做法是用Redis的set nx ex当分布式锁$lockKey order:grab: . $orderId; $locked $redis-set($lockKey, $workerId, [nx, ex 5]); if ($locked) { $pdo-beginTransaction(); $stmt $pdo-prepare( UPDATE dt_order SET worker_id ?, status 1 WHERE id ? AND status 0 ); $stmt-execute([$workerId, $orderId]); if ($stmt-rowCount() 1) { // 记录抢单日志到 dt_dispatch_log $pdo-commit(); $redis-del($lockKey); } else { $pdo-rollBack(); $redis-del($lockKey); } } else { // 返回提示信息订单已被抢 }注意UPDATE ... WHERE status 0这层条件不能省它和Redis锁构成双保险。万一锁提前失效数据库层的状态判断也能兜底防止重复指派。自动派单后如果服务人员超过10分钟不接单需要自动转派。常见做法是定时任务扫表SELECT id FROM dt_order WHERE status 0 AND create_time DATE_SUB(NOW(), INTERVAL 10 MINUTE) LIMIT 50;扫出来的订单重新进入派单流程。订单量大的时候把订单ID丢进Redis队列让后台CLI进程消费。关联php redis 消费组的典型用法是php think queue:work --queuedispatch_timeout --tries3这比crontab每60秒扫一次表及时也避免高峰时段扫描行数过多锁住dt_order表。4. 订单状态机从下单到完工的PHP状态流转实现4.1 状态机到底在防什么问题订单状态字段如果到处直接update很容易出现“已取消的订单被改成已完成”这类非法跳转。比如用户在支付页把订单取消了支付回调因为是异步的比取消请求晚到又把订单状态改了回来。这个问题的标准解法是引入状态机每次改状态前先校验当前状态能否跳转到目标状态。4.2 用PHP类封装状态流转表我一般把状态机抽成一个单独的类所有改状态操作必须通过它class OrderStateMachine { const CREATED 0; // 待派单 const DISPATCHED 1; // 已派单 const SERVING 2; // 服务中 const WAIT_PAY 3; // 待支付 const DONE 4; // 已完成 const CANCELED 5; // 已取消 const RESENDING 6; // 转派中 private static array $allowed [ self::CREATED [self::DISPATCHED, self::CANCELED, self::RESENDING], self::RESENDING [self::DISPATCHED, self::CANCELED], self::DISPATCHED [self::SERVING, self::CANCELED, self::RESENDING], self::SERVING [self::WAIT_PAY], self::WAIT_PAY [self::DONE, self::CANCELED], ]; public static function can(int $from, int $to): bool { return in_array($to, self::$allowed[$from] ?? [], true); } public static function transition(int $orderId, int $from, int $to, PDO $pdo): bool { if (!self::can($from, $to)) { throw new RuntimeException(非法状态跳转: {$from} - {$to}); } $stmt $pdo-prepare( UPDATE dt_order SET status ? WHERE id ? AND status ? ); $stmt-execute([$to, $orderId, $from]); return $stmt-rowCount() 1; } }参数说明$allowed数组定义每个状态能跳转到哪里CREATED不能直接跳到SERVING必须经过派单DISPATCHED允许跳回RESENDING这是转派入口。transition方法里的WHERE status ?是乐观锁rowCount为0说明当前状态已经被别人改过这次跳转直接失败。4.3 改状态时要做的配套动作不止一条UPDATE状态跳转成功后还有很多配套动作要做。典型的几条配套动作执行内容失败影响状态变更日志写入dt_order_status_log无法审计消息通知微信模板消息推送给用户或服务人员用户体验断裂结算触发WAIT_PAY - DONE后算服务人员佣金财务对不上扣减额度取消订单返还用户优惠券用户投诉CREATE TABLE dt_order_status_log ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, from_status tinyint(1) NOT NULL, to_status tinyint(1) NOT NULL, operator varchar(50) DEFAULT COMMENT user/admin/system, remark varchar(255) DEFAULT , create_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;改状态的核心原则是日志先落事务再提交。如果通知失败日志里能查到状态已经变了运营可以手动补发如果先发通知再改状态通知发了但状态没改成用户那边就变成“收到已接单消息但订单还挂着”。转派场景额外要注意转派不能简单把worker_id清空回到待派单那样会失去转派轨迹。正确流程是把状态改成RESENDING在dispatch_log写一条transfer记录再进派单流程生成新的候选。这样排障时能看清“第一任服务人员是谁、为什么转掉”。5. 本地部署跑通得推源码环境要求、伪静态与PHP排错清单5.1 环境组合PHP版本别追求最新组件推荐版本说明PHP7.4兼容性最稳5.6以下跑不动新语法MySQL5.7utf8mb4支持完善Web服务器Nginx 1.18伪静态规则简单Redis5.0抢单锁、验证码缓存、队列都要用操作系统CentOS 7 / Ubuntu 20.04生产环境最常见刚拿到源码不建议直接上PHP 8.x。老系统的each()、create_function()这类函数在PHP 8里直接被移除打开页面就是白屏。先用PHP 7.4跑通再评估升级成本。5.2 Nginx伪静态与public入口配置ThinkPHP项目的正确做法是站点根目录指向public/而不是项目根目录。这样源码里的application、runtime、vendor通过URL直接访问不到server { listen 80; server_name your-domain.com; root /www/wwwroot/dt-home/public; index index.php index.html; location / { if (!-e $request_filename) { rewrite ^(.*)$ /index.php?s$1 last; } } location ~ \.php$ { fastcgi_pass 127.0.0.1:9000; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } location ~* \.(sql|log|ini)$ { deny all; } }最后一段deny all很关键。解压包里经常带着install.sql、README.txt如果不屏蔽别人在浏览器里直接就能访问下载数据库结构。5.3 安装过程中三个常见PHP错误处理场景第一个坑PHP版本过高导致废弃函数报错。处理思路是先把php.ini里的display_errors打开清掉框架的error_reporting覆盖看到具体报错再去替换废弃函数。php -v php -m | grep -iE pdo|mysql|redis|curl第二个坑伪静态没配好首页能开点任何详情页都404。原因就是location /里的rewrite没生效。用curl验证一下curl -I http://127.0.0.1/index.php?s/order/detailid1返回200说明伪静态正常返回404查Nginx的rewrite规则是不是写成了last而不是break。第三个坑目录权限导致上传图片失败。不要图省事直接chmod 777正确的做法是把runtime和uploads目录的所有者改成运行用户chown -R www:www /www/wwwroot/dt-home chmod -R 755 /www/wwwroot/dt-home/runtime chmod -R 755 /www/wwwroot/dt-home/uploadsNginx是www用户跑的就这么设Docker场景下是容器里的www-data用户。5.4 数据库导入与配置文件位置数据库导入直接用命令行最可靠别在phpMyAdmin里传大SQL文件mysql -uroot -p -e CREATE DATABASE dt_home DEFAULT CHARSET utf8mb4; mysql -uroot -p dt_home install.sql配置文件的常见位置是config/database.php或者.env文件。ThinkPHP 5.1之后.env里的写法APP_DEBUG true [DATABASE] TYPE mysql HOSTNAME 127.0.0.1 DATABASE dt_home USERNAME root PASSWORD your_password HOSTPORT 3306 PREFIX dt_改完配置先访问/admin看后台能否登录再测试下单接口。前端页面正常但接口500多半是APP_DEBUG没开错误信息被吞了。6. 二次开发与代码审计接口数组规范、队列消费与安全加固把一套源码改成别人能接手的项目核心是两件事接口返回标准统一安全漏洞堵住。先说接口数组对象规范。得推旧代码里有的接口返回纯字符串有的返回数字编码前端接起来很痛苦。统一改成结构化数组public function apiResult(int $code, string $msg, array $data []): string { return json_encode([ code $code, msg $msg, data $data ], JSON_UNESCAPED_UNICODE); }code用0表示成功非0表示具体错误码msg直接放给用户看的提示data永远是一个数组。这样小程序端、H5端和后台管理端共用一个接口层不用各写一套解析。代码审计重点看三处。SQL注入搜索$_GET、$_POST直接拼接进SQL的位置全部改成预处理语句$stmt $pdo-prepare(SELECT * FROM dt_order WHERE id ? AND user_id ?); $stmt-execute([$orderId, $userId]);越权问题得推这类系统后台订单详情页如果只验证了登录态没验证操作人角色普通用户改个order_id就能看到别人订单明本文还有配套的精品资源点击获取
返回列表