微信小程序推送公众号模版消息

微信推送公众号模版消息通知 -- machao


业务场景:

某一天,小明开车出去接老婆的机,路上不小心碰撞到了其他的车,于是他通过车险小程序把事故现场上传给理赔人员处理.
过了一段时间,经过理赔人员的审核后,觉得赔付没有问题,需要发送案件进度通知给小明.
这时候,理赔人员问他们的小程序的开发人员,我们应该选择那种消息通知策略比较合理呢?


消息通知策略:

1. 小程序模版消息

    模板推送位置:服务通知

    模板下发条件:用户本人在微信体系内与页面有交互行为后触发,包括: 支付,提交表单

    模板跳转能力:点击查看详情,仅能跳转 下发模板的小程序的各个页面

2. 公众号模版消息

    模板推送位置:公众号

    模版下发能力: 服务端可主动下发消息

    模板跳转能力:点击查看详情,能跳转 下发模板消息的公众号是绑定关联关系的小程序的各个页面

3. 小程序模版消息效果与公众号模版消息效果对比:

效果对比

分析:

通过小程序模版消息推送, 消息会被推送到“服务通知”栏目中,“服务通知”栏目中会存在很多其他小程序的推送,这样看起来会很杂乱.

另外,小程序模版消息推送前提, 必须要在用户本人在微信体系内与页面有交互行为后触发, 即不能延迟推送. 当然也可以通过保存交互过程中的fromid达到延迟推送的效果, 但是这个消息只能推送给触发这个交互行为的用户, 这样会导致另外一个问题: 假如理赔人员除了希望推送案件进度通知给小明外,还希望把消息推送给上级领导,那这个就做不到了.

因此,推送公众号模版消息才是最便捷的策略.


如何实现微信小程序推送公众号模版消息?

首先我们需要清楚以下几点:
1. 消息发给谁? -- who?
2. 消息怎么发? -- how?
3. 消息内容发什么? -- what?

对于第一点(who?)

由于我们的主体是小程序, 因此我们没有办法直接通过用户的小程序openid直接进行公众号模版消息的发送, 这时候上面的unionId机制就起了至关重要的作用了.


UnionID机制

我们可以通过将小程序和公众号挂载在同一个微信开放平台帐号下, 通过unionId进行逻辑关联, 这时候我们就可以通过用户的小程序openid找到用户的公众号openid,进而进行公众号的模版消息推送.

由于开发者经常有需在多个平台(移动应用、网站、公众帐号)之间共通用户帐号,统一帐号体系的需求,微信开放平台提供了UnionID机制。
换句话说,同一用户,对同一个微信开放平台帐号下的不同应用,UnionID是相同的。
微信开放平台帐号下的不同应用包括: 小程序, 公众号, 移动应用等.

对于第二点(how?)

小程序的模版消息有自己的推送模版消息的api接口, 公众号的模版消息也有自己的推送模版消息的api接口. 为了便捷管理优化, 于是微信api推出一个“统一服务消息”接口, 我们接下来也将使用这个接口进行消息的下发.


统一服务消息
对于第三点(what?)

这里当然就是发送模版消息啦, 需要到 公众号-模版消息 新增自己的模版.


思路:

  • 将小程序和公众号挂载在同一个开发平台账号下,这样就能多个不同的主体共用一个相同的UnionID啦.
  • 在用户登录小程序时, 通过小程序的获取用户信息接口, 得到并保存用户的小程序openid + unionId.
  • 同时引导用户在小程序内通过公众号网页授权, 从而得到并保存用户的公众号openid + unionId.
  • 小程序用户通过unionId关联找到公众号openid,进而下发模版消息.

准备:

  1. 公众号的appid+ secret
    小程序的appid+ secret
  2. 前往微信开放平台-管理中心,将公众号,小程序都绑定在同一个开放平台中
  3. 认证服务器为公众号开发者[只需认证一次]
    a. 进入微信公众平台,登录公众号账号
    b. 开发-基本配置, 填写服务器配置
    填写服务器配置

    c. 验证服务器地址的有效性

公众号开发者认证服务器详细步骤

认证服务器为公众号开发者详细步骤:

  • 添加开发者服务器认证接口
@Api("微信公众号开发者API接口")
@Controller
@RequestMapping("/wxPublic/serverApi")
public class WxServerApi {
  private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
  
  //在 微信公众号-服务器配置 中配置, 对应“令牌(Token)”
  @Value("${wx.public.wxServerAuthenToken}")
  String wxServerAuthenToken;

