☰
GeoJSON行政区划边界数据采集、缓存与四级联动实践
2026/10/27 8:07:36 网站建设 项目流程

1. 需求拆解与整体方案设计

前阵子有个朋友找我,说他们要做个门店选址的小系统,前端要省、市、区县、街道四级下拉,选完还得在地图上把这块地方的轮廓画出来。他原话是“这个网上不是到处都有现成数据吗,随便拉一份不就完了”。我当时回他:这事儿一共三个难点——数据从哪儿来、怎么保证它是新的、怎么把它塞进一个响应够快的服务里。前两个决定了你后面三个月要不要反复返工,第三个决定了用户点下拉框的时候会不会转圈。

为什么“实时获取地图边界数据”这件事值得单独拿出来说?因为行政区划边界根本不是静态资源。每年都有撤县设区、乡镇合并、街道重新划分,一次区划调整在新闻里可能就是一句话,但在你的系统里就是一批失效的 code、一批画错的轮廓、一批对不上的报表。很多团队的做法是项目启动时下载一份“全国省市区边界 GeoJSON”,打进代码仓库,然后就默认它永远正确。等到第二年做数据核对,发现某个区的下级列表是空的,或者一个街道被拆成两个但系统里还是一个,排查起来极其难受。

我的判断很直接:边界数据必须当成“外部依赖”来管理,而不是当成“静态资源”来打包。它跟字典表、汇率、物流价格属于同一类东西——会变,但你又不希望它频繁打扰你。2024 年这套需求在技术上已经很成熟,难的不是“能不能拿到”,而是“拿得干不干净、更新得省不省事、联动得顺不顺”。

适合看这篇的人:正在做后台管理系统里省市区街道选择器的前端、需要给地图画行政区域轮廓的后端,以及被“数据网上随便下”这句话坑过一次的人。下面所有示例里的省市名我都用代号(S01 省、A 市、B 区、C 街道)代替,你自己替换成实际数据即可。

1.1 动手前必须定下来的三件事

第一个问题:你需要几级?省市区三级和加上街道的四级,是完全不同量级的工作量。三级的数据源相对好找,街道级颗粒度细、体积大、各地发布口径不一,很多平台要么不提供,要么只提供一部分。如果你业务上其实只需要到区县(大多数电商收货地址都是这个粒度),就别为了“看起来完整”硬上街道级,那会把数据维护成本翻好几倍。

第二个问题:你要的是“名称”还是“轮廓”?只做下拉框,你要的其实是一棵树,几十 KB 的 JSON 就够了。要在地图上画轮廓,你才需要 geometry。这两者的存储和传输策略完全不一样——树可以一次性全量推给前端,轮廓必须按需加载。很多人一开始不分,把带 geometry 的全量数据塞给前端,首屏直接几 MB 起步,手机上打开就是白屏。

第三个问题:更新频率要求多高?行政区划调整一年可能就几次,但因为你要做“实时”,往往误以为要每天全量拉一遍。我的经验是:每周做一次轻量比对(只比数量、比 code 列表、比哈希),发现变化再触发全量拉取。既不会漏,也不浪费带宽,更重要的是不会因为频繁请求被人限流。

1.2 数据来源怎么取舍

我把常见来源按“能不能用”横向列一下,比的是实用性,不是权威性。

来源类型优点问题适用场景
某地图开放平台的行政区划接口更新及时、层级完整、带轮廓有调用配额、坐标体系与 GPS 有偏移、使用需遵守其条款需要轮廓、需要街道级
政务数据开放门户的公开边界数据集免费、可离线、精度较高更新周期不定、格式不统一、部分只到区县内部系统、不需要高频更新
开源社区整理的行政区划树轻量、纯名称、接入快往往没有 geometry、更新靠志愿者只做下拉选择
从自身存量业务数据反推与业务完全对齐覆盖不全、无轮廓已有地址数据的补全

