微信小程序获取用户openId并通过服务端向用户发送模板消息

news2024/9/20 18:28:36

1.引言

注意:

1.标题中的服务端是自己研发的服务端,不是腾讯公司的服务端。

2.小程序的模板消息分为一次性订阅消息与长期订阅,一次性订阅就是每次在给用户发送消息之前都需要获得用户的同意(即用户订阅),长期性订阅是只需要用户同意一次,长期性订阅需要的小程序的分类为腾讯规定的服务种类(金融,公共服务,政务服务等),要求比较严格。本文所描述的为长期性订阅服务,默认用户已经订阅了此模板消息的通知。

3.此服务通知是代替短信的功能,是一个消息通知的形式,发短信是需要钱的,而服务通知不需要。

说说本篇文章诞生的业务环境,会议室预定系统,包含服务端,技术栈主要为Spring Boot+Spring Security+Spring AlibabaCloud+Nacos+MyBatis Plus+MySql;Web端,技术栈使用Vue3+Element UI;移动端,技术栈为uni-app+ts,用户通过用户名密码在Web端或者移动端登录进入系统后,进入会议室预定界面,填写会议室预定所需信息,保存后提交到服务端,服务端验证信息通过后,通过微信小程序的模板消息,调用腾讯开放的服务接口,通知对应的人员。

给对应人员发送服务通知,必须知道此人员在小程序中的唯一身份标识,如果是在小程序中,可以通过微信的wx.pluginLogin接口获取,如果在Web端中,如何通知了?

因为用户的openId在小程序中是唯一不变的,所以我们可以在用户使用小程序端输入用户名密码调用服务端登录接口时,获取其openId,并存储到用户信息表中,那么理论上就可以给系统中的任何用户发送模板消息的通知。所以,要给用户发送此订阅消息,用户必须登录过小程序,否则不能实现。

2.获取用户的openId并存储

在小程序中,通过wx.login获取code,此code连同用户名密码一起传递到服务端,服务端验证用户名密码后,调用小程序登录接口,拿到返回的openId并存储。

2.1 小程序端登录

小程序端登录先获取code,然后调用后端的登录接口。代码中区分了H5与小程序,H5是不需要调用腾讯的wx.login的。

1.小程序端关键代码
// 登录系统的form
const loginForm = reactive<WXProgramLoginReqVO>({
  username: '',
  password: '',
  clientId: import.meta.env.VITE_CLIENT_ID,
  code: '',
})

const formRef = ref() // 表单
// 登录系统 一进系统就需要登录
const handleLogin = async () => {
  // #ifdef MP-WEIXIN
  const res = await wx.login()
  console.log(res.code)
  loginForm.code = res.code
  // #endif
  // 校验表单
  formRef.value
    .validate()
    .then(async () => {
      let loginRes = {
        accessToken: '',
        refreshToken: '',
      }
      // #ifdef H5
      loginRes = await loginApi.login(loginForm)
      // #endif
      // #ifdef MP-WEIXIN
      loginRes = await loginApi.wxProgramLogin(loginForm)
      // #endif
      setAccessToken(loginRes.accessToken)
      setRefreshToken(loginRes.refreshToken)
      // 设置用户信息
      const userInfoRes = await permissionApi.getUserPermissionInfo()
      userStore.setUserInfo(userInfoRes)

      // 设置字典信息
      dictStore.setDictMap()
      // 直接跳转到首页
      uni.switchTab({ url: '/pages/index/index' })
    })
    .catch((err) => {
      console.log('登录表单错误信息:', err)
    })
}
/** 用户登录 h5 */
export const login = (data: LoginReqVO) => {
  return http.post({ url: '/auth/login', data })
}

/** 用户登录 微信小程序 携带一个微信登录时返回的code */
export const wxProgramLogin = (data: WXProgramLoginReqVO) => {
  return http.post({ url: '/auth/wx-program/login', data })
}

代码中,H5与小程序端调用的登录接口为两个,其实可以简化为一个接口,后端通过有没有code的值来判断是不是需要调用腾讯的登录接口。

2.服务端关键代码

