fix(地址): 选点改走服务端天地图,直辖县按所属省匹配

正式版不能直接请求地图域名时,由后端转发逆地理。保存区划时带上省的 id,避免同名直辖县挂错省份。

Co-authored-by: Cursor <cursoragent@cursor.com>
这个提交包含在:
wikiboss
2026-10-26 19:14:16 +08:00
父节点 c4d7c1fce8
当前提交 afac67a101
共有 4 个文件被更改,包括 458 次插入 和 0 次删除

查看文件

@@ -0,0 +1,251 @@
<?php
namespace app\common\library;
use addons\shopro\model\Area;
/**
* 天地图逆地理编码。
* 微信选点返回 GCJ-02,天地图使用 CGCS2000,请求前先转换。
*/
class Tianditu
{
public static function reverse($longitude, $latitude)
{
$key = self::key();
if ($key === '') {
throw new \Exception('未配置天地图密钥');
}
list($lon, $lat) = self::gcj02ToWgs84((float)$longitude, (float)$latitude);
$result = self::request($lon, $lat, $key);
$component = isset($result['addressComponent']) && is_array($result['addressComponent'])
? $result['addressComponent']
: [];
$area = self::matchArea($component);
$address = self::detailAddress($result, $component);
$province = $area['province'];
$city = $area['city'] === $province ? '' : $area['city'];
return [
'area_text' => $area['province'] . '-' . $area['city'] . '-' . $area['district'],
'address' => $address,
'formatted_address' => $province . $city . $area['district'] . $address,
];
}
protected static function key()
{
$key = function_exists('config') ? config('tianditu.key') : '';
if (!$key) {
$file = dirname(__DIR__, 2) . '/extra/tianditu.php';
$config = is_file($file) ? include $file : [];
$key = is_array($config) && !empty($config['key']) ? $config['key'] : '';
}
return trim((string)$key);
}
protected static function request($longitude, $latitude, $key)
{
$postStr = json_encode([
'lon' => round($longitude, 6),
'lat' => round($latitude, 6),
'ver' => 1,
], JSON_UNESCAPED_UNICODE);
$url = 'https://api.tianditu.gov.cn/geocoder?postStr=' . rawurlencode($postStr) . '&type=geocode&tk=' . rawurlencode($key);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
curl_setopt($ch, CURLOPT_TIMEOUT, 8);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_USERAGENT, 'chufang-server/1.0');
$body = curl_exec($ch);
$errno = curl_errno($ch);
curl_close($ch);
if ($errno || $body === false || $body === '') {
throw new \Exception('地址解析失败,请稍后重试');
}
$json = json_decode($body, true);
if (!is_array($json) || !isset($json['status']) || (string)$json['status'] !== '0' || empty($json['result']) || !is_array($json['result'])) {
$message = is_array($json) && !empty($json['msg']) ? $json['msg'] : '地址解析失败';
throw new \Exception($message);
}
return $json['result'];
}
protected static function matchArea(array $component)
{
$district = self::findDistrict($component);
if (!$district) {
throw new \Exception('未能匹配区县,请手动选择所在地区');
}
$city = Area::where('id', $district['pid'])->find();
$province = $city ? Area::where('id', $city['pid'])->find() : null;
if (!$city || !$province || (int)$city['level'] !== 2 || (int)$province['level'] !== 1) {
throw new \Exception('未能匹配省市区,请手动选择所在地区');
}
return [
'province' => $province['name'],
'city' => $city['name'],
'district' => $district['name'],
];
}
protected static function findDistrict(array $component)
{
$countyId = self::adminCode(isset($component['county_code']) ? $component['county_code'] : '');
if ($countyId) {
$district = Area::where('id', $countyId)->where('level', 3)->find();
if ($district) {
return $district;
}
}
$county = trim(isset($component['county']) ? $component['county'] : '');
if ($county === '') {
return null;
}
$cityId = self::adminCode(isset($component['city_code']) ? $component['city_code'] : '');
$query = Area::where('name', $county)->where('level', 3);
if ($cityId) {
$query->where('pid', $cityId);
}
return $query->find();
}
protected static function detailAddress(array $result, array $component)
{
$segments = [];
$adminNames = [];
foreach (['province', 'city', 'county'] as $field) {
$name = trim(isset($component[$field]) ? (string)$component[$field] : '');
if ($name !== '') {
$adminNames[] = $name;
}
}
foreach (['town', 'road', 'address', 'poi'] as $field) {
$segment = trim(isset($component[$field]) ? (string)$component[$field] : '');
$segment = self::stripAdmin($segment, $adminNames);
if ($segment === '' || self::isRelative($segment)) {
continue;
}
$replaced = false;
foreach ($segments as $index => $exist) {
if (mb_strpos($segment, $exist) !== false) {
$segments[$index] = $segment;
$replaced = true;
break;
}
if (mb_strpos($exist, $segment) !== false) {
$replaced = true;
break;
}
}
if (!$replaced) {
$segments[] = $segment;
}
}
if ($segments) {
return implode('', $segments);
}
$detail = isset($result['formatted_address']) ? $result['formatted_address'] : '';
foreach (['province', 'city', 'county'] as $field) {
$part = trim(isset($component[$field]) ? (string)$component[$field] : '');
if ($part !== '') {
$detail = preg_replace('/' . preg_quote($part, '/') . '/u', '', $detail, 1);
}
}
return trim($detail);
}
protected static function stripAdmin($segment, array $adminNames)
{
foreach ($adminNames as $name) {
if ($name !== '' && mb_strpos($segment, $name) !== false) {
$segment = str_replace($name, '', $segment);
}
$short = preg_replace('/(省|市|自治区|特别行政区)$/u', '', $name);
if ($short === '' || $short === $name || mb_strlen($short) < 2) {
continue;
}
if ($segment === $short) {
return '';
}
$suffixLength = mb_strlen($short);
if (mb_strlen($segment) > $suffixLength && mb_substr($segment, -$suffixLength) === $short) {
$segment = mb_substr($segment, 0, mb_strlen($segment) - $suffixLength);
}
}
return trim($segment);
}
protected static function isRelative($text)
{
return (bool)preg_match('/[东西南北].{0,4}约?\d+米/u', $text);
}
protected static function adminCode($code)
{
$digits = preg_replace('/\D/', '', (string)$code);
if (strlen($digits) < 6) {
return 0;
}
return (int)substr($digits, -6);
}
protected static function gcj02ToWgs84($longitude, $latitude)
{
if (self::outOfChina($longitude, $latitude)) {
return [$longitude, $latitude];
}
$offset = self::offset($longitude, $latitude);
return [$longitude * 2 - $offset[0], $latitude * 2 - $offset[1]];
}
protected static function offset($longitude, $latitude)
{
$a = 6378245.0;
$ee = 0.00669342162296594323;
$dlng = self::transformLng($longitude - 105.0, $latitude - 35.0);
$dlat = self::transformLat($longitude - 105.0, $latitude - 35.0);
$radLat = $latitude / 180.0 * M_PI;
$magic = sin($radLat);
$magic = 1 - $ee * $magic * $magic;
$sqrtMagic = sqrt($magic);
$dlat = ($dlat * 180.0) / (($a * (1 - $ee)) / ($magic * $sqrtMagic) * M_PI);
$dlng = ($dlng * 180.0) / ($a / $sqrtMagic * cos($radLat) * M_PI);
return [$longitude + $dlng, $latitude + $dlat];
}
protected static function outOfChina($longitude, $latitude)
{
return $longitude < 72.004 || $longitude > 137.8347 || $latitude < 0.8293 || $latitude > 55.8271;
}
protected static function transformLat($lng, $lat)
{
$ret = -100.0 + 2.0 * $lng + 3.0 * $lat + 0.2 * $lat * $lat + 0.1 * $lng * $lat + 0.2 * sqrt(abs($lng));
$ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
$ret += (20.0 * sin($lat * M_PI) + 40.0 * sin($lat / 3.0 * M_PI)) * 2.0 / 3.0;
$ret += (160.0 * sin($lat / 12.0 * M_PI) + 320 * sin($lat * M_PI / 30.0)) * 2.0 / 3.0;
return $ret;
}
protected static function transformLng($lng, $lat)
{
$ret = 300.0 + $lng + 2.0 * $lat + 0.1 * $lng * $lng + 0.1 * $lng * $lat + 0.1 * sqrt(abs($lng));
$ret += (20.0 * sin(6.0 * $lng * M_PI) + 20.0 * sin(2.0 * $lng * M_PI)) * 2.0 / 3.0;
$ret += (20.0 * sin($lng * M_PI) + 40.0 * sin($lng / 3.0 * M_PI)) * 2.0 / 3.0;
$ret += (150.0 * sin($lng / 12.0 * M_PI) + 300.0 * sin($lng / 30.0 * M_PI)) * 2.0 / 3.0;
return $ret;
}
}