From 8816dd28675bcb8203e04f3c6fe2f44dd182be4c Mon Sep 17 00:00:00 2001 From: weicw1996 Date: Fri, 16 Oct 2026 21:23:48 +0800 Subject: [PATCH] =?UTF-8?q?fix(visit):=20=E4=BF=AE=E5=A4=8D=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E6=8B=9C=E8=AE=BF=E9=A1=B5=E5=AE=9A=E4=BD=8D=E6=97=B6?= =?UTF-8?q?=E5=A5=BD=E6=97=B6=E5=9D=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根因:定位链路用区县「中文名」查 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 个重名区县全部可正常 定位(后端修复尚未部署即已生效)。 --- api/request.js | 18 ++-- package.json | 2 +- pages/visit/index.vue | 36 ++++++-- util/geo.js | 107 ++++++++++++++++++++++ util/geo.test.mjs | 168 ++++++++++++++++++++++++++++++++++ util/index.js | 208 +++++++++++++++++++++++------------------- 6 files changed, 425 insertions(+), 114 deletions(-) create mode 100644 util/geo.js create mode 100644 util/geo.test.mjs diff --git a/api/request.js b/api/request.js index 83b7f91..c4dd341 100644 --- a/api/request.js +++ b/api/request.js @@ -76,21 +76,15 @@ const post = (url, formData, options) => { } export const get = (url, options) => { - // console.log(options) - return new Promise((resolve, reject) => { - http.get(url, options).then(res => { - resolve(res.data) - }) - }) + // 直接返回,不再用 new Promise 包一层纯转发: + // http.get 成功时给出 {data},失败(非 2xx,如天地图 400/429)时 reject, + // 链式 then 会让错误自然向下传递。若吞掉 reject,调用方的 loading / + // locating 等状态将再也回不到初始值,表现为「点了没反应」。 + return http.get(url, options).then(res => res.data) } export const del = (url, options) => { - // console.log(options) - return new Promise((resolve, reject) => { - http.delete(url, options).then(res => { - resolve(res.data) - }) - }) + return http.delete(url, options).then(res => res.data) } export const put = (url, formData, options) => { diff --git a/package.json b/package.json index fb0ebfd..a4d1b85 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "main.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test \"util/*.test.mjs\"" }, "keywords": [], "author": "", diff --git a/pages/visit/index.vue b/pages/visit/index.vue index 408abf2..d34390b 100644 --- a/pages/visit/index.vue +++ b/pages/visit/index.vue @@ -281,6 +281,8 @@ export default { showVisitDatePicker: false, loading: false, locating: false, + // 定位运行号:用于丢弃超时后迟到的旧定位结果(见 fetchLocation) + locateRun: 0, } }, computed: { @@ -373,29 +375,47 @@ export default { return `${label}:${value}` }, async fetchLocation(silent = false, force = false) { + // 兜底:即使底层定位 Promise 因意外情况迟迟不结算,也不会把 locating + // 永久卡在 true(那正是「反复点定位没反应」的成因)。超时后释放标志, + // 用户可再次点击重试。 if (this.locating) return if (!force && this.visitForm.location && this.visitForm.lat && this.visitForm.lng) return this.locating = true + // 每次定位分配一个运行号:超时释放后用户可能立刻重试,此时上一次调用 + // 仍在后台跑(Promise.race 无法取消它)。用运行号丢弃迟到结果, + // 避免旧结果覆盖新定位、或重复弹出失败提示。 + const runId = ++this.locateRun if (!silent) { uni.showLoading({ title: '定位中' }) } + let timeoutId = null try { - const res = await getLocationOnly() + const res = await Promise.race([ + getLocationOnly(), + new Promise((resolve, reject) => { + timeoutId = setTimeout(() => reject(new Error('定位超时')), 15000) + }) + ]) + if (runId !== this.locateRun) return + if (!res) throw new Error('定位结果为空') this.visitForm.lat = res.latitude this.visitForm.lng = res.longitude this.visitForm.location = res.full || [res.province_str, res.city_str, res.area_str].filter(Boolean).join(' ') } catch (err) { - if (!silent) { - uni.showToast({ - title: '定位失败', - icon: 'none' - }) - } + if (runId !== this.locateRun) return + // 静默失败(进页面自动定位)也要给出可见提示,否则用户只看到空白 + // 定位栏,不知道发生了什么、也不知道可以点。 + uni.showToast({ + title: silent ? '定位失败,请点击定位信息重试' : '定位失败,请点击重试', + icon: 'none' + }) } finally { + if (timeoutId) clearTimeout(timeoutId) if (!silent) uni.hideLoading() - this.locating = false + // 仅在仍是当前运行号时释放,避免把新一轮定位的 locating 提前清掉 + if (runId === this.locateRun) this.locating = false } }, async uploadPhotos() { diff --git a/util/geo.js b/util/geo.js new file mode 100644 index 0000000..bb3ae2b --- /dev/null +++ b/util/geo.js @@ -0,0 +1,107 @@ +/** + * 定位相关的纯函数集合。 + * + * 这里刻意不 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('') +} diff --git a/util/geo.test.mjs b/util/geo.test.mjs new file mode 100644 index 0000000..abb787b --- /dev/null +++ b/util/geo.test.mjs @@ -0,0 +1,168 @@ +import test from 'node:test' +import assert from 'node:assert/strict' + +import { + buildGeocodeUrl, + normalizeAdcode, + parseGeocodeResult +} from './geo.js' + +/** + * 回归测试:客户拜访页定位链路的两处确定性缺陷。 + * + * 1) 天地图 geocoder 的 postStr 未编码 —— 实测同一 key: + * 原样 {'lon':..} -> HTTP 400 参数格式错误 + * 仅编码引号 {%27lon%27:..} -> HTTP 400 + * 花括号/冒号编码(encodeURI) -> HTTP 200 + * 全编码(admin 端写法) -> HTTP 200 + * 所以必须确保 postStr 经过完整 URL 编码。 + * + * 2) 行政区划按「中文名」查询在重名区县上必然失败(白云区=广州/贵阳、 + * 朝阳区=北京/长春…)。天地图已直接返回 county_code,其去 156 前缀 + * 即 sys_area.id(实测:越秀区 156440104→440104 与库中 id 一致), + * 可直接精确命中,绕开重名。 + */ + +// ==================== buildGeocodeUrl ==================== + +test('buildGeocodeUrl:postStr 必须被 URL 编码(不得残留裸花括号,否则接口返回 400)', () => { + const url = buildGeocodeUrl(113.2644, 23.1291) + + const query = url.slice(url.indexOf('?') + 1) + const encodedPostStr = query.split('&').find(p => p.startsWith('postStr=')) + + // 解码后应还原为天地图要求的原始形态 + assert.equal( + decodeURIComponent(encodedPostStr.slice('postStr='.length)), + "{'lon':113.2644,'lat':23.1291,'ver':1}" + ) + + // 关键:裸的 { } 必须被编码。未编码时接口实测返回 HTTP 400「参数格式错误」。 + // 注意单引号 ' 按 RFC 3986 属 unreserved 之外的子分隔符,encodeURIComponent + // 不会编码它;实测该形态(花括号/冒号已编码、引号保留)接口返回 HTTP 200, + // 因此这里只断言花括号与冒号必须被编码。 + assert.ok(!/[{}]/.test(encodedPostStr), `花括号未被编码: ${encodedPostStr}`) + assert.ok(!encodedPostStr.includes(':'), `冒号未被编码: ${encodedPostStr}`) +}) + +test('buildGeocodeUrl:经纬度与 tk 参数齐全', () => { + const url = buildGeocodeUrl(121.4737, 31.2304) + + assert.match(url, /^https:\/\/api\.tianditu\.gov\.cn\/geocoder\?/) + assert.ok(url.includes('type=geocode')) + assert.ok(url.includes('tk=')) + assert.ok(decodeURIComponent(url).includes("'lon':121.4737")) + assert.ok(decodeURIComponent(url).includes("'lat':31.2304")) +}) + +test('buildGeocodeUrl:负数/小数坐标不应破坏参数结构', () => { + const url = buildGeocodeUrl(-73.9857, 40.7484) + const query = url.slice(url.indexOf('?') + 1) + + // 恰好 3 个参数,说明坐标里的符号没有把它撑开 + assert.equal(query.split('&').length, 3) + assert.ok(decodeURIComponent(url).includes("'lon':-73.9857")) +}) + +// ==================== normalizeAdcode ==================== + +test('normalizeAdcode:去掉天地图的 156(中国)前缀,得到 6 位 adcode', () => { + assert.equal(normalizeAdcode('156440104'), '440104') // 广州越秀区 + assert.equal(normalizeAdcode('156520113'), '520113') // 贵阳白云区 + assert.equal(normalizeAdcode('156310101'), '310101') // 上海黄浦区 +}) + +test('normalizeAdcode:已是 6 位裸码时原样返回', () => { + assert.equal(normalizeAdcode('440104'), '440104') + assert.equal(normalizeAdcode(440104), '440104') + assert.equal(normalizeAdcode(' 440104 '), '440104') +}) + +test('normalizeAdcode:非法值返回空串,绝不产出错误编码', () => { + for (const bad of [null, undefined, '', ' ', 'abc', '156', '12345', {}, []]) { + assert.equal(normalizeAdcode(bad), '', `应拒绝非法输入: ${JSON.stringify(bad)}`) + } +}) + +// ==================== parseGeocodeResult ==================== + +const successPayload = { + status: '0', + msg: 'ok', + result: { + formatted_address: '广东省广州市越秀区北京街道禺山社区北京路374号', + addressComponent: { + province: '广东省', + city: '广州市', + county: '越秀区', + county_code: '156440104', + city_code: '156440100', + province_code: '156440000' + } + } +} + +test('parseGeocodeResult:正常响应解析出省/市/区县/adcode/地址', () => { + const geo = parseGeocodeResult(successPayload) + + assert.equal(geo.province, '广东省') + assert.equal(geo.city, '广州市') + assert.equal(geo.county, '越秀区') + assert.equal(geo.adcode, '440104') + assert.equal(geo.address, '广东省广州市越秀区北京街道禺山社区北京路374号') +}) + +test('parseGeocodeResult:直辖市 city 为空时用省级名兜底(与 sys_area 一致)', () => { + // 实测天地图对上海/北京/天津/重庆返回 city="" ,而 sys_area 中黄浦区的 city 是「上海市」 + const shanghai = { + status: '0', + result: { + formatted_address: '上海市黄浦区人民广场', + addressComponent: { + province: '上海市', + city: '', + county: '黄浦区', + county_code: '156310101', + city_code: '', + province_code: '156310000' + } + } + } + + const geo = parseGeocodeResult(shanghai) + + assert.equal(geo.city, '上海市', '直辖市应以省名兜底,避免 city_str 为空导致筛选失效') + assert.equal(geo.county, '黄浦区') + assert.equal(geo.adcode, '310101') +}) + +test('parseGeocodeResult:天地图的各类错误响应一律返回 null(不得当成成功)', () => { + const failures = [ + { msg: '参数格式错误', status: 400 }, // postStr 未编码 + { code: 301012, msg: '权限类型错误' }, // key 类型不符 + { code: 302010, msg: '该tk已限流' }, // 限流 + { status: '0' }, // 缺 result + { result: {} }, // 缺 addressComponent + {}, + null, + undefined + ] + + for (const payload of failures) { + assert.equal(parseGeocodeResult(payload), null, + `错误响应必须返回 null: ${JSON.stringify(payload)}`) + } +}) + +test('parseGeocodeResult:adcode 缺失时不崩,且不产生假编码', () => { + const geo = parseGeocodeResult({ + status: '0', + result: { + formatted_address: '某地', + addressComponent: { province: '某省', city: '某市', county: '某区' } + } + }) + + assert.equal(geo.adcode, '', '缺 county_code 时应为空串,由调用方降级为按名字查') + assert.equal(geo.county, '某区') +}) diff --git a/util/index.js b/util/index.js index 3dab220..9ad9191 100644 --- a/util/index.js +++ b/util/index.js @@ -2,6 +2,7 @@ import {getLocation, getUserInfo, setLocation} from "@/util/user"; import {API, MAIN_API} from "@/config"; import {get} from "@/api/request"; import {areaInfoByNameReq, areaInfoReq} from "@/api/common"; +import {buildGeocodeUrl, formatLocationText, parseGeocodeResult} from "./geo"; import dayjs from "dayjs"; import md5 from "md5"; @@ -117,61 +118,116 @@ export const uploadFileToAliOss = async (filePath) => { } -export const getLocationAction = (save = true) => { - return new Promise((resolve, reject) => { - const res = getLocation() - if (res.area_id) resolve(res) - else { - uni.authorize({ - scope: 'scope.userLocation', - success: () => { - uni.getLocation({ - isHighAccuracy: true, - success: async ({longitude, latitude}) => { - const {result} = await get(`https://api.tianditu.gov.cn/geocoder?postStr={'lon':${longitude},'lat':${latitude},'ver':1}&type=geocode&tk=1099ec280e86f4e9b632173326cde90b`) - const areaRes = await areaInfoByNameReq({name: result.addressComponent.county}).catch(() => { - if (!this.areaId) reject(false) - }) - if (save) setLocation({ - longitude, - latitude, - province_str: result.addressComponent.province, - city_str: result.addressComponent.city, - area_str: result.addressComponent.county, - area_id: areaRes.data.id, - city_id: areaRes.data.fid, - province_id: areaRes.data.city.fid - }) - resolve({ - longitude, - latitude, - area_id: areaRes.data.id, - city_id: areaRes.data.fid, - province_id: areaRes.data.city.fid, - province_str: result.addressComponent.province, - city_str: result.addressComponent.city, - area_str: result.addressComponent.county, - }) - }, - fail: () => { - reject(false) - } - }) - }, - fail: () => { - uni.showModal({ - title: '提示', - content: '为了更好的提供服务,请开启定位服务', - success: ({confirm}) => { - if (confirm) uni.openSetting() - } - }) - } - }) +/** + * 调天地图逆地理编码,返回结构化结果(见 util/geo.js)。 + * 网络失败 / 限流(429) / 参数错误(400) 一律 reject,不再静默吞掉。 + */ +const fetchGeocode = async (longitude, latitude) => { + const body = await get(buildGeocodeUrl(longitude, latitude)) + const geo = parseGeocodeResult(body) + if (!geo) throw new Error('天地图逆地理编码返回异常') + return geo +} + +/** + * 把天地图结果映射到 sys_area,取 area_id / city_id / province_id。 + * + * 关键修复:**优先按 adcode 精确查**(天地图 county_code 去 156 前缀即 + * sys_area.id),从根上绕开区县重名问题。 + * + * 降级说明:拿不到 adcode 时才按中文名查,此时若区县重名(如「南山区」 + * 深圳/鹤岗都有),后端只能稳定返回 id 最小的那条,**可能不是用户实际 + * 所在的那个区县**。因此这里的结果仅用于 area_id/city_id/province_id; + * 用于展示的省市区文本一律取天地图原始字段(geo.province/city/county), + * 不受该歧义影响。 + * + * 无论哪种方式查不到都返回 null 而不是让整条定位失败——页面提交只需要 + * 经纬度与地址文本,行政区划编码缺失不应阻断用户。 + */ +const resolveAreaInfo = async geo => { + if (geo.adcode) { + const byCode = await areaInfoByNameReq({id: Number(geo.adcode)}).catch(() => null) + if (byCode && byCode.data) return byCode.data + } + if (geo.county) { + // 有歧义风险(见上),仅作为 adcode 缺失时的兜底 + const byName = await areaInfoByNameReq({name: geo.county}).catch(() => null) + if (byName && byName.data) return byName.data + } + return null +} + +/** 引导用户开启定位授权。用户此前已拒绝过时 authorize 会直接失败,这里兜底提示。 */ +const showLocationSettingGuide = () => { + uni.showModal({ + title: '提示', + content: '为了更好的提供服务,请开启定位服务', + success: ({confirm}) => { + if (confirm) uni.openSetting({}) } }) } +/** + * 定位一次并返回结果:授权 → 取经纬度 → 逆地理编码 → 映射行政区划。 + * + * 所有失败路径都显式 reject,保证调用方的 await 一定结算。 + * (原实现在 catch 回调里用了模块作用域的 this.areaId,在严格模式下会抛 + * TypeError 并让 Promise 永久悬空,调用方的 locating 标志再也回不到 false, + * 表现为「点定位没反应、反复点也没用」。) + * + * @param {boolean} save 是否写入本地缓存(首页等复用缓存时用) + */ +const locateOnce = ({save = false} = {}) => { + return new Promise((resolve, reject) => { + uni.authorize({ + scope: 'scope.userLocation', + success: () => { + uni.getLocation({ + isHighAccuracy: true, + success: async ({longitude, latitude}) => { + try { + const geo = await fetchGeocode(longitude, latitude) + const area = await resolveAreaInfo(geo) + const payload = { + longitude, + latitude, + area_id: area ? area.id : '', + city_id: area ? area.fid : '', + province_id: area && area.city ? area.city.fid : '', + province_str: geo.province, + city_str: geo.city, + area_str: geo.county, + full: formatLocationText(geo) + } + if (save) setLocation(payload) + resolve(payload) + } catch (error) { + reject(false) + } + }, + fail: () => { + reject(false) + } + }) + }, + fail: () => { + showLocationSettingGuide() + reject(false) + } + }) + }) +} + +export const getLocationAction = (save = true) => { + // 保持原有判据:缓存中已有 area_id 才复用。刻意不改判据,避免悄悄改变 + // 首页/业绩/分享/我的这 4 个调用方的刷新行为(它们只消费 city_str,但 + // 「有坐标就跳过重定位」会让跨城移动后的 currentCity 长期陈旧)。 + const cached = getLocation() + if (cached.area_id) return Promise.resolve(cached) + return locateOnce({save}) +} + export const chooseVideo = (sourceType = ['album', 'camera']) => { return new Promise((resolveMain, reject) => { uni.chooseVideo({ @@ -221,48 +277,14 @@ export const chooseVideo = (sourceType = ['album', 'camera']) => { }) } -export const getLocationOnly = () => { - return new Promise((resolve, reject) => { - uni.authorize({ - scope: 'scope.userLocation', - success: () => { - uni.getLocation({ - isHighAccuracy: true, - success: async ({longitude, latitude}) => { - const {result} = await get(`https://api.tianditu.gov.cn/geocoder?postStr={'lon':${longitude},'lat':${latitude},'ver':1}&type=geocode&tk=1099ec280e86f4e9b632173326cde90b`) - const {data} = await areaInfoByNameReq({name: result.addressComponent.county}).catch(() => { - if (!this.areaId) reject(false) - }) - resolve({ - longitude, - latitude, - area_id: data.id, - city_id: data.fid, - province_id: data.city.fid, - province_str: result.addressComponent.province, - city_str: result.addressComponent.city, - area_str: result.addressComponent.county, - full: result.formatted_address - }) - }, - fail: () => { - uni.setStorageSync("no_more_realtime_location", 1) - reject(false) - } - }) - }, - fail: err => { - uni.showModal({ - title: '提示', - content: '为了更好的提供服务,请开启定位服务', - success: ({confirm}) => { - if (confirm) uni.openSetting() - } - }) - } - }) - }) -} +/** + * 强制实时定位(不读缓存),供拜访页等「每次都要当前位置」的场景使用。 + * + * 原实现的问题:把天地图 URL 的 postStr 裸拼在查询串里(实测接口返回 + * HTTP 400),且 catch 回调里误用 this.areaId 导致 Promise 可能永不结算。 + * 现已统一走 locateOnce,保证一定 reject。 + */ +export const getLocationOnly = () => locateOnce({save: false}) export const getExpressMode = () => parseInt(uni.getStorageSync("express_mode"))