  /**
   * 开发者认证接口
   * 微信api用于认证 服务器 是否为可用服务器
   * @param signature
   * @param timestamp
   * @param nonce
   * @param echostr
   * @return
   */
  @ApiOperation(value = "开发者认证接口", notes = "开发者认证接口")
  @GetMapping("/wxAuthenConfig")
  @ResponseBody
  public String wxAuthenConfig(String signature, String timestamp, String nonce, String echostr) {

      LOGGER.info("开发者认证接口 - 开始签名验证:" + " PARAM VAL: >>>" + signature + "\t" + timestamp + "\t" + nonce + "\t" + echostr);
      
      if (StringUtils.isNotEmpty(signature) && StringUtils.isNotEmpty(timestamp) && StringUtils.isNotEmpty(nonce)
              && StringUtils.isNotEmpty(echostr)) {
          String sTempStr = "";
          try {
              sTempStr = SHA1.getSHA1(timestamp, nonce, wxServerAuthenToken, "");
          } catch (Exception e) {
              e.printStackTrace();
          }

          if (StringUtils.isNotEmpty(sTempStr) && StringUtils.equals(signature, sTempStr)) {
              LOGGER.info("开发者认证接口 - 开始签名验证 - 验证成功:-----------:" + sTempStr);
              return echostr;
          } else {
              LOGGER.info("开发者认证接口 - 开始签名验证 - 验证失败:-----------:00000");
              return "-1";
          }
      } else {
          LOGGER.info("开发者认证接口 - 开始签名验证 - 验证失败:-----------:11111");
          return "-1";
      }
  }

}
/**
* SHA1 class
*
* 计算公众平台的消息签名接口.
*/
public class SHA1 {

  /**
   * 用SHA1算法生成安全签名
   * @param token 票据
   * @param timestamp 时间戳
   * @param nonce 随机字符串
   * @param encrypt 密文
   * @return 安全签名
   * @throws AesException 
   */
  public static String getSHA1(String token, String timestamp, String nonce, String encrypt) throws AesException
            {
      try {
          String[] array = new String[] { token, timestamp, nonce, encrypt };
          StringBuffer sb = new StringBuffer();
          // 字符串排序
          Arrays.sort(array);
          for (int i = 0; i < 4; i++) {
              sb.append(array[i]);
          }
          String str = sb.toString();
          // SHA1签名生成
          MessageDigest md = MessageDigest.getInstance("SHA-1");
          md.update(str.getBytes());
          byte[] digest = md.digest();

          StringBuffer hexstr = new StringBuffer();
          String shaHex = "";
          for (int i = 0; i < digest.length; i++) {
              shaHex = Integer.toHexString(digest[i] & 0xFF);
              if (shaHex.length() < 2) {
                  hexstr.append(0);
              }
              hexstr.append(shaHex);
          }
          return hexstr.toString();
      } catch (Exception e) {
          e.printStackTrace();
          throw new AesException(AesException.ComputeSignatureError);
      }
  }
}
  • 在开发-基本配置, 填写服务器配置
    “服务器地址(URL)” 填写 认证接口的地址
    “令牌(Token)” 填写 代码中的wxServerAuthenToken的值
    "消息加解密方式" 选择 明文
  • 提交修改后, 微信会请求访问接口, 这样认证就完成了.

在用户登录小程序时, 通过小程序的获取用户信息接口,获得用户的小程序openid + unionId

步骤
  • 小程序通过wx.login()获得 code
wx.login({
       success(res){
         let code=res.code
       },  
       fail(res){
         that.$message("微信登录失败,请退出重试!")
       }
     })
  • 服务后端通过code + 小程序appid + 小程序secret, 请求 auth.code2Session API接口得到用户的小程序openid + unionId
/**
* 微信小程序openid工作类
* @author mac
*
*/
@SuppressWarnings("deprecation")
@Component
public class WxOpenidUtil {
  private Logger LOGGER = >LoggerFactory.getLogger(this.getClass());

  final String openidUrl = "https://api.weixin.qq.com/sns/jscode2session";

  @Value("${wx.applet.appid}")
  String wxAppid;
  @Value("${wx.applet.secret}")
  String wxSecret;
  @Value("${wx.applet.grantType}")
  String grantType;

