news 2026/10/6 4:00:26

PHP网约车H5源码实战:乘客端与司机端双端协同开发指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PHP网约车H5源码实战:乘客端与司机端双端协同开发指南

简介:这是一套基于Yii框架开发的PHP网约车H5打车系统源码,面向希望快速搭建或二次开发打车平台的开发者与创业团队,乘客端与司机端功能齐备,可满足出行类项目的完整业务需求。资源包共约2000个文件,压缩后139.82MB,涵盖591个md说明文档、441个js脚本、208个html页面、169个json配置、74个css样式及36个sql数据库脚本等,前后台结构清晰,便于按模块查阅与调试。三端共用同一数据库,后台入口为/admin,默认账号admin、密码123456,数据库连接可在common\config/main-local.php中修改,并附有后台、前台与司机端演示地址供参考。目前已有629人学习下载,适合具备一定PHP与Yii基础、需要完整打车解决方案的开发者,可据此快速理解订单调度、司机接单与后台管理等核心流程,节省从零搭建的时间成本。

1. 从一份 PHP 网约车 H5 源码说起:乘客端与司机端到底怎么协同

前阵子接了个本地出行项目,客户预算有限,要求两周内出一个能跑通「乘客下单—司机接单—行程结束—计费」闭环的 H5 打车系统。市面上 SaaS 按单抽成太狠,自研又来不及,最后我翻出一套 PHP 网约车 H5 打车系统源码,乘客端与司机端完整解决方案,前后端全在包里,部署完当天就跑通了模拟订单。这套东西本质是一个基于 PHP + MySQL 的 H5 双端应用:乘客用手机浏览器打开就能叫车,司机端同样走 H5,不依赖 App 商店审核,省掉了 iOS 签名和安卓渠道那一堆破事。它解决的核心问题是「轻量级网约车业务的最小可行闭环」——订单状态机、司机抢单、位置上报、计价规则、支付回调,这些模块源码里都有现成实现。适合谁?中小团队做区域化出行、企业内部用车调度、或者想研究网约车业务逻辑的 PHP 开发者。不适合直接拿去跟滴滴对打,但做垂直场景足够用。下面我按实际拆包和部署的顺序,把这份源码的关键路径讲透。

2. 环境搭建与数据库初始化:把 PHP 跑起来的第一道坎

2.1 PHP 版本与扩展依赖的选型理由

拿到源码先别急着往服务器扔,本地跑通再上生产,这是我踩过无数次坑之后的铁律。这套 PHP 网约车 H5 源码对运行环境有明确要求,常见做法是 PHP 7.4 或 PHP 8.0/8.1,具体看源码里用的是 mysqli 还是 PDO。我一般先看composer.json和入口文件,确认框架是 ThinkPHP 还是原生 PHP。如果是 ThinkPHP 5.x,PHP 版本别超过 7.4,否则一些老扩展会报 deprecated 警告;如果是原生 PHP 写的,PHP 8.1 也能跑,但要注意each()这类函数在 8.0 已经移除。

必须装的扩展清单:pdo_mysql(数据库连接)、curl(调地图和支付接口)、json(接口返回)、mbstring(中文处理)、gd或imagick(司机头像和行程截图)、redis(可选,用于订单队列和司机位置缓存)。缺一个都可能让某个接口静默失败,接口返回空但日志里啥也没有,这种玄学问题八成是扩展没开。

# 检查 PHP 版本和已装扩展 php -v php -m | grep -E 'pdo_mysql|curl|json|mbstring|gd|redis' # 如果缺扩展,以 Ubuntu 为例安装 sudo apt install php7.4-mysql php7.4-curl php7.4-mbstring php7.4-gd php7.4-redis sudo systemctl restart php7.4-fpm

上面命令先确认版本,再按需补扩展。php -m列出的模块名要和源码require的对上,大小写不敏感但拼写必须一致。装完记得重启 PHP-FPM,否则phpinfo()里看不到新扩展。

2.2 数据库导入与配置文件修改

