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,79 @@
<?php
namespace addons\shopro\controller;
use addons\shopro\exception\Exception;
use addons\shopro\model\UserAddress;
use addons\shopro\model\Area;
class Address extends Base
{
protected $noNeedLogin = ['area'];
protected $noNeedRight = ['*'];
public function index()
{
$this->success('收货地址', UserAddress::getUserAddress());
}
public function defaults()
{
$this->success('默认收货地址', UserAddress::getUserDefaultAddress());
}
public function area()
{
$data['provinceData'] = Area::where('level', 1)->order('id asc')->field('id as value, name as label, pid, level')->select();
foreach ($data['provinceData'] as $k => $p) {
$data['cityData'][$k] = Area::where(['level' => 2, 'pid' => $p->value])->order('id asc')->field('id as value, name as label, pid, level')->select();
foreach ($data['cityData'][$k] as $i => $c) {
$data['areaData'][$k][$i] = Area::where(['level' => 3, 'pid' => $c->value])->order('id asc')->field('id as value, name as label, pid, level')->select();
}
}
$this->success('省市区', $data);
}
public function edit()
{
$params = $this->request->post();
// 表单验证
$this->shoproValidate($params, get_class(), 'edit');
$this->success('编辑地址', UserAddress::edit($params));
}
public function info()
{
$params = $this->request->get();
$this->success('地址详情', UserAddress::info($params));
}
public function del()
{
$params = $this->request->post();
$this->success('地址详情', UserAddress::del($params));
}
public function regeo()
{
$latitude = $this->request->post('latitude');
$longitude = $this->request->post('longitude');
if (!is_numeric($latitude) || !is_numeric($longitude) || abs($latitude) < 0.000001 || abs($longitude) < 0.000001) {
$this->error('请先选择位置');
}
try {
$data = \app\common\library\Tianditu::reverse((float)$longitude, (float)$latitude);
} catch (\Exception $e) {
$this->error($e->getMessage() ?: '地址解析失败');
}
$this->success('解析成功', $data);
}
}

查看文件

@@ -0,0 +1,122 @@
<?php
namespace addons\shopro\model;
use addons\shopro\model\Area;
use think\Model;
use addons\shopro\exception\Exception;
use traits\model\SoftDelete;
/**
* 用户地址模型
*/
class UserAddress extends Model
{
use SoftDelete;
protected $name = 'shopro_user_address';
// 开启自动写入时间戳字段
protected $autoWriteTimestamp = 'int';
// 定义时间戳字段名
protected $createTime = 'createtime';
protected $updateTime = 'updatetime';
protected $deleteTime = 'deletetime';
protected $hidden = ['createtime', 'updatetime', 'deletetime'];
// 追加属性
protected $append = [];
public static function getUserAddress()
{
$user = User::info();
return self::all([
'user_id' => $user->id,
'deletetime' => null
]);
}
public static function getUserDefaultAddress()
{
$user = User::info();
$default = self::get([
'user_id' => $user->id,
'deletetime' => null,
'is_default' => '1'
]);
return $default;
}
public static function edit($params)
{
$user = User::info();
extract($params);
$areaNameArray = explode("-", $area_text);
try {
$province = Area::get(['name' => $areaNameArray[0], 'level' => 1]);
$city = $province ? Area::get(['name' => $areaNameArray[1], 'pid' => $province->id, 'level' => 2]) : null;
$area = $city ? Area::get(['name' => $areaNameArray[2], 'pid' => $city->id, 'level' => 3]) : null;
if (!$province || !$city || !$area) {
new Exception('暂不支持,请手动选择行政区');
}
} catch (\Exception $e) {
new Exception('暂不支持,请手动选择行政区');
}
$is_default = (isset($is_default) && $is_default) ? '1' : '0';
if ($is_default) {
self::where(['user_id' => $user->id, 'is_default' => '1'])->update(['is_default' => '0']);
}
if (!isset($id) || $id == 0) {
$edit = self::create([
'consignee' => $consignee,
'phone' => $phone,
'province_id' => $province->id,
'province_name' => $province->name,
'city_id' => $city->id,
'city_name' => $city->name,
'area_id' => $area->id,
'area_name' => $area->name,
'is_default' => $is_default,
'latitude' => (isset($latitude) && $latitude) ? $latitude : null,
'longitude' => (isset($longitude) && $longitude) ? $longitude : null,
'user_id' => $user->id,
'address' => $address
]);
} else {
$edit = self::update([
'consignee' => $consignee,
'phone' => $phone,
'province_id' => $province->id,
'province_name' => $province->name,
'city_id' => $city->id,
'city_name' => $city->name,
'area_id' => $area->id,
'area_name' => $area->name,
'is_default' => $is_default,
'latitude' => (isset($latitude) && $latitude) ? $latitude : null,
'longitude' => (isset($longitude) && $longitude) ? $longitude : null,
'user_id' => $user->id,
'address' => $address,
'id' => $id
]);
}
return $edit;
}
public static function info($params)
{
$user = User::info();
extract($params);
return self::get(['id' => $id, 'user_id' => $user->id]);
}
public static function del($params)
{
$user = User::info();
extract($params);
return self::get(['id' => $id, 'user_id' => $user->id])->delete();
}
}

查看文件

@@ -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;
}
}

查看文件

@@ -0,0 +1,6 @@
<?php
return [
// 天地图服务器端应用密钥,仅后端调用
'key' => '55aee666be14cd0100874d6faf3c963c',
];