帮助首页 > 网站操作说明 · 9. 功能 · 逻辑流程 > 定位与分站逻辑流程(全面说明)

定位与分站逻辑流程(全面说明)

定位与分站逻辑流程(全面说明)

项目:baoan(ThinkPHP 分站系统)
更新:2026-07-03
关联简版说明:docs/wap_location_module.md(WAP 模块解耦与部署规范)

1. 总览

系统采用 服务端城市解析 + 前端自动定位 + 分站 URL 生成 三层架构:

层级职责核心文件
服务端解析每个请求确定 city_id,供业务查询与模板渲染LocationService.class.phpCommonAction
前端定位弹层内 GPS → IP → 默认城市,手动切换locationService.jswapLocationCore.js
分站跳转按城市配置生成独立域名 / 静态页 / 主站回退 URLassembleSubstationStatus()checkSubstation API
flowchart TB
    subgraph Server["服务端(每次请求)"]
        A[CommonAction._initialize] --> B{URL 有 city_id?}
        B -->|是| C[setManualCity]
        B -->|否| D[detectCityFromHost]
        C --> E[resolveCity]
        D --> E
        E --> F[assign city_id / city_name 到模板]
    end

    subgraph WAP_FE["WAP 前端(弹层打开时)"]
        G[wapLocationBoot 打开弹层] --> H[scheduleAutoLocate]
        H --> I[quickIpLocate Home/IP]
        H --> J[autoLocate LocationService]
        J --> K[GPS → IP → 默认]
        K --> L[applyCity]
        L --> M[fetchSubstationStatus]
    end

    subgraph Sub["分站"]
        M --> N[checkSubstation API]
        N --> O[assembleSubstationStatus]
        O --> P[static_url / dynamic_url]
    end

    F --> G

2. 文件清单

2.1 服务端核心