服务端接受到请求后,首先验证用户名密码是否正确,验证通过后,调用腾讯的小程序登录接口,获取对应的openId,并存储。

public LoginRespVO wxLogin(WXProgramLoginReqVO reqVO) {
        // 验证用户名密码
        UserResponseDTO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
        if (user.getOpenId() == null){
            // 说明是第一次登录 需要更新其openId
            // 登录微信 获取 openid
            WxProgramLoginResDTO res = wxApi.wxProgramLogin(reqVO.getCode()).getCheckedData();
            String openId = res.getOpenid();
            // 更新用户的openId
            userApi.updateUserOpenId(reqVO.getUsername(), openId);
        }
        //创建token
        return createTokenAfterLoginSuccess(user.getId(), user.getNickname(),
                reqVO.getUsername(),  reqVO.getClientId(), LoginLogTypeEnum.LOGIN_USERNAME);
    }



    private UserResponseDTO authenticate(String adminName, String password) {
        // 校验账号是否存在
        UserResponseDTO user = userApi.getUserByLoginInfo(adminName, password).getCheckedData();
        if (user == null) {
            throw exception(AUTH_LOGIN_BAD_CREDENTIALS);
        }
        return user;
    }
public WxProgramLoginResDTO wxProgramLogin(String code) {
        WxProgramLoginResVO wxProgramLoginRes = wxProgramWebClient.loginByCode(code).getCheckedData();
        WxProgramLoginResDTO dto = new WxProgramLoginResDTO();
        dto.setOpenid(wxProgramLoginRes.getOpenid());
        return dto;
    }
public BaseResponse<WxProgramLoginResVO> loginByCode(String code){
        String res = webClient.buildWebClient()
                .get()
                .uri(wxProgramIdentityProperties.getUrl() + "/sns/jscode2session"
                        + "?grant_type=authorization_code"
                        + "&appid=" + wxProgramIdentityProperties.getAppId()
                        + "&secret=" + wxProgramIdentityProperties.getSecret()
                        + "&js_code=" + code)
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .exchangeToMono(response -> response.bodyToMono(String.class))
                .block();
        BaseResponse<WxProgramLoginResVO> stringBaseResponse = parseResponse(res, WxProgramLoginResVO.class);
        if (stringBaseResponse.isError()){
            log.error("登录失败:{}", stringBaseResponse.getMessage());
        }
        return stringBaseResponse;
    }

3.获取接口调用凭据

获取小程序全局唯一后台接口调用凭据,token有效期为7200s。在调用一切小程序端接口时,都需要使用此凭据,此凭据两小时之内都有效,两小时后需要重新申请。

暂时想法为,接口调用时,获取access_token,获取后判断是否过期,如果过期,重新申请access_token,并更新存储,返回最新的获取access_token。

public AccessTokenDO getAccessToken() {
        AccessTokenDO accessTokenDO = accessTokenService.getAccessTokenByCode(TOKEN_CODE);
        if (accessTokenDO == null){
            // 第一次 需要创建
            accessTokenDO = createToken();
        }
        // 判断accessToken是否过期 如果过期 需要更新
        if (DateUtils.isExpired(accessTokenDO.getExpireTime())) {
            WxProgramGetAccessTokenResVO accessTokenRes = wxProgramWebClient.getAccessToken().getCheckedData();
            accessTokenDO.setValue(accessTokenRes.getAccess_token());
            // 提前5分钟过期 然后刷新token
            accessTokenDO.setExpireTime(LocalDateTime.now().plusSeconds(Integer.parseInt(accessTokenRes.getExpires_in()) - 300));
        }
        // 更新
        updateToken(accessTokenDO);
        return accessTokenDO;
    }