  @SuppressWarnings("resource")
  public WxOpenIdPo GetWxOpenId(String wxcode) {

      WxOpenIdPo info = null;
      // 微信API接口
      String url = openidUrl + "?appid=" + wxAppid + "&secret=" + wxSecret + "&js_code=" + wxcode + "&grant_type=" + grantType + "";

      HttpGet request = new HttpGet(url);
      HttpResponse response = null;
      try {
          HttpClient client = new DefaultHttpClient();
          response = client.execute(request);
          if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
              String strResult = EntityUtils.toString(response.getEntity());
              if (!StringUtils.isEmpty(strResult)) {
                  info = JacksonUtil.defaultInstance().json2pojo(strResult, WxOpenIdPo.class);
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
          LOGGER.error("根据wxcode获取微信小程序openid失败: wxcode(" + wxcode + ")");
      }

      return info;
  }

}
  • 将获取到的 用户的小程序openid + unionId保存

引导用户在小程序内通过公众号网页授权, 从而得到并保存用户的公众号openid + unionId.

步骤:
  • 服务后端添加“公众号授权网页url”接口, 返回“公众号授权网页url“
  • 服务后端添加回调接口, 用于微信api回调, 接口会携带用户的公众号code(划重点)
  • 小程序内通过web-view标签访问“公众号授权网页url”接口返回的url, 引导用户公众号授权, 授权通过后, 微信api会回调“公众号授权网页url”提供的回调接口
  • 在回调接口接口中, 通过微信api携带的用户公众号code, 获取网页授权access_token+用户的公众号openid
    (我们这里
    把 [网页授权access_token] 叫为 [Oauth2AccessToken],
    把 [基础支持中的access_token] 叫为 [PublicAccessToken] ,
    这样大家容易理解
    )
  • 这时候,我们已经得到用户的公众号openid了,但是没有用户的unionId,没有办法与用户的小程序openid进行逻辑关联. 于是我们还要通过 Oauth2AccessToken + openid 拉取用户信息, 这个接口会返回用户的unionId,这时公众号与小程序的用户正式逻辑关联起来了.
贴代码啦:
  • 1.“公众号授权网页url”接口
  @Value("${wx.public.appid}")
  String publicAppid;
  @Value("${wx.public.secret}")
  String publicSecret;
  @Value("${yd.baseHost}")
  String baseHost;//应用链接
  @Value("${wx.public.clientReceiveOpenidUrl}")
  String clientReceiveOpenidUrl;//接口路径
  @Value("${wx.public.clientState}")
  String clientState;//state, 用于回调接口检验
  
  /**
   * 返回 微信公众号获取openid的url (并添加回调路径)
   * @throws UnsupportedEncodingException 
   */
  @ApiOperation(value = "返回 微信公众号获取openid的url (并添加回调路径)", notes = "返回 微信公众号获取openid的url (并添加回调路径)")
  @PostMapping("/return2OpenidUrl")
  @ResponseBody
  public String return2OpenidUrl() throws UnsupportedEncodingException{
      LOGGER.info("微信公众号OpenId接口 - 返回 微信公众号获取openid的url - 开始");
      
      StringBuffer encodeUrl = new StringBuffer(300);
      encodeUrl.append(baseHost + clientReceiveOpenidUrl);
      String redirectUrl = URLEncoder.encode(encodeUrl.toString(), "utf-8");
      
      
      StringBuffer sb = new StringBuffer();
      sb.append("https://open.weixin.qq.com/connect/oauth2/authorize?appid=");
      sb.append(publicAppid);
      sb.append("&redirect_uri=");
      sb.append(redirectUrl);
      sb.append("&response_type=code&scope=snsapi_userinfo");
      sb.append("&state=");
      sb.append(clientState);
      
      LOGGER.info("微信公众号OpenId接口 - 返回 微信公众号获取openid的url - 结束 - redirectUrl("+sb.toString()+")");
      
      return sb.toString();
  }
    1. 服务后端添加回调接口
  @Autowired
  Oauth2AccessTokenBuilder oauth2AccessTokenBuilder;
  @Autowired
  SnsapiUserinfoUtil snsapiUserinfoUtil;
  @Autowired
  UserMapper userMapper;
  
  @Value("${wx.public.appid}")
  String publicAppid;
  @Value("${wx.public.secret}")
  String publicSecret;
  @Value("${yd.baseHost}")
  String baseHost;
  @Value("${wx.public.clientReceiveOpenidUrl}")
  String clientReceiveOpenidUrl;
  @Value("${wx.public.clientState}")
  String clientState;

