当前位置: 首页 > news >正文

手把手教你给女朋友编写一个公众号定时推送(java版本)

2022-08-15 开通微信云托管

  • 在公众号推送的云服务器选择上,我选择的是微信云托管。

在这里插入图片描述

  • 扫码登陆后,如果是第一次注册试用,没有环境,可以选择自己擅长的语言进行一键部署模板。此处我选择的是SpringBoot作为我的服务后端。

在这里插入图片描述

  • 随后一路点击,等待它部署完成就可以了。出现“部署成功”四个字后,通过公网域名访问demo示例,如果能正常访问就表明搭建成功。部署的同时也会为你启用云mysql服务。如果不需要使用微信云托管的mysql服务,可以在后面前往控制台,关闭数据库服务。如果不是通过控制台部署模板代码,而是通过复制/下载模板代码后,手动新建一个服务并部署,需要在「服务设置」中补全环境变量,才可正常使用,否则会引发无法连接数据库,进而导致部署失败。

在这里插入图片描述

2022-08-15 配置代码仓库

  • 微信云托管给我们提供了免运维服务,我们只需要配置流水线仓库,完成代码提交后,自动拉取仓库最新代码帮我们部署到服务器上。因此在我们专注开发之前,需要先配置我们的代码储存仓库。关于代码仓库的选择可以根据自己的需求自定,我选择的是gitee进行储存。
  • 采用模板开发的话,需要我们把模板代码拷贝到我们的仓库。拷贝完成后,在服务-服务设置-流水线中,配置代码仓库,选择指定的代码源、仓库、分支,选中推送到master时触发流水线(如果是有其它分支也可以选择其他分支)。
    在这里插入图片描述
  • 至此我们就完成了开发环境的准备,准备可以开始敲代码啦!官方参考教程

2022-08-15 申请公众号

  • 由于我要完成的功能是公众号的定时推送,因此我需要有一个公众号。但是模板推送功能需要企业验证才可以使用,获取难度高,所以我选择了申请测试号,除了公众号名字,头像等内容是无法自定义的,在功能上都是一样的。微信公众测试号申请。

  • 申请完成后可以得到自己测试号的appId,appsecret信息。这两个信息主要用于获取公众号全局调用的唯一凭证Access_Token。怎么获取我们后面再说。
    在这里插入图片描述

  • URL(即:你的公网访问域名+你的服务器对应的接口Url)代表的是你服务器对应的校验接口,Token是自己定义的字符串。
    在这里插入图片描述

  • 具体校验操作看如下

2022-08-15 校验服务器合规性

  • 微信为了校验我们服务器的合法性,需要向我们服务器发起一个GET请求,若确认此次 GET 请求来自微信服务器,则原样返回 echostr 参数内容,则接入生效,成为开发者成功,否则接入失败
  • 微信发送的get请求参数有4个
signaturetimestampnonceechostr
微信加密签名,signature结合了开发者填写的 token 参数和请求中的 timestamp 参数、nonce参数时间戳随机数随机字符串
  • 工具类SHA1
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);
		}
	}
}
  • 异常类

