还是这个点才停了车住进民宿了 这几天感觉开车把几年的量都开回来了 仿佛要开到南极去...
时间紧 又不想断更又不想水文字 不容易 😁 今天提一嘴我们团队里实现本地业务的一个小技术点吧 不是我这边主要写的 刚才和后台同事联系的时候 提起这一块的一些小BUG 分享下 算是今天的选题了 其实很早的时候 分享goeasy的时候提过 [GoEasy]一个好用的Websocket消息推送服务-1 但是后来没具体写这个技术点的分享 我们在业务里需要更新很多位配送员的实时位置 类似某团 某了么里配送员接单了有实时位置能通过后台更新 然后显示到各个订单买家APP里看得到这么一个需求
后台同事用的是Redis GEO 是一种专门用于处理地理位置信息的数据结构
底层是基于Redis的有序集合[ZSet]实现的 当添加一个地理位置[如配送员坐标]时 Redis 会使用 GeoHash 算法 将二维的经纬度编码成一个一维的字符串 并将其转换为一个分数(score)存入ZSet
这种编码的好处是 地理位置相近的点 它的GeoHash值的前缀也是相似的 这样处理之后 查询就会非常高效 贴几个AI出的代码流程 有需求的小伙伴可以复制过去项目里看看
<?phpnamespace app\service;use think\facade\Cache;use think\facade\Log;class GeoService{ // Redis Key const RIDER_LOCATION_KEY = 'rider:locations'; // 配送员实时位置 const RIDER_HISTORY_KEY_PREFIX = 'rider:history:'; // 配送员历史轨迹 /** * 添加或更新配送员位置 * @param int $riderId 配送员ID * @param float $lng 经度 * @param float $lat 纬度 * @param int $orderId 订单ID(用于记录轨迹) * @return bool */ public static function updateRiderLocation($riderId, $lng, $lat, $orderId = null) { try { $redis = self::getRedisConnection(); // 1. 更新实时位置到GEO $result = $redis->geoadd( self::RIDER_LOCATION_KEY, $lng, $lat, $riderId ); if ($result === false) { Log::error("更新配送员位置失败: rider_id={$riderId}, lng={$lng}, lat={$lat}"); return false; } // 2. 记录历史轨迹(按订单) if ($orderId) { $historyKey = self::RIDER_HISTORY_KEY_PREFIX . $orderId; $timestamp = time(); // 使用有序集合存储轨迹,score为时间戳 $redis->zadd($historyKey, $timestamp, json_encode([ 'lng' => $lng, 'lat' => $lat, 'time' => $timestamp, 'rider_id' => $riderId ])); // 设置轨迹数据过期时间(24小时) $redis->expire($historyKey, 86400); } // 3. 更新配送员最后活跃时间 $redis->hset('rider:activity', $riderId, time()); Log::info("配送员位置更新成功: rider_id={$riderId}, lng={$lng}, lat={$lat}"); return true; } catch (\Exception $e) { Log::error("更新配送员位置异常: " . $e->getMessage()); return false; } } /** * 获取配送员实时位置 * @param int $riderId * @return array|null */ public static function getRiderLocation($riderId) { try { $redis = self::getRedisConnection(); // 使用GEOPOS获取单个配送员位置 $location = $redis->geopos(self::RIDER_LOCATION_KEY, $riderId); if (empty($location) || empty($location[0])) { return null; } return [ 'lng' => (float)$location[0][0], 'lat' => (float)$location[0][1], 'rider_id' => $riderId ]; } catch (\Exception $e) { Log::error("获取配送员位置异常: " . $e->getMessage()); return null; } } /** * 获取附近可用配送员 * @param float $lng 中心经度 * @param float $lat 中心纬度 * @param int $radius 搜索半径(米) * @param int $limit 最大返回数量 * @return array */ public static function getNearbyRiders($lng, $lat, $radius = 5000, $limit = 10) { try { $redis = self::getRedisConnection(); // GEORADIUS返回附近配送员 $result = $redis->georadius( self::RIDER_LOCATION_KEY, $lng, $lat, $radius, 'm', // 单位:米 ['WITHDIST', 'WITHCOORD', 'ASC'] // 返回距离和坐标,按距离升序 ); $riders = []; foreach ($result as $item) { $riders[] = [ 'rider_id' => $item[0], 'distance' => round($item[1], 2), // 距离(米) 'lng' => $item[2][0], 'lat' => $item[2][1] ]; if (count($riders) >= $limit) { break; } } return $riders; } catch (\Exception $e) { Log::error("获取附近配送员异常: " . $e->getMessage()); return []; } } /** * 计算配送员与目标点的距离 * @param int $riderId * @param float $targetLng * @param float $targetLat * @return float|null */ public static function calculateDistance($riderId, $targetLng, $targetLat) { try { $redis = self::getRedisConnection(); // 临时添加目标点到Redis(用于计算距离) $tempKey = 'temp:distance:' . time(); $redis->geoadd($tempKey, $targetLng, $targetLat, 'target'); // 计算距离 $distance = $redis->geodist( self::RIDER_LOCATION_KEY, $riderId, 'target', 'm' // 单位:米 ); // 删除临时key $redis->del($tempKey); return $distance ? round($distance, 2) : null; } catch (\Exception $e) { Log::error("计算距离异常: " . $e->getMessage()); return null; } } /** * 获取配送员历史轨迹 * @param int $orderId * @param int $startTime * @param int $endTime * @return array */ public static function getRiderHistory($orderId, $startTime = null, $endTime = null) { try { $redis = self::getRedisConnection(); $historyKey = self::RIDER_HISTORY_KEY_PREFIX . $orderId; if (!$redis->exists($historyKey)) { return []; } // 按时间范围查询轨迹 if ($startTime && $endTime) { $items = $redis->zrangebyscore($historyKey, $startTime, $endTime); } else { $items = $redis->zrange($historyKey, 0, -1); } $history = []; foreach ($items as $item) { $data = json_decode($item, true); if ($data) { $history[] = $data; } } return $history; } catch (\Exception $e) { Log::error("获取历史轨迹异常: " . $e->getMessage()); return []; } } /** * 配送完成后清理位置数据 * @param int $riderId * @param int $orderId */ public static function clearRiderData($riderId, $orderId = null) { try { $redis = self::getRedisConnection(); // 从GEO集合中移除配送员 $redis->zrem(self::RIDER_LOCATION_KEY, $riderId); // 删除历史轨迹 if ($orderId) { $historyKey = self::RIDER_HISTORY_KEY_PREFIX . $orderId; $redis->del($historyKey); } // 删除活跃时间记录 $redis->hdel('rider:activity', $riderId); } catch (\Exception $e) { Log::error("清理配送员数据异常: " . $e->getMessage()); } } /** * 获取Redis连接 */ private static function getRedisConnection() { return Cache::store('redis')->handler(); }}
AI出的这个只是个大概流程 全部业务需要结合数据库[mysql或者其他]和WebSocket 才能比较完整的跑通 不过还是看自己的具体业务来做相应处理 Redis强大的功能比较多 最近没选题 应该会学习下多分享点这方面的学习心得
保持好奇 不断进步 下期见~