public BaseResponse<WxProgramGetAccessTokenResVO> getAccessToken(){
        String res = webClient.buildWebClient()
                .get()
                .uri(wxProgramIdentityProperties.getUrl() + "/cgi-bin/token"
                        + "?grant_type=client_credential"
                        + "&appid=" + wxProgramIdentityProperties.getAppId()
                        + "&secret=" + wxProgramIdentityProperties.getSecret())
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .exchangeToMono(response -> response.bodyToMono(String.class))
                .block();
        BaseResponse<WxProgramGetAccessTokenResVO> stringBaseResponse = parseResponse(res, WxProgramGetAccessTokenResVO.class);
        if (stringBaseResponse.isError()){
            log.error("获取token失败:{}", stringBaseResponse.getMessage());
        }
        return stringBaseResponse;
    }

4.发送订阅模板消息

首先在小程序基础功能中的订阅消息中申请一个模板,获取此模板的ID以及模板的详细内容。

根据模板内容,调用发送订阅消息接口,把对应的参数以及内容的body传递过去。

public void sendBookingMeetingMsg(WxSendBookingMeetingMsgReqDTO meetingMsgReqDTO) {
        WxProgramSendTemplateMsgReqVO reqVO = new WxProgramSendTemplateMsgReqVO();
        MessageTemplateDO messageTemplate = messageTemplateService.getMessageTemplateByCode(OFFICE_TEMPLATE_CODE);
        reqVO.setTemplate_id(messageTemplate.getTemplateId());
        reqVO.setPage(messageTemplate.getPage());
        reqVO.setTouser(meetingMsgReqDTO.getOpenid());
        reqVO.setMiniprogram_state("developer");
        reqVO.setLang("zh_CN");
        // 构造data
        // 会议内容
        JSONObject meetingName = new JSONObject();
        meetingName.putOpt("value", meetingMsgReqDTO.getMeetingName());
        // 会议室
        JSONObject meetingRoom = new JSONObject();
        meetingRoom.putOpt("value", meetingMsgReqDTO.getMeetingRoomName());
        // 会议时间
        JSONObject meetingTime = new JSONObject();
        meetingTime.putOpt("value", meetingMsgReqDTO.getMeetingTime());
        // 会议开始时间
        JSONObject meetingStartTime = new JSONObject();
        meetingStartTime.putOpt("value", meetingMsgReqDTO.getMeetingStartTime());
        // 会议申请人
        JSONObject meetingApplyUserName = new JSONObject();
        meetingApplyUserName.putOpt("value", meetingMsgReqDTO.getMeetingApplyUserName());

        JSONObject data = new JSONObject();
        data.putOpt("thing4", meetingName);
        data.putOpt("thing1", meetingRoom);
        data.putOpt("character_string2", meetingTime);
        data.putOpt("time6", meetingStartTime);
        data.putOpt("thing3", meetingApplyUserName);

        reqVO.setData(data);

        BaseResponse<WxProgramSendTemplateMsgResVO> templateMsgRes
                = wxProgramWebClient.sendTemplateMsg(getAccessToken().getValue(), reqVO);
        log.info(templateMsgRes.getCheckedData().toString());
    }
/**
     * 发送消息模板
     * @param accessToken 凭据
     * @param wxProgramSendTemplateMsgReqVO 消息内容
     * @return 是否成功
     */
    public BaseResponse<WxProgramSendTemplateMsgResVO> sendTemplateMsg(String accessToken,
                                                                       WxProgramSendTemplateMsgReqVO wxProgramSendTemplateMsgReqVO){
        String res = webClient.buildWebClient()
                .post()
                .uri(wxProgramIdentityProperties.getUrl() + "/cgi-bin/message/subscribe/send"
                        + "?access_token=" + accessToken)
                .bodyValue(wxProgramSendTemplateMsgReqVO)
                .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .exchangeToMono(response -> response.bodyToMono(String.class))
                .block();
        BaseResponse<WxProgramSendTemplateMsgResVO> stringBaseResponse = parseResponse(res, WxProgramSendTemplateMsgResVO.class);
        if (stringBaseResponse.isError()){
            log.error("登录失败:{}", stringBaseResponse.getMessage());
        }
        return stringBaseResponse;
    }

5.错误调试

在调用发送订阅消息接口时,总不是一帆风顺的,期间遇到了很多错误,错误通过返回的错误码可以看出一个大概,错误具体如何造成的,可以调用对应的接口来获取具体的信息。

