- 新增商家/信息详情页电话拨打接口,支持每日额度限制与拨打日志记录 - 新增平台免责声明内容及协议接口,完善用户服务协议相关配置 - 新增农产品分类图标与上市角标展示,优化分类管理界面样式 - 新增全国行政区划工具类与城市选择组件,重构地区数据结构 - 新增SSH自检与部署脚本,完善项目部署流程
189 行
6.0 KiB
PHP
189 行
6.0 KiB
PHP
<?php
|
||
|
||
namespace app\common\library;
|
||
|
||
use think\Db;
|
||
|
||
/**
|
||
* 全国标准省/市/区县服务区域工具类
|
||
*
|
||
* 规则:
|
||
* - 默认全国所有省、市、区县全部开通,无需后台手动维护;
|
||
* - 数据源取自 application/extra/national_areas.json(由全国行政区划字典构建,涵盖全国 34 个省级行政区、全部地级市及区县);
|
||
* - 同时合并 fa_fc_area 表中历史自定义记录,确保旧 ID 与历史数据 100% 兼容。
|
||
*/
|
||
class NationalArea
|
||
{
|
||
/** @var array|null 内存级缓存 */
|
||
protected static $allCache = null;
|
||
|
||
/**
|
||
* 获取全国全部省/市/区县平铺列表
|
||
*
|
||
* @return array<int, array{id:int,province:string,city:string,district:string,lat:float|null,lng:float|null}>
|
||
*/
|
||
public static function all()
|
||
{
|
||
if (self::$allCache !== null) {
|
||
return self::$allCache;
|
||
}
|
||
|
||
$jsonFile = APP_PATH . 'extra' . DS . 'national_areas.json';
|
||
$list = [];
|
||
if (is_file($jsonFile)) {
|
||
$decoded = json_decode((string)file_get_contents($jsonFile), true);
|
||
if (is_array($decoded)) {
|
||
$list = $decoded;
|
||
}
|
||
}
|
||
|
||
// 合并数据库中已有的自定义启用区域(避免重复)
|
||
try {
|
||
$dbRows = Db::name('fc_area')
|
||
->where('enabled', 1)
|
||
->order('sort asc, id asc')
|
||
->select();
|
||
$seenKey = [];
|
||
$seenId = [];
|
||
foreach ($list as $idx => $item) {
|
||
$k = $item['province'] . '|' . $item['city'] . '|' . $item['district'];
|
||
$seenKey[$k] = $idx;
|
||
$seenId[(int)$item['id']] = $idx;
|
||
}
|
||
foreach ($dbRows as $row) {
|
||
$p = (string)$row['province'];
|
||
$c = (string)$row['city'];
|
||
$d = (string)$row['district'];
|
||
$k = $p . '|' . $c . '|' . $d;
|
||
$id = (int)$row['id'];
|
||
if (isset($seenKey[$k])) {
|
||
$targetIdx = $seenKey[$k];
|
||
if ($row['lat'] !== null && $row['lng'] !== null) {
|
||
$list[$targetIdx]['lat'] = (float)$row['lat'];
|
||
$list[$targetIdx]['lng'] = (float)$row['lng'];
|
||
}
|
||
continue;
|
||
}
|
||
if (!isset($seenId[$id])) {
|
||
$list[] = [
|
||
'id' => $id,
|
||
'province' => $p,
|
||
'city' => $c,
|
||
'district' => $d,
|
||
'lat' => $row['lat'] !== null ? (float)$row['lat'] : null,
|
||
'lng' => $row['lng'] !== null ? (float)$row['lng'] : null,
|
||
];
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
// 忽略数据库读取异常,以全国静态字典兜底
|
||
}
|
||
|
||
self::$allCache = $list;
|
||
return self::$allCache;
|
||
}
|
||
|
||
/**
|
||
* 按区域 ID 查找单条省/市/区县记录
|
||
*
|
||
* @param int $areaId 区县ID
|
||
* @return array|null
|
||
*/
|
||
public static function findById($areaId)
|
||
{
|
||
$areaId = (int)$areaId;
|
||
if ($areaId <= 0) {
|
||
return null;
|
||
}
|
||
foreach (self::all() as $row) {
|
||
if ((int)$row['id'] === $areaId) {
|
||
return $row;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 按区县名称(及可选省/市)查找匹配记录
|
||
*
|
||
* @param string $district 区县名称
|
||
* @param string $city 可选城市名称
|
||
* @param string $province 可选省份名称
|
||
* @return array|null
|
||
*/
|
||
public static function findByName($district, $city = '', $province = '')
|
||
{
|
||
$district = trim((string)$district);
|
||
$city = trim((string)$city);
|
||
$province = trim((string)$province);
|
||
if ($district === '') {
|
||
return null;
|
||
}
|
||
foreach (self::all() as $row) {
|
||
if ($row['district'] === $district) {
|
||
if ($city !== '' && $row['city'] !== $city) {
|
||
continue;
|
||
}
|
||
if ($province !== '' && $row['province'] !== $province) {
|
||
continue;
|
||
}
|
||
return $row;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 按经纬度坐标(gcj02)匹配最近的全国区县
|
||
*
|
||
* @param float $lat 纬度
|
||
* @param float $lng 经度
|
||
* @return array|null {areaId, province, city, district, distance}
|
||
*/
|
||
public static function locate($lat, $lng)
|
||
{
|
||
$best = null;
|
||
foreach (self::all() as $row) {
|
||
if ($row['lat'] === null || $row['lng'] === null) {
|
||
continue;
|
||
}
|
||
$dist = self::haversine($lat, $lng, (float)$row['lat'], (float)$row['lng']);
|
||
if ($best === null || $dist < $best['distance']) {
|
||
$best = ['row' => $row, 'distance' => $dist];
|
||
}
|
||
}
|
||
if (!$best || $best['distance'] > 1000) {
|
||
return null;
|
||
}
|
||
$area = $best['row'];
|
||
return [
|
||
'areaId' => (int)$area['id'],
|
||
'province' => (string)$area['province'],
|
||
'city' => (string)$area['city'],
|
||
'district' => (string)$area['district'],
|
||
'distance' => $best['distance'],
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 计算两点间 Haversine 球面距离(km,保留 1 位小数)
|
||
*
|
||
* @param float $lat1
|
||
* @param float $lng1
|
||
* @param float $lat2
|
||
* @param float $lng2
|
||
* @return float
|
||
*/
|
||
public static function haversine($lat1, $lng1, $lat2, $lng2)
|
||
{
|
||
$earth = 6371.0;
|
||
$rad = M_PI / 180;
|
||
$dLat = ($lat2 - $lat1) * $rad;
|
||
$dLng = ($lng2 - $lng1) * $rad;
|
||
$a = sin($dLat / 2) * sin($dLat / 2)
|
||
+ cos($lat1 * $rad) * cos($lat2 * $rad) * sin($dLng / 2) * sin($dLng / 2);
|
||
$c = 2 * asin(min(1.0, sqrt($a)));
|
||
return round($earth * $c, 1);
|
||
}
|
||
}
|