短信和邮件里唤起小程序:URL Scheme 与 URL Link 的实践与边界

2026-09-25 01:20:16 1 次浏览
微信小程序URL SchemeURL Link外部唤起短信营销后端开发

去年 11 月我们接了个连锁宠物店的会员召回项目,运营的需求一句话:往会员手机号发短信,点开直接进小程序的优惠券页。当时组里的小陈在群里原话是「短信点链接开小程序,不就是发个 a 标签嘛」。结果这事儿比 a 标签复杂得多——链接要服务端调接口生成、有有效期上限、有调用配额,而且最关键的一条:微信里点不开。整条链路我们折腾了三天才稳下来,这篇把 URL Scheme 和 URL Link 两条路的区别、服务端实现、以及那些文档里不会写在显眼位置的坑,一次讲完。

两个东西到底差在哪

先纠正一个常见误解:URL Scheme 和 URL Link 不是同一件事的两种写法,它们是两个独立接口、两种不同形态的链接,适用场景有重叠但边界不同。

外部渠道唤起小程序

URL Scheme(urlscheme.generate)生成的形如 weixin://dl/business/?t=xxxxx,这是微信自己的私有协议头,只有装了微信的设备、在系统层面注册过这个协议,才能被唤起。URL Link(urllink.generate)生成的形如 https://wxaurl.cn/xxxxx 的 https 短链,靠的是 https 跳转,对邮件客户端和部分对自定义协议敏感的环境更友好。

我们实测后的粗略体感(自建监测口径,样本是我们自己的召回短信流量,2025 年 11 月到 12 月约 4.6 万条短信,统计来源是小程序页面 onLoad 里上报的 src 参数,不代表行业水平):

对比项 URL Scheme URL Link
生成接口 POST urlscheme.generate POST urllink.generate
链接形态 weixin://dl/business/?t=... https://wxaurl.cn/...
微信外短信/邮件 可唤起 可唤起
微信内点击 打不开 同样打不开
有效期 最长 30 天 最长 30 天
是否需要用户装微信 必须 必须(最终落到微信)
落地页要求 已发布版本中的页面 同左
主体要求 非个人主体 非个人主体

划重点的那行再说一遍:两种链接都只能在微信外部唤起小程序,微信会话内的点击都不会生效。这是很多人翻车的第一现场。

服务端怎么生成

生成链接必须走服务端,因为要带 access_token,不能把 token 暴露在前端。我们后端是 Node.js,核心代码不长。

// 环境:Node.js 20 / axios 1.x / 微信小程序 access_token 已由统一的 token 服务维护
// 生成逻辑放在内部服务里,业务侧只传页面路径和参数,不直接碰 access_token
// token 过期由统一服务刷新,这里不做缓存,避免多处各自维护出现不一致
// 超时建议设 5 秒,微信接口偶发抖动别把召回主流程拖死
const axios = require('axios');

// 生成 URL Link:适合放进短信正文,https 形态对运营商短链改写更稳
async function genUrlLink(token, pagePath, query) {
  const res = await axios.post(
    `https://api.weixin.qq.com/wxa/generateurllink?access_token=${token}`,
    {
      path: pagePath,          // 落地页路径,必须是已发布版本里存在的页面
      query: query,            // 页面参数,比如 'id=1024&src=sms1123'
      is_expire: true,         // 开启过期,别用永久链接,配额金贵
      expire_type: 1,          // 1 表示按失效间隔时间算
      expire_interval: 28,     // 单位天,上限 30,我们留两天余量
      // 要按具体日期过期的活动改用 expire_type 0 传时间戳
    }
  );
  // errcode 非零一律当失败抛出,调用方要记录到批次日志里方便对账
  if (res.data.errcode) throw new Error(`${res.data.errcode}: ${res.data.errmsg}`);
  // 拿到的短链直接拼进短信模板,不再二次转短链
  return res.data.url_link;  // 形如 https://wxaurl.cn/xxxxx 的短链
}

URL Scheme 的接口几乎一个模子,换成 urlscheme.generate 就行:

// 生成 URL Scheme:jump_w 后面决定用户点击后的行为
// 注意 expire_type 二选一,这里演示按时间戳失效的写法
// 单条链接的生成结果要落库,方便运营临时改期时回查
// errorcode 常见值是 40029 无效 key,先查 access_token 再查参数
async function genScheme(token, pagePath, query) {
  const res = await axios.post(
    `https://api.weixin.qq.com/wxa/generatescheme?access_token=${token}`,
    {
      jump_wxa: {
        path: pagePath,        // 同样要求已发布
        query: query,
      },
      is_expire: true,
      expire_type: 0,          // 0 表示按失效时间戳,适合「活动当天 23:59 过期」这种需求
      // 时间戳用秒不是毫秒,传成毫秒会直接报参数错误
      expire_time: Math.floor(Date.now() / 1000) + 7 * 86400, // 七天后失效
    }
  );
  // openlink 就是给短信/邮件正文用的协议链接
  return res.data.openlink;    // 形如 weixin://dl/business/?t=xxxxx
}

后台团队用的 .NET 也要接一套,写法上就是普通 HttpClient 调用,没有什么特殊姿势:

// 环境:ASP.NET Core 8 / System.Text.Json
// 与 Node 侧共用同一套链接落库表,生成记录带批次号方便审计
// _http 注入时记得设置 BaseAddress,别每处都写完整域名
public async Task<string?> GenUrlLinkAsync(string token, string path, string query)
{
    var payload = new
    {
        path = path,           // 已发布版本的页面路径
        query = query,         // 页面启动参数
        is_expire = true,
        expire_type = 1,       // 按间隔天数失效
        expire_interval = 30,  // 顶格 30 天
    };
    var resp = await _http.PostAsJsonAsync(
        $"https://api.weixin.qq.com/wxa/generateurllink?access_token={token}", payload);
    // 返回体里 errcode 为 0 才算成功,url_link 字段才是链接本体
    // 拿不到字段时记日志再返回 null,调用侧决定是否重试
    // token 由调用方传入,方法内不做刷新,职责单一
    var body = await resp.Content.ReadFromJsonAsync<JsonElement>();
    return body.TryGetProperty("url_link", out var link) ? link.GetString() : null;
}

链接落库时我们加了批次号和生成时间两个字段,出问题时按批次回查,比翻短信通道后台快得多。这里有个工程上的取舍值得说:链接要不要落库。我们的做法是落。2025 年 12 月运营临时要求把一场活动的过期时间从 30 天改成 7 天,因为链接落了库,我们只改了生成侧的策略,历史链接该过期的让它过期,新链接按新策略走,没到返工的程度。如果链接是即用即抛不落库,遇到这种需求就只能干瞪眼。

原理层面:为什么微信里点不开

这个设计不是为了恶心开发者,是安全模型的必然结果。

唤起小程序这件事,本质上是「一个外部指令要求微信打开自家生态里的某个页面」。微信把入口分了两类:生态内入口(聊天卡片、搜一搜、公众号菜单)和生态外入口(短信、邮件、外部浏览器)。生态内的入口微信自己可管可控,谁发的、链到哪、用户举报了多少,全在掌握里。而生态外入口带着一个微信控制不了的来源——短信可以是任何人发的,链接指向的 query 参数可以是任何人拼的。

所以微信的方案是把生态外入口做成服务端签发的一次性凭据:每个 openlink / url_link 都是小程序管理员通过接口、带着自己的 access_token 生成的,微信侧有签发记录,出事能追溯到主体。如果允许这种链接在微信会话内直接点开,那生态内就多了一条绕过审核的通路,伪造卡片和钓鱼会立刻多起来。

理解了这个机制,几个边界条件就不难推出来:

  • query 参数不能当鉴权依据。链接是明文的,参数拼在后面谁都能改,敏感操作必须在页面内再验一次身份。
  • 链接生成即消耗配额,不是用户点击时才校验。所以「先批量生成一堆放着」的做法要克制。
  • 微信内点开不生效不是 bug,别浪费时间去测试机上加白名单。
flowchart TD
    A[用户收到短信/邮件] --> B{点击链接}
    B --> C[系统识别协议或域名]
    C -->|weixin:// 已注册| D[拉起微信并校验凭据]
    C -->|https 短链 302| D
    C -->|微信内会话| E[拒绝唤起 流程终止]
    D --> F{小程序是否已安装}
    F -->|是| G[直达 path + query 页面]
    F -->|否| H[落地到小程序资料页 提示使用]

从时序上看完整链路是这样的:

sequenceDiagram
    participant OPS as 召回系统(服务端)
    participant WX as 微信接口
    participant SMS as 短信通道
    participant U as 用户手机

    OPS->>WX: POST urlscheme.generate (带token/过期策略)
    WX-->>OPS: 返回 openlink(签发记录入账)
    OPS->>SMS: 提交短信模板+链接
    SMS->>U: 短信送达
    U->>U: 点击链接(微信外)
    U->>WX: 系统唤起微信并校验凭据
    WX-->>U: 打开小程序优惠券页