6.写在最后

本篇文章写了使用小程序的订阅模板消息,给对应用户发送服务通知。本人设想是利用此服务通知作为一个消息的承接载体,替代短信通知,节省成本。

本篇文章的代码只给了一些片段,可能有些地方看起来有些吃力,如果有需要解释或者有指教的地方,欢迎留言或者私信,感谢大家的支持。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2073203.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构(树、平衡树、红黑树)

目录 树 树的遍历方式 平衡二叉树 旋转机制 左旋 右旋 旋转实例 左左 左右 右右 右左 总结 红黑树 树 相关概念 节点的内部结构如下 二叉树与二叉搜索树的定义 树的遍历方式 前序遍历&#xff1a;当前节点&#xff0c;左子节点&#xff0c;右子结点 中序遍历&a…

React学习day01-React-开发环境配置、JSX基础-本质、JSX中js表达式的用法、JSX的条件渲染

1、React &#xff08;1&#xff09;概念&#xff1a;由Meta公司研发&#xff0c;是一个用于构建Web和原生交互页面的库 &#xff08;2&#xff09;优点&#xff1a; 1&#xff09;相较于传统基于DOM开发的优势&#xff1a;组件化的开发方式、不错的性能 2&#xff09;相较于…

软件设计原则之单一职责原则

目录 单一职责原则单一职责原则的主要特点应用范围Demo用户信息日志记录 单一职责原则 单一职责原则&#xff08;Single Responsibility Principle&#xff0c;简称SRP&#xff09;是面向对象设计中的一个重要原则&#xff0c;其核心思想是&#xff1a;一个类应该仅有一个引起…

ollma 本地部署大模型

因为我本地是 windows 的系统&#xff0c;所以这里直接写的是通过 docker 来实现本地大模型的部署。 windows 下 WSl 的安装这里就不做重复&#xff0c;详见 windows 部署 mindspore GPU 开发环境&#xff08;WSL&#xff09; 一、Docker 部署 ollma 1. 拉取镜像&#xff08;…

Ubuntu系统设置Java项目开机自启

1、创建自启动脚 sudo vi /etc/systemd/system/java-service.service 2、编辑自启脚本 [Unit]部分包含了service的描述和依赖关系。在这个示例中&#xff0c;我们将其设置为在系统启动后执行。 [Service]部分定义了service的执行方式。在这个示例中&#xff0c;我们指定了Java…

shell工具箱集合!!

shell工具箱集合 1.shell工具箱集合 2.Chrony 时间同步 3.Get_host_Info 设备信息收集 4.Init_host 系统初始化 5.Iperf 带宽测试套件 6.Lagscope_test 时延测试套件 7.Mtr_test 双向路由探测套件 下载地址&#xff1a; https://pan.quark.cn/s/6936cc13bc04

学习笔记——Redis基础

文章目录 Redis五种常用数据类型Redis常用命令Spring Data Redis使用方式操作步骤 Redis五种常用数据类型 Redis存储的是key-values结构的数据&#xff0c;其中key是字符串类型&#xff0c;value有五种常用的数据类型&#xff1a; 字符串&#xff08;string&#xff09;&…

C++入门基础知识32——【关于C++ 存储类之auto存储类】

成长路上不孤单&#x1f60a;【14后&#xff0c;C爱好者&#xff0c;持续分享所学&#xff0c;如有需要欢迎收藏转发&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#x1f60a;&#xff01;&#xff01;&#xff01;&#xff01;&#xff…

Flex的基本使用+综合案例

组成 弹性盒子没有设置高&#xff0c;就会自动拉伸 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport&q…

高并发下阻塞队列的选择

高并发下阻塞队列的选择 一、队列 队列&#xff1a;queue。简称队&#xff0c;它和堆栈一样&#xff0c;也是一种运算受限的线性表&#xff0c;其限制是仅允许在表的一端进行插入&#xff0c;而在表的另一端进行删除。 简单的说&#xff0c;采用该结构的集合&#xff0c;对元素…

洛谷 P2569 [SCOI2010] 股票交易