实际项目里我基本都是“组合拳”:树用轻量源,轮廓按需从带 geometry 的源拉,拉下来落到自己的库里,前端永远只访问自己的服务。这么做的理由很简单——你不可能把外部服务的可用性当成自己的可用性。一旦对方限流或者字段调整,你的整个下单流程就挂了,而这时候你连回滚都做不到。

1.3 整条链路分四层

我把整条链路拆成四层,每层职责单一,出问题好定位。

  1. 采集层:按 code 列表去外部拉数据,做格式归一、坐标统一、去重、简化。
  2. 存储层:主表存行政区划树(code、name、parentCode、level、中心点),附表存 geometry(按 code 一行)。
  3. 服务层:对外只暴露“按 parentCode 查子级”和“按 code 查轮廓”两个接口,前面挂缓存。
  4. 前端层:下拉联动加地图渲染,轮廓懒加载。

四层之间用 code 作为唯一纽带,全链路共用一套行政区划代码,绝不混用名称做主键——名称会改,code 相对稳定,这是避坑的第一原则。我见过用名称做主键的系统,遇到“同名区”直接崩盘,两个不同城市的“新城区”互相覆盖,最后只能重构。

2. GeoJSON 到底怎么读:格式、坐标系与体积

拿到一份边界文件,打开一看是一大坨括号,很多人第一反应是“这咋用”。其实 GeoJSON 的结构非常规整,理解三个关键字就够:FeatureCollection、Feature、Geometry,剩下的都是属性。

2.1 结构拆解与几个致命细节

一个典型的行政区划边界文件长这样(我做了简化)。

{ "type": "FeatureCollection", "features": [ { "type": "Feature", "properties": { "code": "440300", "name": "示例市", "level": "city", "parentCode": "440000", "center": [114.05, 22.55] }, "geometry": { "type": "MultiPolygon", "coordinates": [[[[114.0, 22.5], [114.1, 22.5], [114.1, 22.6], [114.0, 22.5]]]] } } ] }

几个必须记住的点:

  • coordinates 的嵌套层数决定几何类型。Point 是[x, y],LineString 是[[x,y],...],Polygon 是[[[x,y],...]](外面一层是环,第一个环是外环,后面的环是洞),MultiPolygon 再往外套一层。数错括号是新手最常见的错误,画出来要么是个点,要么什么都看不见。
  • 坐标顺序是[经度, 纬度],不是[纬度, 经度]。这和很多 GIS 工具的习惯相反,前端图表库又常常用[lat, lng],混着用必然出问题。
  • 环必须闭合,第一个点和最后一个点要相同。部分数据源不闭合,渲染库会报错或者自动补,但你自己算面积时会算错。
  • properties 里放什么由数据源决定,所以采集层一定要做字段映射,把外部字段统一成你自己的字段名,别让上层的代码依赖外部字段命名。

还有一个很多人不注意的细节:FeatureCollection 里的 features 顺序是不保证稳定的。有的数据源每次返回顺序都不一样。如果你做哈希比对时直接对整个 JSON 字符串取哈希,会得到“每次都变了”的假象。所以比对前要先按 code 排序再序列化,这是个小动作,但能省掉大量误报警。

2.2 坐标系:这一步错了后面全错

这是我最想强调的一段。地图上的坐标不是唯一的,同一栋楼在不同体系下的数值能差几百米。

坐标体系常见来源特点
WGS84GPS 设备、国际通用数据全球统一,无偏移
GCJ-02多数国内地图平台与 WGS84 存在非线性偏移
BD-09某类地图产品在 GCJ-02 基础上再偏移

踩坑现场通常是这样的:边界数据是 GCJ-02,你自己的定位点是 WGS84,叠在一起看,边界整体偏了。用户反馈“我明明站在路边,为什么显示我在小区里”。这不是代码 bug,是数据层面的口径不一致,而且你盯着代码看一整天也看不出来。

我的处理原则很简单:入库统一存 WGS84,展示时按目标底图实时转换。理由有三个:WGS84 是国际标准,长期最稳;转换是单向可逆的,反着存会累积误差;将来换底图供应商,你只需要改展示层一行配置。

