简介面向微信小程序开发者与物流、电商后台集成人员解决非结构化收货地址难以标准化的问题。项目基于腾讯云地址解析API围绕HMAC-SHA1签名、Base64编码、请求构造与JSON响应解析展开代码结构清晰适合刚接触小程序云API调用的开发者对照学习。压缩包共18个文件涵盖6个js脚本含sha1、base64、util等核心工具、5个json配置、3个wxss样式、2个wxml页面与2个ds_store文件整体仅16KB可快速导入微信开发者工具查看调用逻辑。目前已有8239人学习下载是观察腾讯云地址解析能力在小程序中落地流程的轻量案例。通过阅读源码能理解地址解析API的完整调用链路与签名细节并看到前端确认按钮事件尚未完成的部分为后续补充用户交互和异常处理提供了清晰起点。1. 快递地址解析小程序里把「随便填的地址」变成数据库能用的标准字段做电商小程序或者快递代收类小程序时最头疼的往往不是支付而是用户手填的收货地址。用户可能写「东城区朝阳门内大街 288 号院」也可能写「北京朝阳门内大街288」还可能在备注里补一句「公司前台收」。这些字符串进到数据库等于把省市区、街道、门牌号全部堆在一个字段里后续做按区域统计、按城市筛选、对接电子面单全都要回头去拆字符串。标题里这个 AddressParseTest 方案核心就是用腾讯云地址解析能力把这段用户输入的文本自动拆成省、市、区、街道、门牌号等字段再回填到表单里让小程序收货地址标准化。适合所有做电商、快递、同城配送、多商户平台且被用户地址格式坑过的人。2. 为什么不用正则硬拆地址解析的真实难点和腾讯云 API 的选型理由2.1 正则拆地址为什么是玄学几千条区划名录只是开始很多团队拿到这个需求第一反应是写正则。省市区分别匹配常见的做法是准备一份行政区划表用「省 市 区」的字符串顺序去做 indexOf 或 includes 匹配。这个做法在小范围验证时能跑通但一到真实用户数据就会翻车原因有三个。一是行政区划表本身会变。撤县设区、新区成立、街道合并这些是常态化的事情。某次遇到用户在「海南省三沙市」下单正则匹配表里根本没有这个区划结果整个地址解析成了 null订单直接卡在提交环节。第二是简写和别称。用户写「上海」而不是「上海市」写「内蒙」而不是「内蒙古自治区」甚至写「闸北」——这个区划已经撤销合并多年但用户习惯还在。第三是跨省同名的镇和街道。「城关镇」在甘肃有、在安徽有、在浙江也有正则只能按顺序匹配遇到「河北省某县城关镇」这种地址匹配到的第一个城关镇很可能不是用户所在的。正则方案不是不能用而是它本质上是黑匣子跑通一个地址靠运气跑通一百个地址靠名录跑通一万个地址靠的是把全国所有历史区划、别名、口语化写法全部维护进去这个维护成本远大于调用一次解析 API。常见做法是正则只用来做兜底解析也就是当前置解析接口不可用或者返回为空的时候才用一份精简名录做粗略拆分。2.2 腾讯云地址解析接口的调用方式一次请求拆出省市区与门牌号腾讯云提供的地址解析能力本质上是一个 NLP 地名分词服务。把用户输入的完整文本传进去返回结果是结构化的 JSON包含省、市、区、乡镇/街道、门牌号、经纬度等字段。这个接口在腾讯云的「地址识别」服务里可以通过云 API 网关调用也可以用 HTTP 请求直接调签名方式和腾讯云其他 API 一致。先看一个最小调用示例使用 HTTP 请求直接调场景化 API路径是/v3/Asr/AddressParse按腾讯云 API 3.0 的签名规范拼接参数。下面是实际可替换成自己的 SecretId 和 SecretKey 的请求骨架# 取当前时间戳Unix 秒和请求日期 TIMESTAMP$(date %s) DATE$(date -u -d $TIMESTAMP %Y-%m-%d 2/dev/null || date -u -r $TIMESTAMP %Y-%m-%d) # 拼接要签名的字符串HTTPMethod \n Host \n Path \n 参数 # 参数必须按字典序: Action, Region, SecretId, SignatureMethod, Timestamp, Version, Address # 实际生产环境建议用官方 SDK这里仅为展示签名算法细节 PARAM_STRActionAddressParseAddress$(python3 -c import urllib.parse; print(urllib.parse.quote(北京市朝阳区望京街道阜通东大街6号院)))Regionap-guangzhouSecretIdYOUR_SECRET_IDSignatureMethodHmacSHA256Timestamp${TIMESTAMP}Version2020-05-14 # 计算 HMAC-SHA256 签名 SIGNATURE$(printf %s $PARAM_STR | openssl dgst -sha256 -hmac YOUR_SECRET_KEY -binary | base64) # 发起请求 curl -X POST https://asr.tencentcloudapi.com \ -H Authorization: TC3-HMAC-SHA256 CredentialYOUR_SECRET_ID/${DATE}/asr/tc3_request \ -H Content-Type: application/json \ -H X-TC-Action: AddressParse \ -H X-TC-Version: 2020-05-14 \ -H X-TC-Timestamp: ${TIMESTAMP} \ -H X-TC-Region: ap-guangzhou \ -d {\Address\: \北京市朝阳区望京街道阜通东大街6号院\, \ReturnRaw\: true}这里的逻辑要点是签名用的参数串必须包含Address字段而且要对地址文本做 URL 编码中文地址直接拼进去几乎必然导致签名校验失败。ReturnRaw参数决定是否返回原始分词结果调试阶段建议设为true可以顺带看到接口把地址拆分成了哪些词块这对排查「为什么这个地址解析不对」很有帮助。返回的 JSON 里关键字段如下字段含义是否必返回Province省份/直辖市匹配到时返回City城市匹配到时返回District区县匹配到时返回Town乡镇/街道匹配到时返回AddressLine街道级以下的门牌号和详细地址匹配到时返回Pc邮政编码部分地址返回Lng、Lat坐标需开启相关能力时返回ParSed解析可信度标记始终返回实际用的时候最需要关注的是ParSed这个返回值。它是一个数组标记了Province、City、District等字段是否可信。如果ParSed里某个字段的标记为 0说明接口对这段文本的该字段解析信心不足这种情况下直接把返回结果回填给用户可能把用户的「朝阳区」解析成另一个同名区。这也是后面要讲的兜底策略的触发条件。2.3 用小程序还是云函数拼 URL 的两种常见路径小程序端不能直接暴露 SecretKey签名逻辑写在客户端等于把密钥送给别人。常见做法有两种一种是用腾讯云开发平台的云函数在云函数里完成签名和请求另一种是自建一个后端服务小程序端wx.request到自己的接口由服务端转发。如果项目本身已经用了微信云开发云函数是首选省掉域名备案和 HTTPS 证书的配置。如果项目有自己的 Java 或 Node 后端那就把解析逻辑封装成一个内部接口小程序端提交地址文本后端返回结构化字段。标题里提到的 AddressParseTest 这个本地测试工程就是在接入小程序之前先验证解析效果用的避免在小程序里来回改代码调试——先用测试脚本把返回结构看清楚再决定小程序端怎么处理字段。3. 小程序对接实现请求、提取、回填三段式写法3.1 小程序侧地址解析工具类把网络请求封装成可复用的 Promise这一节给的是在小程序端可以直接套用的代码。先写一个工具类负责调用后端或云函数完成解析。注意这里假设小程序端已经有一个可用的解析接口地址接口返回格式统一为{ code: 0, data: { province: 北京市, city: 北京市, district: 朝阳区, town: 望京街道, street: 阜通东大街6号院 } }工具类代码如下// utils/addressParser.js —— 小程序端地址解析封装 function parseAddress(rawText) { // 空值过滤地址都没填没必要发请求 if (!rawText || !rawText.trim()) { return Promise.reject(new Error(地址不能为空)); } // 调用后端转接接口注意这里有 5 秒超时 return new Promise((resolve, reject) { wx.request({ url: https://your-domain.com/api/address/parse, // 自己的后端或云函数HTTP触发地址 method: POST, data: { address: rawText }, timeout: 5000, success: (res) { if (res.statusCode 200 res.data res.data.code 0) { // 这里做一层防御后端返回的数据结构可能缺字段 // 缺省时给空字符串避免 setData 拿到 undefined 导致渲染异常 const { data } res.data; resolve({ province: data.province || , city: data.city || , district: data.district || , town: data.town || , street: data.street || }); } else { // 业务错误也要走 reject由调用方决定是否提示用户 reject(new Error(res.data res.data.message ? res.data.message : 解析失败)); } }, fail: (err) { // 网络异常交给调用方统一处理 reject(new Error(网络异常请稍后重试)); } }); }); } module.exports { parseAddress };关键参数说明timeout设为 5000 毫秒是经验值地址解析服务在数据偏冷的时候可能到 2 到 3 秒加上网络往返5 秒是一个不会让用户等太久、又不会误杀慢请求的阈值。code 0是业务成功约定实际接入时以后端定义为准。这里的防御逻辑很关键——后端接口正常时可能只返回province但如果服务端加了新字段比如town小程序端不处理undefined渲染层很容易出现空值报错。3.2 表单里的地址回填用户在输入完地址后自动补全省市区调通了解析接口接下来要把解析结果用起来。地址录入的场景是用户在文本输入框里填完完整地址点击「保存」或「识别地址」按钮后把省市区自动回填到表单项里。这里给出一个常见的表单交互写法// pages/address-edit/address-edit.js —— 地址表单页核心逻辑 const { parseAddress } require(../../utils/addressParser); Page({ data: { addressText: , // 用户输入的完整地址 province: , city: , district: , detail: , // 门牌号/详细地址 parsing: false // 防止重复提交 }, // 用户填写完整地址后触发解析 async handleParse() { const { addressText, parsing } this.data; // 节流请求未返回时不允许再次点击 if (parsing) return; if (!addressText || addressText.trim().length 5) { wx.showToast({ title: 地址信息过短请补充后重试, icon: none }); return; } this.setData({ parsing: true }); try { const result await parseAddress(addressText); // 可信度判断如果解析结果为空让用户手动选省市区 const hasValidInfo result.province || result.city || result.district; if (!hasValidInfo) { wx.showToast({ title: 未识别到有效地区请手动填写, icon: none }); this.setData({ parsing: false }); return; } this.setData({ province: result.province, city: result.city, district: result.district, detail: result.street || addressText // 兜底没拆出门牌号时保留原文 }); } catch (err) { wx.showToast({ title: err.message || 解析失败, icon: none }); } finally { this.setData({ parsing: false }); } } });这段逻辑里有一个容易被忽略的细节detail字段的兜底处理。解析接口偶尔能拆出省市区但拆不出门牌号——比如用户只写了「北京市朝阳区望京SOHO」没有具体楼号和楼层。这时候如果把detail设为空字符串用户要重新手动输入直接回填原始addressText则保留了用户输入的完整信息用户只需要微调即可。把「解析失败」和「解析部分成功」分开处理用户体验差距很大。3.3 后端或云函数的解析中转密钥绝不能出现在小程序包体里小程序端代码里不能出现腾讯云 SecretKey否则小程序包审核时就能被扫出来而且任何人解包都能看到密钥。中转逻辑的常见写法是云函数。下面是一个腾讯云开发云函数的示例这里以parseAddress云函数为例// cloudfunctions/parseAddress/index.js —— 云函数中转地址解析 const crypto require(crypto); const axios require(axios); exports.main async (event) { const { address } event; if (!address) return { code: 1, message: 缺少地址参数 }; // 腾讯云 API 3.0 签名所需的公共参数 const SECRET_ID process.env.SECRET_ID; // 在云函数环境变量里配置 const SECRET_KEY process.env.SECRET_KEY; const service asr; const host asr.tencentcloudapi.com; const region ap-guangzhou; const action AddressParse; const version 2020-05-14; const algorithm TC3-HMAC-SHA256; const timestamp Math.floor(Date.now() / 1000); const date new Date(timestamp * 1000).toISOString().slice(0, 10); // 1. 拼接签名头部和请求体 const payload { Address: address, ReturnRaw: true }; const body JSON.stringify(payload); // 2. 构建待签名串请求方法、Host、路径、Content-Type 等 const hashedRequestPayload crypto .createHash(sha256) .update(body) .digest(hex); const canonicalRequest [ POST, /, , content-type:application/json; charsetutf-8\nhost: host \nx-tc-action: action.toLowerCase(), , content-type;host;x-tc-action, hashedRequestPayload ].join(\n); // 3. 计算签名摘要 const credentialScope ${date}/${service}/tc3_request; const stringToSign ${algorithm}\n${timestamp}\n${credentialScope}\n${crypto .createHash(sha256) .update(canonicalRequest) .digest(hex)}; const secretDate crypto.createHmac(sha256, TC3 SECRET_KEY) .update(date) .digest(); const secretService crypto.createHmac(sha256, secretDate) .update(service) .digest(); const secretSigning crypto.createHmac(sha256, secretService) .update(tc3_request) .digest(); const signature crypto.createHmac(sha256, secretSigning) .update(stringToSign) .digest(hex); // 4. 发起请求 const authorization ${algorithm} Credential${SECRET_ID}/${credentialScope}, SignedHeaderscontent-type;host;x-tc-action, Signature${signature}; try { const response await axios.post(https://${host}, body, { headers: { Authorization: authorization, Content-Type: application/json; charsetutf-8, Host: host, X-TC-Action: action, X-TC-Timestamp: timestamp, X-TC-Version: version, X-TC-Region: region } }); const data response.data.Response; // 把原始返回转成字段清晰的结构返回给小程序 return { code: 0, data: { province: data.Province || , city: data.City || , district: data.District || , town: data.Town || , street: data.AddressLine || , parsed: data.ParSed || [] } }; } catch (err) { console.error(address parse error:, err); return { code: 1, message: 地址解析服务异常 }; } };这里把 SecretId 和 SecretKey 放在云函数的环境变量里而不是硬编码进代码是因为云函数的代码更新也走发布流程审计要保证密钥不落库、不进代码仓库。process.env.SECRET_ID这种取法可以在腾讯云开发控制台的云函数配置页面直接编辑环境变量而不是改代码重新部署改一个密钥只需要在控制台改一次。签名逻辑看着复杂但核心就是「参数排序 HMAC 加密 拼接 Header」一旦调试通过就不用再碰它。3.4 地址解析与省市区组件的联动解析结果 手动选择互为补充小程序里标准的收货地址表单通常用的是 picker 省市区联动组件。地址解析结果回填后还需要同步联动 picker 的索引否则会出现一个诡异现象解析地址时把省市区写进了隐藏字段但用户打开 picker 时显示的还是「请选择省市区」。需要维护一个省市区索引映射。假如省市区数据来自vant/area-data或同样结构的数组常见做法是解析出省市区文本后在数组中查找对应的 code// 省市区文本转 code 索引 function findRegionCode(provinceName, cityName, districtName) { for (let pIndex 0; pIndex areaData.length; pIndex) { const p areaData[pIndex]; if (p.name provinceName) { for (let cIndex 0; cIndex p.children.length; cIndex) { const c p.children[cIndex]; if (c.name cityName) { for (let dIndex 0; dIndex c.children.length; dIndex) { const d c.children[dIndex]; if (d.name districtName) { return { provinceCode: p.code, cityCode: c.code, districtCode: d.code, index: [pIndex, cIndex, dIndex] }; } } } } } } return null; // 区县匹配不上时返回 null让用户手动选 }注意这里district匹配不上的情况并不少见。比如用户填的是「河北省廊坊市三河市燕郊镇」返回的区县可能是「三河」但省市区联动数据里三河市的层级可能挂错县级市也可能是「河北省廊坊市香河县」但在某些联动库里县下面没有子级而 picker 又必须三级联动。这类情况不要强制回填返回null后保持手动选择模式地址详情里仍然可以保留解析到的街道字符串。4. 地址解析接入避坑项目上线前后最常见的 4 个坑4.1 坑一「门牌号缺失」不等于解析失败提前区分省市区可用和门牌号为空现象用户地址「北京市朝阳区望京SOHO T1」没有具体门牌号解析接口返回了省市区但AddressLine为空。小程序端直接把detail回填成空字符串用户看到表单里详细地址被清空误以为系统把他的地址丢了重新手动输入一整遍。原因把「解析结果是否为空」当成了唯一判断标准忽略了门牌号缺失是正常情况。很多地址本身就到楼宇级别没有门牌号是常态。解决解析结果拆成三级兜底——省市区全部识别且AddressLine非空直接回填省市区识别但AddressLine为空回填省市区原文本给detail省市区识别不全只回填能识别的部分其余留给用户手动选。第 3.2 节里用的正是第二种策略detail: result.street || addressText。4.2 坑二签名参数里的中文地址没有 URL 编码调试报错 301 或验签失败现象本地测试时直接用中文地址拼参数串发送返回错误提示签名不匹配或Signature校验失败。部分开发者换成签名后依然报错于是怀疑是时间戳问题。原因腾讯云 API 3.0 的签名过程里参数串中的中文必须使用百分号编码而且编码后的结果还要继续参与签名计算。很多示例代码用的是英文参数中文地址一旦出现编码后的字符串变了签名串也变了。解决参考 2.2 节示例所有参数值先做一次 URL Encode再去拼签名串。注意编码时encodeURIComponent不会编码~!*()这几个字符而某些语言的 URL Encode 规则对*有差异建议统一用同一个编码函数前端、云函数、后端测试脚本保持一致的编码规则。4.3 坑三小程序真机上网络请求直接失败开发者工具却正常现象开发者在微信开发者工具里测试一切正常预览版在真机上打开解析接口直接走fail回调报request:fail。原因小程序正式版要求所有请求域名必须在小程序管理后台配置到「request 合法域名」里。开发者工具默认勾选了「不校验合法域名」所以本地调试没问题真机上就翻了车。地址解析接口的域名不在白名单里请求会被微信拦截。解决申请 ICP 备案的域名把域名配置到小程序后台的 request 白名单里。注意腾讯云 API 网关分配的默认域名通常不适合直接作为白名单——它可能是共享域名会被微信判定为不合规。自建中转服务时要绑定自定义域名云函数通过 HTTP 访问路径也一样需要自定义域名。另外 HTTPS 证书不能过期微信会优先校验证书有效性。4.4 坑四用户在小程序切后台导致 setData 竞态结果还没返回页面已经销毁现象用户点击「识别地址」后立刻切到微信主页解析请求在 onHide 或 onUnload 之后才返回setData被调用时报setData failed或者页面崩溃。原因wx.request是异步的页面销毁并不是中断请求回调仍会被触发。此时对已经卸载的页面实例执行setData会触发警告。解决在自定义工具类里维护一个「当前页面是否可用」的标记云函数或后端转发返回后先判断getCurrentPages()是否存在且当前页不是上一个页面再执行回调。或者更简单在小程序页面实例上绑定一个unloaded标志onUnload里置为true回调里判断后再决定是否setData。这类问题在用户网络慢、第三方接口响应超过 3 秒时尤其容易出现。5. 让地址解析从「能用」变成「好用」验证脚本与三个进阶习惯一套地址解析方案上线前至少应该用大批量真实地址文本做回归。常见做法是准备一份 txt 测试样本每行一条地址包含各种颗粒度的写法带门牌号的、带楼宇名的、只写到街道的、包含「省 市 区」的、省略省级的、纯手写乱序的。然后跑一个批量对比脚本把解析结果和人工标注结果比对记录字段级准确率。# 批量压测读取 test_addresses.txt 每行地址输出解析后 JSON # 假设已配置好云函数地址和 token while IFS read -r addr; do curl -s -X POST https://your-domain.com/api/address/parse \ -H Content-Type: application/json \ -d {\address\: \$addr\} parse_results.jsonl done test_addresses.txt这个脚本的意义不在脚本本身而是把解析准确率量化出来。建议关注三个指标省市区字段整体命中率三个字段全部命中占比、门牌号/详细地址的命中率这个字段最难、以及由解析兜底策略处理的比例也就是上面代码中result.street || addressText触发的比例。如果兜底比例超过 5%说明用户输入中大量地址本身不完整产品层面应引导用户补充详细地址。三个进阶习惯建议新项目直接采纳。第一个是解析结果缓存。地址解析是收费接口用户反复编辑同一地址会重复计费可以在本地存储解析过的地址哈希命中缓存直接返回。第二个是回填时与用户确认地址的展示交互把解析结果放到表单回显区用户确认后再保存不自动静默保存。第三个是解析失败后保留原始输入并额外提供一个「手动选择省市区 自动带出详情」的降级路径——即使解析接口挂了用户的购买流程也不能因此断裂。最后说一个踩过的小坑用户地址里写「xx路xx号xx幢」解析接口返回的AddressLine可能是「xx路xx号」而「xx幢」被当成干扰词丢弃。处理方式是把AddressLine和用户原始输入做一次对比如果发现原始输入长度明显更长且包含解析结果没有覆盖的数字串就把差异部分拼接到detail末尾。地址解析的边界永远存在关键不在于追求完美而是把不完美的结果用兜底策略补到用户可以接受的录入体验。这个习惯留给接手的每一个项目希望帮到你。本文还有配套的精品资源点击获取