第3方系统扫码绑定微信公众号,附源码!

1.前言

前面我给大家讲了如何通过微信公众号发送模板消息 ,但是那个案例只能做群发消息

但是在实际场景中,有些企业需要给指定用户发送特定消息。比如瑞幸咖啡给你发送了优惠券、顺丰会员等级变更通知、生活缴费通知、快递物流信息等等

那么这时候就需要将微信公众号的用户信息和第三方系统的用户进行绑定。

接下来我给大家讲一下第3方系统扫码绑定微信公众号的核心步骤:

  • 1.第三方系统根据微信官方提供的 api 生成一个二维码
  • 2.用户扫码关注公众号
  • 3.微信公众号根据你配置的自己的服务器地址进行回调,回调信息包含用户操作事件(关注、已关注扫码、取消关注)和 openid
  • 4.将 openid 存到数据库里面,和本地用户信息进行绑定
  • 5.接下来就可以根据某个用户的 openid 和模板 id 发送特定消息了。

2.服务器配置

  • URL 就是你自己公司服务器的接口地址,这里只支持 80 和 443 接口。
  • Token 随便写一个,一定要记住
  • EncodingAESKey 随机生成一个
  • 消息加解密方式建议安全模式,也就是进行加密

在点击提交之前需要在后台提供一个校验接口:校验成功需要原样返回 echostr 参数内容

后台用的SpringBoot 项目:

Controller:

css 复制代码
  @GetMapping(value = "/checkWechat")
  public String checkWechatLink(HttpServletRequest request) {
      String signature = request.getParameter("signature");
      String timestamp = request.getParameter("timestamp");
      String nonce = request.getParameter("nonce");
      String echostr = request.getParameter("echostr");

      if (StringUtils.isEmpty(signature) || StringUtils.isEmpty(timestamp) || StringUtils.isEmpty(nonce)) {
          return "";
      }
      // 验证消息的确来自微信服务器
      wechatService.checkSignature(signature, timestamp, nonce);
      // 原样返回echostr参数
      return echostr;
  }

Service:

java 复制代码
 /**
     * 公众号服务器配置校验
     * @param signature
     * @param timestamp
     * @param nonce
     */
    @Override
    public void checkSignature(String signature, String timestamp, String nonce) {
        String[] arr = new String[] {token, timestamp, nonce};
        Arrays.sort(arr);
        StringBuilder content = new StringBuilder();
        for (String str : arr) {
            content.append(str);
        }
        String tmpStr = DigestUtils.sha1Hex(content.toString());
        if (tmpStr.equals(signature)) {
            log.info("check success");
            return;
        }
        log.error("check fail");
        throw new RuntimeException("check fail");
    }

运行项目之后,服务器配置页面点击提交按钮

注:因为URL只支持80和443端口,所以在本地测试的时候可以使用贝锐花生壳进行内网穿透。

3.后台生成二维码,前端进行显示

3.1 后端

SpringBoot 环境配置:

引入微信官方依赖

java 复制代码
<dependency>
    <groupId>com.github.binarywang</groupId>
    <artifactId>weixin-java-mp</artifactId>
    <version>3.5.0</version>
</dependency>

配置 application.yml

css 复制代码
wechat:
  appId: xxxx
  appSecret: xxxx
  tokenUrl: https://api.weixin.qq.com/cgi-bin/token
  tokenGrantType: client_credential
  templateUrl: https://api.weixin.qq.com/cgi-bin/message/template/send
  userListUrl: https://api.weixin.qq.com/cgi-bin/user/get
  userInfoUrl: https://api.weixin.qq.com/cgi-bin/user/info
  token: xxxx
  aesKey: xxxx
  qrcodeUrl: https://api.weixin.qq.com/cgi-bin/qrcode/create
  showQrcode: https://mp.weixin.qq.com/cgi-bin/showqrcode

获取微信公众号二维码接口:

Controller:

java 复制代码
@GetMapping("/getQrCode")
public Result getUserPage() {
    WechatQrCode qrCode = wechatService.getQrCode();
    return Result.success(qrCode);
}

