定位与分站逻辑流程(全面说明)
定位与分站逻辑流程(全面说明)
项目:baoan(ThinkPHP 分站系统)
更新:2026-07-03
关联简版说明:docs/wap_location_module.md(WAP 模块解耦与部署规范)
1. 总览
系统采用 服务端城市解析 + 前端自动定位 + 分站 URL 生成 三层架构:
| 层级 | 职责 | 核心文件 |
|---|---|---|
| 服务端解析 | 每个请求确定 city_id,供业务查询与模板渲染 | LocationService.class.php、CommonAction |
| 前端定位 | 弹层内 GPS → IP → 默认城市,手动切换 | locationService.js、wapLocationCore.js |
| 分站跳转 | 按城市配置生成独立域名 / 静态页 / 主站回退 URL | assembleSubstationStatus()、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.php | WAP 基类:加载城市、解析 city_id |
Banyan/Lib/Action/Home/CommonAction.class.php | PC 基类:同上 + 分站跳转逻辑(部分已注释) |
Banyan/Lib/Action/Wap/LocationApiAction.class.php | WAP 定位/分站 AJAX 接口 |
Banyan/Lib/Action/Home/LocationApiAction.class.php | PC 定位/分站 AJAX 接口(IP 定位线上稳定) |
Banyan/Lib/Action/Backstage/SettingAction.class.php | 后台「定位设置」保存(location 配置组) |
2.2 WAP 前端模块(独立,与首页版式解耦)
| 路径 | 说明 |
|---|---|
themes/default/Wap/public/header.html | 唯一入口:输出 CITY_ID、GLOBAL_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.html | PC 城市显示 + 内联 JS(GPS/IP/分站,逻辑在模板内) |
2.4 规范与规则
| 路径 | 说明 |
|---|---|
docs/wap_location_module.md | WAP 定位模块维护规范 |
.cursor/rules/wap-location-isolated.mdc | Cursor 规则:改首页不得动定位文件 |
3. 数据与配置
3.1 数据库表
| 表 | 用途 |
|---|---|
by_city | 城市主数据:city_id、name、pinyin、province_id、is_substation、domain、is_open |
by_province | 省份 |
by_location_config | 城市级 KV 配置(如 redirect_enabled) |
by_setting(k=location) | 全局定位配置(序列化 PHP array) |
3.2 城市分站相关字段(by_city)
| 字段 | 含义 |
|---|---|
is_substation | 1 = 该城市启用分站 |
domain | 0 未启用;1 自动二级域;其他字符串 = 自定义独立域名 |
pinyin | 拼音,用于 {pinyin}.{hostdo} 及静态路径 /city/{pinyin}/ |
3.3 全局配置(Setting.location,后台保存)
| 配置键 | 默认值 | 说明 |
|---|---|---|
substation_enabled | 0 | 系统分站总开关 |
substation_redirect | 0 | 是否自动跳转分站 |
default_city_id | - | 默认城市 ID(IP/GPS 失败回退) |
ip_location_enabled | 1 | 是否启用 IP 定位 |
browser_location_enabled | 1 | 是否允许浏览器 GPS(前端 locationService.js) |
map_service | baidu | GPS 逆地理:baidu / gaode / tencent / google |
ip_library | ip2region | IP 库:ip2region、gaode、baidu、tencent、free |
substation_domain_format | {pinyin}.{site_hostdo} | 自动二级域模板 |
location_cache_time | 3600 | 定位缓存 TTL(秒) |
jump_confirm | - | 前端分站跳转是否弹窗确认 |
city_switch_enabled | 1 | 是否显示城市切换入口 |
配置保存入口:Banyan/Lib/Action/Backstage/SettingAction.class.php(location 相关 POST 处理)。
3.4 城市级配置(by_location_config)
通过 CityLocationModel::getByCityId($cityId) 读取,常见键:
| 键 | 说明 |
|---|---|
redirect_enabled | 0 = 禁止该城市分站跳转;缺省或 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.php → resolveCity()
| 优先级 | 来源 | 方法 |
|---|---|---|
| 1 | 手动选择 | getManualSelectedCity() |
| 2 | 域名分站 | detectCityFromHost() |
| 3 | 默认城市 | getDefaultLocation() |
| 4 | 兜底 | 返回 (0, [], '') |
4.4 getManualSelectedCity() 优先级
| 优先级 | 来源 |
|---|---|
| 1 | URL 参数 ?city_id= |
| 2 | Session city_id(非 manual 来源且未过期) |
| 3 | Cookie manual_city_id |
| 4 | Cookie city_id(非 manual 且未过期) |
4.5 detectCityFromHost() 匹配规则
文件:LocationService.class.php → detectCityFromHost()
- 自定义独立域:
city.domain非0/1且 host 包含该域名 - 拼音二级域: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_cache、city_id、city_name、province_namecity_cache_source:manual|gps|ip|auto等city_cache_expires_at:manual 为 0(永不过期);自动定位为 TTL
TTL 来源:location.location_cache_time 或 getLocationCacheTTL()(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-For → X-Real-IP → HTTP_CLIENT_IP → REMOTE_ADDR
5.2 GPS 定位:LocationService::locateCityByGPS($lat, $lng)
按 location.map_service 分发:
| 值 | 实现方法 |
|---|---|
gaode / amap | locateCityByGaode() |
tencent | locateCityByTencent() |
google | locateCityByGoogle() |
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)
应用定位结果的核心函数:
- 检查
from_substation=1+city_id防覆盖分站城市 - 更新
state.current(method、source、IP、GPS 错误等) updateDisplay()→ 更新.wap-location-city、#header-current-citycacheCity()→ localStoragefetchSubstationStatus(city_id)- 可选
persistCity()→ POSTsetManualCity - 可选
reload→redirectToCity()
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 分站启用条件(全部满足)
location.substation_enabled = 1by_city.is_substation = 1by_location_config.redirect_enabled ≠ 0(城市级未禁用)- (跳转时)目标 host ≠ 当前 host
7.2 URL 类型
assembleSubstationStatus() 生成完整状态对象,主要 URL 字段:
| 字段 | 含义 | 示例 |
|---|---|---|
preview_url / preview_url_web | PC 分站预览 URL | https://hefei.example.com/ |
preview_url_wap | WAP 分站 URL | https://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) | 直接使用该域名 |
1 | 按 substation_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 | 方法 | 说明 |
|---|---|---|
getCityByGPS | POST | GPS 逆地理,latitude/longitude |
getCityByIP | GET | IP 定位(线上可能未部署) |
getDefaultCity | GET | 默认城市 |
setManualCity | POST | 手动选择,city_id |
getProvinceList | GET | 省份列表 |
getCitiesByProvince | GET | 省内城市 |
getCityList | GET | 热门城市 |
searchCity | GET | 搜索,keyword |
checkSubstation | GET | 分站 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 差异
| 维度 | WAP | PC (Home) |
|---|---|---|
| 前端实现 | 独立 JS 模块(wapLocationCore.js) | city_display.html 内联 JS |
| IP API 优先 | Home/getCityByIP | Home/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() | locationService | Promise 定位 |
WapLocationRetry() | wapLocationCore | 重试定位 |
WapLocationCoreToggleDebug() | wapLocationCore | 调试面板 |
window.CITY_ID / CITY_NAME | header 输出 | 服务端解析结果 |
12. 常见问题与排查
| 现象 | 可能原因 | 排查 |
|---|---|---|
| 面板一直「检测中…」 | 模板内联 JS 被 ThinkPHP 破坏;或脚本未加载 | 确认无内联 JS;查控制台语法错误 |
| IP 定位失败 | Wap/getCityByIP 未部署 | 用 Home/getCityByIP;查 Network |
| 点击城市无反应 | hide_wap_city_location=1 或缺少 wap-location-trigger | 查 header 是否加载 boot |
| 分站按钮无 URL | substation_enabled=0 或 is_substation=0 | 调 checkSubstation 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.php | resolveCity() ~2123 |
| 手动城市 | LocationService.class.php | getManualSelectedCity() ~115, setManualCity() ~242 |
| 域名识别 | LocationService.class.php | detectCityFromHost() ~2103 |
| IP 定位 | LocationService.class.php | getIPLocation() ~317 |
| GPS 定位 | LocationService.class.php | locateCityByGPS() ~1688 |
| 缓存 | LocationService.class.php | cacheLocation() ~1529 |
| 分站状态 | LocationService.class.php | assembleSubstationStatus() ~2308 |
| 分站 URL 构建 | LocationService.class.php | buildSubstationUrl() ~2219 |
| WAP 请求初始化 | Wap/CommonAction.class.php | _initialize() ~40-53 |
| WAP 自动定位 | wapLocationCore.js | scheduleAutoLocate() ~241, autoLocate() ~348 |
| 应用城市 | wapLocationCore.js | applyCity() ~497 |
| 分站检查 | wapLocationCore.js | fetchSubstationStatus() ~1082 |
15. 版本记录
| 日期 | 说明 |
|---|---|
| 2026-07-03 | 首版:定位 + 分站全流程文档 |
| 2026-06-23 | WAP 定位模块化(见 wap_location_module.md) |
