Skip to content

LBS位置服务开发指南

地图坐标系说明

1. 各大地图服务商坐标系

天地图

  • 坐标系:采用 CGCS2000(2000国家大地坐标系)
  • 特点:天地图的坐标精度较高,适用于需要高精度地理信息的应用场景,如测绘、科研等
  • 在实际应用中,很多时候可以用 WGS84 坐标来代替 CGCS2000 坐标,因为两者的定义非常接近

高德地图

  • 坐标系:采用 GCJ02(火星坐标系)
  • 特点:高德地图的坐标系在国内广泛使用,适用于大多数国内的地图应用和导航服务
  • 这是国家测绘局为了国家安全在原始坐标的基础上进行偏移得到的坐标

百度地图

  • 坐标系:采用 BD09II 坐标系
  • 特点:百度地图的坐标系具有较高的加密性,适用于百度生态内的各种应用和服务
  • 百度地图在国际标准坐标 WGS-84 的基础上进行了一次加密,形成了火星坐标(GCJ02),然后又进行了二次加密

腾讯地图

  • 坐标系:腾讯地图拾取的坐标为 GCJ02(火星坐标系)
  • 特点:与高德地图类似,腾讯地图的坐标系在国内广泛使用,适用于腾讯生态内的地图应用和导航服务

2. 系统统一坐标系

系统统一采用 GCJ02(火星坐标系)

  • 高德地图、腾讯地图:不需要转换
  • 天地图、百度地图:读取以及存储需要转成 GCJ02(火星坐标系)

概述

DSMall Pro LBS位置服务系统基于多地图服务商构建,提供统一的位置服务接口,支持IP定位、逆地理编码、周边搜索、城市搜索等功能。

环境要求

1. 系统要求

  • PHP >= 8.0
  • ThinkPHP 8.0+
  • cURL扩展
  • 地图服务商API Key

2. 支持的地图服务商

  • 腾讯地图(默认)
  • 高德地图
  • 百度地图
  • 天地图

LBS架构

1. 架构图

API接口 → SysLbs控制器 → ThirdPartyLoader → LbsManager → 地图服务商驱动
  ↓           ↓              ↓              ↓            ↓
前端调用  参数验证处理   第三方服务加载   驱动管理器   腾讯/高德/百度/天地图

2. 核心组件

  • SysLbs: LBS API控制器 (app\api\controller\system\SysLbs)
  • LbsManager: LBS驱动管理器 (app\deshang\third_party\lbs\LbsManager)
  • BaseLbs: LBS驱动基类 (app\deshang\third_party\lbs\providers\BaseLbs)
  • ThirdPartyLoader: 第三方服务加载器

API接口

1. 通过IP获取经纬度

接口地址: GET /api/system/lbs/getCoordsByIp

功能说明: 根据客户端IP地址获取对应的经纬度坐标

请求参数: 无(自动获取客户端IP)

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": {
        "longitude": 116.404,
        "latitude": 39.915
    }
}

2. 根据经纬度获取位置信息

接口地址: GET /api/system/lbs/getAddressByLngLat

功能说明: 根据经纬度坐标获取详细地址信息

请求参数:

参数名类型必填说明示例
longitudefloat经度116.404
latitudefloat纬度39.915

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": {
        "longitude": 116.404,
        "latitude": 39.915,
        "province": "北京市",
        "city": "北京市",
        "adcode": "110101",
        "citycode": "010",
        "district": "东城区",
        "street": "王府井大街",
        "name": "北京市东城区王府井大街",
        "address": "北京市东城区王府井大街"
    }
}

3. 获取周边位置列表

接口地址: POST /api/system/lbs/getAroundAddressList

功能说明: 根据经纬度获取周边地址列表

请求参数:

参数名类型必填说明示例
longitudefloat经度116.404
latitudefloat纬度39.915
keywordstring搜索关键词餐厅

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": [
        {
            "longitude": 116.404,
            "latitude": 39.915,
            "province": "北京市",
            "city": "北京市",
            "adcode": "110101",
            "citycode": "010",
            "district": "东城区",
            "street": "",
            "name": "王府井小吃街",
            "address": "北京市东城区王府井小吃街",
            "distance": 0.5
        }
    ]
}

4. 获取城市地址列表

接口地址: POST /api/system/lbs/getCityAddressList

功能说明: 根据城市和关键词搜索地址列表

请求参数:

