手把手教你给女朋友编写一个公众号定时推送(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内存,够用了);

  • 7
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
通达信是一款广受中国投资者喜爱的股票行情分析软件,而编写通达信的插件可以帮助用户更方便地进行股票分析和决策。下面是一个图文手把手你如何使用VC 2010编写通达信缠论插件的步骤。 第一步:准备工作 1. 确保已经安装好Visual Studio 2010和通达信软件。 2. 打开Visual Studio 2010,创建一个新的C++项目。 第二步:导入相关文件 1. 在项目中右键单击“资源文件”,选择“添加->现有项”。 2. 导航到通达信的安装目录,找到插件相关的头文件和库文件,选择并导入到项目中。 第三步:编写代码 1. 在项目中创建一个新的C++源文件,命名为“plugin.cpp”。 2. 在“plugin.cpp”中编写你的插件代码,可以包括初始化、数据处理、绘图等功能。 第四步:编译和调试 1. 点击Visual Studio 2010的“生成->生成解决方案”菜单,编译你的插件代码。 2. 将编译生成的插件文件(例如“plugin.dll”)复制到通达信的插件目录中。 3. 启动通达信软件,并在菜单栏中找到你的插件,选择并运行它。 第五步:测试和验证 1. 在通达信软件中打开一个股票行情页面,确保你的插件正确运行。 2. 测试你的插件是否能够按照你的预期显示或处理数据。 3. 如果发现错误或需要改进,返回Visual Studio 2010,修改代码后重新编译和调试。 通过以上步骤,你就可以使用VC 2010编写通达信缠论插件了。当然,这只是一个简单的程示例,实际的插件编写可能涉及更复杂的功能和算法。希望这个回答能对你有所帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值