本文目录导读:

PHP 项目进行基于定位数据的客流统计,通常依赖用户设备的 GPS、Wi-Fi 或 基站 定位数据,核心挑战在于:如何从大量、嘈杂的离散定位点(轨迹)中,准确判断一个“人”是否进入了某个区域,并去重统计。
下面是一个从数据采集到算法实现的完整技术方案。
核心挑战与解决思路
- 数据噪音:GPS漂移会导致人“瞬移”到墙外或隔壁店铺。
- 去重问题:一个用户可能在区域内停留很久,产生几十个点,只应计数1次。
- 边界判断:人在边界反复进出,如何定义“进入”而非“路过”。
- 精度依赖:室内(Wi-Fi/蓝牙)和室外(GPS)精度不同。
解决思路:
- 区域定义:使用地理围栏(Geo-fence)——即用一组经纬度坐标圈定店铺、商场或展区。
- 状态机判断:不是判断单点,而是判断人的状态变化(从区域外 -> 区域内)。
技术架构与数据流
graph TD
A[移动端SDK/小程序/APP] -->|1. 采集GPS/基站/Wi-Fi定位| B(API网关)
B -->|2. 上报原始定位点| C[消息队列: Kafka / RabbitMQ]
C -->|3. 流式消费| D[流处理器: Flink / Spark Streaming]
D -->|4. 计算状态 & 去重| E[(分布式数据库: Redis / PostgreSQL)]
F[后台管理] -->|5. 查询统计| E
D -->|6. 实时聚合| G[实时大屏: WebSocket]
关键算法:基于地理围栏的停留判断
这是最核心的部分,不能只看一次定位点,需要结合时间和空间。
算法 1:空间索引 + 时间窗口(推荐)
这是生产环境中常用的方法,兼顾精度和性能。
-
数据预处理:
- 使用地理空间索引(如 MySQL 的 Spatial Index、PostgreSQL 的 GiST、MongoDB 的 2dsphere Index)快速过滤出“可能”在区域内的定位点。
- 示例 SQL(MySQL):
-- 创建空间字段 ALTER TABLE user_location ADD location POINT; -- 添加空间索引 ALTER TABLE user_location ADD SPATIAL INDEX(location); -- 查询某个围栏(多边形)内的所有点 SELECT * FROM user_location WHERE ST_Within(location, ST_GeomFromText('POLYGON((...))'));
-
核心逻辑:轨迹滤波 + 状态机
假设一个用户上报了N个点(
P1, P2, ... Pn),每个点有[user_id, lng, lat, timestamp]。步骤:
- 漂移过滤:计算相邻两点的距离,如果距离 > 100米(可配置)且时间差 < 5秒,则认为是异常漂移点,舍弃。
- 进入检测:当用户的连续M个点(M=3)都在围栏内部,且第一个内部点和最后一个内部点的时间差 > 阈值(如 10秒),标记为“进入”。
- 停留判定:用户标记为“进入”后,只要有任意一个点在围栏内,就认为是“停留”。
- 离开检测:当用户的连续N个点(如 N=3)都不在围栏内,或者点集的时间差 > 超时阈值(如 30分钟),标记为“离开”。
- 计数逻辑:一个
user_id在同一个区域,从“进入”到“离开”为一个 Session,只计 1 次。
-
代码示例(伪代码 / PHP + Redis 实现)
<?php // 模拟处理单个用户的上报事件 function handleLocationEvent($userId, $lng, $lat, $timestamp, $geoFencePolygon) { $redis = new Redis(); $userStateKey = "geofence:state:{$userId}"; $isInside = pointInPolygon($lng, $lat, $geoFencePolygon); $currentState = json_decode($redis->get($userStateKey), true) ?? ['status' => 'outside', 'last_points' => []]; // 1. 记录最近10个点用于滤波 $currentState['last_points'][] = ['lng' => $lng, 'lat' => $lat, 'ts' => $timestamp, 'inside' => $isInside]; if (count($currentState['last_points']) > 10) { array_shift($currentState['last_points']); } // 2. 简单滤波:检查最近3个点是否都在区域内 $recentPoints = array_slice($currentState['last_points'], -3); $allInside = count(array_filter($recentPoints, fn($p) => $p['inside'])) === 3; // 3. 状态机转换 if ($currentState['status'] === 'outside' && $allInside) { // 从外部进入 → 判定为一次进入 $currentState['status'] = 'inside'; $currentState['enter_time'] = $timestamp; // **重要:将进入事件写入MySQL,用于统计** saveEnterEvent($userId, $lng, $lat, $timestamp, 'shop_A'); $redis->incr('geofence:count:shop_A'); // 实时加1 } elseif ($currentState['status'] === 'inside') { // 检查是否离开(连续3个点不在区域内) $allOutside = count(array_filter($recentPoints, fn($p) => !$p['inside'])) === 3; if ($allOutside || ($timestamp - $currentState['enter_time'] > 1800)) { // 离开或超时 $currentState['status'] = 'outside'; saveLeaveEvent($userId, $lng, $lat, $timestamp, 'shop_A'); } } // 4. 保存状态到Redis(TTL 30分钟) $redis->setex($userStateKey, 1800, json_encode($currentState)); return $currentState['status']; }
算法 2:DBSCAN 聚类(离线 / 日结)
适合分析历史数据,找出“哪些地方人密集”以及“有多少独立个体”。
// 伪代码:对一天内某个区域的所有点按用户聚类
function dailyFlowCount($areaPolygon, $date) {
$rawPoints = DB::select("SELECT user_id, lng, lat, time FROM locations
WHERE ST_Within(point, :area) AND date = :date",
['area' => $areaPolygon, 'date' => $date]);
// 按用户分组
$userGroups = [];
foreach ($rawPoints as $p) {
$userGroups[$p->user_id][] = ['lng' => $p->lng, 'lat' => $p->lat, 'time' => $p->time];
}
$count = 0;
foreach ($userGroups as $uid => $points) {
// 对每个用户的点进行DBSCAN(或简单的时间跨度判断)
// 如果同一用户的点跨度 > 5分钟,且存在连续点,认为是一次有效访问
if (isValidVisit($points)) {
$count++;
}
}
return $count;
}
PHP 项目具体实现建议
数据采集层
- 前端 SDK:微信小程序可通过
wx.getLocation获取 GPS 坐标;H5 可使用navigator.geolocation(需用户授权,在 HTTPS 下工作)。 - 上报策略:不要实时上报,应采用节流(例如每 5-10 秒上报一次,或者当位置变化 > 10 米时上报),降低后端压力。
地理围栏存储与判断
-
定义围栏:在数据库中存储多边形的顶点坐标字符串(如
POLYGON((x1 y1, x2 y2, ...)))。 -
判断算法:PHP 实现
pointInPolygon函数(射线法),如果并发量高(>1000qps),建议使用 Redis Geo(支持圆形围栏)或 PostgreSQL + PostGIS(支持多边形,性能极佳)。// 简单射线法判断点是否在多边形内 function pointInPolygon($point, $polygon) { $vertices = $polygon; // 数组: [[lng,lat], ...] $n = count($vertices); $inside = false; for ($i = 0, $j = $n - 1; $i < $n; $j = $i++) { $xi = $vertices[$i][0]; $yi = $vertices[$i][1]; $xj = $vertices[$j][0]; $yj = $vertices[$j][1]; if ((($yi > $point[1]) != ($yj > $point[1])) && ($point[0] < ($xj - $xi) * ($point[1] - $yi) / ($yj - $yi) + $xi)) { $inside = !$inside; } } return $inside; }
去重与计数
- 使用 Redis:
userId -> 区域ID作为 Key,设置过期时间(如 30 分钟),每次用户上报,检查是否已有该 Key,如果没有,则计数+1,并设置 Key;如果有,则更新过期时间(维持在线状态),但不计数。 - 使用数据库:
INSERT ... ON DUPLICATE KEY UPDATE或INSERT IGNORE,利用user_id和area_id的唯一索引去重。
数据可视化与报表
- PHP 后台定时任务(如每5分钟)汇总数据库中的进入事件,计算出实时客流、累计客流、平均停留时长。
- 热力图:如果收集了大量定位点,可以用 PHP 生成 GeoJSON 数据,前端用 Leaflet 或 Mapbox 渲染热力图。
方案对比与选型
| 维度 | 高并发/实时方案(推荐) | 中小规模/低成本方案 |
|---|---|---|
| 后端语言 | PHP 负责 API,重计算交给 Go/Java/Flink | 纯 PHP+ Swoole |
| 存储 | Redis(状态) + PostgreSQL + PostGIS(历史) | MySQL + 内存表 |
| 地理围栏 | Redis Geo(圆形) 或 PostGIS(任意形状) | PHP 函数计算 + MySQL |
| 判断逻辑 | 流处理(Flink) 或 批量 Redis Lua | PHP 定时脚本,使用 st_intersects |
| 去重 | Redis 原子性 INCR |
数据库唯一索引 |
| 适用场景 | 大型商场、景区、机场(>10万并发) | 中小型店铺、展会(<1000人/分钟) |
必须注意的问题
- 隐私合规:
- 基于《个人信息保护法》和《数据安全法》,收集精确位置数据必须获得用户明确授权。
- 统计时应对
user_id进行匿名化(如使用不可逆的 Hash),后台只保留统计数字,不保留个人与位置的映射关系。
- 精度误差:
- 室内建议使用 iBeacon(蓝牙信标)辅助定位,精度可达 1-3 米。
- 纯 GPS 在室内可能失效,需结合 Wi-Fi MAC 地址采集。
- 数据质量:
- 必须过滤掉静止设备(如收银机、固定监控)上报的定位点,否则会虚增客流。
- 可结合加速度计(判断是否在移动)辅助判断。
要基于定位数据用 PHP 项目统计客流,核心步骤是:
- 定义区域:用坐标圈出要统计的地方。
- 采集并滤波:获取用户位置,去掉异常跳点。
- 状态机判断进入/离开:避免单点误判,结合时间窗口。
- 去重计数:同一用户、同一区域只统计一次,直到他离开后再回来才第二次统计。
- 存储与展示:结果存数据库,通过图表展示实时及历史客流。
如果你的项目是 大型商场/景区(高并发),建议将核心算法部分下沉到 Redis + Lua 或 PostgreSQL 中,PHP 只负责业务调度和展示,如果是 小型店铺(日客流 < 1000),纯 PHP + 数据库组合完全够用,开发成本低且易于维护。