转换本身不建议自己实现,用成熟的开源转换库即可,输入输出都是[lng, lat]数组,把整条 coordinates 递归遍历一遍替换。注意 MultiPolygon 嵌套深,写递归比写三重循环可靠得多,后者容易在遇到异常数据时越界。

提示:转换前后一定要做“回环校验”,也就是转过去再转回来,看偏差是否在可接受阈值内(一般要求小于 1 米)。如果偏差很大,八成是你把经纬度顺序搞反了,而不是算法有问题。

2.3 体积:不简化你会被流量费哭

街道级边界的数据量,如果原样存,一个省的街道轮廓加起来轻松上百 MB。为什么?因为官方发布的轮廓精度常常是米级甚至亚米级,一条海岸线能给你上万个点,而你在一个 375 像素宽的手机屏幕上看,根本看不出区别。

所以要做的第一件事是轮廓抽稀(简化)。常用的是 Douglas-Peucker 算法,核心参数是容差 tolerance,单位和你坐标系一致(度)。经验值如下。

使用场景建议容差(度)效果
全国/全省总览0.01 ~ 0.05轮廓粗,点数降 90% 以上
市级地图0.002 ~ 0.01保留主要形状
区县级详情0.0005 ~ 0.002细节较完整
街道级高亮0.0002 ~ 0.001建议单独出一份文件

实测下来,一份 8 MB 的区县边界,容差取 0.005 简化后能压到 400 KB 左右,视觉上几乎无差别。想更省可以上 TopoJSON,共享边只存一次,体积能再降 30%~50%,代价是前端要多引一个解析库,值不值得看你项目对包体积的敏感程度。

注意:简化后的数据只用来看,不要用来算面积和判点位。判断某个坐标是否落在区域内这类业务,必须用未简化的原始几何,否则边界附近会大量误判。我的做法是库里存两份:geom_full和geom_simple,展示用后者,判断用前者。

3. 采集实现:从“手动下载”到“定时比对”

这一节讲实操。假设场景是:你需要四级数据,每周检查一次是否更新,更新了就把差异拉下来。

3.1 先摸清数据源的规律

不管用哪个源,动手写脚本之前先干三件事:

  1. 打开浏览器开发者工具,手动点几下,把请求抓下来,看 URL 里的参数名和 code 的传法。
  2. 用同一个 code 请求两次,对比响应,确认有没有签名、时间戳、随机串之类的动态参数。
  3. 用不同层级(省、市、区、街道)各试一次,确认返回结构是否一致——很多平台街道级返回的是点而不是面。

这一步花半小时,能省你后面两天的调试。我见过有人直接照抄网上的脚本,结果对方接口早改了参数名,返回一直是空数组,他还在那儿排查字符编码问题。

3.2 采集脚本骨架

下面是我常用的结构,Python 写的,重点是“可重入”和“可断点续传”。

import json import time import hashlib from pathlib import Path import requests BASE_URL = "https://example-data-host/api/boundary" RAW_DIR = Path("./data/raw") RAW_DIR.mkdir(parents=True, exist_ok=True) HEADERS = { "User-Agent": "Mozilla/5.0 (compatible; boundary-collector/1.0)", "Accept": "application/json", } def fetch_boundary(code: str, level: str, retry: int = 3) -> dict: params = {"code": code, "level": level, "polygon": "1"} for i in range(retry): try: resp = requests.get(BASE_URL, params=params, headers=HEADERS, timeout=15) resp.raise_for_status() data = resp.json() if not data.get("features"): raise ValueError(f"empty features for {code}") return data except Exception as e: wait = 2 ** i print(f"[retry {i + 1}] {code} failed: {e}, sleep {wait}s") time.sleep(wait) raise RuntimeError(f"give up on {code}") def save(code: str, data: dict) -> str: # 先按 code 排序再序列化,保证哈希稳定 if data.get("features"): data["features"].sort(key=lambda f: f.get("properties", {}).get("code", "")) payload = json.dumps(data, ensure_ascii=False, sort_keys=True) digest = hashlib.sha1(payload.encode("utf-8")).hexdigest()[:12] path = RAW_DIR / f"{code}.{digest}.json" if not path.exists(): path.write_text(payload, encoding="utf-8") return digest def fetch_tree(parent_code: str, depth: int = 0): """先只拉树,不拉轮廓,速度快一个量级""" data = fetch_boundary(parent_code, "child") seen = set() for feat in data["features"]: props = feat["properties"] code = props["code"] if code in seen or code == parent_code: continue # 防止数据源把父级自己也返回 seen.add(code) save(code, feat) yield code, props if depth < 3: yield from fetch_tree(code, depth + 1)

