每次开发需要用户填写地址的表单时,最头疼的就是那个省市区三级联动的下拉框。手动维护城市列表不仅耗时,还容易出错。去年我们电商项目就因为手动维护的地址数据有误,导致一批订单配送出错,损失不小。后来发现高德地图的adcode接口简直就是救星——它提供了标准化的行政区划编码,还能自动更新。
adcode(Administrative Division Code)是高德地图对国内行政区划的唯一编码系统。这套编码由6位数字组成,前两位代表省份,中间两位代表城市,最后两位代表区县。比如:
与常见的邮政编码不同,adcode有几个显著特点:
注意:adcode与citycode不同,后者是电话区号,而adcode是纯粹的地理编码系统
实际项目中,我们通常需要处理这样的数据结构:
json复制{
"name": "广东省",
"adcode": "440000",
"districts": [
{
"name": "广州市",
"adcode": "440100",
"districts": [
{"name": "天河区", "adcode": "440106"},
{"name": "越秀区", "adcode": "440104"}
]
}
]
}
高德提供了行政区划查询API,这是最动态的方式:
javascript复制// 获取省级数据
fetch(`https://restapi.amap.com/v3/config/district?keywords=中国&subdistrict=1&key=您的高德KEY`)
.then(response => response.json())
.then(data => console.log(data.districts[0].districts));
参数说明:
| 参数 | 必填 | 说明 |
|---|---|---|
| keywords | 是 | 查询关键字,全国数据用"中国" |
| subdistrict | 是 | 子级行政区划层级:1-3 |
| key | 是 | 高德开发者KEY |
| extensions | 否 | 返回结果扩展:base/all |
对于需要离线使用的场景,可以从高德开放平台下载完整的行政区划JSON文件。我们团队维护了一个定期更新的Gist:
bash复制wget https://gist.githubusercontent.com/username/xxxx/raw/cities.json
对于大型应用,建议将adcode存入数据库。这是我们的MySQL表结构设计:
sql复制CREATE TABLE `amap_adcode` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL,
`adcode` varchar(6) NOT NULL,
`parent_id` int(11) DEFAULT NULL,
`level` tinyint(1) NOT NULL COMMENT '1-省 2-市 3-区县',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_adcode` (`adcode`),
KEY `idx_parent` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
使用Vue+ElementUI的典型实现:
html复制<template>
<div>
<el-select v-model="province" @change="handleProvinceChange">
<el-option
v-for="item in provinces"
:key="item.adcode"
:label="item.name"
:value="item.adcode"
/>
</el-select>
<el-select v-model="city" @change="handleCityChange">
<el-option
v-for="item in cities"
:key="item.adcode"
:label="item.name"
:value="item.adcode"
/>
</el-select>
<el-select v-model="district">
<el-option
v-for="item in districts"
:key="item.adcode"
:label="item.name"
:value="item.adcode"
/>
</el-select>
</div>
</template>
<script>
export default {
data() {
return {
provinces: [],
cities: [],
districts: [],
province: '',
city: '',
district: ''
}
},
async created() {
this.provinces = await this.fetchDistricts('中国');
},
methods: {
async fetchDistricts(keyword, parentAdcode) {
const params = {
keywords: keyword,
subdistrict: 1,
key: 'YOUR_AMAP_KEY'
};
if(parentAdcode) params.adcode = parentAdcode;
const res = await fetch(`https://restapi.amap.com/v3/config/district?${new URLSearchParams(params)}`);
const data = await res.json();
return data.districts[0]?.districts || [];
},
async handleProvinceChange(adcode) {
this.cities = await this.fetchDistricts('', adcode);
this.city = '';
this.district = '';
},
async handleCityChange(adcode) {
this.districts = await this.fetchDistricts('', adcode);
this.district = '';
}
}
}
</script>
数据缓存:使用localStorage缓存API响应
javascript复制const cacheKey = `amap_district_${adcode}`;
const cached = localStorage.getItem(cacheKey);
if(cached) return JSON.parse(cached);
// 调用API后
localStorage.setItem(cacheKey, JSON.stringify(data));
懒加载:只在需要时加载下级数据
防抖处理:对频繁的select change事件做防抖
虚拟滚动:对城市数量多的省份(如广东)使用虚拟滚动
基础实现:
javascript复制function searchCity(keyword, list) {
return list.filter(item =>
item.name.includes(keyword) ||
item.adcode.startsWith(keyword)
);
}
高级实现(支持拼音搜索):
javascript复制import pinyin from 'pinyin';
function searchCity(keyword, list) {
const kw = keyword.toLowerCase();
return list.filter(item => {
const py = pinyin(item.name, { style: pinyin.STYLE_NORMAL }).join('');
return (
item.name.includes(kw) ||
item.adcode.startsWith(kw) ||
py.includes(kw)
);
});
}
我们遇到过几种典型情况:
行政变更滞后:比如某县改区后,高德数据更新延迟
用户输入历史数据:用户保存的旧adcode可能失效
某些特殊案例需要特别注意:
处理代码示例:
javascript复制function isMunicipality(adcode) {
return ['110000', '120000', '310000', '500000'].includes(adcode);
}
function getDisplayText(province, city, district) {
if(isMunicipality(province.adcode)) {
return `${province.name} ${district.name}`;
}
return `${province.name} ${city.name} ${district.name}`;
}
移动端需要考虑:
微信小程序实现示例:
javascript复制// pages/address/address.js
Page({
data: {
columns: [
{ values: [] }, // 省
{ values: [] }, // 市
{ values: [] } // 区
],
selected: [0, 0, 0]
},
onColumnChange(e) {
const { column, index } = e.detail;
if(column === 0) {
// 省变化,加载市数据
this.loadCities(this.data.provinces[index].adcode);
} else if(column === 1) {
// 市变化,加载区数据
this.loadDistricts(this.data.cities[index].adcode);
}
}
})
典型的地图+选择器联动效果:
javascript复制// 当选择区域变化时
function onDistrictChange(adcode) {
const map = new AMap.Map('container');
AMap.plugin('AMap.DistrictSearch', () => {
const district = new AMap.DistrictSearch({
extensions: 'all',
level: 'district'
});
district.search(adcode, (status, result) => {
const bounds = result.districtList[0].boundaries;
const polygon = new AMap.Polygon({
path: bounds,
strokeColor: '#FF33FF',
strokeWeight: 2
});
map.setFitView();
map.add(polygon);
});
});
}
反向从文本地址解析adcode:
javascript复制function parseAddress(text) {
return fetch(`https://restapi.amap.com/v3/geocode/geo?address=${encodeURIComponent(text)}&key=YOUR_KEY`)
.then(res => res.json())
.then(data => {
if(data.geocodes && data.geocodes.length) {
return data.geocodes[0].adcode;
}
return null;
});
}
// 使用示例
parseAddress("北京市海淀区中关村大街27号").then(adcode => {
console.log("解析到的adcode:", adcode);
});
使用adcode进行区域数据统计展示:
javascript复制// 使用ECharts地图
fetch('/api/statistics?adcode=440000')
.then(res => res.json())
.then(data => {
const chart = echarts.init(document.getElementById('map'));
chart.setOption({
visualMap: {
min: 0,
max: 100,
text: ['High', 'Low'],
inRange: {
color: ['#e0f3f8', '#abd9e9', '#74add1', '#4575b4', '#313695']
}
},
series: [{
name: '广东数据',
type: 'map',
map: '广东',
data: data.items,
label: {
show: true
}
}]
});
// 注册地图数据
echarts.registerMap('广东', {
type: 'FeatureCollection',
features: data.geojson
});
});
对于大型应用,我们推荐这样的架构:
code复制┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client │───▶│ API Gateway │───▶│ AMap Proxy │
└─────────────┘ └─────────────┘ └─────────────┘
▲ │ │
│ ▼ ▼
│ ┌─────────────┐ ┌─────────────┐
└────────────┤ Cache │ │ Database │
│ (Redis) │ │ (MySQL) │
└─────────────┘ └─────────────┘
关键组件说明:
Node.js代理服务示例:
javascript复制const express = require('express');
const axios = require('axios');
const NodeCache = require('node-cache');
const app = express();
const cache = new NodeCache({ stdTTL: 86400 });
app.get('/api/districts', async (req, res) => {
const { keyword = '中国', adcode } = req.query;
const cacheKey = `districts_${keyword}_${adcode || ''}`;
const cached = cache.get(cacheKey);
if(cached) return res.json(cached);
try {
const params = {
keywords: keyword,
subdistrict: 1,
key: process.env.AMAP_KEY
};
if(adcode) params.adcode = adcode;
const response = await axios.get('https://restapi.amap.com/v3/config/district', { params });
cache.set(cacheKey, response.data);
res.json(response.data);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
app.listen(3000);
使用Jest进行组件测试:
javascript复制import { mount } from '@vue/test-utils';
import AddressPicker from '@/components/AddressPicker.vue';
describe('AddressPicker', () => {
it('正确加载省份数据', async () => {
const wrapper = mount(AddressPicker);
await wrapper.vm.$nextTick();
expect(wrapper.vm.provinces.length).toBeGreaterThan(0);
});
it('选择省份后加载城市数据', async () => {
const wrapper = mount(AddressPicker);
await wrapper.vm.$nextTick();
const provinceSelect = wrapper.find('.province-select');
await provinceSelect.setValue('440000'); // 选择广东
await wrapper.vm.$nextTick();
expect(wrapper.vm.cities.length).toBeGreaterThan(0);
});
});
需要监控的关键指标:
Prometheus监控配置示例:
yaml复制scrape_configs:
- job_name: 'address_service'
metrics_path: '/metrics'
static_configs:
- targets: ['address-service:8080']
完善的错误处理应该包括:
JavaScript错误处理示例:
javascript复制async function loadDistricts(adcode) {
try {
const response = await fetch(apiUrl, { timeout: 3000 });
if(!response.ok) throw new Error('API请求失败');
return await response.json();
} catch (error) {
console.error('加载行政区划失败:', error);
// 尝试从本地存储获取
const cached = localStorage.getItem(`fallback_${adcode}`);
if(cached) return JSON.parse(cached);
// 上报错误
trackError(error);
throw error;
}
}