参数名类型必填说明示例
citystring城市名称北京市
keywordstring搜索关键词餐厅
longitudefloat经度(可选)116.404
latitudefloat纬度(可选)39.915

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": [
        {
            "longitude": 116.404,
            "latitude": 39.915,
            "province": "北京市",
            "city": "北京市",
            "adcode": "110101",
            "citycode": "010",
            "district": "东城区",
            "street": "",
            "name": "全聚德烤鸭店",
            "address": "北京市东城区全聚德烤鸭店",
            "distance": 1.2
        }
    ]
}

5. 获取城市列表

接口地址: GET /api/system/lbs/getCityList

功能说明: 获取系统支持的城市列表,按首字母分组

请求参数: 无

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": {
        "city_list": [
            {
                "letter": "A",
                "data": ["安庆市", "安阳市"]
            },
            {
                "letter": "B",
                "data": ["北京市", "保定市"]
            }
        ],
        "hot_city_list": [
            "北京市",
            "上海市",
            "杭州市",
            "南京市",
            "武汉市"
        ]
    }
}

6. 根据关键词检索城市

接口地址: GET /api/system/lbs/getCityListByKeyword

功能说明: 根据关键词模糊搜索城市名称

请求参数:

参数名类型必填说明示例
keywordstring搜索关键词北京

响应示例:

json
{
    "code": 10000,
    "msg": "操作成功",
    "data": {
        "search_city_list": [
            "北京市",
            "北京朝阳区"
        ]
    }
}

前端位置获取

1. 位置获取工具类

系统提供了前端位置获取工具类 (uniapp/src/utils/location.ts),支持多端适配:

typescript
import { getCoordsByIp } from '@/api/system/sysLbs'

// 位置信息接口
interface LocationResult {
    latitude: number
    longitude: number
}

// IP定位处理函数
function handleIpLocation(): Promise<LocationResult> {
    return getCoordsByIp().then(res => {
        if (res.code === 10000 && res.data) {
            return {
                latitude: res.data.latitude,
                longitude: res.data.longitude,
            }
        } else {
            uni.showToast({
                title: res.message,
                icon: 'none'
            })
            throw new Error('IP定位失败')
        }
    })
}

// 通用的 uni.getLocation 获取位置方法
function getLocationByUni(isHighAccuracy: boolean = true): Promise<LocationResult> {
    return new Promise((resolve, reject) => {
        uni.getLocation({
            type: 'gcj02',  // 使用GCJ02坐标系
            altitude: false,
            isHighAccuracy,
            highAccuracyExpireTime: 3000,
            success: (res) => {
                resolve({
                    latitude: res.latitude,
                    longitude: res.longitude,
                })
            },
            fail: (err) => {
                if (isHighAccuracy) {
                    // 高精度失败,尝试普通精度
                    getLocationByUni(false).then(resolve).catch(reject)
                } else {
                    reject(err)
                }
            }
        })
    })
}

// 小程序授权获取位置(专门处理小程序的授权逻辑)
function getMiniProgramLocation(): Promise<LocationResult> {
    return new Promise((resolve, reject) => {
        // 先授权
        uni.authorize({
            scope: 'scope.userLocation',
            success() {
                // 授权成功,开始定位
                getLocationByUni().then(resolve).catch(reject)
            },
            fail(err) {
                // 授权失败,提示用户
                uni.showToast({
                    title: '获取位置失败,请在设置中打开位置权限',
                    icon: 'none'
                })

                if (err.errMsg.indexOf('authorize:fail') !== -1) {
                    // 提示用户打开位置权限
                    uni.showModal({
                        title: '位置权限未开启',
                        content: '获取位置失败,请在设置中打开位置权限',
                        confirmText: '去设置',
                        success(modalRes) {
                            if (modalRes.confirm) {
                                // 打开设置页
                                uni.openSetting({
                                    success(settingRes) {
                                        if (settingRes.authSetting['scope.userLocation']) {
                                            // 用户在设置页开启了权限,重新尝试
                                            getMiniProgramLocation().then(resolve).catch(reject)
                                        } else {
                                            reject(new Error('用户未授权位置权限'))
                                        }
                                    },
                                    fail() {
                                        reject(new Error('打开设置页失败'))
                                    }
                                })
                            } else {
                                reject(new Error('用户取消授权'))
                            }
                        }
                    })
                } else {
                    reject(err)
                }
            }
        })
    })
}