踩过的坑,一个一个说

坑一:个人主体小程序根本调不了这个接口。 2026 年 3 月有个朋友做个人作品集小程序,想加短信唤起,接口直接返回 40165 之类的权限错误。翻文档才看到生成接口要求非个人主体。这事儿没有绕路,个人主体就老老实实用公众号文章里嵌小程序卡片这类生态内方式。

坑二:频次配额比想象中紧。 接口按 access_token 维度有生成上限,具体额度在小程序后台的接口权限页看。我们第一轮压测时脚本循环生成了两万多条,撞了限额,当天没恢复,召回计划顺延了一天。后来定了两条纪律:链接按发送批次生成,用多少生多少;生成服务加了一个本地计数器,快到配额就告警。

坑三:过期策略选错字段,白费劲排查。 expire_type 是 0 按失效时间戳、1 按失效间隔天数,两种不能同时传。我们有个同事把 expire_time 和 expire_interval 都塞进请求体,返回的链接行为对不上预期,排查了一下午才发现是参数打架。记住:二选一,别都传。

坑四:短信里的链接会被运营商改写。 有批次短信被通道侧转成了自家短链域名,用户点过去 302 跳到 wxaurl.cn 才唤起微信,多一跳就多一层失败可能。后来我们要求通道商对这类短信用 https 原链(URL Link 本身够短),Scheme 因为协议头 weixin:// 在部分短信模板审核里反而更容易被拦。这算我们自己的渠道经验,不同通道商策略不一样,仅供参考。

坑五:query 参数丢字符。 邮件正文里链接如果被客户端按富文本重新编码,& 可能被转义。稳妥做法是参数值先 urlencode,页面 onLoad 里再 decode。

还有一个不算坑但容易忽略的点:同一批次发出去的链接,用户转发给别人的行为完全拦不住。链接本身不绑定收件人身份,A 收到的短信链接转给 B,B 点开一样能进页面看到 A 的 query 参数。我们优惠券这类链接用的是「券码+手机号尾号校验」双因子,页面内再验一次,才避免了几个客诉。做营销召回的同行在设计参数结构时,最好一开始就按「链接会外泄」的前提来。

降级方案:唤不起的时候给用户一条路

总有唤不起的情况:没装微信、协议被拦、链接过期。我们的处理是在召回页(H5)上做兜底——短信里的主链接其实指向自家 H5 页面,页面加载后自动尝试跳 URL Link,同时页面上永远放一个「微信内打开」的引导:提示用户截图后用微信扫一扫,或者引导到绑定好的公众号,从公众号菜单进小程序。

场景 主路径 降级路径
短信唤起 URL Scheme / URL Link 落地到自建 H5,页内引导
邮件唤起 URL Link(https 更稳) 邮件里附公众号二维码之外的文字指引
微信内点击 不支持 引导复制链接到浏览器打开,或走公众号菜单
链接过期 唤起失败 H5 检测参数后展示「活动已结束」页

公众号这条降级路值得多说两句:把召回用户先沉淀到公众号,之后可以用模板消息、客服消息带小程序页面路径继续触达,这些生态内入口没有 30 天有效期的烦恼。短期单次召回看 scheme/link,长期留存靠公众号,两件事不冲突。另外提醒一句,H5 页面做自动跳转时留个 1 秒左右的延时和手动入口,部分安卓机型上立即跳协议链接会被系统当广告拦截,这是我们 12 月上线当天才发现的,改完之后唤起成功率才回到正常水位。

参考与延伸

按官方文档为准,接口参数各家版本偶尔有微调:

  • 微信小程序官方文档·获取不限制的小程序码之外的打开能力说明:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-scheme.html
  • 微信小程序 URL Link 说明:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/url-link.html
  • 服务端接口调用凭证 access_token:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/mp-access-token/getAccessToken.html
  • 小程序页面路径与启动参数:https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/page.html

微信小程序 · URL Scheme · URL Link · 短信唤起 · 外部引流 · 服务端接口

🤖
本内容由 AI 辅助生成,经人工校对审核;部分素材、资料来源于公开网络,仅作个人观点分享与交流使用,无任何商业侵权意图。若内容、图片、文字涉及您的合法著作权、版权权益,请联系本人,核实后将第一时间删除、修改相关内容。