/** * 定位相关的纯函数集合。 * * 这里刻意不 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('') }