Service:

java 复制代码
@Override
public WechatQrCode getQrCode() {
    SysUser userInfo = SecurityUtil.getUserInfo();
    JSONObject jsonObject = new JSONObject();
    jsonObject.put("expire_seconds",QR_CODE_TICKET_TIMEOUT);
    jsonObject.put("action_name","QR_STR_SCENE");
    JSONObject sceneObject = new JSONObject();
    JSONObject sceneStrObject = new JSONObject();
    sceneStrObject.put("scene_str",userInfo.getUsername());
    sceneObject.put("scene",sceneStrObject);
    jsonObject.put("action_info",sceneObject);
    // 获取 access_token
    String accessToken = getAccessToken();
    String requestUrl = wechatConfig.getQrcodeUrl()+"?access_token=" + accessToken;
    String result = HttpUtil.createPost(requestUrl).body(jsonObject.toJSONString()).execute().body();
    log.info("getQrCodeResult:{}",result);
    JSONObject jsonResult = JSONObject.parseObject(result);
    // 返回二维码信息
    WechatQrCode wechatQrCode = new WechatQrCode();
    wechatQrCode.setUrl(jsonResult.getString("url"));
    wechatQrCode.setTicket(jsonResult.getString("ticket"));
    wechatQrCode.setExpireSeconds(jsonResult.getLong("expire_seconds"));
    wechatQrCode.setQrCodeUrl(wechatConfig.getShowQrcode()+"?ticket=" +  URI.create(wechatQrCode.getTicket()).toASCIIString());
    // 将二维码存入Redis,map 格式:  key 是 ticket,map:{username:"",openid:""}
    return wechatQrCode;
}

获取 AccessToken 方法:

java 复制代码
    @Override
    public String getAccessToken() {
        // 先判断 Redis 中是否存在
        String wechat_access_token = stringRedisTemplate.opsForValue().get("wechat_access_token");
        if(StrUtil.isBlank(wechat_access_token)){
            String accessTokenUrl  = wechatConfig.getTokenUrl()+"?grant_type=client_credential&";
            String requestUrl = accessTokenUrl + "appid=" + wechatConfig.getAppId() + "&secret=" + wechatConfig.getAppSecret();
            String result = HttpUtil.get(requestUrl);
            log.info("wechat_access_token:{}",result);
            JSONObject jsonObject = JSONObject.parseObject(result);
            wechat_access_token = jsonObject.getString("access_token");
            // 将 access_token 存入 Redis
            stringRedisTemplate.opsForValue().set("wechat_access_token", wechat_access_token, 7000, TimeUnit.SECONDS);
        }
        return  wechat_access_token;
    }

讲解:

  • 1.请求获取微信二维码地址:注意是 POST 请求
ini 复制代码
https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=TOKEN
  • 2.POST数据例子
json 复制代码
{"expire_seconds": 604800, 
"action_name": "QR_STR_SCENE", 
"action_info": 
{"scene": 
{"scene_str": "test"}}}

这里我把用户的编号放进了 scene_str 的参数值里面,当然你也可以放用户的id。

  • 3.返回结果

后端主要就是将 mp.weixin.qq.com/cgi-bin/sho... 和 ticket 拼接成一个二维码图片地址返回给前端:

3.2 前端展示并轮询状态

点击绑定按钮获取公众号二维码地址,通过 ElementUI 的 el-popover 和 el-image 进行展示:

css 复制代码
<span style="font-size: 15px; float: right; margin: 15px 10px"
  >绑定公众号:
  <el-popover
    :visible="bindWechatVisible"
    transition="el-zoom-in-top"
    placement="left-end"
    title="绑定知否技术公众号"
    :width="200"
    trigger="click"
  >
    <template #reference>
      <el-tag
        style="cursor: pointer"
        v-if="!userForm.openId"
        size="small"
        @click="getWechatQrcode"
        >绑定</el-tag
      >
    </template>
    <el-image :src="wechatUrl" style="width: 180px; height: 180px" :fit="fit" />

    <div style="text-align: right; margin: 0">
      <el-button size="small" type="primary" plain @click="getWechatQrcode"
        >刷新</el-button
      >
      <el-button size="small" type="primary" @click="bindWechatVisible = false"
        >关闭</el-button
      >
    </div>
  </el-popover>
  <el-tag v-if="userForm.openId" type="success" size="small">已绑定</el-tag>
