
简介这份面向微信小程序开发者的地址解析示例工程围绕快递收货地址的智能识别与标准化展开借助腾讯云地址解析API将用户输入的非结构化地址提取为省、市、区等信息适用于物流、电商及需要表单地址规范化的练手与课程设计场景。压缩包共18个文件、约16KB以JavaScript逻辑代码和JSON配置为主配合WXML页面结构与WXSS样式另有少量系统缓存文件整体目录精简便于快速导入微信开发者工具查看运行脉络。已有8242人学习下载说明该主题在实战学习中具有一定关注度。读者可据此梳理API接入、HMAC-SHA1签名、Base64编码、请求构造与JSON响应解析的完整链路并参考页面交互与异常处理思路项目中确认按钮事件尚待补全也留出了继续完善地址确认与微调功能的练习空间。1. 一段乱糟糟的收货地址为什么非要拆成省市区再存用户在小程序里粘贴一句「朝阳区建国路 88 号 SOHO 现代城 A 座 1201 张三 13800000000」如果把这串原样塞进数据库后面的运费模板、分区库存、电子面单全都得靠人肉判断。地址解析要干的事就一件用腾讯云 API 把这坨自由文本做地址识别拆出省、市、区、街道、门牌再按统一的地址格式化规则回填让地址变成能被程序消费的结构化字段。压缩包里的 AddressParseTest 就是跑通这条链路的小程序 Demo——输入框贴一段文本点一下出结构化结果省市区直接回填三级联动。这个 Demo 适合两类人一是手上正做电商、外卖、寄件类小程序被地址输入折磨过的同学二是想看清「小程序 云函数 第三方 API」这条标准链路该怎么搭的后端或全栈开发者。它不复杂但该踩的坑一个不少。2. 选型与链路正则、位置服务、腾讯云 API 3.0 三条路怎么选先说结论手工正则只能做「兜底清洗」做不了「识别」。这不是代码写得糙的问题是地址本身就没有语法。中文地址是一套约定俗成的省略表达「北京市朝阳区」省略成「朝阳区」「浙江省杭州市余杭区」省略成「余杭」「内蒙古自治区」写成「内蒙」这些都不是规则能穷举的。凡是试图用一条正则打天下的方案上线两周就会被真实数据打回原形。2.1 正则到底卡在哪几个地方我把线上遇到过的脏样本归了几类看一遍就明白为什么必须调接口原始输入正则方案的判断结果接口方案的判断结果朝阳区建国路 88 号全国有两个「朝阳区」无法区分结合上下文/region 参数给出一个另一个作为候选江苏昆山玉山镇 XX 路「昆山」不在省市区字典里直接漏识别为江苏省苏州市昆山市县级市落到 district北京市海淀区上地十街 10 号院能拆但姓名、手机号粘在尾部需要额外剥离结构化字段本身就是分开的浙江省杭州市余杭区仓前街道「仓前街道」是 street 还是 detail正则没有依据street / street_number / detail 分层返回重庆渝北区龙溪街道直辖市省份字段为空前端三级联动直接崩补齐 province city 重庆市真正让开发者感到困惑的是本地开发者工具里一切正常真机上却解析得乱七八糟——那是两条链路的输入清洗逻辑不一致下面会讲。所以这个项目里正则依然有位置但只负责「前置清洗」比如把姓名、手机号、收货人:这种前缀从文本里剔掉让送给接口的字符串尽可能干净。清洗不做接口返回的 detail 里会混进张三13800000000后面写面单时你还得再洗一次。2.2 位置服务直连与腾讯云 API 3.0 网关的差别这个 Demo 用的是腾讯位置服务的 WebService 地址解析接口鉴权就是 URL 上带一个key返回体里除了省市区街道还会带上经纬度和相似度分数。另有一条路是把能力包一层腾讯云 API 3.0 网关用SecretId/SecretKey做 TC3-HMAC-SHA256 签名。两者不是替代关系是不同阶段的选择维度位置服务 WebService 直连腾讯云 API 3.0 网关鉴权成本URL 带 key一行搞定要写 TC3 签名几十行密钥安全key 会出现在请求里不能裸露在小程序端密钥只在服务端前端拿不到配额与审计控制台看调用量粒度粗可以按子账号、按接口做配额和审计小程序接入要把接口域名加进 request 合法域名走自己的网关域名域名自己控适合阶段验证期、Demo、小体量已有云账号体系、要合规留痕的项目体量不大、想先把功能跑通就先用位置服务把 key 放在云函数环境变量里小程序端只调云函数。等业务上来了、需要按业务线分账或做调用审计再换成 API 3.0 网关。签名的代价是一次性的写一次封装好就行后面所有接口复用同一个签名函数。2.3 一次解析请求的完整链路从用户点按钮到三级联动亮起来中间其实有五步每一环都能单独出问题所以我在项目里把它们拆成独立函数方便定位到底是哪一步坏了// miniprogram/utils/address.js —— 请求前的清洗尽量别把脏东西送给接口 const NOISE_PATTERNS [ /(收货人|收件人|联系人)\s*[:]?/g, // 收货人张三 /1[3-9]\d{9}/g, // 手机号 /[\u4e00-\u9fa5]{2,4}\s*1[3-9]\d{9}/g, // 姓名和手机号粘在一起 /\s/g // 多空格归一 ] function clean(raw) { let s String(raw || ) NOISE_PATTERNS.forEach((re) { s s.replace(re, ) }) return s.trim() } module.exports { clean }clean只做减法不改写内容。注意手机号那条正则要放在姓名前面执行否则「张三13800000000」这种连写的输入会先被姓名规则切一半剩下半截数字反而更难处理。清洗完的字符串长度如果小于 6 个字符直接在小程序端拦掉不必浪费一次调用——这是我用真实数据换来的经验短文本调接口的失败率高得离谱还占配额。链路剩下的四步是调云函数拿结构化结果 → 前端做标准化补全直辖市、县级市→ 校验必填字段 → 回填三级联动和详细地址输入框。第 4 章会把每一段代码摊开讲。3. 目录与模块拆解AddressParseTest 里每个文件管什么这类 Demo 的组织方式基本一致按「页面层 / 组件层 / 工具层 / 云函数层」四块切。拆开压缩包之后先看目录心里就有数了3.1 目录结构AddressParseTest/ ├── miniprogram/ │ ├── app.js # 云开发初始化 │ ├── app.json # 页面注册、navigationBar 配置 │ ├── app.wxss │ ├── pages/ │ │ ├── index/ # 单条地址解析主入口 │ │ │ ├── index.js │ │ │ ├── index.wxml │ │ │ ├── index.wxss │ │ │ └── index.json │ │ └── batch/ # 批量粘贴解析二次开发预留 │ │ └── ... │ ├── components/ │ │ └── region-picker/ # 省市区三级联动负责回填和取值 │ └── utils/ │ ├── request.js # 云函数调用封装统一错误码 │ ├── address.js # 清洗 标准化核心 │ └── validate.js # 必填校验、长度校验 ├── cloudfunctions/ │ └── parseAddress/ │ ├── index.js # 调腾讯位置服务key 走环境变量 │ ├── package.json # 依赖 axios │ └── config.json └── project.config.json两个地方值得留意。一是utils/address.js被页面和组件同时引用清洗逻辑只写一份避免「列表页能解析、详情页解析不出来」这种低级不一致。二是cloudfunctions/parseAddress是独立云函数不是塞在页面里直接发请求——这条设计决定了密钥会不会泄露后面第 5 章会专门讲翻车的场景。3.2 页面层的状态设计pages/index/index.js的data里其实只放四个状态就够用字段类型作用rawTextString用户粘贴的原始文本不清洗留着做二次解析parsingBoolean请求中标记用来禁用按钮、显示 loadingresultObject | null结构化结果null表示还没解析过errMsgString错误提示任何一步失败都往这里写rawText保留原文这个细节很重要。用户看到解析结果不对第一反应是改原文重试如果你解析完就把输入框清空他会非常烦躁。parsing这个布尔值不只是用来展示 loading 的它还是连点保护的开关——地址解析接口是按次计费的手抖点三下就是三次调用。3.3 工具层把「请求」和「业务」隔开utils/request.js里只做一件事调云函数把各种异常收敛成一个统一结构{ ok, data, code, msg }。页面里不出现wx.cloud.callFunction也不出现任何errCode判断这样后面从云函数切到自建服务器只改这一个文件。// miniprogram/utils/request.js function parseAddress(address, region ) { return new Promise((resolve) { wx.cloud.callFunction({ name: parseAddress, data: { address, region }, success: (res) resolve(res.result || { ok: false, msg: 云函数无返回 }), fail: (err) resolve({ ok: false, code: CLOUD_FAIL, msg: err.errMsg || 网络异常 }) }) }) } module.exports { parseAddress }注意这里用的是resolve而不是reject。地址解析失败是很常见的业务分支地址太短、识别不出、配额超了不该当成异常抛出去让上层try/catch到处包。统一走ok字段判断页面的错误处理就只有一条路径可读性高很多。这是我踩过坑之后改的写法早期版本用 reject结果每个调用点都要写两层 catch代码越写越乱。4. 核心代码从一段文本到省市区三级联动的完整实现整条链路的核心就三段代码云函数里发请求、页面里接结果、工具层做标准化。逐段看。4.1 云函数侧密钥收进环境变量请求加超时// cloudfunctions/parseAddress/index.js const cloud require(wx-server-sdk) const axios require(axios) cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const KEY process.env.QQMAP_KEY // 密钥只存在云函数环境变量里 const ENDPOINT https://apis.map.qq.com/ws/addressparsing/v1/ exports.main async (event) { const raw String(event.address || ).trim() if (raw.length 6) { return { ok: false, code: TOO_SHORT, msg: 地址太短请补全省市区 } } try { const { data } await axios.get(ENDPOINT, { params: { address: raw, region: event.region || , // 传当前城市名歧义地址命中率明显提升 key: KEY, output: json }, timeout: 3000 // 超过 3 秒就走降级别让用户干等 }) if (data.status ! 0) { // 常见key 未开通该接口、配额用尽、参数不合法 return { ok: false, code: MAP_${data.status}, msg: data.message || 解析服务返回异常 } } return { ok: true, data: data.result } } catch (e) { const code e.code ECONNABORTED ? TIMEOUT : (e.code || NETWORK) return { ok: false, code, msg: e.message } } }几个参数值得说明。timeout: 3000是用户体验的红线小程序里用户等 5 秒以上基本就会退出页面宁可返回失败让他手动填也别让界面卡着。region传当前定位城市能把「朝阳区」这种同名区县判对——不传的话接口按全国范围猜猜错的概率不低。output: json是必须的不传某些环境会走到 JSONP 分支。返回体里的结构化字段不同接口版本可能平铺在result上也可能包在address_components里取字段那段写一行兼容就行别把字段名写死在八个地方。如果走的是腾讯云 API 3.0 网关而不是位置服务直连鉴权换成 TC3 签名封装一次全项目复用// TC3-HMAC-SHA256 签名仅腾讯云 API 3.0 网关需要 const crypto require(crypto) const sha256hex (s) crypto.createHash(sha256).update(s).digest(hex) const hmac (key, s) crypto.createHmac(sha256, key).update(s).digest() function tc3Sign({ secretId, secretKey, host, service, payload, timestamp }) { const date new Date(timestamp * 1000).toISOString().slice(0, 10) const canonicalRequest [ POST, /, , content-type:application/json; charsetutf-8\nhost:${host}\n, content-type;host, sha256hex(payload) ].join(\n) const scope ${date}/${service}/tc3_request const stringToSign [TC3-HMAC-SHA256, timestamp, scope, sha256hex(canonicalRequest)].join(\n) const kSigning hmac(hmac(hmac(TC3 secretKey, date), service), tc3_request) const signature crypto.createHmac(sha256, kSigning).update(stringToSign).digest(hex) return TC3-HMAC-SHA256 Credential${secretId}/${scope}, SignedHeaderscontent-type;host, Signature${signature} }payload必须是实际发送的 JSON 字符串一个字节都不能差host要跟请求头里的 Host 完全一致否则签名对不上返回 401而错误信息只说「签名错误」排查起来毫无头绪。4.2 小程序侧输入不做实时请求只做连点保护// miniprogram/pages/index/index.js const { parseAddress } require(../../utils/request) const { clean, standardize } require(../../utils/address) Page({ data: { rawText: , parsing: false, result: null, errMsg: }, onInput(e) { // 输入过程中绝不发请求地址解析是按次计费的 this.setData({ rawText: e.detail.value, errMsg: }) }, async onParse() { if (this.data.parsing) return // 连点保护 const raw this.data.rawText.trim() if (!raw) return this.setData({ errMsg: 先把收货信息粘进来 }) this.setData({ parsing: true, errMsg: }) try { const res await parseAddress(clean(raw)) if (!res.ok) throw new Error(res.msg || 解析失败请手动填写) const normalized standardize(raw, res.data) if (!normalized.province) throw new Error(没识别出省份请手动选择) this.setData({ result: normalized }) this.selectComponent(#regionPicker).setValue(normalized) // 回填三级联动 } catch (e) { this.setData({ errMsg: e.message }) } finally { this.setData({ parsing: false }) // 无论如何都要复位 } } })finally里复位parsing是必须的。早期版本我把复位写在success回调里结果一超时按钮就永远处于禁用状态用户只能杀掉小程序重进——这种问题在开发阶段极难复现上线后才被投诉。selectComponent拿到组件实例再setValue比在data里挂一堆regionValue数组要干净得多三级联动的内部状态由组件自己管。4.3 结果标准化直辖市、县级市、置信度三道兜底接口返回的结构化字段不能直接拿来用必须再过一层标准化否则三级联动会因为选项匹配不上而回填失败。// miniprogram/utils/address.js const MUNICIPALITY [北京市, 上海市, 天津市, 重庆市] function standardize(raw, result) { const c result.address_components || result || {} const pick (v) (typeof v string ? v.trim() : ) let province pick(c.province) let city pick(c.city) const district pick(c.district) const street pick(c.street) const streetNumber pick(c.street_number) // 兜底 1直辖市的 city 字段常为空补成同名否则联动组件找不到下标 if (MUNICIPALITY.includes(province) !city) city province // 兜底 2只回了省没回市用省占位让用户自己去选 if (province !city !MUNICIPALITY.includes(province)) city // 详细地址 原文去掉已识别的省市区的部分避免丢失小区名和楼栋号 let detail raw ;[province, city, district].filter(Boolean).forEach((seg) { detail detail.replace(seg, ) }) detail (detail street streetNumber).replace(/\s/g, ).slice(0, 120) return { province, city, district, detail, full: [province, city, district, detail].filter(Boolean).join(), confidence: Number(result.similarity ?? result.score ?? 0) // 0~1 或 0~100看接口 } } module.exports { clean, standardize }三个点要解释。第一直辖市的province和city必须都填成同一个名字因为省市区选择器的数据结构通常是[省, 市, 区]三级数组少一级就取不到 value。第二detail我是从原文里「减掉」已识别部分推导出来的不是直接取接口字段——接口的detail有时会把小区名吃掉而小区名恰恰是快递员最需要的信息。第三confidence低于阈值我一般设 0.6 以下时界面要显式提示「请核对识别结果」别让用户闭眼点提交一个错地址的退件成本远高于一次提示。5. 避坑与排查域名、密钥、配额、回填这四类问题最容易翻车以下五条都是真实遇到过、且新人一定会遇到的按「现象 → 原因 → 解决」记。第一条开发者工具里正常真机上一律失败。现象是小程序真机点解析转圈两秒然后提示「网络异常」但开发者工具完全正常。原因是开发者工具默认勾了「不校验合法域名」真机不会给你这个豁免。解决是把位置服务的接口域名加进小程序后台的request合法域名列表并且确认是 https、没有端口号、不带路径。改完不用重新提审但要在真机上重新进一次小程序才生效。第二条接口返回status非 0但提示信息很含糊。现象是云函数日志里能看到返回体status是个三位数message写着「参数错误」或干脆空白。原因一般三种key 没开通这个接口的权限、当日配额跑满、或者address传了空字符串。解决是先在云函数里把event.address和data.status一起打日志然后去控制台确认这个 key 的接口权限和剩余配额。配额这个问题特别隐蔽白天测试都好好的晚上做压测才发现半天都在报错。我一般会给这类外部调用加个简单的当日计数超阈值就降级成手动填写。第三条key 出现在抓包里。现象是抓小程序网络请求能看到完整的 key 明文。原因是早期图省事在页面里直接wx.request打了位置服务域名。解决是把请求整体挪进云函数key 放环境变量小程序端只认wx.cloud.callFunction。已经泄露过的 key 直接去控制台重置顺便配上调用来源限制。密钥这事的后悔药是没有的重置一次要改所有环境。第四条解析结果明明有省市区三级联动就是不回填。现象是console.log打出来province: 重庆市, city: 界面上选择器还是空的。原因是联动组件的value是三级下标数组city为空时算不出下标整组就失效。解决是在standardize里补齐直辖市字段见 4.3 的兜底 1并且在组件的setValue里做一次indexOf校验找不到就落回默认第一项而不是静默失败。第五条用户连点日志里同一个地址出现三次请求。现象是云函数日志里同一秒出现三条完全相同入参的记录。原因是按钮没做禁用parsing标记只在回调里改异步间隙里点得进去。解决办法是进入方法第一行就判断if (this.data.parsing) return并且setData之后才发请求。另外给云函数侧也加一层幂等同样的 address 在 5 秒内重复进来直接返回缓存结果双重保险。6. 二次开发把解析能力接进真实下单页的三个改造点Demo 是单条解析真实业务里要复杂得多。我一般按下面三步改第一步加本地缓存。同一个地址文本在 24 小时内重复解析没有意义直接用wx.getStorageSync缓存地址哈希 → 结果命中就跳过接口。上线后这一层能砍掉三到四成调用量对配额敏感的项目几乎是必做项。第二步做成可降级的表单。解析不是主流程填地址才是。所以onParse失败时不能弹个 toast 就完事要直接把省市区选择器和详细地址输入框亮出来让用户手填同时埋个点记录失败率。真实项目里解析成功率能到 90% 就不错了剩下那一成必须有人工兜底路径否则订单直接流失。第三步批量导入场景单独处理。如果是商家后台一次粘几十条地址就不能串行调接口——接口有 QPS 限制串行跑起来慢得让人怀疑人生。做法是并发控制到 35 路失败的重试一次最后把成功和失败的分别列出来给用户核对// 批量解析控制并发失败重试一次 async function batchParse(list, concurrency 3) { const queue [...list] const ok [], failed [] const worker async () { while (queue.length) { const item queue.shift() let r await parseAddress(clean(item)) if (!r.ok) r await parseAddress(clean(item)) // 重试一次多半是超时 r.ok ? ok.push(standardize(item, r.data)) : failed.push({ item, msg: r.msg }) } } await Promise.all(Array.from({ length: concurrency }, worker)) return { ok, failed } }验证改造效果别靠眼睛看。我习惯准备 30 条脏样本——直辖市、县级市、姓名手机号粘连、纯小区名、超长带备注的——每次改完标准化逻辑就跑一遍比对省市区三级是否全部命中detail有没有丢关键信息。这套样本文件一直留在项目里比任何文档都管用。从那次「开发者工具正常、真机全挂」之后我每次接第三方接口都强制走一遍真机跑一次、断网跑一次、连点五次、把 key 临时改错一次看错误提示能不能让用户看懂。四步走完基本不会在上线后半夜被叫起来查日志。希望帮到你。本文还有配套的精品资源点击获取