'use strict'; /** * Overpass 兜底路径验证(Photon 全挂场景)。 * * 起一个**本地假 Overpass**(识别 query 里的特征串返回不同数据), * 把 config.ENDPOINTS.overpass 指过去,验证: * 1. Photon 全挂时确实走 Overpass 兜底 * 2. Overpass 候选的归属正确(环江喀斯特 → 河池市/环江县,不跑到荔波县) * 3. is_in 反查结果经过统一查表归一(OSM 简称 → 标准县全称) * 4. Overpass 返回 200 但内容是 HTML 错误页时,要被识别为失败而不是当数据解析 */ const http = require('http'); const path = require('path'); const SRC = path.join(__dirname, '..', 'lib', 'sources.js'); const ORIGIN = { lat: 24.69562, lon: 108.08091 }; // 假 Overpass:按 query 特征分发 const ADMIN_ISIN = JSON.stringify({ version: 0.6, elements: [ { type: 'relation', id: 1, tags: { admin_level: '4', name: '广西壮族自治区' } }, { type: 'relation', id: 2, tags: { admin_level: '5', name: '河池市' } }, { type: 'relation', id: 3, tags: { admin_level: '6', name: '环江毛南族自治县' } }, ], }); // 行政区别名:故意用**简称**「环江」,验证会过查表归一成全称 const ADMIN_ISIN_SHORT = JSON.stringify({ version: 0.6, elements: [ { type: 'relation', id: 2, tags: { admin_level: '5', name: '河池市' } }, { type: 'relation', id: 3, tags: { admin_level: '6', name: '环江' } }, ], }); // POI:环江喀斯特(25.1, 108.28,环江县境内) const POIS = JSON.stringify({ version: 0.6, elements: [ { type: 'node', id: 11, lat: 25.10, lon: 108.28, tags: { name: '环江喀斯特世界自然遗产-核心功能区', tourism: 'attraction' } }, { type: 'node', id: 12, lat: 25.24, lon: 107.90, tags: { name: '小七孔古镇', tourism: 'attraction' } }, ], }); const HTML_ERROR = 'Error : runtime error: duplicate_query'; const server = http.createServer((req, res) => { let body = ''; req.on('data', (c) => { body += c; }); req.on('end', () => { const q = decodeURIComponent(body.replace(/^data=/, '')); const send = (txt, code = 200) => { res.writeHead(code, { 'content-type': 'application/json' }); res.end(txt); }; if (q.includes('is_in')) { // 25.10,108.28 用全称;25.24,107.90 用简称(测试归一) if (q.includes('25.10000,108.28000')) return send(ADMIN_ISIN); if (q.includes('25.24000,107.90000')) return send(ADMIN_ISIN_SHORT); return send(JSON.stringify({ version: 0.6, elements: [] })); } if (q.includes('"tourism"')) { // 第二次尝试时返回 HTML 错误页(测「200 但不是 JSON」) if (process.env.HTML_MODE === '1') return send(HTML_ERROR); return send(POIS); } if (q.includes('admin_level')) { return send(JSON.stringify({ version: 0.6, elements: [] })); } send(JSON.stringify({ version: 0.6, elements: [] })); }); }); server.listen(0, '127.0.0.1', async () => { const port = server.address().port; const cfg = { ENDPOINTS: { overpass: [`http://127.0.0.1:${port}/api/interpreter`], photon: 'http://127.0.0.1:1/api', // 必然失败,强制走兜底 reverse: 'http://127.0.0.1:1/reverse', amapAround: '', amapGeocode: '', amapPlaceText: '', amapRegeo: '', amapDriving: '', amapStaticMap: '', }, AMAP_KEY: '', SEARCH: { maxRadiusKm: 200, maxCandidates: 20 }, PHOTON: { categories: [], concurrency: 1, perCategory: 2, timeoutMs: 500, retries: 0 }, }; // 强制清掉模块缓存,让新的 cfg 生效 for (const k of Object.keys(require.cache)) if (k.includes(path.sep + 'lib' + path.sep)) delete require.cache[k]; const S = require(SRC); console.log('=== Overpass 兜底路径验证(Photon 全挂)==='); const res = await S.discoverCandidates(ORIGIN, 150, cfg, { excludeLocal: true }); const cands = res.candidates || []; console.log('候选数 =', cands.length); console.log('stats =', JSON.stringify(res.stats)); for (const c of cands) { console.log(` ${c.name} → ${c.place || '(空)'} / ${c.placeDetail || '(空)'} [${c.source}]`); } // 断言 const fail = []; const find = (kw) => cands.find((c) => c.name.includes(kw)); const huanjiang = find('环江喀斯特'); if (!huanjiang) fail.push('环江喀斯特没进候选'); else { if (huanjiang.place !== '河池市') fail.push(`环江喀斯特地级应为 河池市,实际 ${huanjiang.place}`); if (huanjiang.placeDetail !== '环江毛南族自治县') fail.push(`环江喀斯特县应为 环江毛南族自治县,实际 ${huanjiang.placeDetail}`); } const xiaoqikong = find('小七孔古镇'); if (!xiaoqikong) fail.push('小七孔古镇没进候选'); else if (xiaoqikong.placeDetail !== '环江毛南族自治县') { // mock 的 is_in 只给了 OSM 简称「环江」,应被查表归一成标准全称 fail.push(`小七孔的县名没归一,实际 ${xiaoqikong.placeDetail}`); } for (const c of cands) { if (!c.place || c.place === '?') fail.push(`${c.name} 分组名为空`); if (c.placeDetail === '?') fail.push(`${c.name} 下层地名为空`); } console.log(fail.length ? '\n❌ 失败:\n ' + fail.join('\n ') : '\n✅ 全部通过'); // 第二组:Overpass 返回 200 但内容是 HTML 错误页(duplicate_query 之类)。 // 这种必须被识别为失败,不能当数据解析出一个空数组还「看起来成功」。 console.log('\n=== Overpass 返回 200 + HTML 错误页 ==='); process.env.HTML_MODE = '1'; // 第二轮必须绕过缓存,否则拿到的是第一轮的结果 for (const k of Object.keys(require.cache)) if (k.includes(path.sep + 'lib' + path.sep)) delete require.cache[k]; const S2 = require(SRC); const res2 = await S2.discoverCandidates(ORIGIN, 150, cfg, { excludeLocal: true }); const cands2 = res2.candidates || []; console.log('stats =', JSON.stringify(res2.stats)); const overpassPois = cands2.filter((c) => c.source === 'OpenStreetMap / Overpass'); if (overpassPois.length) fail.push(`HTML 错误页被当成了数据,解析出 ${overpassPois.length} 个假 POI`); else console.log('✅ HTML 错误页被正确识别为失败,未产生假候选'); delete process.env.HTML_MODE; server.close(); process.exit(fail.length ? 1 : 0); });