</span>

获取二维码接口:

css 复制代码
// 获取微信二维码
const bindWechatVisible = ref(false);
const wechatUrl = ref(null);
const getWechatQrcode = async () => {
  const res = await wechatApi.createQrcode();
  wechatUrl.value = res.data.data.qrCodeUrl;
  bindWechatVisible.value = true;
  // 检验用户绑定公众号状态
  checkBindOpenid();
};

生成二维码之后立即轮询校验用户是否绑定微信公众号,其实就是校验系统数据库是否设置了 openid

css 复制代码
let timer = null;
const checkBindOpenid = () => {
  timer = setInterval(async () => {
    const res = await wechatApi.checkBindOpenid();
    // 如果已经绑定
    if (res.data.data && res.data.data !== "") {
      // 清除轮询
      clearInterval(timer);
      // ......业务逻辑
      bindWechatVisible.value = false;
    }
  }, 1500);
};

后台校验用户绑定公众号接口:就是查询数据库这个用户是否设置 openid

java 复制代码
/**
 * 校验用户是否绑定公众号
 * @return
 */
@GetMapping("/checkBindOpenid")
public Result checkBind() {
    String openid = wechatService.checkBindOpenid();
    return Result.success(openid);
}

4.处理微信公众号的回调信息(核心逻辑)

和公众号服务器配置校验的接口路径一样,只不过多了一个核心的 requestBody 参数,这里面包含详细的回调信息:

java 复制代码
  @PostMapping("/checkWechat")
    public String checkWechat(@RequestBody String requestBody, @RequestParam("signature") String signature,
                              @RequestParam("timestamp") String timestamp, @RequestParam("nonce") String nonce) {
        log.info("requestBody:{}", requestBody);
        log.info("signature:{}", signature);
        log.info("timestamp:{}", timestamp);
        log.info("nonce:{}", nonce);

        // 处理微信服务器推送的消息
        wechatService.handleWechatMsg(requestBody);
        return "success";
    }

处理回调消息核心逻辑:

java 复制代码
    /**
     * 处理微信推送的消息
     * @param body
     * @return
     */
    @Override
    public String handleWechatMsg(String body) {
        WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(body);
        String eventKey = wxMpXmlMessage.getEventKey();
        String openId = wxMpXmlMessage.getFromUser();
        log.info("wxMpXmlMessage:{}",wxMpXmlMessage);
        log.info("openId:{}",openId);
        log.info("eventKey:{}",eventKey);
        log.info("event:{}",wxMpXmlMessage.getEvent());
        // 关注
        if(wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_SUBSCRIBE)){
            log.info("event:{}",wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_SUBSCRIBE));
            // eventKey:qrscene_admin
            String username  = eventKey.split("_")[1];
            // 绑定 openid
            sysUserService.lambdaUpdate().eq(SysUser::getUsername,username)
                    .set(SysUser::getOpenId,openId).update();
        }
        // 已关注,扫描
        if( wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_SCAN)){
            String username  = eventKey;
            SysUser currentUser = sysUserService.lambdaQuery().eq(SysUser::getUsername, username)
                    .one();
            if(StrUtil.isBlank(currentUser.getOpenId())){
                sysUserService.lambdaUpdate().eq(SysUser::getUsername,username)
                        .set(SysUser::getOpenId,openId).update();
            }
            return "success";
        }
        // 取消关注
        if(wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_UNSUBSCRIBE) ){
            log.info("event:{}",wxMpXmlMessage.getEvent().equals(WechatMsgUtil.EVENT_UNSUBSCRIBE));
            // 取消绑定 openid
            sysUserService.lambdaUpdate().eq(SysUser::getOpenId,openId)
                    .set(SysUser::getOpenId, null).update();
        }

        return XmlUtil.toStr( XmlUtil.beanToXml(wxMpXmlMessage));
    }