```java
package com.tencent.wxcloudrun.utils;

@SuppressWarnings("serial")
public class AesException extends Exception {

	public final static int OK = 0;
	public final static int ValidateSignatureError = -40001;
	public final static int ParseXmlError = -40002;
	public final static int ComputeSignatureError = -40003;
	public final static int IllegalAesKey = -40004;
	public final static int ValidateAppidError = -40005;
	public final static int EncryptAESError = -40006;
	public final static int DecryptAESError = -40007;
	public final static int IllegalBuffer = -40008;
	//public final static int EncodeBase64Error = -40009;
	//public final static int DecodeBase64Error = -40010;
	//public final static int GenReturnXmlError = -40011;

	private int code;

	private static String getMessage(int code) {
		switch (code) {
		case ValidateSignatureError:
			return "签名验证错误";
		case ParseXmlError:
			return "xml解析失败";
		case ComputeSignatureError:
			return "sha加密生成签名失败";
		case IllegalAesKey:
			return "SymmetricKey非法";
		case ValidateAppidError:
			return "appid校验失败";
		case EncryptAESError:
			return "aes加密失败";
		case DecryptAESError:
			return "aes解密失败";
		case IllegalBuffer:
			return "解密后得到的buffer非法";
//		case EncodeBase64Error:
//			return "base64加密错误";
//		case DecodeBase64Error:
//			return "base64解密错误";
//		case GenReturnXmlError:
//			return "xml生成失败";
		default:
			return null; // cannot be
		}
	}

	public int getCode() {
		return code;
	}

	AesException(int code) {
		super(getMessage(code));
		this.code = code;
	}

}

  • 校验接口
/**
     * 用于校验服务器是否合规,此处校验方式可以根据自己选择进行加密算法的选择
     * 理论上此处可以省略工具类的校验,直接返回请求中的参数。
     * 但是笔者直接返回却报错,不知道原因为何,看到这里的小伙伴可以尝试一下直接返回,即:return request.getEchostr();
     * @return String
     */
    @GetMapping(value = "/checkToken")
    public String checkToken(VerifyRequest request) {
        //配置中自己填写的Token
        String token = "网页端你填写的token";
        String sha1 = "";
        try {
            sha1 = SHA1.getSHA1(token, request.getTimestamp(), request.getNonce(), "");
        } catch (AesException e) {
            e.printStackTrace();
        }
        System.out.println("加密:"+sha1);
        System.out.println("本身:"+request.getSignature());
        //如果校验成功,则返回请求中的echostr参数。
       
        if(sha1.equals(request.getSignature())){
            return request.getEchostr();
        }
        else {
            return "非法访问";
        }
    }

2022-08-16 编写推送模板,获取关注的用户ID

  • 模板推送接口文档

  • 在我们的测试公众号平台上,我们可以配置我们的推送模板。其中模板参数统一格式为{{你的返回值.DATA}},如下所示。
    在这里插入图片描述

  • 通过扫描二维码,邀请需要推送的朋友,获取他们的OPENID
    在这里插入图片描述

  • 编写完模板,获取用户ID后,我们就可以调用微信公众号的模板推送接口,进行推送啦。以下为一个标准的POST请求格式。

//请求URL:https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=ACCESS_TOKEN(此处的ACCESS_TOKEN的获取会在后面进行补充)
//请求体
{
    //关注你公众号的用户ID
    "touser":"OPENID",
    //你创建的模板ID
    "template_id":"ngqIpbwh8bUfcSsECmogfXcV14J0tQlEpBO27izEYtY",
    //点击消息模板跳转的地址
    "url":"http://weixin.qq.com/download",
    //顶色
    "topcolor":"#FF0000",
    //相应的具体数据,此处的User,Date,Type就是你模板里面对应的{{XXX.DATA}},如{{User.DATA}},{{Date.DATA}}
    "data":{
            "User": {
                "value":"黄先生",
                "color":"#173177"
            },
            "Date":{
                "value":"06月07日 19时24分",
                "color":"#173177"
            },
            "CardNumber": {
                "value":"0426",
                "color":"#173177"
            },
            "Type":{
                "value":"消费",
                "color":"#173177"
            },

            "Money":{
                "value":"人民币260.00元",
                "color":"#173177"
            },

            "DeadTime":{
                "value":"06月07日19时24分",
                "color":"#173177"
            },

            "Left":{
                "value":"6504.09",
                "color":"#173177"
            }
    }
}

2022-08-16 获取全局调用唯一凭证ACCESS_TOKEN

  • access_token是公众号的全局唯一接口调用凭据,公众号调用各接口时都需使用access_token。关于Access_Token的说明请参考Access token

  • 此处获取Access_Token的方法也很简单,直接调用接口即可:https://api.weixin.qq.com/cgi-bin/token?appid=APPID&secret=APPSECRET。其中appId以及appSecret来自于测试号信息。
    在这里插入图片描述

  • 获取Access_Token工具类

/**
 * 获取微信token工具类
 */
@Component
public class WeChetAccessToken {

	//http请求类
    @Autowired
    private RestTemplate restTemplate;

    //微信配置类文件
    @Autowired
    private WxMpProperties wxMpProperties;

    public String getToken() {
    	//如果缓存中的Token过期,则请求获取Token
        if (WxChatCache.AccessToken.expiration <= System.currentTimeMillis()) {
        	//URL:https://api.weixin.qq.com/cgi-bin/token?appid=APPID&secret=APPSECRET
            String url = WxChatConstant.Url.ACCESS_TOKEN_URL
                    .replace("APPID", wxMpProperties.getAppId())
                    .replace("APPSECRET", wxMpProperties.getSecret());
            //使用restTemplate发起Http请求
            ResponseEntity<String> forEntity = restTemplate.getForEntity(url, String.class);
            JSONObject jsonObject = JSON.parseObject(forEntity.getBody());
            Object errcode = jsonObject.get("errcode");
            if (errcode != null && "40013".equals(errcode.toString())) {
                System.out.println("不合法的APPID");
            }
            // expiration:为当前执行到此处的时间+2小时有效时间为过期时间
            WxChatCache.AccessToken.token = jsonObject.get("access_token").toString();
            WxChatCache.AccessToken.expiration = System.currentTimeMillis()+7200000;
            return WxChatCache.AccessToken.token;
        }
        //如果没过期则使用缓存中的Token
        else {
            System.out.println("返回缓存中的token:"+ WxChatCache.AccessToken.token);
            return WxChatCache.AccessToken.token;
        }
    }
}
  • restTemplate配置文件
/**
 * RestTemplate工具类,主要用来提供RestTemplate对象
 */
@Configuration//加上这个注解作用,可以被Spring扫描
public class RestTemplateConfig {
    /**
     * 创建RestTemplate对象,将RestTemplate对象的生命周期的管理交给Spring----踩坑一,编码问题
     */
    @Bean
    public RestTemplate restTemplate(){
        RestTemplate restTemplate = new RestTemplate();
        //设置中文乱码问题方式一
        //restTemplate.getMessageConverters().add(1,new StringHttpMessageConverter(Charset.forName("UTF-8")));
        // 设置中文乱码问题方式二
        restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        return restTemplate;
    }
}
  • 缓存类
public class WxChatCache {

    /**
     * 微信 accessToken缓存
     */
    public static class AccessToken {
        public static String token = null;   // accessToken
        public static Long expiration = 0L;  // accessToken 过期时间(获取的token 默认有效期2小时)
    }
}

2022-08-16 修改云托管容器时间为上海时区

  • 由于我们需要实现定时发送功能,因此我们必须获取系统的时间。容器系统时间默认为 UTC 协调世界时间 (Universal Time Coordinated),与本地所属时区 CST (上海时间)相差 8 个小时。因此我们需要修改容器时间。
  • 修改方法也很简单,只需要在项目的dockerfile中,将上海时区的注释打开即可。
# 容器默认时区为UTC,如需使用上海时间请启用以下时区设置命令
RUN apk add tzdata && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezone

2022-08-17 开启令牌调用

  • 此处配置需要调用的微信的接口,允许我们服务器请求。相关说明

在这里插入图片描述

2022-08-17 实现定时推送

  • 在经过上面的步骤准备即可开始实现定时推送啦。以下展示一个完整的demo示例。

  • 启动类

@SpringBootApplication
@MapperScan(basePackages = {"com.tencent.wxcloudrun.dao"})
//开启定时任务
@EnableScheduling
public class WxCloudRunApplication {  

  public static void main(String[] args) {
    SpringApplication.run(WxCloudRunApplication.class, args);
  }
}
  • Controller层
@RestController
public class GirlFriendController {

	//上下文,用于策略模式获取对应策略
    @Autowired
    private ApplicationContext applicationContext;
	
	//表示每个月星期一到星期五下午4点50分执行
    @Scheduled(cron = "0 50 16 ? * MON-FRI")
    public void sendOffWork() throws ExecutionException, InterruptedException {
        System.out.println("开始发送下班提醒");
        //参数一发送类型,参数二是推送的对象OpenId
        SendTypeRequest sendTypeRequest = new SendTypeRequest("OffWorkSend","推送的对象OpenId");
        WxChatService chatService = applicationContext.getBean(sendTypeRequest.getType(),WxChatService.class);
        chatService.sendTest(sendTypeRequest);
    }
}
  • Service层
public interface WxChatService {
    /**
     * 向一个用户推送消息(测试)
     * @param
     */
    void sendTest(SendTypeRequest send) throws ExecutionException, InterruptedException;
}
  • Service层实现类
/**
 * 下班推送策略
 * */
@Service("OffWorkSend")
public class OffWorkSend implements WxChatService {

	@Autowired
    protected WxSendMessageUtils wxSendMessageUtils;

    @Override
    public void sendTest(SendTypeRequest send) {

        // 下班模板Id
        String templateId = "你的模板Id";
        // 模板参数
        Map<String, WeChatTemplateMsg> sendMag = new HashMap<String, WeChatTemplateMsg>();
        sendMag.put("offWork", new WeChatTemplateMsg("宝~马上就要下班咯,收拾好随身物品准备早退啦!","#b89485"));
        // 发送
        wxSendMessageUtils.send(send.getOpenId(), templateId, sendMag);
    }
}
  • 发送工具类WxSendMessageUtils
@Component
public class WxSendMessageUtils{

	//获取Access_Token工具类
    @Autowired
    protected WeChetAccessToken weChetAccessToken;

    //restTemplate的请求方式
    @Autowired
    protected RestTemplate restTemplate;

    /**
    * 发送方法
    * */
    public String send(String openId, String templateId, Map<String, WeChatTemplateMsg> data) {
        String accessToken = weChetAccessToken.getToken();
        //System.out.println("send方法里的token:"+accessToken);
        String url = WxChatConstant.Url.SEND_URL.replace("ACCESS_TOKEN", accessToken);
        //String url = "http://api.weixin.qq.com/cgi-bin/message/template/send";
        //拼接base参数
        System.out.println("调用的接口地址为:"+url);
        Map<String, Object> sendBody = new HashMap<>();
        sendBody.put("touser", openId);               // openId
        sendBody.put("url","www.baidu.com");          // 跳转url
        sendBody.put("topcolor", "#FF0000");          // 顶色
        sendBody.put("data", data);                   // 模板参数
        sendBody.put("template_id", templateId);      // 模板Id
        ResponseEntity<String> forEntity = restTemplate.postForEntity(url, sendBody, String.class);
        System.out.println("响应:"+forEntity.getBody());
        return forEntity.getBody();
    }

}
  • 策略请求类型
@Data
@AllArgsConstructor
public class SendTypeRequest {

    /**
     * 推送类型
     * */
    private String type;

    /**
     * 接收人的openId
     * */
    private String openId;

}
  • 运行结果

在这里插入图片描述

2022-08-18 一些总结

  • 使用restTemplate请求第三方接口时,JSON序列化的时候,返回的body乱码,无法转换成对象。

关于这个问题,无解。尝试了网上的一些主流的方案,无果。最后还是使用了HttpClient进行http的请求调用

  • javax.net.ssl.SSLException: Connection reset

出现这个问题,请及时查看是否在“服务-服务列表”中开启是否允许公网访问。

在这里插入图片描述

  • errCode: 102002 | errMsg: 请求超时

由于动态扩容的原因,超过30分钟没有调用,实例缩容为0。此时服务停止。需要在「服务设置」中,将「实例副本数」的最小值设为1,保持服务常驻,无论服务是否被请求都不会缩容到0,从根本上避免冷启动。(会产生更多资源消耗及费用,请自行权衡,学生党可以将规格修改为0.25核,0.5G内存,够用了);

相关文章:

  • 数据结构与算法--动态规划
  • 标准IO移动光标
  • 咕咕驴2022新版AI短视频一键换脸小程序源码/带流量主
  • 检查异常处理方式:1,抛出;2,捕获
  • 数组清空(bzero与memset)
  • LocalDateTime获取时间类(当前时间)
  • java计算机毕业设计校园二手交易网站源码+系统+数据库+lw文档+mybatis+运行部署
  • 第1章Linux基础篇-Linux入门
  • java计算机毕业设计校园共享单车管理系统源码+系统+数据库+lw文档+mybatis+运行部署
  • 网课答案API 题库搜题查题系统
  • 手写Promise
  • Automatic differentiation package - torch.autograd
  • 第8章Linux实操篇-实用指令
  • net基于asp.net的社区团购网站-计算机毕业设计
  • 山石网科等级保护培训笔记
  • 【跃迁之路】【735天】程序员高效学习方法论探索系列(实验阶段492-2019.2.25)...
  • ECMAScript 6 学习之路 ( 四 ) String 字符串扩展
  • EventListener原理
  • HashMap ConcurrentHashMap
  • JavaScript HTML DOM
  • Nginx 通过 Lua + Redis 实现动态封禁 IP
  • Python3爬取英雄联盟英雄皮肤大图
  • SwizzleMethod 黑魔法
  • VUE es6技巧写法(持续更新中~~~)
  • 初识MongoDB分片
  • 分布式事物理论与实践
  • 马上搞懂 GeoJSON
  • 全栈开发——Linux
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • 少走弯路,给Java 1~5 年程序员的建议
  • 鱼骨图 - 如何绘制?
  • 原生 js 实现移动端 Touch 滑动反弹
  • 追踪解析 FutureTask 源码
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​七周四次课(5月9日)iptables filter表案例、iptables nat表应用
  • ​油烟净化器电源安全,保障健康餐饮生活
  • #[Composer学习笔记]Part1:安装composer并通过composer创建一个项目
  • #include<初见C语言之指针(5)>
  • #pragma data_seg 共享数据区(转)
  • (4)Elastix图像配准:3D图像
  • (C++)八皇后问题
  • (二)七种元启发算法(DBO、LO、SWO、COA、LSO、KOA、GRO)求解无人机路径规划MATLAB
  • (附源码)ssm高校社团管理系统 毕业设计 234162
  • (附源码)计算机毕业设计ssm-Java网名推荐系统
  • (一)eclipse Dynamic web project 工程目录以及文件路径问题
  • (转)C#调用WebService 基础
  • .NET设计模式(7):创建型模式专题总结(Creational Pattern)
  • //解决validator验证插件多个name相同只验证第一的问题
  • /deep/和 >>>以及 ::v-deep 三者的区别
  • :中兴通讯为何成功
  • @RestController注解的使用
  • @Transactional类内部访问失效原因详解
  • [ 云计算 | Azure 实践 ] 在 Azure 门户中创建 VM 虚拟机并进行验证
  • [AutoSar]BSW_Com07 CAN报文接收流程的函数调用
  • [Docker]十二.Docker consul集群搭建、微服务部署,Consul集群+Swarm集群部署微服务实战