几个关键设计说明:

  • 指数退避重试。对外部接口必须假设它会抽风,2 秒、4 秒、8 秒退避,比固定间隔重试友好得多,也不容易触发限流。
  • 文件名带内容哈希。同一份数据不会重复落盘,天然实现了增量:哈希不变就是没变。
  • 递归深度限制加 code 去重。别写无限递归,某些数据源的“子级”会返回自己和父级,无限循环能把磁盘写满。
  • 限速。脚本里我省略了,但实际一定要加,比如每次请求之间time.sleep(0.2),并发控制在 2~3。抓取速度和账号安全之间,永远选后者。

3.3 增量更新怎么判断

最朴素也最有效的办法:对比 code 集合加内容哈希。

def diff(old_list: list[str], new_list: list[str]) -> dict: old, new = set(old_list), set(new_list) return { "added": sorted(new - old), "removed": sorted(old - new), "unchanged": sorted(old & new), }

每周跑一次“只拉列表不拉轮廓”的轻量任务(通常也就几十个请求),拿到 code 集合和上一版对比。有三种结果,处理方式完全不同。

情况表现处理
新增 code新设区、新设街道拉轮廓入库
消失 code撤并标记失效,不要直接删
集合不变大多数时候抽样比对哈希,全一致就结束

这里有个很实际的坑:消失的 code 千万别物理删除。用户的收货地址、历史报表、订单记录里都存着这个 code,你删了,历史数据就变成“未知区域”,报表直接缺一块。正确做法是加一个status字段(1 正常 / 0 已失效)和一个merged_to字段(记录它并入了哪个新 code),查询时默认过滤 status=1,历史数据仍然能翻译出名称。

提示:如果要把“旧 code 到新 code”的映射维护起来,建议单独建一张boundary_mapping表,只增不改,作为审计和回溯用。区划调整期间,这张表的实际价值比你预想的大得多。

4. 存储与服务:让下拉框不转圈

数据采下来只是原料,真正决定用户体验的是存储结构和服务接口。

4.1 库怎么选

两种方案,按项目体量选。

方案一:单机文件加内存索引。把每个 code 的轮廓存成一个压缩后的.geojson,启动时把树(不含 geometry)全部读进内存字典,轮廓按需读文件,前面挂一层缓存。适合中小项目,几十万条树数据也就几十 MB 内存,零依赖,运维成本低。

方案二:PostGIS。把轮廓存成geometry类型,能直接做空间查询。适合有空间分析需求的项目,缺点是引入了一个较重的依赖,小项目有点杀鸡用牛刀。

我大部分时候选方案一,只有在确实需要“给定一个经纬度,返回它属于哪个街道”这类查询时,才上方案二。判断标准很简单:如果空间查询的调用量占比不到 5%,就本机算;超过 20%,再考虑迁到空间数据库。

4.2 表结构

走关系型数据库的话,我一般是这么设计的。

