8816dd2867
根因:定位链路用区县「中文名」查 sys_area,重名区县(白云区=广州/贵阳、
朝阳区=北京/长春、南山区=深圳/鹤岗…)命中多条,后端 getOne 默认
throwEx=true 抛 TooManyResultsException,页面表现为「有时能定位、有时不能」
——取决于用户当前所在区县名是否唯一。
改动:
- 新增 util/geo.js(纯函数):buildGeocodeUrl / normalizeAdcode /
parseGeocodeResult / formatLocationText
· 天地图 postStr 补 URL 编码:实测未编码时接口返回 HTTP 400
「参数格式错误」(admin 端 merchantEdit.vue 已是正确写法)
· 严格校验响应结构,限流(429)/参数错误(400) 一律返回 null
· 补直辖市 city 为空时用省名兜底(天地图对上海/北京/天津/重庆返回 city:"")
- util/index.js:抽出 locateOnce / fetchGeocode / resolveAreaInfo 共用
· 清除两处模块作用域的 this.areaId 误用(严格模式下抛 TypeError 会让
Promise 永久悬空,locating 再也回不到 false,即「反复点没反应」)
· 优先按天地图 county_code(去 156 前缀 = sys_area.id)精确查询,
从根上绕开重名;按中文名仅作降级,并注释其歧义风险
· 行政区划查不到不再阻断定位:返回 null,仍给出经纬度与地址
(area_id/city_id/province_id 全仓库无消费方)
- api/request.js:get/del 不再吞掉非 2xx 的 reject,避免调用方状态永久悬空
- pages/visit/index.vue:fetchLocation 加 15s 超时兜底 + 运行号丢弃迟到结果,
保证失败后仍可重试;静默失败也给出可见提示
- 新增 util/geo.test.mjs(10 个单测),package.json 补 test 脚本
验证:前端单测 10/10;真实天地图 + 生产后端端到端 5 个重名区县全部可正常
定位(后端修复尚未部署即已生效)。
108 lines
4.5 KiB
JavaScript
108 lines
4.5 KiB
JavaScript
/**
|
||
* 定位相关的纯函数集合。
|
||
*
|
||
* 这里刻意不 import uni / 任何 API 封装:定位链路最容易出错的正是
|
||
* 「URL 编码」与「行政区划编码归一化」这两段纯逻辑,抽出来才能被
|
||
* node --test 直接覆盖(见 geo.test.mjs)。
|
||
*
|
||
* 背景(客户拜访页「有时能定位、有时不能」):
|
||
* - 天地图 geocoder 的 postStr 未编码时返回 HTTP 400「参数格式错误」;
|
||
* - 用区县中文名去查 sys_area 时,重名区县(白云区=广州/贵阳、
|
||
* 朝阳区=北京/长春…)会命中多条并抛 TooManyResultsException。
|
||
* 而天地图返回的 county_code 去掉 156 前缀即国标 adcode(= sys_area.id),
|
||
* 可直接精确命中,绕开重名。
|
||
*/
|
||
|
||
/** 天地图 geocoder 逆地理编码接口。tk 与 admin 端(merchantEdit.vue)保持同一体系。 */
|
||
const TIANDITU_GEOCODER = 'https://api.tianditu.gov.cn/geocoder'
|
||
const TIANDITU_TK = '1099ec280e86f4e9b632173326cde90b'
|
||
|
||
/** 中国行政区划编码的国别前缀(天地图返回形如 156440104)。 */
|
||
const CHINA_PREFIX = '156'
|
||
|
||
/**
|
||
* 构造天地图逆地理编码请求 URL。
|
||
*
|
||
* postStr 必须整体 encodeURIComponent:实测未编码时该接口返回
|
||
* HTTP 400 {"msg":"参数格式错误"},因为其中的 { } ' : 会破坏查询串结构。
|
||
*
|
||
* @param {number|string} longitude 经度
|
||
* @param {number|string} latitude 纬度
|
||
* @returns {string} 完整请求 URL
|
||
*/
|
||
export const buildGeocodeUrl = (longitude, latitude) => {
|
||
const postStr = `{'lon':${longitude},'lat':${latitude},'ver':1}`
|
||
return `${TIANDITU_GEOCODER}?postStr=${encodeURIComponent(postStr)}&type=geocode&tk=${TIANDITU_TK}`
|
||
}
|
||
|
||
/**
|
||
* 归一化行政区划编码:去掉天地图的 156 前缀,得到 6 位国标 adcode。
|
||
*
|
||
* 说明:156 是天地图返回的国别码(中国)。实测大陆与港澳的 county_code
|
||
* 均带该前缀(越秀区 156440104、香港油尖旺区 156810109),而境外坐标
|
||
* 天地图不返回 county,因此调用方只会在中国境内走到这里。
|
||
* 本函数只做「长度 + 数字」校验,不做国别语义推断:无法识别时返回空串,
|
||
* 由调用方降级为按名字查,绝不把脏值当成合法编码去查库。
|
||
*
|
||
* @param {string|number} value county_code(勿传 province_code/city_code:
|
||
* 它们同样能被解析成 6 位码,但那不是区县,会导致命中省市级)
|
||
* @returns {string} 6 位 adcode;无法识别时返回 ''
|
||
*/
|
||
export const normalizeAdcode = value => {
|
||
if (value === null || value === undefined) return ''
|
||
const raw = String(value).trim()
|
||
if (!raw) return ''
|
||
const digits = raw.startsWith(CHINA_PREFIX) ? raw.slice(CHINA_PREFIX.length) : raw
|
||
return /^\d{6}$/.test(digits) ? digits : ''
|
||
}
|
||
|
||
/**
|
||
* 解析天地图逆地理编码响应。
|
||
*
|
||
* 天地图在失败时并不总是返回非 2xx:限流返回 429、参数错误返回 400,
|
||
* 也见过 HTTP 200 但 body 是 {"msg":"参数格式错误"} 的情况。
|
||
* 因此这里严格校验结构,任何异常一律返回 null,绝不让调用方把错误当成定位成功。
|
||
*
|
||
* @param {object} payload 接口响应体(已 JSON 解析)
|
||
* @returns {{province:string,city:string,county:string,adcode:string,
|
||
* address:string}|null}
|
||
*/
|
||
export const parseGeocodeResult = payload => {
|
||
if (!payload || typeof payload !== 'object') return null
|
||
const result = payload.result
|
||
if (!result || typeof result !== 'object') return null
|
||
const comp = result.addressComponent
|
||
if (!comp || typeof comp !== 'object') return null
|
||
|
||
const province = comp.province || ''
|
||
const county = comp.county || ''
|
||
// 必须有区县名才算有效结果:调用方按名字降级查询时依赖它
|
||
if (!county) return null
|
||
|
||
// 实测天地图对直辖市(上海/北京/天津/重庆)返回 city:"",
|
||
// 而 sys_area 中这些区县的 city 是「上海市」这类省名。用省名兜底,
|
||
// 否则 currentCity 会变成空串,影响后续按城市筛选。
|
||
const city = comp.city || province
|
||
|
||
return {
|
||
province,
|
||
city,
|
||
county,
|
||
adcode: normalizeAdcode(comp.county_code),
|
||
address: result.formatted_address || ''
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 组装用于展示与落库的地址文本。
|
||
* 优先用天地图的完整地址,退化为「省+市+区县」拼接。
|
||
*
|
||
* @param {object} geo parseGeocodeResult 的返回值
|
||
* @returns {string}
|
||
*/
|
||
export const formatLocationText = geo => {
|
||
if (!geo) return ''
|
||
if (geo.address) return geo.address
|
||
return [geo.province, geo.city, geo.county].filter(Boolean).join('')
|
||
}
|