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 个重名区县全部可正常
定位(后端修复尚未部署即已生效)。
This commit is contained in:
2026-10-16 21:23:48 +08:00
parent 945cf9c104
commit 8816dd2867
6 changed files with 425 additions and 114 deletions
+28 -8
View File
@@ -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() {