Files
weicw-dshsa 8816dd2867 fix(visit): 修复客户拜访页定位时好时坏
根因:定位链路用区县「中文名」查 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 个重名区县全部可正常
定位(后端修复尚未部署即已生效)。
2026-10-16 21:23:48 +08:00

108 lines
4.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 定位相关的纯函数集合。
*
* 这里刻意不 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('')
}