// 获取当前位置(判断环境的主入口方法)
export function getCurrentPosition(): Promise<LocationResult> {
    const { deviceModel } = uni.getSystemInfoSync()

    return new Promise((resolve, reject) => {
        // #ifdef H5
        if (deviceModel === 'PC') {
            // PC端直接使用IP定位
            handleIpLocation().then(resolve).catch(reject)
        } else {
            // H5移动端,直接定位,失败则IP定位
            getLocationByUni()
                .then(resolve)
                .catch(() => {
                    uni.showToast({
                        title: 'H5定位失败,使用IP定位',
                        icon: 'none'
                    })
                    handleIpLocation().then(resolve).catch(reject)
                })
        }
        // #endif

        // #ifdef APP-PLUS
        // APP端,直接定位,失败则IP定位
        getLocationByUni()
            .then(resolve)
            .catch(() => {
                uni.showToast({
                    title: 'APP定位失败,使用IP定位',
                    icon: 'none'
                })
                handleIpLocation().then(resolve).catch(reject)
            })
        // #endif

        // #ifdef MP-WEIXIN || MP-ALIPAY || MP-BAIDU || MP-TOUTIAO || MP-QQ
        // 小程序端,需要先授权
        getMiniProgramLocation()
            .then(resolve)
            .catch(() => {
                uni.showToast({
                    title: '小程序定位失败,使用IP定位',
                    icon: 'none'
                })
                handleIpLocation().then(resolve).catch(reject)
            })
        // #endif
    })
}

2. 多端适配策略

H5端:

  • PC端: 直接使用IP定位(无法获取GPS)
  • 移动端: 优先GPS定位,失败则IP定位

APP端:

  • 优先GPS定位,失败则IP定位
  • 支持高精度和普通精度定位

小程序端:

  • 需要先授权位置权限
  • 授权成功后GPS定位,失败则IP定位
  • 提供完整的授权流程处理

3. 坐标系处理

前端统一使用 GCJ02(火星坐标系)

typescript
uni.getLocation({
    type: 'gcj02',  // 指定坐标系
    altitude: false,
    isHighAccuracy: true,
    highAccuracyExpireTime: 3000,
    success: (res) => {
        // res.latitude 和 res.longitude 已经是GCJ02坐标系
        console.log('经度:', res.longitude)
        console.log('纬度:', res.latitude)
    }
})

4. 使用示例

typescript
import { getCurrentPosition } from '@/utils/location'

// 获取当前位置
getCurrentPosition().then(result => {
    console.log('当前位置:', result.latitude, result.longitude)
    
    // 调用后端API获取地址信息
    return getAddressByLngLat(result.longitude, result.latitude)
}).then(addressInfo => {
    console.log('地址信息:', addressInfo)
}).catch(error => {
    console.error('获取位置失败:', error)
})

5. 错误处理

typescript
// 完整的错误处理示例
getCurrentPosition().then(result => {
    // 位置获取成功
    return result
}).catch(error => {
    // 处理不同类型的错误
    if (error.message.includes('授权')) {
        uni.showModal({
            title: '位置权限',
            content: '需要位置权限才能提供服务',
            showCancel: false
        })
    } else if (error.message.includes('IP定位')) {
        uni.showToast({
            title: '定位精度较低',
            icon: 'none'
        })
    } else {
        uni.showToast({
            title: '获取位置失败',
            icon: 'none'
        })
    }
    throw error
})

6. 权限处理

小程序权限处理:

typescript
// 检查位置权限
uni.getSetting({
    success(res) {
        if (res.authSetting['scope.userLocation'] === false) {
            // 用户拒绝了位置权限
            uni.showModal({
                title: '位置权限',
                content: '需要位置权限才能提供服务',
                success(modalRes) {
                    if (modalRes.confirm) {
                        uni.openSetting()
                    }
                }
            })
        }
    }
})

APP权限处理:

typescript
// APP端权限检查
uni.getLocation({
    type: 'gcj02',
    success: (res) => {
        // 权限正常,获取位置成功
    },
    fail: (err) => {
        if (err.errMsg.includes('auth deny')) {
            // 权限被拒绝
            uni.showModal({
                title: '位置权限',
                content: '请在设置中开启位置权限',
                confirmText: '去设置',
                success(modalRes) {
                    if (modalRes.confirm) {
                        plus.runtime.openURL('app-settings:')
                    }
                }
            })
        }
    }
})

驱动开发

1. LBS驱动基类

所有LBS驱动都需要继承 BaseLbs 基类:

php
abstract class BaseLbs
{
    // 根据IP获取位置信息
    abstract public function getCoordsByIp(string $ip): array;

    // 根据经纬度获取位置信息
    abstract public function getAddressByLngLat(float $lng, float $lat): array;