CREATE TABLE boundary ( code VARCHAR(12) PRIMARY KEY, name VARCHAR(64) NOT NULL, short_name VARCHAR(32), parent_code VARCHAR(12) NOT NULL DEFAULT '0', level SMALLINT NOT NULL, -- 1省 2市 3区县 4街道 center_lng DOUBLE PRECISION, center_lat DOUBLE PRECISION, status SMALLINT NOT NULL DEFAULT 1, merged_to VARCHAR(12), version INTEGER NOT NULL DEFAULT 1, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX idx_boundary_parent ON boundary(parent_code, status); CREATE INDEX idx_boundary_level ON boundary(level, status); CREATE TABLE boundary_geom ( code VARCHAR(12) PRIMARY KEY, geom_full LONGTEXT, geom_simple LONGTEXT, bbox_min_lng DOUBLE PRECISION, bbox_min_lat DOUBLE PRECISION, bbox_max_lng DOUBLE PRECISION, bbox_max_lat DOUBLE PRECISION, simplified_tolerance DOUBLE PRECISION );

几个设计点值得说明:

  • 树和几何分表。树是高频读、小体积,几何是低频读、大体积。放一张表里,一个SELECT *就能把几十 MB 拉出来,数据库连接池直接被打爆。
  • 存 bbox 外接矩形。前端渲染时先用 bbox 判断区域是否在当前视野内,不在就不加载 geometry,这是让地图流畅的关键一步,收益比任何前端优化技巧都大。
  • 加 version 字段。每次数据更新加一,前端缓存和 CDN 就能靠它做失效判断,比时间戳更准,时间戳会被时区和格式坑。
  • level 用数字。字符串比较容易出错,数字排序方便,也省空间。

4.3 接口设计

只需要两个接口,别过度设计。

// 1) 按 parentCode 取子级(只返回轻量字段,不含 geometry) GET /api/region/children?parentCode=0 // 返回 { "code": 0, "children": [ { "code": "S01", "name": "示例省", "level": 1, "hasChildren": true } ], "version": 12 } // 2) 按 code 取轮廓(支持批量,单次建议不超过 20 个) GET /api/region/geometry?codes=S01,A01,B01&simple=1 // 返回 FeatureCollection

第一个接口里的hasChildren字段非常重要。前端做联动时,如果不知道下一级有没有数据,就没法决定“选中后要不要发请求”。有了一棵树的是否叶子标记,可以避免大量空请求,也能避免用户选了半天发现下面什么都没有的尴尬。

第二个接口建议支持批量。地图初始化时往往要一次画好几个区域,一个个请求会让浏览器并发排队。但一定要限制批量上限(我一般设 20),防止有人拿它去做全量导出。

4.4 缓存策略

分三层来布。

层存什么失效方式
内存(进程内)完整的树服务重启 / 订阅到版本变更事件时重建
Redis常用 code 的 geometry、children 结果key 带 version,version 变了自然换新 key
浏览器/CDNchildren 结果、已加载的 geometry长缓存加 URL 带 version

实测效果:一个省级下拉的 children 接口,走内存缓存后 P99 从 80ms 降到 3ms 以内;轮廓接口走 Redis 后命中率在 90% 以上,因为用户大多只看自己所在的那几个区域,长尾分布很明显。

注意:Redis 里存 geometry 时别存 JSON 字符串再序列化两次,直接存压缩后的字符串(比如 zlib 压一遍),体积能小 70%。一个 500 KB 的省界,压完大概 120 KB,对内存友好得多。

5. 前端联动:四级下拉怎么写得又稳又快

前端部分看起来最简单,其实问题最多。因为这里的 bug 都是“偶发”的,测试不容易复现,线上却天天有人报。

5.1 联动的核心是状态机,不是四个下拉框

很多人上手就写四个 select,每个 onchange 里清空后面三个、发请求。这样写出来的东西在快速操作时必然错乱:用户手速快一点,请求 A 还没回来,用户又选了 B,回来的是 A 的结果,把 B 的列表覆盖了。

正确思路是把“当前选择”当成一个状态机来管。

const state = { level1: null, level2: null, level3: null, level4: null, seq: { level2: 0, level3: 0, level4: 0 }, }; async function onChange(level, code) { // 1. 清空当前级之后的所有选择 for (let i = level + 1; i <= 4; i++) state[`level${i}`] = null; state[`level${level}`] = code; if (level >= 4) return; // 2. 递增序号,保证只有最新请求生效 const key = `level${level + 1}`; const mySeq = ++state.seq[key]; const children = await fetchChildren(code); // 3. 回来晚了就直接丢弃 if (mySeq !== state.seq[key]) return; renderOptions(level + 1, children); }

这个“序号比对”的写法是我踩过坑之后固定下来的。不用它的时候,我在弱网环境(浏览器限速到 3G)下测过,快速切换省份有 10% 左右的概率列表错乱,而且很难复现。加上序号之后,一次都没出过。如果要更彻底,可以直接上AbortController把旧请求取消掉,性能更好,但序号法兼容性更广,看你的运行环境。

5.2 数据预加载与懒加载的平衡

用户填地址的心理预期是“我点一下就有”。所以策略要分级。

  • 一级(省):随页面一起加载,或首屏后立即预取。数据量小,几十条,几十 KB。
  • 二级(市):选中省之后立刻请求,响应通常 10~30 KB,加个 loading 就行。
  • 三级(区县):同样请求,但可以提前预取——鼠标 hover 到某个市的时候就开始拉。
  • 四级(街道):数据量大且多数场景用不上,严格懒加载。

还有一点值得单独提:把上一级选完就自动选中第一个的“贴心”行为去掉。看起来省事,实际上会让用户误操作,尤其在他只是想浏览一下的时候,选中的省被清空、变成另一个市,体验很差。

另一个很有效的优化是把常用省市的完整子树打进首屏包。比如用 localStorage 记住用户上次选择所在省,第二次进来直接秒开。这招在后台管理系统里效果特别明显,因为同一批用户反复访问同一批地区。

5.3 地图渲染

地图这块,非专业 GIS 项目我一般用轻量渲染库直接画 GeoJSON,不引重依赖。要点如下。

  1. 先算 bbox 再缩放。拿到 FeatureCollection 后遍历所有坐标算外接矩形,然后fitBounds。不做这一步,地图会停在一个莫名其妙的位置。
  2. 点和面的绘制顺序。面永远比线、点先画,否则点会被盖住。
  3. 高亮用图层切换而不是改样式。每次改样式会触发重绘,把整个图层重画一遍。正确做法是维护两个图层(底色层加高亮层),只在高亮层里放当前选中的那一块。
  4. 交互命中测试交给渲染库。别自己写“点在多边形内”的算法,简化后的图形自己算容易误判,而且性能差。

用户点击地图上某个区域反查属于哪个 code,这个需求也挺常见(点地图选地址)。实现方式是给每个 feature 的 properties 带上 code,点击事件直接读e.features[0].properties.code,再拿它去更新下拉框状态。前提是你画上去的 feature 本身就是行政区域,而不是合并后的大多边形。

5.4 边界情况自查清单

这部分是我整理的一张表,前端联动每次上线前对着过一遍。

场景期望行为
某市下无区县(直筒子市)第三级禁用或隐藏,不能卡住流程
某区县下无街道数据第四级显示“请选择”但可空,不算必填
用户快速连续切换上级下级列表只反映最后一次选择
编辑回填(改地址)按 code 逐级回填,不依赖名称匹配
名称重复(同名区)一律按 code 处理,显示时带上级名称区分
已失效的历史 code显示“原 XX(已撤销)”,允许用户重新选

最后一条特别容易忽略。用户三年前下的单,地址里的区已经撤并了,你打开详情页要显示名称,如果查询直接过滤掉 status=0 的记录,就会显示成空白。所以展示类查询要带上失效数据,选择类查询才过滤。这两个查询走的是不同接口,别图省事合成一个。

6. 常见问题与排查实录

这一节全是我和身边人实际遇到过的,按出现频率排。

6.1 问题速查表

现象最可能原因排查动作
下拉框某级一直是空的parentCode 层级传错,或该级确实无数据直接查库SELECT count(*) WHERE parent_code=?
地图画出来是横的或斜的经纬度顺序反了检查 coordinates 是否[lng, lat]
轮廓整体偏移几百米坐标系不一致确认边界与底图是否同一体系
边界画出来只有一个小三角Polygon 环没闭合或嵌套层数错打印 coordinates 长度和首尾点
首屏加载特别慢一次性拉了带 geometry 的全量数据看 Network,超过 1 MB 就该拆
数据更新后前端还是旧的缓存没失效检查 version 是否透传到 URL 和响应头
街道级接口报 400该源不支持该层级换源,或降级到区县级
偶发下拉列表错乱并发请求互相覆盖加请求序号或 AbortController

6.2 几个不那么显然的坑

坑一:把“省直辖县级行政区”当成错误数据。有些省下面直接挂县级单位,没有地级市这一层。如果你的代码写死了“省下面是市,市下面是区”,这类数据就会被丢掉。解决办法是不要硬编码层级数字,而是靠 parentCode 关系构建树,渲染时如果某节点直接跳到第三级,就跳过中间那一级下拉。

坑二:跨省的飞地。某些区域在空间上和它的行政归属地不连在一起。如果你用“取省市轮廓再合并”的方式生成地级市边界,飞地会丢。所以每一级的 geometry 都要单独从数据源取,不要自己拼。

坑三:MultiPolygon 里混着空环。我遇到过一次,某个小岛的坐标数组是空数组,导致整个 MultiPolygon 渲染失败,整张地图白屏。采集层要加清洗,过滤掉坐标点少于 4 个的环(少于 3 个点构不成面,加上闭合点至少 4 个)。

坑四:把 center 当成“几何中心”。数据源给的 center 往往是政府驻地或标注点,不一定在多边形内部,尤其形状奇怪的区。如果你要拿中心点做地图初始定位,最好自己用 bbox 中心,或者用“面内点”算法算一个保证落在多边形内的坐标。

坑五:忽略数据源的使用条款。这条不是技术问题,但影响最大。某些平台的边界数据对使用场景、存储时长、二次分发都有要求。动手之前把条款读一遍,别等做完了才发现不能用。这也是我优先推荐“政务公开数据加自建缓存”的原因之一。

6.3 更新运维的几条心得

跑顺之后,日常运维其实很轻,但有几个习惯建议养成。

第一,每次更新都留一份快照。文件名带日期和哈希,出问题能一键回滚。我见过上游数据发版发错的情况,某个市的轮廓变成了另一块地方,如果没有快照,你只能干等对方修。

第二,更新后自动跑一次自检。自检项包括:总数量变化是否超过 5%(超了就告警,可能是源出问题)、有没有 code 重复、有没有孤儿节点(parentCode 找不到对应记录)、每个 geometry 的 bbox 是否合理(经度在 -180 到 180,纬度在 -90 到 90)。这几项能挡住九成以上的数据事故。

第三,把数据版本暴露出去。我一般在管理后台的页脚显示当前边界数据版本和更新时间。用户反馈“某个区选不到”的时候,你第一件事就是确认他看到的版本是不是最新的,比在那儿猜快得多。

第四,街道级数据单独发版。街道变化比区县频繁得多,如果和区县数据绑在一起发,会导致整个包每次都更新,前端缓存全部失效。分开之后,区县数据可能半年不动,街道数据每月小改,互不影响。

6.4 关于“实时”两个字的一点个人看法

标题里写“实时获取”,但说实话,行政区划数据不存在真正的实时需求。它的变化频率是以月甚至年计的,你每秒去拉一次,除了给对方增加压力、给自己增加风险,没有任何收益。

我更愿意把“实时”理解成**“随时可以拿到最新版”**,落地就是三句话:有可靠的比对机制、有可回滚的版本、有清晰的更新时间。做到这三点,业务上就已经是实时的了——用户永远看到的是当前正确的那一份,而不是三年前打进代码仓库里那一份。

真要提升时效,值得投入的地方不是把定时任务从每周改成每小时,而是把“变更发现”做得更聪明:监控自己数据库里出现的 code

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询