  /**
   * 初始化 公众号用户基本信息(openid+unionid) 接口
   * - 微信公众号回调的接口 [携带上 code=CODE&state=STATE]
   * @param request
   * @param response
   * @return
   */
  @GetMapping("/initPublicUserInfo")
  @ResponseBody
  public void initPublicUserInfo(@RequestParam("code") String code,@RequestParam("state") String state){
      LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - 开始 - code("+code+") state("+state+")");
      
      //校验 重定向携带的state参数
      if(!clientState.equals(state)){
          LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - state不匹配 - 结束");
          return;
      }
      
      
      //通过code换取网页授权access_token + openid
      //这里通过code换取的是一个特殊的网页授权access_token,与基础支持中的access_token(该access_token用于调用其他接口)不同
      if(StringUtils.isEmpty(code)){
          LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - code为空 - 结束");
          return;
      }
      Oauth2AccessToken oauth2AccessToken = new Oauth2AccessToken(oauth2AccessTokenBuilder, code);
      String accessToken = oauth2AccessToken.getAccess_token();
      String openid = oauth2AccessToken.getOpenid();
      LOGGER.info("微信公众号OpenId接口 - 初始化 公众号用户基本信息(openid+unionid) 接口 - 通过code换取网页授权access_token + openid - accessToken("+accessToken+") openid("+openid+")");
      
      
      //根据 access_token + openid 拉取用户信息(需scope为 snsapi_userinfo)
      //得到 unionid + 用户基本信息
      SnsapiUserinfo snsapiUserinfo = snsapiUserinfoUtil.getSnsapiUserinfo(accessToken, openid);
      String unionid = snsapiUserinfo.getUnionid();
      
      
      //保存 公众号openid
      User usr = new User();
      usr.setUnionid(unionid);
      User one = userMapper.selectOne(usr);
      if(one != null){
          one.setPublicAccountOpenid(openid);
          userMapper.updateByPrimaryKey(one);
      }
      
 }

网页授权access_token(Oauth2AccessToken)

/**
* 网页OAuth2授权 接口返回值
* @author mac
*
*/
public class Oauth2AccessToken {
      
  //网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
  private String access_token;
  //用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
  private String openid;
  
  
  public String getAccess_token() {
      return access_token;
  }
  public String getOpenid() {
      return openid;
  }
  
  /**
   * 使用构建类 构建
   */
  public Oauth2AccessToken(Oauth2AccessTokenBuilder builder, String code){
      Oauth2AccessTokenBuilder build = builder.build(code);
      this.access_token = build.accessToken;
      this.openid = build.openid;
  }
  
}

网页OAuth2授权 构建类(Oauth2AccessTokenBuilder)

/**
* 网页OAuth2授权 构建类
* 
* @author mac
*
*/
@SuppressWarnings("deprecation")
@Component
public class Oauth2AccessTokenBuilder {
  private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

  @Value("${wx.public.appid}")
  String publicAppid;
  @Value("${wx.public.secret}")
  String publicSecret;
  
  //网页OAuth2授权 url
  final private String oauth2AccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
  //刷新OAuth2授权 url 
  //final private String oauth2RefreshTokenUrl = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=APPID&grant_type=refresh_token&refresh_token=REFRESH_TOKEN";

  // 网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
  public String accessToken;
  // 用户刷新access_token
  //public String refreshToken;
  // 用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
  public String openid;

  /**
   * 构建 oauth2AccessTokenBuilder
   * @param code 微信用户code
   * @return
   */
  public Oauth2AccessTokenBuilder build(String code) {
      //不需要重复刷新网页授权token
      //直接根据code请求得到token+openid即可
      Oauth2AccessTokenRespPo po = getAccessToken(code);
      if(po != null && po.errcode != null && !StringUtils.isEmpty(po.errmsg)){
          return null;
      }
      this.accessToken = po.access_token;
      this.openid = po.openid;
      return this;
  }

  /**
   * 获取网页授权的access_token + openID
   * 
   * @return
   */
  @SuppressWarnings({ "resource" })
  private Oauth2AccessTokenRespPo getAccessToken(String code) {
      // 获取小程序全局唯一后台接口调用凭据 接口
      String url = oauth2AccessTokenUrl;
      url = url.replace("APPID", publicAppid);
      url = url.replace("SECRET", publicSecret);
      url = url.replace("CODE", code);
      LOGGER.info("网页OAuth2授权 构建类 - 获取网页授权的access_token + openID - url: "+ url);

      HttpGet request = new HttpGet(url);
      HttpResponse response = null;
      Oauth2AccessTokenRespPo po = null;

      try {
          HttpClient client = new DefaultHttpClient();
          response = client.execute(request);
          if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
              String strResult = EntityUtils.toString(response.getEntity());
              if (!StringUtils.isEmpty(strResult)) {
                  LOGGER.info("网页OAuth2授权 构建类 - 获取网页授权的access_token + openID - 返回: " + strResult);
                  po = JacksonUtil.defaultInstance().json2pojo(strResult, Oauth2AccessTokenRespPo.class);
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
          LOGGER.error("获取网页授权的access_token + openID失败!!!");
      }

      return po;
  }
  
  /**
   * 接口返回参数PO
   * @author mac
   *
   */
  @JsonIgnoreProperties(ignoreUnknown = true)
  public static class Oauth2AccessTokenRespPo{
      //网页授权接口调用凭证,注意:此access_token与基础支持的access_token不同
      private String access_token;
      //access_token接口调用凭证超时时间,单位(秒)
      private Long expires_in;
      //用户刷新access_token
      private String refresh_token;
      //用户唯一标识,请注意,在未关注公众号时,用户访问公众号的网页,也会产生一个用户和公众号唯一的OpenID
      private String openid;
      //用户授权的作用域,使用逗号(,)分隔
      private String scope;
      
      //错误码 - 错误时返回
      private Integer errcode;
      //错误信息 - 错误时返回
      private String errmsg;
      
      public Oauth2AccessTokenRespPo(){}
      
      public String getAccess_token() {
          return access_token;
      }
      public void setAccess_token(String access_token) {
          this.access_token = access_token;
      }
      public Long getExpires_in() {
          return expires_in;
      }
      public void setExpires_in(Long expires_in) {
          this.expires_in = expires_in;
      }
      public String getRefresh_token() {
          return refresh_token;
      }
      public void setRefresh_token(String refresh_token) {
          this.refresh_token = refresh_token;
      }
      public String getOpenid() {
          return openid;
      }
      public void setOpenid(String openid) {
          this.openid = openid;
      }
      public String getScope() {
          return scope;
      }
      public void setScope(String scope) {
          this.scope = scope;
      }
      public Integer getErrcode() {
          return errcode;
      }
      public void setErrcode(Integer errcode) {
          this.errcode = errcode;
      }
      public String getErrmsg() {
          return errmsg;
      }
      public void setErrmsg(String errmsg) {
          this.errmsg = errmsg;
      }
      
  }

}

微信公众号用户信息Po(SnsapiUserinfo)

/**
* 微信公众号用户信息
* @author mac
*
*/
@JsonIgnoreProperties(ignoreUnknown = true)
public class SnsapiUserinfo {
  