    // 获取周边位置列表
    abstract public function getAroundAddressList($data): array;
    
    // 获取城市地址列表 
    abstract public function getCityAddressList($data): array;
}

2. 腾讯地图驱动示例

php
class Tencent extends BaseLbs
{
    protected $config;

    public function __construct(array $config)
    {
        $this->config = $config;
    }

    /**
     * 通过IP获取位置信息
     */
    public function getCoordsByIp(string $ip): array
    {
        $url = 'https://apis.map.qq.com/ws/location/v1/ip';
        
        $param = [
            'ip' => $ip,
            'key' => $this->config['service_key'],
            'output' => 'json'
        ];
        
        $response = $this->request_get($url, $param);
        $result = json_decode($response, true);
        
        if (isset($result['status']) && $result['status'] === 0 
            && isset($result['result']['location'])) {
            $location = $result['result']['location'];
            
            return InternalResultHelper::success('获取经纬度成功', [
                'longitude' => $location['lng'],
                'latitude' => $location['lat']
            ]);
        }
        
        return InternalResultHelper::error('腾讯地图IP定位失败');
    }
}

3. 驱动管理器

php
class LbsManager extends BaseDriverManager
{
    protected $namespace = 'app\deshang\third_party\lbs\providers'; 

    protected function getDefaultDriverName(): string
    {
        return 'Tencent';
    }
}

配置说明

1. 地图服务商配置

系统通过 ThirdPartyLoader 加载LBS服务:

php
// 获取LBS服务实例
$lbs = ThirdPartyLoader::lbs();

// 调用IP定位
$result = $lbs->getCoordsByIp($ip);

2. 支持的驱动

驱动名称类名说明
TencentTencent腾讯地图(默认)
GaodeGaode高德地图
BaiduBaidu百度地图
TiandituTianditu天地图

业务场景应用

1. 用户地址选择

php
// 获取用户当前位置
$ip = request()->ip();
$lbs = ThirdPartyLoader::lbs();
$location = $lbs->getCoordsByIp($ip);

if ($location['success']) {
    $coords = $location['data'];
    
    // 获取周边地址列表
    $aroundList = $lbs->getAroundAddressList([
        'longitude' => $coords['longitude'],
        'latitude' => $coords['latitude'],
        'keyword' => '住宅'
    ]);
}

2. 订单配送地址

php
// 根据用户选择的地址获取详细信息
$addressInfo = $lbs->getAddressByLngLat($lng, $lat);

// 保存订单地址信息
$orderAddress = [
    'order_id' => $orderId,
    'reciver_name' => $userName,
    'reciver_mobile' => $userMobile,
    'reciver_address' => $addressInfo['address'],
    'reciver_longitude' => $lng,
    'reciver_latitude' => $lat,
];

3. 城市选择器

php
// 获取城市列表
$cityList = $lbs->getCityList();

// 根据关键词搜索城市
$searchResult = $lbs->getCityListByKeyword($keyword);

错误处理

1. 常见错误

错误类型说明解决方案
API Key无效地图服务商API Key错误检查API Key配置
请求超时网络请求超时检查网络连接
参数错误请求参数格式错误检查参数格式
配额超限API调用次数超限检查API配额

2. 异常处理

php
try {
    $lbs = ThirdPartyLoader::lbs();
    $result = $lbs->getCoordsByIp($ip);
    
    if ($result['success']) {
        return ds_json_success('操作成功', $result['data']);
    } else {
        return ds_json_error($result['message']);
    }
} catch (\Exception $e) {
    return ds_json_error('LBS服务异常: ' . $e->getMessage());
}

性能优化

1. 缓存策略

  • 城市列表缓存1小时
  • IP定位结果缓存30分钟
  • 地址信息缓存15分钟

2. 请求优化

  • 使用HTTP/2协议
  • 设置合理的超时时间
  • 批量请求合并

部署检查清单

环境检查

  • [ ] PHP版本 >= 8.0
  • [ ] cURL扩展已安装
  • [ ] 网络连接正常

配置检查

  • [ ] 地图服务商API Key已配置
  • [ ] 默认驱动设置正确
  • [ ] 第三方服务加载器正常

功能检查

  • [ ] IP定位功能正常
  • [ ] 逆地理编码正常
  • [ ] 周边搜索正常
  • [ ] 城市搜索正常

安全检查

  • [ ] API Key安全存储
  • [ ] 请求频率限制
  • [ ] 错误日志记录

最后更新:2024-01-20
维护者:DSPlatform技术团队