源码包里通常有个sql目录,里面是.sql文件。导入前先建库,字符集用utf8mb4,不然司机姓名里的生僻字和乘客备注里的 emoji 会变问号。

# 创建数据库并导入表结构 mysql -u root -p -e "CREATE DATABASE ride_hailing DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" mysql -u root -p ride_hailing < ./sql/ride_hailing.sql # 确认表数量 mysql -u root -p ride_hailing -e "SHOW TABLES;"

导入完成后,找到配置文件,一般在config/database.php或.env文件里。把数据库地址、库名、用户名、密码改成自己的。有些源码把配置写死在application/database.php,改完记得清缓存。

// config/database.php 典型配置 return [ 'type' => 'mysql', 'hostname' => '127.0.0.1', 'database' => 'ride_hailing', 'username' => 'ride_user', 'password' => 'your_password', 'hostport' => '3306', 'charset' => 'utf8mb4', 'prefix' => 'rh_', // 表前缀,按源码实际填 ];

参数说明:prefix是表前缀,源码里写 SQL 时用了rh_order这种形式,配置里必须一致,否则查不到表。hostname用127.0.0.1比localhost稳,避免 socket 连接问题。改完配置后访问入口文件,如果报「数据库连接失败」,先看runtime/log下的日志,再确认 MySQL 用户权限。

提示:导入 SQL 时如果报Specified key was too long,说明 MySQL 版本低于 5.7 或字符集不是 utf8mb4,把建表语句里的varchar(255)主键改成varchar(191)即可。

3. 乘客端下单流程拆解:从选上车点到生成订单

3.1 地图选点与地址解析的接口对接

乘客端 H5 的核心交互是「选上车点—选目的地—确认叫车」。源码里一般用高德或腾讯地图的 JS API 做选点,后端用对应的 Web 服务 API 做逆地理编码。我拆的这套用的是高德,前端amap加载后监听click事件拿经纬度,再调后端/api/address/regeo换文字地址。

// 乘客端选点核心逻辑(简化) const map = new AMap.Map('mapContainer', { zoom: 15 }); let pickup = { lng: 0, lat: 0, address: '' }; map.on('click', async (e) => { pickup.lng = e.lnglat.getLng(); pickup.lat = e.lnglat.getLat(); // 调后端逆地理编码 const res = await fetch('/api/address/regeo?lng=' + pickup.lng + '&lat=' + pickup.lat); const data = await res.json(); pickup.address = data.regeocode.formatted_address; document.getElementById('pickupInput').value = pickup.address; });

这段代码的关键是map.on('click')拿到经纬度后立刻请求后端,不要在前端直接调高德 Web API,否则你的 key 会暴露在浏览器里,被人刷爆配额。后端/api/address/regeo里用 curl 调高德接口,把 key 放在服务端配置文件。

// 后端逆地理编码接口 public function regeo() { $lng = input('lng'); $lat = input('lat'); $key = config('amap.web_key'); // 从配置读,不写死 $url = "https://restapi.amap.com/v3/geocode/regeo?location={$lng},{$lat}&key={$key}"; $result = json_decode(file_get_contents($url), true); if ($result['status'] != 1) { return json(['code' => 400, 'msg' => '地址解析失败']); } return json(['code' => 200, 'data' => $result]); }

参数说明:location格式是「经度,纬度」,顺序别反了,反了会解析到南极。key必须从配置读,源码里如果写死了,第一件事就是挪到配置文件。返回的regeocode.formatted_address是完整地址,addressComponent里有省市区,计价规则可能按区域不同,这些字段后面算费用会用到。

3.2 订单状态机与计价规则实现

订单表是整个系统的核心,状态字段一般用数字表示:0 待接单、1 已接单、2 行程中、3 已完成、4 已取消。乘客点「确认叫车」后,后端插入一条status=0的记录,同时把订单 ID 推给司机端。