  //用户的唯一标识
  private String openid;
  //用户昵称
  private String nickname;
  //用户的性别,值为1时是男性,值为2时是女性,值为0时是未知
  private Integer sex;
  //用户个人资料填写的省份
  private String province;
  //普通用户个人资料填写的城市
  private String city;
  //国家,如中国为CN
  private String country;
  //用户头像,最后一个数值代表正方形头像大小(有0、46、64、96、132数值可选,0代表640*640正方形头像),用户没有头像时该项为空。若用户更换头像,原有头像URL将失效。
  private String headimgurl;
  //用户特权信息,json 数组,如微信沃卡用户为(chinaunicom)
  private List<String> privilege;
  //只有在用户将公众号绑定到微信开放平台帐号后,才会出现该字段。
  private String unionid;
  
  private String language;
  
  private String errcode;
  private String errmsg;
  
  
  public String getOpenid() {
      return openid;
  }
  public void setOpenid(String openid) {
      this.openid = openid;
  }
  public String getNickname() {
      return nickname;
  }
  public void setNickname(String nickname) {
      this.nickname = nickname;
  }
  public Integer getSex() {
      return sex;
  }
  public void setSex(Integer sex) {
      this.sex = sex;
  }
  public String getProvince() {
      return province;
  }
  public void setProvince(String province) {
      this.province = province;
  }
  public String getCity() {
      return city;
  }
  public void setCity(String city) {
      this.city = city;
  }
  public String getCountry() {
      return country;
  }
  public void setCountry(String country) {
      this.country = country;
  }
  public String getHeadimgurl() {
      return headimgurl;
  }
  public void setHeadimgurl(String headimgurl) {
      this.headimgurl = headimgurl;
  }
  
  public List<String> getPrivilege() {
      return privilege;
  }
  public void setPrivilege(List<String> privilege) {
      this.privilege = privilege;
  }
  public String getUnionid() {
      return unionid;
  }
  public void setUnionid(String unionid) {
      this.unionid = unionid;
  }
  public String getErrcode() {
      return errcode;
  }
  public void setErrcode(String errcode) {
      this.errcode = errcode;
  }
  public String getErrmsg() {
      return errmsg;
  }
  public void setErrmsg(String errmsg) {
      this.errmsg = errmsg;
  }
  public String getLanguage() {
      return language;
  }
  public void setLanguage(String language) {
      this.language = language;
  }
  
  
}

微信公众号用户信息 工具类(SnsapiUserinfoUtil)

/**
* 微信公众号用户信息 工具类
* @author mac
*
*/
@SuppressWarnings("deprecation")
@Component
public class SnsapiUserinfoUtil {

  private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

  final String openidUrl = "https://api.weixin.qq.com/sns/userinfo?access_token=ACCESS_TOKEN&openid=OPENID&lang=zh_CN";

