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 个重名区县全部可正常
定位(后端修复尚未部署即已生效)。
169 lines
6.3 KiB
JavaScript
169 lines
6.3 KiB
JavaScript
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, '某区')
|
||
})
|