// 创建订单 public function createOrder() { $data = [ 'order_no' => date('YmdHis') . rand(1000, 9999), 'passenger_id' => session('user_id'), 'pickup_lng' => input('pickup_lng'), 'pickup_lat' => input('pickup_lat'), 'pickup_addr' => input('pickup_addr'), 'dest_lng' => input('dest_lng'), 'dest_lat' => input('dest_lat'), 'dest_addr' => input('dest_addr'), 'status' => 0, 'create_time' => time(), ]; $orderId = Db::name('order')->insertGetId($data); // 推送到司机端待接单列表(Redis 队列) Redis::lpush('waiting_orders', $orderId); return json(['code' => 200, 'order_id' => $orderId]); }

计价规则通常在config/price.php或数据库price_rule表里。常见结构是起步价 + 里程费 + 时长费。里程按地图 API 返回的distance算,时长按duration算。注意:地图返回的距离是驾车距离,不是直线距离,计价必须用驾车距离,否则乘客会投诉绕路。

// 预估费用 public function estimate() { $distance = input('distance'); // 米 $duration = input('duration'); // 秒 $rule = Db::name('price_rule')->where('city_id', input('city_id'))->find(); $fee = $rule['start_fee']; if ($distance > $rule['start_distance']) { $fee += ceil(($distance - $rule['start_distance']) / 1000) * $rule['per_km']; } $fee += ceil($duration / 60) * $rule['per_min']; return json(['code' => 200, 'fee' => round($fee, 2)]); }

参数说明:start_distance是起步包含的米数,per_km是每公里单价,per_min是每分钟时长费。ceil向上取整,避免出现 0.3 公里这种零头。实际落地时还要加夜间费、远途费,源码里如果有price_rule表,直接加字段扩展即可。

注意:订单号生成用date + rand在高并发下可能重复,生产环境建议用uniqid或雪花算法,源码里如果没做唯一索引,自己加一个。

4. 司机端接单与位置上报:实时性的关键实现

4.1 抢单逻辑与并发控制

司机端 H5 打开后轮询或走 WebSocket 拿待接单列表。源码里常见做法是每 3 秒调一次/api/driver/waiting,返回附近订单。司机点「接单」时调/api/driver/accept,这里有个并发问题:两个司机同时点同一单,必须保证只有一个成功。

// 司机接单,用乐观锁防并发 public function accept() { $orderId = input('order_id'); $driverId = session('driver_id'); // 只有 status=0 的订单才能被接 $affected = Db::name('order') ->where('id', $orderId) ->where('status', 0) ->update(['status' => 1, 'driver_id' => $driverId, 'accept_time' => time()]); if ($affected == 0) { return json(['code' => 400, 'msg' => '手慢了,订单已被抢']); } return json(['code' => 200, 'msg' => '接单成功']); }

这段代码的精髓在where('status', 0)和update的组合,MySQL 的行锁保证只有一个请求能把 status 从 0 改成 1,其他请求affected为 0。这比先查再改靠谱得多,先查再改在并发下必然翻车。参数上accept_time记录接单时间,后面算司机响应率会用到。

4.2 位置上报与轨迹存储

司机在行程中要持续上报位置,乘客端才能看到车到哪了。H5 用navigator.geolocation.watchPosition拿经纬度,每 5 到 10 秒 POST 一次到后端。

// 司机端位置上报 if (navigator.geolocation) { navigator.geolocation.watchPosition((pos) => { fetch('/api/driver/report', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ order_id: currentOrderId, lng: pos.coords.longitude, lat: pos.coords.latitude, speed: pos.coords.speed }) }); }, (err) => { console.error('定位失败', err); }, { enableHighAccuracy: true, maximumAge: 5000 }); }

后端收到后,最新位置写 Redis(driver:location:{driver_id}),同时追加到轨迹表。轨迹表数据量大,建议按天分表或只存最近 7 天。

// 位置上报接口 public function report() { $data = json_decode(file_get_contents('php://input'), true); $driverId = session('driver_id'); // 更新 Redis 最新位置,过期时间 30 秒 Redis::setex("driver:location:{$driverId}", 30, json_encode($data)); // 追加轨迹 Db::name('track')->insert([ 'order_id' => $data['order_id'], 'driver_id' => $driverId, 'lng' => $data['lng'], 'lat' => $data['lat'], 'speed' => $data['speed'] ?? 0, 'create_time'=> time(), ]); return json(['code' => 200]); }