路径说明
Banyan/Lib/Service/LocationService.class.php定位与分站核心服务(约 2500+ 行)
Banyan/Lib/Model/CityLocationModel.class.php城市级定位/分站配置(表 by_location_config
Banyan/Lib/Action/Wap/CommonAction.class.phpWAP 基类:加载城市、解析 city_id
Banyan/Lib/Action/Home/CommonAction.class.phpPC 基类:同上 + 分站跳转逻辑(部分已注释)
Banyan/Lib/Action/Wap/LocationApiAction.class.phpWAP 定位/分站 AJAX 接口
Banyan/Lib/Action/Home/LocationApiAction.class.phpPC 定位/分站 AJAX 接口(IP 定位线上稳定
Banyan/Lib/Action/Backstage/SettingAction.class.php后台「定位设置」保存(location 配置组)

2.2 WAP 前端模块(独立,与首页版式解耦)

路径说明
themes/default/Wap/public/header.html唯一入口:输出 CITY_IDGLOBAL_LOCATION_CONFIG、加载脚本
themes/default/Wap/public/city_display.html弹层 HTML + CSS only(禁止内联 JS)
Public/js/wapLocationBoot.js启动层:顶栏点击打开/关闭、调试面板切换
Public/js/locationService.js定位链路:移动 GPS→IP→默认;PC 仅 IP
Public/js/wapLocationCore.js城市切换、缓存、分站检查、手动选择(约 1700 行)

2.3 PC 前端(独立实现,未模块化)

路径说明
themes/default/Home/public/city_display.htmlPC 城市显示 + 内联 JS(GPS/IP/分站,逻辑在模板内)

2.4 规范与规则

路径说明
docs/wap_location_module.mdWAP 定位模块维护规范
.cursor/rules/wap-location-isolated.mdcCursor 规则:改首页不得动定位文件

3. 数据与配置

3.1 数据库表

用途
by_city城市主数据:city_idnamepinyinprovince_idis_substationdomainis_open
by_province省份
by_location_config城市级 KV 配置(如 redirect_enabled
by_settingk=location全局定位配置(序列化 PHP array)

3.2 城市分站相关字段(by_city

字段含义
is_substation1 = 该城市启用分站
domain0 未启用;1 自动二级域;其他字符串 = 自定义独立域名
pinyin拼音,用于 {pinyin}.{hostdo} 及静态路径 /city/{pinyin}/

3.3 全局配置(Setting.location,后台保存)

配置键默认值说明
substation_enabled0系统分站总开关
substation_redirect0是否自动跳转分站
default_city_id-默认城市 ID(IP/GPS 失败回退)
ip_location_enabled1是否启用 IP 定位
browser_location_enabled1是否允许浏览器 GPS(前端 locationService.js
map_servicebaiduGPS 逆地理:baidu / gaode / tencent / google
ip_libraryip2regionIP 库:ip2regiongaodebaidutencentfree
substation_domain_format{pinyin}.{site_hostdo}自动二级域模板
location_cache_time3600定位缓存 TTL(秒)
jump_confirm-前端分站跳转是否弹窗确认
city_switch_enabled1是否显示城市切换入口

配置保存入口:Banyan/Lib/Action/Backstage/SettingAction.class.phplocation 相关 POST 处理)。

3.4 城市级配置(by_location_config

通过 CityLocationModel::getByCityId($cityId) 读取,常见键:

说明
redirect_enabled0 = 禁止该城市分站跳转;缺省或 1 = 允许

4. 服务端城市解析流程

每个 WAP/PC 页面请求在 CommonAction._initialize() 中执行以下步骤。

4.1 WAP:Banyan/Lib/Action/Wap/CommonAction.class.php

1. include LocationService
2. 若 $_GET['city_id'] > 0 → LocationService::setManualCity()
3. detectCityFromHost($citys, HTTP_HOST) → 匹配则 cookie('city_id')
4. resolveCity($citys, $CONFIG) → 得到 city_id、city 对象、pinyin
5. 查省份名 → assign('city_id', 'city_name', 'province_name', 'default_city_id', 'city_ids')

4.2 PC:Banyan/Lib/Action/Home/CommonAction.class.php

与 WAP 类似,额外包含 分站自动跳转 逻辑(当前代码中 已注释禁用,避免循环重定向):

// 访问主域且 city_id ≠ 默认城市时
$redirectUrl = LocationService::evaluateSubdomainRedirect($city, $hostdo, HTTP_HOST, $https);
// if ($redirectUrl) header('Location: ...');

4.3 resolveCity() 优先级

文件:LocationService.class.phpresolveCity()

优先级来源方法
1手动选择getManualSelectedCity()
2域名分站detectCityFromHost()
3默认城市getDefaultLocation()
4兜底返回 (0, [], '')

4.4 getManualSelectedCity() 优先级

优先级来源
1URL 参数 ?city_id=
2Session city_id(非 manual 来源且未过期)
3Cookie manual_city_id
4Cookie city_id(非 manual 且未过期)

4.5 detectCityFromHost() 匹配规则

文件:LocationService.class.phpdetectCityFromHost()

  1. 自定义独立域city.domain0/1 且 host 包含该域名
  2. 拼音二级域:host 以 {pinyin}. 开头(如 hefei.example.com

4.6 setManualCity($city_id)

  • 写入 session('manual_city_id')cookie('manual_city_id', 30天)
  • 调用 cacheLocation(..., ['source'=>'manual', 'ttl'=>30天])
  • 手动选择 不会 被自动定位 TTL 过期清除(expires_at = 0

4.7 cacheLocation() / getCachedLocation()

缓存写入 Session + Cookie:

  • location_cachecity_idcity_nameprovince_name
  • city_cache_sourcemanual | gps | ip | auto
  • city_cache_expires_at:manual 为 0(永不过期);自动定位为 TTL

TTL 来源:location.location_cache_timegetLocationCacheTTL()(300~86400 秒)。


5. 定位 API 流程(服务端)

5.1 IP 定位:LocationService::getIPLocation()

读取 location.ip_location_enabled
  ├─ 未启用 → getDefaultLocation()
  └─ 已启用 → getClientIP()
        ├─ 内网 IP → 可能不准,走配置的 ip_library
        └─ 调用 ip2region / 高德 / 百度 / 腾讯 / free API
              ├─ 成功 → 匹配 by_city 表返回 city_id
              └─ 失败 → getDefaultLocation()

getClientIP() 优先级:X-Forwarded-ForX-Real-IPHTTP_CLIENT_IPREMOTE_ADDR

5.2 GPS 定位:LocationService::locateCityByGPS($lat, $lng)

location.map_service 分发:

实现方法
gaode / amaplocateCityByGaode()
tencentlocateCityByTencent()
googlelocateCityByGoogle()
baidu(默认)locateCityByBaidu()

逆地理得到省市区名称后,在 by_city 中模糊匹配城市记录。

5.3 默认城市:getDefaultLocation()

读取 location.default_city_id,查询 by_city + by_province 返回城市信息。

5.4 智能定位:getSmartLocation()(PC 等场景)

优先级:手动选择 → 登录用户注册城市 → 缓存 → IP → 默认城市。


6. WAP 前端定位流程

6.1 加载顺序(header.html

jQuery / base.js
  → wapLocationBoot.js      (hide_wap_city_location≠1 时)
  → locationService.js
  → 内联脚本:CITY_ID, GLOBAL_LOCATION_CONFIG, WAP_LOCATION_META
  → city_display.html(HTML)
  → wapLocationCore.js

6.2 页面初始化(wapLocationCore.js $(function)

restoreCachedCity()          // localStorage wap_selected_city_v2
updateInfoPanel()
bindEvents()
WapLocationBoot.register({ open, close })
scheduleAutoLocate()         // 若未手动选择
  ├─ quickIpLocate()         // 立即 GET Home/getCityByIP
  └─ setTimeout 400ms → autoLocate()

6.3 自动定位链路(locationService.js

PC 打开 WAP(非移动 UA):

locatePc() → locateByIp()
  → Home/getCityByIP
  → Wap/getCityByIP(备选)
  → Wap/getDefaultCity
  → Home/getDefaultCity
  → fallbackCity()(window.CITY_ID)

移动环境:

locateMobile()
  ├─ browser_location_enabled=0 → 直接 IP
  └─ navigator.geolocation.getCurrentPosition
        ├─ 成功 → POST Wap/getCityByGPS
        │     ├─ 成功 → 返回城市
        │     └─ 失败 → 服务端回退默认城市(is_fallback=true)
        └─ 失败/超时 → locateByIp()(同上)
重要:线上 Wap/getCityByIP 可能返回「非法操作」;前端 优先 使用 Home/getCityByIP

6.4 applyCity(city, options)

应用定位结果的核心函数:

  1. 检查 from_substation=1 + city_id 防覆盖分站城市
  2. 更新 state.current(method、source、IP、GPS 错误等)
  3. updateDisplay() → 更新 .wap-location-city#header-current-city
  4. cacheCity() → localStorage
  5. fetchSubstationStatus(city_id)
  6. 可选 persistCity() → POST setManualCity
  7. 可选 reloadredirectToCity()

6.5 手动切换城市

用户点击「确认」→ checkCitySubstationAndApply()

checkCitySubstation(cityId)
  ├─ 有分站且 jump_confirm=1 → 弹窗「跳转分站 / 仅切换城市」
  └─ 否则 → applyCityById(cityId, cityName, reload=true)
        → POST setManualCity
        → applyCity(..., method:'manual')
        → redirectToCity()(非默认城市则 ?city_id=)

省份/城市列表、搜索:

操作API
省份列表Wap/LocationApi/getProvinceList(失败 fallback Home
城市列表Wap/LocationApi/getCitiesByProvince
热门城市Wap/LocationApi/getCityList
搜索Wap/LocationApi/searchCity

6.6 页面开关(Controller assign

变量效果典型页面
(默认)完整模块 + 默认顶栏普通 WAP 页
hide_wap_city_header=1隐藏 city_display 顶栏,弹层/脚本仍加载IndexAction 首页
hide_wap_city_location=1整页关闭定位模块ShopAction 商家页、AiAction

7. 分站逻辑流程

7.1 分站启用条件(全部满足)

  1. location.substation_enabled = 1
  2. by_city.is_substation = 1
  3. by_location_config.redirect_enabled ≠ 0(城市级未禁用)
  4. (跳转时)目标 host ≠ 当前 host

7.2 URL 类型

assembleSubstationStatus() 生成完整状态对象,主要 URL 字段:

字段含义示例
preview_url / preview_url_webPC 分站预览 URLhttps://hefei.example.com/
preview_url_wapWAP 分站 URLhttps://hefei.example.com/wap/?city_id=123
static_url / preview_url_static静态路径分站https://www.example.com/city/hefei/
dynamic_url动态分站(常同 preview_url_wap)city_id 参数
fallback_url / fallback_url_wap主站回退https://www.example.com/wap/?city_id=123

7.3 域名解析:resolveCityDomainHost()

city.domain结果
自定义字符串(非 0/1)直接使用该域名
1substation_domain_format 替换 {pinyin}{site_hostdo}
0 或空无独立域 → 使用 buildMainHostUrl + ?city_id=

buildSubstationUrl()getSubstationPreviewUrl() 组装最终 URL。

7.4 服务端分站检查方法

方法用途
checkSubstationRedirect($location)根据定位结果返回跳转 URL(无则 false)
evaluateSubdomainRedirect($city, ...)主域访问时判断是否应跳分站(PC CommonAction)
assembleSubstationStatus($city, $config, $cityConfig)返回完整分站状态数组(前端展示用)
buildCityUrl($cityId, $path)按城市 ID 构建 URL(分站关则主域)
buildMainHostUrl($path)主站 URL
redirectToCity($cityId, ...)服务端重定向到城市域

7.5 前端分站交互

自动定位/应用城市后:

fetchSubstationStatus(cityId)GET Wap/LocationApi/checkSubstation?city_id=

→ 更新信息面板「系统分站 / 城市分站 / 域名模式」

→ 若有 URL,显示「立即前往」按钮(受 jump_confirm 控制)

手动选城市时:

#wap-city-select change → checkCitySubstation()showSubstationJumpOption()

显示「静态分站」「动态分站」按钮(若 URL 有效)。

7.6 checkSubstation API 逻辑(Wap)

文件:Banyan/Lib/Action/Wap/LocationApiAction.class.php

校验 city_id、is_substation、substation_enabled
static_url  = site_host + '/city/' + pinyin + '/'
dynamic_url = LocationService::getSubstationPreviewUrl($city, '/wap/', $config)
            (修复 URL 格式、补 city_id 参数)
返回 { static_url, dynamic_url }

Home 端另有 getSubstationStatus 直接返回 assembleSubstationStatus() 完整结构。


8. API 端点汇总

8.1 WAP(g=Wap&m=LocationApi

Action方法说明
getCityByGPSPOSTGPS 逆地理,latitude/longitude
getCityByIPGETIP 定位(线上可能未部署)
getDefaultCityGET默认城市
setManualCityPOST手动选择,city_id
getProvinceListGET省份列表
getCitiesByProvinceGET省内城市
getCityListGET热门城市
searchCityGET搜索,keyword
checkSubstationGET分站 URL,city_id

8.2 Home(g=Home&m=LocationApi

Action说明
getCityByIP推荐 IP 定位入口
getDefaultCity默认城市
setManualCity手动选择
checkSubstation分站 URL
checkSubstationRedirect返回跳转 URL
getSubstationStatus完整 assembleSubstationStatus

8.3 请求示例

# IP 定位(稳定)
curl "https://域名/index.php?g=Home&m=LocationApi&a=getCityByIP"

# 默认城市
curl "https://域名/index.php?g=Wap&m=LocationApi&a=getDefaultCity"

# 分站检查
curl "https://域名/index.php?g=Wap&m=LocationApi&a=checkSubstation&city_id=150"

# 手动设置城市
curl -X POST "https://域名/index.php?g=Wap&m=LocationApi&a=setManualCity" -d "city_id=150"

9. 端到端时序图

9.1 用户首次打开 WAP 首页

sequenceDiagram
    participant U as 用户
    participant B as 浏览器
    participant S as CommonAction
    participant LS as LocationService
    participant API as LocationApi

    U->>B: 访问 /wap/
    B->>S: HTTP 请求
    S->>LS: resolveCity()
    LS-->>S: city_id, city_name
    S-->>B: HTML + CITY_ID + GLOBAL_LOCATION_CONFIG

    U->>B: 点击城市按钮
    B->>B: wapLocationBoot.openSelector()
    B->>B: scheduleAutoLocate()
    B->>API: GET Home/getCityByIP
    API->>LS: getIPLocation()
    API-->>B: city_id
    B->>B: applyCity()
    B->>API: GET checkSubstation?city_id=
    API-->>B: static_url, dynamic_url

9.2 用户手动切换到有分站的城市

sequenceDiagram
    participant U as 用户
    participant C as wapLocationCore
    participant API as LocationApi

    U->>C: 选省/市 → 确认
    C->>API: checkSubstation?city_id=
    API-->>C: static_url, dynamic_url
    alt jump_confirm=1 且有分站
        C->>U: 弹窗:跳转分站 / 仅切换
        U->>C: 跳转分站
        C->>C: location.href = static_url
    else 仅切换
        C->>API: POST setManualCity
        C->>C: redirectToCity(?city_id=)
    end

10. PC 与 WAP 差异

维度WAPPC (Home)
前端实现独立 JS 模块(wapLocationCore.jscity_display.html 内联 JS
IP API 优先Home/getCityByIPHome/getCityByIP
顶栏wap-location-trigger + boot 层Home 模板自有顶栏
服务端自动跳分站WAP CommonAction 未启用Home CommonAction 已注释
定位模块隔离hide_wap_city_location 可关闭无对应开关

11. 全局 JS 接口

接口文件说明
openWapCitySelector()wapLocationCore打开弹层
closeWapCitySelector()wapLocationCore关闭弹层
WapLocationBoot.register({open,close})wapLocationCore → boot注册完整实现
LocationService.getCurrentLocation()locationServicePromise 定位
WapLocationRetry()wapLocationCore重试定位
WapLocationCoreToggleDebug()wapLocationCore调试面板
window.CITY_ID / CITY_NAMEheader 输出服务端解析结果

12. 常见问题与排查

现象可能原因排查
面板一直「检测中…」模板内联 JS 被 ThinkPHP 破坏;或脚本未加载确认无内联 JS;查控制台语法错误
IP 定位失败Wap/getCityByIP 未部署Home/getCityByIP;查 Network
点击城市无反应hide_wap_city_location=1 或缺少 wap-location-trigger查 header 是否加载 boot
分站按钮无 URLsubstation_enabled=0is_substation=0checkSubstation API
切换城市后内容不变仅前端切换未 reload手动切换会 redirectToCity(?city_id=)
调试「点击展开」无效.selector-panel 冒泡被拦截wapLocationBoot.js 在 panel 外绑定

13. 部署与同步清单

修改定位相关代码后,需同步并清缓存:

Public/js/wapLocationBoot.js
Public/js/wapLocationCore.js
Public/js/locationService.js
themes/default/Wap/public/header.html
themes/default/Wap/public/city_display.html
Banyan/Lib/Service/LocationService.class.php
Banyan/Lib/Action/Wap/LocationApiAction.class.php
Banyan/Lib/Action/Home/LocationApiAction.class.php(若改 IP/分站 API)

清除模板缓存:Banyan/Runtime/Cache/

同步目标项目:ba(同路径覆盖)


14. 关键代码索引

功能文件方法/位置
城市解析优先级LocationService.class.phpresolveCity() ~2123
手动城市LocationService.class.phpgetManualSelectedCity() ~115, setManualCity() ~242
域名识别LocationService.class.phpdetectCityFromHost() ~2103
IP 定位LocationService.class.phpgetIPLocation() ~317
GPS 定位LocationService.class.phplocateCityByGPS() ~1688
缓存LocationService.class.phpcacheLocation() ~1529
分站状态LocationService.class.phpassembleSubstationStatus() ~2308
分站 URL 构建LocationService.class.phpbuildSubstationUrl() ~2219
WAP 请求初始化Wap/CommonAction.class.php_initialize() ~40-53
WAP 自动定位wapLocationCore.jsscheduleAutoLocate() ~241, autoLocate() ~348
应用城市wapLocationCore.jsapplyCity() ~497
分站检查wapLocationCore.jsfetchSubstationStatus() ~1082

15. 版本记录

日期说明
2026-07-03首版:定位 + 分站全流程文档
2026-06-23WAP 定位模块化(见 wap_location_module.md
copyright 2013-2113 www.baoanfuwu.cn All Rights Reserved 保安服务网版权所有
皖ICP备2021016109号-1