我们需求是:在登录我们小程序的用户,在进行购买操作后,在用户绑定完样本,我们对样本进行回收-做实验-人工审核-出报告这个一系列过程,等报告出来后需要通知关注我们微信公众号的用户,来提升用户体验。
我们小程序中存在三种通知形式:短信通知、站内信、微信公众号(需关注),以下详细讲解第三种,前两种大家都不陌生,都是常规操作。
1、微信公众平台配置
前提已经是服务号认证
1.1、需要开启消息模版
1.2、选择合适的模版消息
1.3、模版消息详细参数信息:
1.4、服务类目,需要通过合作方协议合同、营业执照等来进行提交获取审核;
1.5、微信公众平台绑定公众号和小程序
2、概念
- 先明确几个概念
2.1、微信-服务号、订阅号、公众号、公众平台、开放平台
概念 | 定位 | 角色 | 功能差异 |
---|---|---|---|
微信开放平台 | 微信生态的“桥梁工具” | 打通多个应用(APP、公众号、小程序、网站) | 提供UnionID体系、用户身份互通、能力开放的开发平台,开发者在此注册技术接口权限。 |
微信公众平台 | 后台管理系统 | 微信公众号的管理工具(类似网站的后台) | 统一管理订阅号、服务号的发布、数据、设置等。 |
微信公众号 | 账号类型总称 | 订阅号 + 服务号的总称 | 根据需求选择子类(订阅号=发内容,服务号=做服务) |
微信订阅号 | 信息传播为主 | 适合新闻媒体、个人、教育机构等 | 每天可发1条(折叠在订阅号列表),无支付/客服,适合长期品牌宣发。 |
微信服务号 | 服务交互为主 | 适合企业、政务机构、电商平台等 | 每月4次群发(直接显示在微信聊天列表),支持微信支付/模板消息/多客服等。 |
微信生态结构:
+-------------------+
| 微信开放平台 | → UnionID跨应用互通,技术接口提供
+---------+---------+
|
↓
+-------------------+
| 微信公众平台 | → 管理具体账号(公众号:订阅号/服务号)
+---------+---------+
|
↓
+-------------------+
| 微信公众号 | → 实际触达用户的内容或服务(用户看到的账号)
+-------------------+
2.2、小程序中的openid和公众号的openid不一样
- appid:AppID是不同类型的产品的账号ID,是账号的唯一标识符。
- openid:微信用户在不同类型的产品的身份ID。(小程序和微信公众号用户openid不同)
- unionid:微信用户在同一个开放平台下的产品的身份ID。
2.3、小程序累计用户数
-
去重统计:同一个用户多次访问小程序,只会计为 1 个用户。
-
基于
openid
:每个用户在小程序中的唯一标识是openid
,微信通过openid
进行去重统计。
完整流程:用户通过微信快捷登录登录注册进入小程序,会保存属于小程序的openid 和 unionid,因为我们是从小程序用户着手,需要拿到unionid去数据表对比(直接无法找到)找到微信公众号的openid,才能消息通知关注的用户:你的小程序中的报告出来了,快去看吧。
2.4、UnionID获取途径
绑定了开发者账号的小程序,可以通过以下途径获取 UnionID。
- 开发者可以直接通过 wx.login +
code2Session
获取到该用户 UnionID,无须用户授权。 - 小程序端调用云函数时,可在云函数中通过 Cloud.getWXContext 获取 UnionID。
- 用户在小程序(暂不支持小游戏)中支付完成后,开发者可以直接通过
getPaidUnionId
接口获取该用户的 UnionID,无需用户授权。注意:本接口仅在用户支付完成后的5分钟内有效,请开发者妥善处理。
2.5、接口文档
(1)模版消息文档:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html
(2)小程序登录,生成小程序的openid和unionid:
参考文档:https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/user-login/code2Session.html
- 在小程序或公众号端调用
wx.login()
获取临时登录凭证code
。 - https://api.weixin.qq.com/sns/jscode2session ,获取openid和unionid。
- 业务数据库中如果没有存unionid,则通过比对openid来更新unionid。
(3)通过unionid查找微信公众号的openid,来完成模版消息发送
参考文档:https://developers.weixin.qq.com/doc/offiaccount/User_Management/Get_users_basic_information_UnionID.html#UinonId
2.6、获取公众号openid完整流程
用户登录小程序 → 获取小程序的openid → 通过UnionID关联 → 查询公众号openid
1. 准备工作
- 绑定到同一开放平台:
- 将小程序和公众号绑定到同一微信开放平台账号(登录微信开放平台 > 管理中心 > 绑定公众号/小程序)。
- 认证要求:
- 公众号必须是已认证的服务号(认证后才能获取UnionID)。
2. 小程序端获取用户信息
在小程序中,用户授权登录后,通过 wx.login
获取 code
,发送到开发者服务器换取用户的 openid
和可能的 unionid
:
wx.login({
success(res) {
if (res.code) {
// 将 code 发送至服务端,换取 openid 和 unionid
wx.request({
url: 'YOUR_SERVER_URL',
data: { code: res.code }
});
}
}
});
3. 服务端调用微信接口,获取 UnionID
小程序
服务端使用小程序的 code
调用 code2Session
接口,获取用户的 session_key
和可能的 unionid
:
请求接口:
GET https://api.weixin.qq.com/sns/jscode2session?appid=小程序APPID&secret=小程序SECRET&js_code=CODE&grant_type=authorization_code
返回示例:
{
"openid": "小程序OPENID",
"session_key": "SESSION_KEY",
"unionid": "UNIONID" // 若用户授权且绑定开放平台,才会有此字段
}
**4. 通过 公众号的 openid 获取 UnionID **
公众号
如果 code2Session
接口返回了 unionid
,可以通过该 unionid
调用开放平台接口,获取用户在不同公众号的 openid
:
请求方式:
GET https://api.weixin.qq.com/cgi-bin/user/info?access_token=公众号的ACCESS_TOKEN&openid=openid&lang=zh_CN
注意:
- 若用户未关注公众号,或在其他场景未授权,可能返回空值。
- 接口返回的数据中,会包含该用户在公众号下的 `openid`。
注意事项
1.UnionID 条件:
- 用户必须在小程序和公众号中授权过登录。
- 需确保小程序和公众号绑定到同一开放平台。(原有业务中遗漏这一步,导致小程序的unionid都是空,需要补充逻辑让用户登录的时候更新)
- 若用户未关注公众号,返回结果可能为空。
2.公众号的 access_token:
- 需要调用
https://api.weixin.qq.com/cgi-bin/token
接口获取。
3.用户未关注公众号的情况:
- 如果用户未关注公众号,微信接口无法返回其
openid
。此时只能通过用户主动关注后,关联 UnionID。
4.小程序和公众号获取unionid的对比
方法及接口 | 适用场景 | 需要授权 | 适用用户 |
---|---|---|---|
wx.getUserProfile() | 获取 unionid | 需要授权 | 小程序用户 |
code -> session_key -> unionid (https://api.weixin.qq.com/sns/jscode2session) | 静默获取 openid 和 unionid | 不需要授权 | 小程序用户 |
access_token + openid (https://api.weixin.qq.com/cgi-bin/user/info) | 通过 openid 查询 unionid | 不需要授权 | 公众号用户 |
总结完整逻辑:
- 提前设置好微信开放平台,绑定小程序和公众号;
- 微信公众号中增加类目,并开通模版消息,添加到自己的模版消息中;
- 通过小程序登录将小程序用户的openid和unionid存储下来;
- 通过接口获取微信公众号中所有用户的openid,通过数据表存储openid及unionid的信息,如数据表:ar_wechat_auth;
- 然后通过业务逻辑中查询到由用户的报告已出,就对应找到小程序中对应的unionid,再对比ar_wechat_auth表中unionid,找到openid;
- 通过openid调用模版消息相关代码,即可发送成功!
3、Postman验证请求
在写代码之前,先将微信提供的文章再postman中演示一遍,是否能走通流程,再继续编写代码就顺理成章了,看以下实际操作。
3.1、获取关注公众号用户列表
https://api.weixin.qq.com/cgi-bin/user/get?access_token=78_5aWNpGmNvTSCzJV9BJq7j3z8j_jJFM7Lgm4ucBg_cjq4l-900emJDQowTbHST5pYaYDovNIIeg9OkpjnQH897rZXM9suGhxjJlckY6jGEhTiOTgREPT2Vw62MoEGHKfAFASTW
- 出错的返回值:
{
"errcode": 40003,
"errmsg": "invalid openid hint: [QKfb1Bmre-3SmgQa] rid: 65ef1678-66a34477-5c06c4f6"
}
- 正确的返回值,(返回所有关注公众号的用户的openid)
3.2、小程序登录接口
功能描述
登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。更多使用方法详见小程序登录。
3.3、小程序登录 接口(微信开放平台绑定了小程序和服务号)
适用小程序
返回值可以看到有unionid了 (注:js_code 只能用一次,再次请求就得重新生成,这个来源在体验版or开发版中进入小程序说明中的开发调试,通过重新登录来获取该随机码)
3.4、微信公众号的openid去请求unionid
适用公众号
通过去微信公众号,找到openid,请求接口获取unionid,跟小程序中通过登录获取的unionid对比 (结果相同,需先在微信开放平台,绑定小程序和微信公众号才行)
3.5、公众号发送模版消息
参数:access_token需要通过公众号appid和appsecret获取
4、基于Go实现过程
这里边注意的是:请求access_token用的appid信息是需要找微信公众号上的,不是小程序里appid,如果用错就是存在后续错误1中遇到的问题。
package main
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// 获取 Access Token
func getAccessToken(appid, appsecret string) (string, error) {
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", appid, appsecret)
resp, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", fmt.Errorf("读取响应失败: %v", err)
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return "", fmt.Errorf("解析 JSON 失败: %v", err)
}
// 检查是否有错误码
if errcode, ok := result["errcode"].(float64); ok && errcode != 0 {
errmsg := result["errmsg"].(string)
return "", fmt.Errorf("微信返回错误: errcode=%v, errmsg=%s", errcode, errmsg)
}
accessToken, ok := result["access_token"].(string)
if !ok {
return "", fmt.Errorf("无法获取 access_token")
}
return accessToken, nil
}
// 发送模板消息
func sendTemplateMessage(accessToken, openid, templateID string, data map[string]interface{}) (map[string]interface{}, error) {
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s", accessToken)
payload := map[string]interface{}{
"touser": openid,
"template_id": templateID,
"data": data,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("JSON 编码失败: %v", err)
}
resp, err := http.Post(url, "application/json", bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("请求失败: %v", err)
}
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
var result map[string]interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("解析 JSON 失败: %v", err)
}
return result, nil
}
func main() {
// 公众号
appid := "xxx" // 替换为您的 AppID
appsecret := "xxx" // 替换为您的 AppSecret
accessToken, err := getAccessToken(appid, appsecret)
if err != nil {
fmt.Println("获取 Access Token 失败:", err)
return
}
fmt.Println("Access Token:", accessToken)
openid := "xxx" // 关注公众号用户的openid
fmt.Println("OpenID:", openid)
templateID := "xxx" // 替换为您的模板 ID
// 模板消息内容
/**
模板详细内容
报告名称{{thing1.DATA}}
生成时间{{time2.DATA}}
跳转文案点击查看详情管理文案
*/
data := map[string]interface{}{
"thing1": map[string]string{"value": "一脉基因全基因组测序报告"}, // 报告名称
"time2": map[string]string{"value": "2025-03-13 16:05:00"}, // 生成时间
}
// 发送模板消息
result, err := sendTemplateMessage(accessToken, openid, templateID, data)
if err != nil {
fmt.Println("发送模板消息失败:", err)
return
}
fmt.Println("发送结果:", result)
}
生成成功的发送结果:
发送结果: map[errcode:0 errmsg:ok msgid:3.896584684167463e+18]
5、错误记录
1、错误代码:48001, 错误信息:API 功能未授权?
代码错误:map[errcode:48001 errmsg:api unauthorized rid: 67d241fc-53e693f8-5b8b4faf]
错误代码:48001, 错误信息:API 功能未授权,微信原始报文:{“errcode”:48001,“errmsg”:“api unauthorized rid: 5f714450-31a74409-24df03ef”}
- 这里就是appid使用不当,或者是接口权限未开启,以及ip白名单配置等问题;
2、“errcode”: 40029,“errmsg”: "invalid code hint:?
是因为,你发送模板消息的access_token是“其他公众号”的,openid-a是公众号A的,而access_token是公众号B的,它不会提示你access_token错误,确实你的access_token是正确的(是B的),也找到了对应的公众号B,在B下没有找到openid-a对应的用户。这个提示貌似也没毛病。
3、发送结果: map[errcode:47003 errmsg:argument invalid! data.thing1.value is empty rid: 67d278fe-58c6e50b-5d22ceca]
- 当传递的参数不是模版要求的格式就会报错
4、小程序wx.login +code2Ssession 获取不到unionid?
- 需要小程序绑定开放平台 https://open.weixin.qq.com/
5、code been used, rid: 67d65260-0100bd5a-3823c34c
- 请求接口:https://api.weixin.qq.com/sns/jscode2session,js_code只能使用一次。