Springboot+微信公众号,实现消息精准推送
背景:
起因,娘子在小红书看到技术男友给其写的公众号早安推送,深受感动,被PUA。
还好在下有些JAVA基础,在春节假期几天无事时研究了一下,基于Springboot框架自写后台,接入第三方API,实现微信公众号每日早8点准时精准推送早安问候功能。
效果图:
基于在此过程中也学习到了不少知识点,再此做记录,待温故而知新
干货:
前期准备:
Springboot的构建、配置、启动略
微信公众号,微信公众号测试账号:需获取appID与appsecret,用户关注测试账号后的微信号OpenID,消息模板中的模板ID(为什么要用测试账号,因为正式账号需要企业级认证才能使用开发API等高级功能,我的公众号主体是个人,只能用测试账号),详情如下:
1.在开发接口管理中,点击开发者工具(标号3)
-- 标号2:如果你不是企业级公众号,暂时用不到此页面(正式公众号)的开发者ID与开发着密码
2.点击上图第三步,进入如下图页面,并再次点击进入公众平台测试账号页面
3.标号5:在测试账号内,需要记下appID
与appsecret
,两个配合会获得access_token
,access_token
是用来调用微信公众平台的各种 API 接口的重要凭证(正式账号内,access_token是每2小时刷新一次);
4.标号6:用你自己的或者亲朋好友的扫码,绑定到测试号内用于后续给其发送消息
5.标号7:在微信号一栏中会生成OpenID唯一标识,用于与模板绑定发送消息
6.标号8:新增测试模板,模板ID与用户OpenID绑定,用于发送指定模板样式的消息
代码:
主要类:
RestTemplate
、HttpEntity
、ObjectMapper
、JsonNode
、RequestEntity
、HttpHeaders
...
主要注解:
@Service
、@Scheduled(cron = "0 0 8 * * ?")
...
创建业务层,随便命个名,本人学术不精,都写在了一个类里,建议分开处理
@Service
class WechatTokenService{
private static final String APPID = "你的appId";
private static final String APPSECRET = "你的app_secret";
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + APPID + "&secret=" + APPSECRET;
private static final String Weather_API_KEY = "我用的和风API,注册找一下给的api_key";
private static final String LOCATION = "109.53,18.22"; //三亚坐标
private static final String WEIBO_API_KEY = "我用的第三方API,百度搜一大把";
private static final String OPENID = "关注用户的open_id";
private static final String TEMPLATE_ID = "你的模板ID";
private long daysBetween; //存储天数
public String getAccessToken() {
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(ACCESS_TOKEN_URL, String.class);
System.out.println("微信返回:" + response);
return parseAccessToken(response);
}
//结婚日期计时,Scheduled()注解不支持返回值
@Scheduled(fixedRate = 24 * 60 * 60 * 1000)
public void updateMarryDate() {
LocalDate startDate = LocalDate.of(2022, 9, 9); // 结婚日期
LocalDate currentDate = LocalDate.now(); // 当前日期
daysBetween = ChronoUnit.DAYS.between(startDate, currentDate); // 计算相差天数
}
//获取结婚至今日期
public String getMarryDate() {
return String.valueOf(daysBetween) + "天";
}
//获取每日一言
public String getLetter() {
String url = "https://v1.hitokoto.cn/";
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.readTree(response);
return node.path("hitokoto").asText();
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
//获取微博热搜
public String getWeibo() {
String url = "https://whyta.cn/api/tx/weibohot?key=" + WEIBO_API_KEY;
RestTemplate restTemplate = new RestTemplate();
String response = restTemplate.getForObject(url, String.class);
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.readTree(response);
JsonNode listNode = node.path("result").path("list");
return listNode.get(0).path("hotword").asText();
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
//获取天气
public String getWeather() {
String url = "https://devapi.qweather.com/v7/weather/now?location=" + LOCATION + "&key=" + Weather_API_KEY;
RestTemplate restTemplate = new RestTemplate();
RequestEntity<Void> request = new RequestEntity<>(HttpMethod.GET, URI.create(url));
ResponseEntity<byte[]> response = restTemplate.exchange(request, byte[].class);
//解压Gzip数据
String responseString = decompressGzip(response.getBody());
//String response = restTemplate.getForObject(url, String.class);
try {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode root = objectMapper.readTree(responseString);
JsonNode now = root.path("now");
String windDir = now.path("windDir").asText();
String windScale = now.path("windScale").asText();
String weatherText = now.path("text").asText();
String temperature = now.path("temp").asText();
String humidity = now.path("humidity").asText();
return "湿度:" + humidity + "%," + windDir + windScale + "级," + weatherText + temperature;
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 解压Weather的Gzip数据
*/
private String decompressGzip(byte[] gzipData) {
try (ByteArrayInputStream bis = new ByteArrayInputStream(gzipData);
GZIPInputStream gzipInputStream = new GZIPInputStream(bis);
ByteArrayOutputStream bos = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int len;
while ((len = gzipInputStream.read(buffer)) > 0) {
bos.write(buffer, 0, len);
}
return bos.toString();
} catch (IOException e) {
throw new RuntimeException("解压缩 Gzip 数据失败", e);
}
}
/**
* 解析JSON
*
* @param json 传入getAccessToken
* @return 获取token的access_token
*/
public String parseAccessToken(String json) {
ObjectMapper mapper = new ObjectMapper();
try {
JsonNode root = mapper.readTree(json);
return root.path("access_token").asText(); //获取 access_token
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
//发送消息模板
@Scheduled(cron = "0 0 8 * * ?") //每天at8:00,会执行这段代码
public void sendTemplateMessage() {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
//当前时间
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String date = now.format(formatter);
DayOfWeek week = now.getDayOfWeek();
String[] weekDays = {"星期一", "星期二", "星期三", "星期四", "星期五", "星期六", "星期日"};
String weekDayChinese = weekDays[week.getValue() - 1];
//获取微博热搜
String weibo = getWeibo();
//每日一言
String letter = getLetter();
//结婚至今日期
String marry = getMarryDate();
//获取天气
String weatherInfo = getWeather();
if (weatherInfo == null) {
weatherInfo = "天气信息获取失败";
}
String requestBody = """
{
"touser": "%s",
"template_id": "%s",
"data": {
"content": {
"value": "❤亲爱的宝,早上好,新的一天开始了❤",
"color": "#173177"
},
"time": {
"value": "%s",
"color": "#173177"
},
"week": {
"value": "%s",
"color": "#173177"
},
"remark": {
"value": "三亚",
"color": "#173177"
},
"weather": {
"value": "%s",
"color": "#173177"
},
"weibo": {
"value": "%s",
"color": "#173177"
},
"letter": {
"value": "%s",
"color": "#173177"
},
"marry": {
"value": "%s",
"color": "#173177"
}
}
}
""".formatted(OPENID, TEMPLATE_ID, date, weekDayChinese, weatherInfo, weibo, letter, marry);
HttpEntity<String> request = new HttpEntity<>(requestBody, headers);
String url = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=" + getAccessToken();
String response = restTemplate.postForObject(url, request, String.class);
System.out.println("微信API响应:" + response);
}
}
在没有部署服务器上时,需要手动触发给微信公众号API发送请求,所以这里随便写个控制器层,真正上线这里是不需要的,通过注解@Scheduled(cron = "0 0 8 * * ?")
码每天早上八点重复执行注解下代
@RestController
class WechatMessageController{
@Autowired
WeChatTokenService service;
@GetMapping("send")
public String sendWeChatMessage(){
service.sendTemplateMessage();
return "消息已发送";
}
}
在浏览器中输入“localhost:8088/send”,如果页面返回了“消息已发送”,就代表成功了,快看看微信有没有收到消息吧。端口号8088在application.properties配置中可自定义