body 参数是 xml 格式的,所以需要用官方提供的接口进行解析:

java 复制代码
  WxMpXmlMessage wxMpXmlMessage = WxMpXmlMessage.fromXml(body);

返回的数据格式有3种:

  • 关注 subscribe
json 复制代码
{EventKey":"qrscene_admin","Event":"subscribe","FromUserName":"xxxxxxx"}
  • 已关注,扫码 SCAN
json 复制代码
{"EventKey":"admin","Event":"SCAN","ToUserName":"xxxxx","FromUserName":"xxxxx"}
  • 取消关注 unsubscribe
json 复制代码
{"event":"unsubscribe","eventKey":"","fromUser":"xxxx"}

其中最重要的 openid

css 复制代码
  String openId = wxMpXmlMessage.getFromUser();
  • 当 event 是 subscribe 时,eventKey 格式是 qrscene_admin,下划线后面就是前面我们设置的用户编号。

  • 当 event 是 SCAN 时,eventKey 就是 admin ,用户编号。

  • 当 event 是 unsubscribe 时,只返回取消用户的 openid,我们可以将自己系统数据库的用户 openid 设置为 null。

5.给特定用户发送模板消息

在前面文章中已经讲过发送模板消息,这里不再贴完整代码。

java 复制代码
JSONObject sendGoodsObject = new JSONObject();
// 用户名
sendGoodsObject.put("thing5", JSONUtil.createObj().putOpt("value", filterUser.getName()));
// 金额
sendGoodsObject.put("amount3", JSONUtil.createObj().putOpt("value", amount));
// 当前余额
sendGoodsObject.put("amount4", JSONUtil.createObj().putOpt("value", balance));
// 日期
sendGoodsObject.put("time1", JSONUtil.createObj().putOpt("value", date));

JSONObject  jsonWechatResult =  wechatService.sendWechatMessage(returnFundObject,user.getOpenId(),"xxxxxxxxxx","https://www.baidu.com");

// 如果微信公众号发送成功
if("ok".equals(jsonWechatResult.get("errmsg"))){
    // 成功之后保存发送的信息
    TextMessage textMessage = new TextMessage();
    textMessage.setTel(tel);
    textMessage.setType(type);
    textMessage.setAmount(amount);
    textMessage.setBalance(balance);
    textMessage.setDealerName(dealerName);
    textMessage.setManager(manager);
    textMessage.setSendTime(date);
    // 保存发送的信息
    textMessageService.save(textMessage);
}
相关推荐
寻月隐君5 分钟前
Web3实战:Solana CPI全解析,从Anchor封装到PDA转账
后端·web3·github
程序员小假6 分钟前
说一说 SpringBoot 中 CommandLineRunner
java·后端
sky_ph15 分钟前
JAVA-GC浅析(一)
java·后端
LaoZhangAI19 分钟前
Claude Code完全指南:2025年最强AI编程助手深度评测
前端·后端
LaoZhangAI23 分钟前
FLUX.1 Kontext vs GPT-4o图像编辑全面对比:2025年最全评测指南
前端·后端
LaoZhangAI24 分钟前
2025最全Supabase MCP使用指南:一键连接AI助手与数据库【实战教程】
前端·javascript·后端
天天摸鱼的java工程师30 分钟前
@Autowired 注入失效?
java·后端
随缘而动,随遇而安1 小时前
第七十四篇 高并发场景下的Java并发容器:用生活案例讲透技术原理
java·大数据·后端
汪子熙1 小时前
Cursor 中代码库索引(codebase indexing)功能背后的核心技术实现原理
人工智能·后端
weixin_436525071 小时前
Spring Boot 实现流式响应(兼容 2.7.x)
java·spring boot·后端