简介:这份资源面向微信小程序开发者与电商、物流场景的后端同学,聚焦用户收货地址非结构化、难以统一入库的痛点,借助腾讯云地址解析 API 将自然语言地址自动识别为省、市、区等标准字段。压缩包共 18 个文件,约 16KB,以 6 个 js 脚本、5 个 json 配置、3 个 wxss 样式与 2 个 wxml 页面为主,js 中可见 sha1.js、base64.js 等签名与编码工具,json 负责项目与页面配置,wxml 与 wxss 承担地址录入与结果展示界面。已有 8241 人学习下载,说明该案例在小程序 API 调用方向具有较高参考度。读者可从中获得 HMAC-SHA1 签名与 Base64 编码的落地写法、请求构造与 JSON 响应解析思路,以及地址标准化结果的展示与确认交互骨架,适合作为接入腾讯云地址服务的起步模板,并在此基础上补齐确认按钮事件与异常处理逻辑。
1. 地址解析这件事,别在正则里硬扛
做小程序电商或同城配送的团队,几乎都会撞上同一个需求:用户在收货地址栏里粘贴一段「张先生 13800000000 广东省深圳市南山区科技园某某大厦 A 座 1203」,系统要自动拆出姓名、手机号、省、市、区、详细地址,还要把「深圳」补成「广东省深圳市」、「南山区」对齐到标准行政区划。很多人第一反应是写正则,结果被「北京市朝阳区」「新疆维吾尔自治区乌鲁木齐市」「内蒙古自治区呼和浩特市」以及各种直辖市、自治州、县级市按在地上摩擦,这就是典型的玄学现场。
这份 AddressParseTest.zip 走的是另一条路:小程序端只负责收集原始文本和展示结果,真正的地址识别与格式化交给腾讯云地址解析 API,前端拿到结构化的省市区字段后再回填表单。它解决的不是「识别一段文字」这么简单,而是「让地址标准化格式」——把脏数据变成可入库、可计算运费、可对接电子面单的结构化数据。适合正在做小程序毕设、课程设计,或者要给现有商城补一块地址智能识别能力的开发者直接参考。
2. 需求拆解与技术选型:为什么把解析交给云 API
2.1 先把「地址标准化」拆成四个可验收的子问题
动手写代码之前,我一般会把需求拆到能单独验收的粒度,否则做到一半就会发现字段对不上。这个项目实际要解决的是四件事:
第一是文本切分,从一段混杂姓名、电话、地址的自由文本里定位出地址片段;第二是行政区划对齐,把「南山」「南山区」「深圳市南山区」统一映射到标准三级区划;第三是字段结构化,输出省、市、区、详细地址、姓名、手机号这些独立字段;第四是格式化回填,把结构化结果拼成统一格式写回表单或数据库。
这四件事里,第一件和第四件是前端能干的,第二件和第三件才是真正的难点。区划数据每年都在变,县级市改名、撤县设区、自治县全称,靠本地维护一份 JSON 迟早会过期。所以选型时把「区划对齐」外包给云服务,是这类项目里性价比最高的决定。
2.2 腾讯云地址解析 API 的输入输出长什么样
腾讯云地址解析接口的核心能力是:传入一段自然语言地址文本,返回结构化的省、市、区、街道、详细地址,以及姓名、手机号等附属信息。请求走 HTTPS,签名方式是该云厂商通用的 TC3-HMAC-SHA256,返回体是 JSON。下面是小程序端发起请求的封装示例,注意密钥不能放在小程序里,真实项目要走自己的后端中转:
// utils/addressParse.js // 小程序端只调用自家后端,绝不直连云 API,避免密钥泄露 export function parseAddress(rawText) { return new Promise((resolve, reject) => { wx.request({ url: 'https://your-backend.example.com/api/address/parse', method: 'POST', data: { text: rawText }, header: { 'content-type': 'application/json' }, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); // { province, city, district, detail, name, phone } } else { reject(new Error(res.data.msg || '解析失败')); } }, fail(err) { reject(err); } }); }); }这段代码的关键点不在wx.request本身,而在三个设计约束:一是url指向自有后端而不是云 API 域名,二是请求体只传原始文本、不传任何凭证,三是成功分支里对业务码code做了判断而不是只看 HTTP 状态码。参数上,text就是用户粘贴的整段地址,长度通常限制在 200 字以内,超长文本要先在前端截断或提示用户分段。
后端中转的伪代码逻辑是:接收text→ 用服务端保存的密钥调用云 API → 把返回的Province/City/Region/Address/Name/Phone映射成前端字段 → 统一返回。这样小程序端永远接触不到密钥,也方便后续加缓存和限流。
2.3 为什么不在小程序里本地做解析
有人会问,能不能在小程序里塞一份省市区 JSON 加正则,离线解析?技术上可行,但代价是:区划数据要自己维护、自治州和直辖市的规则要自己写、地址里夹带姓名电话的切分规则要自己调。这类规则一旦写进前端,后续每改一次都要重新提审发版。用云 API 的另一个隐性收益是,解析逻辑在服务端,规则调整不用动小程序包,这对毕设答辩时讲「可维护性」是个实打实的加分点。
选型结论可以概括成一句话:前端做交互和兜底,后端做解析和缓存,云 API 做区划对齐。三层各管一段,责任清晰,出问题也好定位。
3. 总体设计:小程序端、中转服务与解析结果的三段式结构
3.1 模块划分与数据流
整个方案按职责切成三块,数据单向流动,避免前端和后端互相猜字段:
| 模块 | 职责 | 关键产出 |
|---|---|---|
| 小程序端 | 采集原始文本、调用中转接口、回填表单、异常提示 | 结构化表单字段 |
| 中转服务 | 密钥管理、调用云 API、字段映射、缓存与限流 | 统一 JSON 响应 |
| 云解析服务 | 文本切分、区划对齐、姓名电话识别 | 原始结构化结果 |
数据流是:用户在地址输入框粘贴文本 → 小程序调用parseAddress→ 中转服务签名后请求云 API → 云 API 返回结构化字段 → 中转服务做字段名归一化 → 小程序拿到province/city/district/detail回填省市区选择器和详细地址框。整条链路上,小程序端不感知云厂商的字段命名,后端也不感知小程序的 UI 结构,中间靠一份固定的响应契约隔开。
3.2 响应契约设计:字段名归一化是解耦的关键
云 API 返回的字段名往往是大写开头或者带业务前缀,直接透传给小程序会让前端代码到处是魔法字符串。我一般会在中转服务里做一层映射,固定输出下面这份契约:
// 中转服务:把云 API 结果归一化成前端契约 function normalize(raw) { return { code: 0, data: { name: raw.Name || '', phone: raw.Phone || '', province: raw.Province || '', city: raw.City || '', district: raw.Region || '', detail: raw.Address || '', // 置信度低时前端要提示用户手动确认 confidence: raw.Confidence || 0 } }; }这里Region映射成district是因为前端省市区三级选择器习惯用district命名,Address映射成detail是为了和「详细地址」输入框的字段名一致。confidence是很多地址解析接口会返回的置信度字段,低于阈值时前端不应该直接静默回填,而要弹一个「请确认地址」的提示,这是后面避坑章节要展开的点。
3.3 表单回填的交互设计
回填不是简单地把值塞进data就完事。省市区三级联动选择器通常要求value是数组下标而不是文本,所以拿到province/city/district文本后,还要在本地区划表里反查下标。我的做法是维护一份精简的省市区 JSON(只存名称和编码),用名称匹配出下标,匹配不到就退化成「让用户手动选」。
// 根据解析出的省市区名称,反查三级选择器的下标 function findRegionIndex(regionList, province, city, district) { const pIndex = regionList.findIndex(p => p.name === province); if (pIndex < 0) return null; const cIndex = regionList[pIndex].children.findIndex(c => c.name === city); if (cIndex < 0) return null; const dIndex = regionList[pIndex].children[cIndex].children .findIndex(d => d.name === district); if (dIndex < 0) return null; return [pIndex, cIndex, dIndex]; }这段函数返回null时不要抛异常,而是把已解析出的文本填进只读展示区,让用户自己确认。地址解析本来就有失败率,把失败路径设计好,比追求 100% 识别率更现实。
4. 关键实现:从粘贴文本到标准地址的完整链路
4.1 中转服务的签名与调用封装
云 API 的签名是最容易翻车的地方,时间戳、请求方法、路径、请求体任何一处对不上都会返回签名错误。下面是一个最小可用的调用封装,语言用 Node.js,重点是签名串的拼接顺序不能乱:
// server/tencentAddress.js const crypto = require('crypto'); function sha256Hex(str) { return crypto.createHash('sha256').update(str, 'utf8').digest('hex'); } function hmac256(key, msg) { return crypto.createHmac('sha256', key).update(msg, 'utf8').digest(); } // 构造 TC3-HMAC-SHA256 签名,参数顺序不能调换 function buildAuthorization(secretId, secretKey, payload, timestamp) { const date = new Date(timestamp * 1000).toISOString().slice(0, 10); const service = 'tms'; const host = 'tms.tencentcloudapi.com'; const action = 'AddressParse'; const version = '2020-12-29'; const canonicalRequest = [ 'POST', '/', '', `content-type:application/json\nhost:${host}\n`, 'content-type;host', sha256Hex(payload) ].join('\n'); const stringToSign = [ 'TC3-HMAC-SHA256', timestamp, `${date}/${service}/tc3_request`, sha256Hex(canonicalRequest) ].join('\n'); const kDate = hmac256('TC3' + secretKey, date); const kService = hmac256(kDate, service); const kSigning = hmac256(kService, 'tc3_request'); const signature = crypto.createHmac('sha256', kSigning) .update(stringToSign, 'utf8').digest('hex'); return `TC3-HMAC-SHA256 Credential=${secretId}/${date}/${service}/tc3_request, ` + `SignedHeaders=content-type;host, Signature=${signature}`; }参数说明:secretId和secretKey从服务端环境变量读取,绝不写进代码仓库;timestamp是秒级时间戳,和云服务端时间偏差超过一定范围会直接签名失败;service和host必须和接口文档一致,写错一个字母就是 401。canonicalRequest里各段之间的换行符数量是固定的,多一个少一个都会导致签名不匹配,这是血泪经验里最常见的一条。
4.2 请求体构造与字段映射
签名之外,请求体的字段名也要和接口约定严格一致。地址解析接口通常要求把待解析文本放在一个固定字段里,下面是对应的请求构造:
// 组装请求体并调用云 API async function callAddressParse(text) { const payload = JSON.stringify({ Text: text }); const timestamp = Math.floor(Date.now() / 1000); const authorization = buildAuthorization( process.env.TC_SECRET_ID, process.env.TC_SECRET_KEY, payload, timestamp ); const res = await fetch('https://tms.tencentcloudapi.com', { method: 'POST', headers: { 'Authorization': authorization, 'Content-Type': 'application/json', 'Host': 'tms.tencentcloudapi.com', 'X-TC-Action': 'AddressParse', 'X-TC-Version': '2020-12-29', 'X-TC-Timestamp': String(timestamp) }, body: payload }); return res.json(); }X-TC-Action决定调用哪个能力,X-TC-Version决定接口版本,这两个头必须和签名里用的action、version保持一致。请求体里Text字段就是用户粘贴的原始文本,不要提前做任何清洗,因为云 API 的切分能力本身就依赖原始文本里的上下文,提前删掉手机号反而可能影响姓名识别。
4.3 前端回填与失败兜底
拿到归一化结果后,前端要做三件事:回填省市区选择器、回填详细地址、处理低置信度和空字段。下面是一个回填函数,把「能填就填、填不了就提示」的策略写清楚:
// pages/address/edit.js import { parseAddress } from '../../utils/addressParse'; import regionList from '../../data/region.json'; Page({ data: { regionValue: [], detail: '', name: '', phone: '' }, async onParseTap() { const raw = this.data.rawText; if (!raw || raw.length < 6) { wx.showToast({ title: '地址太短,请补充', icon: 'none' }); return; } try { const data = await parseAddress(raw); const idx = findRegionIndex(regionList, data.province, data.city, data.district); this.setData({ regionValue: idx || [], detail: data.detail, name: data.name, phone: data.phone }); if (!idx || data.confidence < 0.6) { wx.showModal({ title: '请确认地址', content: '部分字段未能自动识别,请手动核对省市区', showCancel: false }); } } catch (e) { wx.showToast({ title: '解析失败,请手动填写', icon: 'none' }); } } });这里的判断顺序值得注意:先判断idx是否为空,再判断置信度,两个条件用或连接。因为即使置信度很高,区划名称在本地表里匹配不到时也不能静默回填,否则用户提交后运费计算会出错。失败分支统一提示「手动填写」,不暴露任何云 API 的错误细节,这也是安全上的基本要求。
4.4 缓存与限流:别让一次粘贴打爆接口
地址解析接口按调用次数计费,用户反复点「识别」按钮很容易产生重复请求。我的做法是在中转服务加一层短时缓存,key 用文本的哈希,过期时间设几分钟:
// 简易内存缓存,生产环境换成 Redis const cache = new Map(); const TTL = 5 * 60 * 1000; function getCached(key) { const item = cache.get(key); if (!item) return null; if (Date.now() - item.time > TTL) { cache.delete(key); return null; } return item.value; } function setCache(key, value) { cache.set(key, { value, time: Date.now() }); }同时在网关层对同一用户做频率限制,比如每分钟不超过 10 次。缓存命中时直接返回上次结果,既省钱又降低延迟。注意缓存 key 要用完整文本的哈希而不是截断文本,否则不同地址可能撞 key。
5. 避坑与排查:地址解析项目里最容易翻车的五件事
5.1 密钥写进小程序包,上线即泄露
现象:开发时图省事,把secretId、secretKey直接写在小程序config.js里,真机调试一切正常。
原因:小程序包可以被反编译,密钥等于公开。云厂商的风控一旦检测到异常调用,轻则限流重则封禁。
解决:所有云 API 调用一律走自有后端中转,小程序端只认自家接口。密钥放服务端环境变量,代码仓库里只留.env.example。已经写进前端的,立刻在控制台轮换密钥。
5.2 签名总是 401,问题出在换行和编码
现象:照着文档拼签名,返回AuthFailure.SignatureFailure,怎么改都对不上。
原因:canonicalRequest各段之间的换行符数量、请求体的 UTF-8 编码、时间戳的秒级精度,任何一处偏差都会导致签名不同。中文地址文本如果被转成别的编码,哈希值也会变。
解决:把canonicalRequest和stringToSign打印出来逐字符比对,确认换行符是\n而不是\r\n;请求体统一JSON.stringify后按 UTF-8 哈希;时间戳用Math.floor(Date.now() / 1000),不要用毫秒。
5.3 直辖市和自治州回填失败
现象:用户填「北京市朝阳区」,解析结果province是「北京市」、city是「北京市」,本地区划表里市级只有「市辖区」,匹配不到下标。
原因:直辖市的行政区划层级和普通省份不同,很多区划表把直辖市处理成「省=北京市、市=市辖区」,而云 API 返回的市级可能就是「北京市」。
解决:在findRegionIndex里对直辖市做特判,市级匹配失败时回退到「市辖区」或直接用省级下标。自治州同理,要准备一份别名映射表,把「恩施州」映射到「恩施土家族苗族自治州」这类全称。
5.4 低置信度结果被静默回填
现象:用户粘贴的文本很模糊,比如只写了「科技园附近」,解析结果把「科技园」当成了区名,前端直接回填,用户没注意就提交了,后面运费算错。
原因:只判断了接口是否成功,没判断置信度和字段完整性。
解决:置信度低于阈值或关键字段为空时,不静默回填,弹确认框让用户核对。宁可多一次点击,也不要让脏数据进库。
5.5 重复点击导致重复计费
现象:用户手抖连点「识别」按钮,一次粘贴产生五六次调用,账单肉眼可见地涨。
原因:按钮没有防抖,接口没有缓存。
解决:前端按钮点击后置loading状态,请求返回前禁用;后端加短时缓存和用户级限流。这两条一起上,基本能把重复调用压到可接受范围。
6. 验收与答辩:怎么证明这套地址解析真的能用
验收这类项目,我一般不看「识别成功了几条」,而是看失败路径处理得怎么样。准备一份测试集,至少覆盖五类样本:标准三级地址、直辖市地址、自治州地址、夹带姓名电话的混合文本、只有模糊地名的脏文本。每类准备 5 到 10 条,逐条跑一遍,记录解析出的省市区和人工预期是否一致,同时记录置信度和耗时。
下面这张表可以直接拿去当验收记录模板:
| 样本类型 | 条数 | 字段完整率 | 区划匹配率 | 平均耗时 | 失败处理是否符合预期 |
|---|---|---|---|---|---|
| 标准三级地址 | 10 | 100% | 100% | 300ms | 是 |
| 直辖市地址 | 8 | 100% | 87% | 320ms | 是 |
| 自治州地址 | 6 | 100% | 83% | 310ms | 是 |
| 混合文本 | 10 | 90% | 90% | 350ms | 是 |
| 模糊脏文本 | 10 | 40% | 30% | 280ms | 是 |
答辩时重点讲三件事:一是为什么把解析放服务端而不是前端,讲密钥安全和区划维护成本;二是签名和字段映射怎么做的,可以现场展示canonicalRequest的拼接;三是失败兜底策略,拿模糊脏文本那一行数据说明「不静默回填」的设计取舍。评委通常对第三点最感兴趣,因为它体现的是工程判断而不是调通接口。
还有一个容易被忽略的验收点:把解析结果写库后,用 SQL 反查一遍省市区编码是否都能对上标准区划表。这一步能抓出「名称对但编码错」的隐蔽问题。
-- 验收:检查入库地址的区划编码是否都能在标准表中找到 SELECT a.id, a.province, a.city, a.district FROM user_address a LEFT JOIN region_std p ON p.name = a.province AND p.level = 1 LEFT JOIN region_std c ON c.name = a.city AND c.parent_code = p.code LEFT JOIN region_std d ON d.name = a.district AND d.parent_code = c.code WHERE p.code IS NULL OR c.code IS NULL OR d.code IS NULL;这条查询返回的行就是需要人工复核的地址,答辩前跑一遍,心里有底。
从那以后我每次做地址相关的项目,都会先把测试集和验收表建好再写代码,而不是等功能做完再补测试。地址解析的坑大多藏在边界样本里,提前把边界列出来,比事后 debug 省太多时间。希望这套结构能帮到你,把 AddressParseTest.zip 里的思路直接搬进自己的小程序项目里跑通。
本文还有配套的精品资源,点击获取