  /**
   * 获得 微信公众号用户信息
   * @param accessToken
   * @param openid
   * @return
   */
  @SuppressWarnings({ "resource" })
  public SnsapiUserinfo getSnsapiUserinfo(String accessToken, String openid) {
      SnsapiUserinfo info = null;
      
      if(StringUtils.isEmpty(accessToken) || StringUtils.isEmpty(openid)){
          LOGGER.info("微信公众号用户信息 工具类 - 获得 微信公众号用户信息 - accessToken||openid 为空");
          return null;
      }

      // 微信API接口
      String url = openidUrl;
      url = url.replace("ACCESS_TOKEN", accessToken);
      url = url.replace("OPENID", openid);
      LOGGER.info("微信公众号用户信息 工具类 - 获得 微信公众号用户信息 - url: " + url);

      HttpGet request = new HttpGet(url);
      HttpResponse response = null;
      try {
          HttpClient client = new DefaultHttpClient();
          response = client.execute(request);
          if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
              String strResult = EntityUtils.toString(response.getEntity());
              if (!StringUtils.isEmpty(strResult)) {
                  info = JacksonUtil.defaultInstance().json2pojo(strResult, SnsapiUserinfo.class);
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
          LOGGER.info("微信公众号用户信息 工具类 - 获得 微信公众号用户信息 失败");
      }

      return info;
  }
}
    1. 小程序内通过web-view标签访问“公众号授权网页url”接口返回的url, 引导用户公众号授权

贴代码啦:

html:
   <div v-if="needPublicAccountAuth">
     <web-view :src="publicAccountAuthUrl" bindmessage="getMessage"></web-view>
   </div>

js:
     //进入公众号授权页面
     authPublicAccount(){
       let that = this
       let param={}
       wx.request({
         url: api.wxPublic.return2OpenidUrl,
         data: param,
         method: 'POST',
         header: {
           'content-type': 'application/json'
         },
         success:function (res) {
           //打开webview
           that.$data.needPublicAccountAuth=true
           that.$data.publicAccountAuthUrl = res.data;
           
           //console.log(res.data)
           setTimeout(() => {
             console.log("公众号授权完成")
             that.$data.needPublicAccountAuth=false
             that.$data.publicAccountAuthUrl = ''
           }, 5000);
             
         },
         fail:function (res) {
           wx.showToast({
             title:"获得公众号授权URL错误",
             icon:"none",
             duration:2000
           })
         }
       })

     }

这样,我们的小程序用户与公众号用户就逻辑关联起来啦

小程序用户通过unionId关联找到公众号openid,进而下发模版消息.

步骤:
  • 通过用户的unionId关联得到用户公众号openid
  • 通过用户公众号openid,下发模版消息(这里使用“统一服务消息接口”)


    统一服务消息接口
贴代码啦:
  • 发送微信统一服务消息工具类
    ***代码中的ReqPo, RespPo, WeappTemplateMsg等类, 是为了方便与接口交互而封装的(接口使用的是json传输) ***
    下发小程序和公众号统一的服务消息

    参考: 下发小程序和公众号统一的服务消息
/**
* 发送微信统一服务消息工具类
* 
* @author mac
*
*/
@SuppressWarnings("deprecation")
@Component
public class WechatMessageUtil {
  private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

  // 发送模版消息接口
  final String sendMessageUrl = "https://api.weixin.qq.com/cgi-bin/message/wxopen/template/uniform_send?access_token=ACCESS_TOKEN";

  @Value("${wx.applet.appid}")
  String wxAppid;
  @Value("${wx.applet.secret}")
  String wxSecret;
  @Value("${wx.public.appid}")
  String publicAppid;
  @Value("${wx.public.secret}")
  String publicSecret;

  @Autowired
  WechatAccessTokenBuilder builder;