题目来源于&#xff1a;洛谷 题目本质&#xff1a;动态规划&#xff0c;单调队列 解题思路&#xff1a; 方程f[i][j]表示第 i 天结束后&#xff0c;手里剩下 j 股的最大利润&#xff0c;则不买不卖&#xff1a;f[i][j]f[i-1][j]。 买入&#xff1a;f[i][j]max{f[i-w-1][k]k*…

Spring DI 数据类型——构造注入

首先新建项目&#xff0c;可参考 初识 IDEA 、模拟三层--控制层、业务层和数据访问层 一、spring 环境搭建 &#xff08;一&#xff09;pom.xml 导相关坐标 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.…

【Kubernetes】K8s 持久化存储方式

K8s 持久化存储方式 1.使用节点数据卷2.使用网络数据卷3.使用临时数据卷 由于容器是一种无状态的服务&#xff0c;所以容器中的文件在宿主机上表现出来的都是临时存放&#xff08;当容器崩溃或者重启时&#xff0c;容器中的文件会丢失&#xff09;。另外&#xff0c;Kubernetes…

C++领进门(第一讲)

目录 1. C关键字&#xff08;C98&#xff09; 2. 命名空间 ​编辑 2.1命名空间的定义 2.2命名空间的使用 3.C的输入&输出 3.1cout与printf的区别 4.缺省参数 4.1缺省函数的概念 4.2缺省参数分类 5.函数重载 C的语法就是在C的基础上弥补了C的缺陷与不足 1. C关键…

Java集合框架(三)---Map

接口Map<K,V> Map集合&#xff1a;该集合存储键值对&#xff0c;一对一对往里存&#xff0c;而且要保证键的唯一性。 1&#xff0c;添加 put(K key, V value) putAll(Map<? extends K, ? extends V> m) 2&#xff0c;删除 clear() remove(Object key) 3&#xff…

【鸿蒙学习】HarmonyOS应用开发者高级认证 - 应用DFX能力介绍(含闯关习题)

学完时间&#xff1a;2024年8月24日 学完排名&#xff1a;第1698名 一、Performance Analysis Kit简介 Performance Analysis Kit&#xff08;性能分析服务&#xff09;为开发者提供应用事件、日志、跟踪分析工具&#xff0c;可观测应用运行时状态&#xff0c;用于行为分析、…

游戏分享网站|基于SprinBoot+vue的游戏分享网站系统(源码+数据库+文档)

游戏分享网站 目录 基于SprinBootvue的游戏分享网站 一、前言 二、系统设计 三、系统功能设计 5.1系统功能模块 5.2后台登录 5.2.1管理员功能模块 5.2.2用户功能模块 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设选题推荐 八、源码获取&#x…

kaggle竞赛宝典 | 量化竞赛第一名的网络模型

本文来源公众号“kaggle竞赛宝典”&#xff0c;仅用于学术分享&#xff0c;侵权删&#xff0c;干货满满。 原文链接&#xff1a;量化竞赛第一名的网络模型 1 简介 今天我们重温Jane Street 大赛第一名的网络模型。该次赛事数据集包含了一组匿名的特征&#xff0c;feature_{0…

2014年4月-2023年上市公司秩鼎ESG评级数据

2014年4月-2023年上市公司秩鼎ESG评级数据 1、时间&#xff1a;2014年4月-2023年11月 2、来源:秩鼎数据 3、指标&#xff1a;证券代码、SC、评级日期、ESG评级、ESG等级、ESG得分、E评级、E等级、E得分、S评级、S等级、S得分、G评级、G等级、G得分、总市值(亿元)、流通市值(…

企业微信聊天记录可以保存多久?员工聊天记录查看指南!合规存档,助力企业规避风险!

在数字化办公的浪潮中&#xff0c;企业微信已成为企业沟通协作的重要工具。然而&#xff0c;聊天记录的保存时长与合规性管理&#xff0c;成为企业不可忽视的问题。 企业微信聊天记录云端最长可保存90天&#xff0c;但企业可根据需求自定义设置。本文将为您详细解析企业微信聊…