参数说明:enableHighAccuracy: true让浏览器用 GPS 而不是基站定位,精度高但耗电;maximumAge: 5000表示 5 秒内的缓存位置可以直接用,减少定位调用。Redis 的setex30 秒过期,乘客端查司机位置时如果 Redis 没有,说明司机掉线了,前端要提示「司机位置更新中」。

提示:H5 在后台标签页时watchPosition会被浏览器节流甚至暂停,这是 H5 打车的天然短板。源码里如果没做处理,司机切到其他 App 再回来,位置会断档。常见做法是提示司机保持页面在前台,或者用 WebSocket 心跳检测掉线。

5. 避坑与常见问题排查:那些让我熬夜的坑

5.1 订单状态卡在「行程中」不结束

现象:乘客到达目的地,司机点「结束行程」,但订单状态还是 2,乘客端一直显示行程中。原因:结束行程接口里先算了费用,再更新状态,如果费用计算抛异常(比如地图 API 超时),状态更新就被跳过。解决:把状态更新和费用计算拆开,状态先改,费用异步补算,或者用事务包起来,费用算失败就回滚并返回明确错误。

5.2 司机端定位权限被浏览器拦截

现象:司机打开 H5 后点「开始接单」,提示定位失败,但手机 GPS 是开的。原因:H5 获取定位必须走 HTTPS,HTTP 下浏览器直接拒绝,且不报具体错误。解决:生产环境必须配 SSL 证书,本地调试用localhost或127.0.0.1例外。另外 iOS Safari 要求用户手势触发后才能请求定位,不能页面加载就自动调。

5.3 支付回调验签失败

现象:乘客支付成功,但订单状态没变,支付平台回调日志显示验签失败。原因:回调地址用了内网地址,支付平台访问不到;或者验签时用了错误的密钥(沙箱密钥用到了生产)。解决:回调地址必须是公网可访问的 HTTPS 地址,验签密钥按环境区分,回调里先记原始日志再验签,方便对账。

5.4 高德地图 key 配额被刷爆

现象:某天开始所有地址解析都返回INVALID_USER_SCODE,控制台显示配额用尽。原因:前端 JS API 的 key 没设域名白名单,被人扒走刷了。解决:JS API 的 key 在控制台绑定域名,Web 服务 key 只放后端,两个 key 分开申请,别混用。

5.5 订单列表分页越翻越慢

现象:司机端待接单列表第一页很快,翻到后面几页要好几秒。原因:limit偏移量大时 MySQL 要扫描前面所有行。解决:用where id > 上一页最后一条 id代替limit offset,或者给status和create_time建联合索引。

6. 二次开发与接口扩展:把源码改成自己的业务

源码跑通只是起点,真正落地一定要改。我一般先做三件事:把计价规则抽成独立服务、把订单状态机加上日志、把司机和乘客的接口做版本隔离。

计价规则抽独立服务,是因为不同城市、不同时段价格不一样,写在 PHP 里改一次要发一次版。抽成配置表加缓存,运营自己就能改。

// 计价服务,带缓存 class PriceService { public static function calc($cityId, $distance, $duration, $time) { $cacheKey = "price_rule:{$cityId}"; $rule = Redis::get($cacheKey); if (!$rule) { $rule = Db::name('price_rule')->where('city_id', $cityId)->find(); Redis::setex($cacheKey, 3600, json_encode($rule)); } else { $rule = json_decode($rule, true); } // 夜间加价 $hour = (int)date('H', $time); $nightFee = ($hour >= 23 || $hour < 5) ? $rule['night_fee'] : 0; $fee = $rule['start_fee'] + $nightFee; if ($distance > $rule['start_distance']) { $fee += ceil(($distance - $rule['start_distance']) / 1000) * $rule['per_km']; } $fee += ceil($duration / 60) * $rule['per_min']; return round($fee, 2); } }