  /**
   * 发送模版信息
   * 
   * @param reqPo
   * @return
   * @throws JsonProcessingException 
   */
  @SuppressWarnings({ "resource" })
  public RespPo uniformSend(ReqPo reqPo) throws JsonProcessingException {
      RespPo respPo = null;
      LOGGER.info("发送公众号模版信息 start");
      
      String accessToken = reqPo.getAccess_token();
      String reqPoJson = JacksonUtil.defaultInstance().pojo2json(reqPo);

      // 获取小程序全局唯一后台接口调用凭据 接口
      String url = sendMessageUrl.replace("ACCESS_TOKEN", accessToken);

      HttpPost request = new HttpPost(url);
      request.setEntity(new StringEntity(reqPoJson, ContentType.DEFAULT_TEXT.withCharset(Charset.defaultCharset())));
      request.setHeader(new BasicHeader("Content-Type", "application/json"));
      HttpResponse response = null;

      try {
          HttpClient client = new DefaultHttpClient();
          response = client.execute(request);
          if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
              String strResult = EntityUtils.toString(response.getEntity());
              if (!StringUtils.isEmpty(strResult)) {
                  LOGGER.info("发送公众号模版信息 - 返回: " + strResult);
                  respPo = JacksonUtil.defaultInstance().json2pojo(strResult, RespPo.class);
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
          LOGGER.error("发送公众号模版信息失败!!!");
      }

      return respPo;
  }

  /**
   * 组装发送模版信息所需的请求参数
   * 
   * @return
   * @throws Exception
   */
  public ReqPo packageReqPo(TmplBase base, String openId) throws Exception {
      ReqPo reqPo = new ReqPo();
      MpTemplateMsg mpTemplateMsg = new MpTemplateMsg();

      String accessToken = getAccessToken();
      if (StringUtils.isEmpty(accessToken)) {
          LOGGER.info("公众号主体无法获得有效的accessToken");
          return null;
      }

      reqPo.setTouser(openId);//用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid
      reqPo.setAccess_token(accessToken);
      reqPo.setMp_template_msg(mpTemplateMsg);
      reqPo.setWeapp_template_msg(null);
      
      mpTemplateMsg.setAppid(publicAppid);// 公众号appid
      mpTemplateMsg.setTemplate_id(base.tmplID);// template_id
      mpTemplateMsg.setData(base.getData());// 公众号模板消息的数据
      mpTemplateMsg.setMiniprogram(base.miniprogram);// 公众号模板消息所要跳转的小程序
      mpTemplateMsg.setUrl(null);// 公众号模板消息所要跳转的url

      return reqPo;
  }

  /**
   * 获得access_token
   * 
   * @return
   * @throws Exception
   */
  private String getAccessToken() {
      WechatAccessToken token = new WechatAccessToken(builder);
      return token.getAccessToken();
  }

}

/**
* 下发小程序和公众号统一的服务消息 请求PO
* https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/uniform-message/uniformMessage.send.html
* @author mac
*
*/
public class ReqPo {

  //接口调用凭证
  private String access_token;
  //用户openid,可以是小程序的openid,也可以是mp_template_msg.appid对应的公众号的openid
  private String touser;
  //小程序模板消息相关的信息,可以参考小程序模板消息接口; 有此节点则优先发送小程序模板消息
  private WeappTemplateMsg weapp_template_msg;
  //公众号模板消息相关的信息,可以参考公众号模板消息接口;有此节点并且没有weapp_template_msg节点时,发送公众号模板消息
  private MpTemplateMsg mp_template_msg;
      
}

/**
* 返回值 PO
* @author mac
*
*/
public class RespPo {
  //错误码 0-成功
  private int errcode;
  //错误信息
  private String errmsg;
}
/**
* 小程序模板消息相关的信息
* @author mac
*
*/
public class WeappTemplateMsg {
  
  //小程序模板ID
  private String template_id;
  //小程序页面路径
  private String page;
  //小程序模板消息formid
  private String form_id;
  //小程序模板数据
  private String data;
  //小程序模板放大关键词
  private String emphasis_keyword;
}
/**
* 公众号模板消息相关的信息
* @author mac
*
*/
public class MpTemplateMsg {

  //公众号appid,要求与小程序有绑定且同主体
  private String appid;
  //公众号模板id
  private String template_id;
  //公众号模板消息所要跳转的url
  private String url;
  //公众号模板消息所要跳转的小程序,小程序的必须与公众号具有绑定关系
  private Miniprogram miniprogram;
  //公众号模板消息的数据
  private Map<String, DataValue> data;
}
/**
* 公众号模版消息 - 小程序跳转PO
* @author mac
*
*/
public class Miniprogram {

  private String appid="wx2a8dd229406ae6ae";
  private String pagepath;
}
  • 微信小程序WechatAccessToken生成器
    由于 发送微信统一服务消息 , 需要使用到微信小程序的accessToken, 因此这里加上微信小程序WechatAccessToken生成器
/**
* 小程序access_token
* @author mac
*
*/
public class WechatAccessToken {

  private String accessToken;
  private Date builderTime;
  
   /**
    * 获得 accessToken
    * @return
    */
   public String getAccessToken() {
       return accessToken;
   }
   /**
    * 获得 builderTime
    * @return
    */
   public Date getBuilderTime() {
      return builderTime;
   }
   
   /**
    * 使用 内部构建类 构建
    * @param builder
    * @throws Exception 
    */
   public WechatAccessToken(WechatAccessTokenBuilder builder){
      WechatAccessTokenBuilder build = builder.build();
      if(!StringUtils.isEmpty(build.accessToken)){
          this.accessToken = build.accessToken;
          this.builderTime = build.builderTime;
      }
   }
}
/**
* 微信小程序WechatAccessToken生成器
* @author mac
*
*/
@Component
@SuppressWarnings("deprecation")
public class WechatAccessTokenBuilder {
  private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