订单状态机加日志,是为了排查「谁在什么时候把状态改了」。建一张order_status_log表,每次状态变更插一条,字段包括订单 ID、旧状态、新状态、操作人、时间。出问题时一查就知道是司机点的还是系统自动改的。

接口版本隔离,是在 URL 里加/v1/、/v2/,乘客端和司机端分开。这样改司机端接口不会影响乘客端,灰度发布也方便。

验证方法:改完计价后,用同一组经纬度分别调预估接口和实际结束行程接口,看费用是否一致。不一致就查是不是缓存没更新,或者夜间费判断的时区不对。我习惯在测试环境把date_default_timezone_set('Asia/Shanghai')写死在入口文件,避免服务器时区是 UTC 导致夜间费算错。

从那以后我每次拿到新源码,都强制走一遍「本地跑通—改配置—模拟下单—模拟接单—模拟支付—查日志」的完整链路,不跑通不上服务器。这套 PHP 网约车 H5 源码的乘客端与司机端逻辑不算复杂,但涉及地图、支付、定位、并发,任何一个环节没对齐都会在真实订单里暴露。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 3:59:41

基于知识图谱的问答系统:SpringBoot与Neo4j实战指南

简介&#xff1a;这份资源是面向Java后端开发者与知识图谱入门者的完整问答系统实战项目&#xff0c;采用SpringBoot整合Neo4j图数据库&#xff0c;聚焦家电行业智能客服场景&#xff0c;帮助读者掌握从数据建模到查询引擎落地的全流程。压缩包共633个文件&#xff0c;约36.99M…

作者头像 李华
网站建设 2026/10/6 3:59:17

6D可移动天线低复杂度统计信道优化:Matlab仿真与调参实战

上一段时间我一直在折腾6D可移动天线的Matlab仿真项目&#xff0c;主题是“统计信道的低复杂度旋转和位置优化为6D可移动天线无线通信”。说实话&#xff0c;刚看到这个题目时我第一反应是&#xff1a;可移动天线不就是在空间里搜一个最优坐标嘛&#xff0c;哪里轮得到“低复杂…

作者头像 李华
网站建设 2026/10/6 3:58:33

单片机5V电源设计实战:从稳压选型到纹波抑制与PCB布局

直接说结论&#xff1a;给单片机供5V电源这件事&#xff0c;看起来简单到不值一提&#xff0c;但实际上手做过几个项目之后&#xff0c;你会发现这里面的坑比想象中多得多。很多人拿着开发板用USB线一插&#xff0c;灯亮了程序跑了&#xff0c;就以为电源设计不过如此&#xff…

作者头像 李华
网站建设 2026/10/6 3:58:20

JRules规则引擎入门:新建规则项目避坑指南

1. 项目概述1.1 为什么2025年还有人写JRules先交代一下背景。WebSphere ILOG JRules&#xff0c;老牌商业规则引擎&#xff0c;后来被IBM收编&#xff0c;改名叫IBM Operational Decision Manager&#xff0c;也就是ODM。这套东西在银行、保险、电信这些行业的遗留系统里存量极…

作者头像 李华
网站建设 2026/10/6 3:58:13

捕鱼游戏设计拆解:鱼群AI、碰撞检测与RTP数值模型

做捕鱼项目这些年&#xff0c;我最大的感受是&#xff1a;决定玩家去留的往往不是炮台多华丽、子弹多花哨&#xff0c;而是屏幕里那群鱼“游得对不对、死得爽不爽”。前几篇把捕鱼游戏的整体框架、炮台、子弹、网讲了一遍&#xff0c;这一篇集中拆“鱼”本身——鱼种怎么分类、…

作者头像 李华
网站建设 2026/10/6 3:58:12

Agent-Reach 实战:Python CLI AI Agent 工具调用与并发设计

1. 从"Agent-Reach"这个名字说起&#xff1a;它到底想解决什么问题第一次看到 Agent-Reach 这个项目名&#xff0c;我的直觉是&#xff1a;这又是一个把 AI Agent 和"触达"绑在一起的工具。事实也确实如此。Agent-Reach 是一个基于 Python 构建的 CLI 工具…

作者头像 李华