  @Value("${wx.applet.appid}")
  String appletAppid;
  @Value("${wx.applet.secret}")
  String appletSecret;
  
  //获取access_token接口
  final String getTokenUrl = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET";
  
  public String accessToken;
  public Date builderTime;
   
   
  /**
   * 生成 WechatAccessToken
   * @return
   * @throws Exception 
   */
  public WechatAccessTokenBuilder build() {
      if(this.accessToken == null || this.builderTime == null){
          this.accessToken = getAccessToken();
          this.builderTime = new Date();
          return this;
      }
      
      //比较时间
      Calendar cal = Calendar.getInstance();   
       cal.setTime(this.builderTime);  
       cal.add(Calendar.HOUR, 1);//往后1小时 
       //cal.add(Calendar.MINUTE, 5);//往后5分钟
       Date outTimeDate = cal.getTime();   
      Date nowDate = new Date();
      
      if(outTimeDate.after(nowDate)){
          return this;
      }else{
          this.accessToken = getAccessToken();
          this.builderTime = new Date();
          return this;
      }
      
   }
   
   /**
   * 获取access_token
   * 
   * @return
   */
  @SuppressWarnings({ "resource"})
  private String getAccessToken() {
      
      // 获取小程序全局唯一后台接口调用凭据 接口 
      String url = getTokenUrl.replace("APPID", appletAppid);
      url = url.replace("APPSECRET", appletSecret);

      HttpGet request = new HttpGet(url);
      HttpResponse response = null;
      AccessTokenRespPo po = null;
      
      try {
          HttpClient client = new DefaultHttpClient();
          response = client.execute(request);
          if (response.getStatusLine().getStatusCode() == HttpStatus.SC_OK) {
              String strResult = EntityUtils.toString(response.getEntity());
              if (!StringUtils.isEmpty(strResult)) {
                  LOGGER.info("WechatAccessTokenBuilder.获取access_token.返回: " + strResult);
                  po = JacksonUtil.defaultInstance().json2pojo(strResult, AccessTokenRespPo.class);
              }
          }
      } catch (IOException e) {
          e.printStackTrace();
          LOGGER.error("获取微信小程序的access_token失败!!!");
      }
      

      if (po != null && !"".equals(po.getAccess_token())) {
          return po.getAccess_token();
      } else {
          return null;
      }
  }

  /**
   * 接口返回参数PO
   * @author mac
   *
   */
  @JsonIgnoreProperties(ignoreUnknown = true)
  public static class AccessTokenRespPo {
      public String access_token;
      public int expires_in;
      public int errcode;
      public String errmsg;
      
      public AccessTokenRespPo(){}
      
      public String getAccess_token() {
          return access_token;
      }
      public void setAccess_token(String access_token) {
          this.access_token = access_token;
      }
      public int getExpires_in() {
          return expires_in;
      }
      public void setExpires_in(int expires_in) {
          this.expires_in = expires_in;
      }
      public int getErrcode() {
          return errcode;
      }
      public void setErrcode(int errcode) {
          this.errcode = errcode;
      }
      public String getErrmsg() {
          return errmsg;
      }
      public void setErrmsg(String errmsg) {
          this.errmsg = errmsg;
      }
      
  }
}
  • 发送模版消息Demo
/**
* 公众号模版类
* @author mac
*
*/
public abstract class TmplBase {
  
  /**
   * 模版id
   */
  public String tmplID;
  /**
   * 模版参数
   */
  public Map<String, DataValue> data;
  
  /**
   * 公总号跳转小程序
   */
  public Miniprogram miniprogram;
  
}
demo:
@Autowired
  WechatMessageUtil wechatMessageUtil;
  //小程序appid
  @Value("${wx.applet.appid}")
  String appletAppid;
  //公众号appid
  @Value("${wx.public.appid}")
  String publicAppid;
  
  @Test
  public void test() throws Exception {
      
      //点击模版消息跳转参数
      Miniprogram miniprogram = new Miniprogram();
      miniprogram.setAppid(appletAppid);//appid需要线上有版本才能匹配对应的路径
      miniprogram.setPagepath("pages/Login");//要跳转的路径+参数
      
      //模版消息
      TestTmpl testTmpl = new TestTmpl(new DataValue("1"), new DataValue("2"), new DataValue("3"), new DataValue("4"), miniprogram);
      
      ReqPo reqPo = wechatMessageUtil.packageReqPo4Customer(testTmpl, publicAppid);
      RespPo uniformSend = wechatMessageUtil.uniformSend(reqPo);
      
      System.out.println(uniformSend);

  }
终于码完字啦,呼~

看到这里,如果对你们有用,请鼓励一下笔者吧
第一次码字, 有错误请见谅~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容