| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135 |
- package com.train.service;
- import com.alibaba.fastjson.JSON;
- import com.alibaba.fastjson.JSONObject;
- import com.train.config.WechatProperties;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.http.HttpEntity;
- import org.springframework.http.HttpHeaders;
- import org.springframework.http.MediaType;
- import org.springframework.http.ResponseEntity;
- import org.springframework.stereotype.Service;
- import org.springframework.web.client.RestTemplate;
- import org.springframework.web.client.RestClientException;
- import javax.annotation.Resource;
- import java.util.Map;
- /**
- * 订阅消息服务。
- * <p>触发点(设计文档 §4.7):报名成功、课前 T-7/T-3/T-1、成果被退回、投票结果揭晓、T+ 跟进提醒。
- * <p>test-mode:直接打日志模拟发送,不依赖微信 access_token 与模板;
- * 生产:调用微信 subscribeMessage.send(需 access_token,见 WechatService.getAccessToken),
- * 模板 ID 通过 {@code wechat.message-template.<key>} 配置(默认随 messageKey 变化)。
- * <p>一次性模板每人每周至多 1 条/模板——发送方需在业务侧保证不触频控。
- */
- @Slf4j
- @Service
- public class SubscribeMessageService {
- @Value("${wechat.test-mode}")
- private boolean testMode;
- /** 微信订阅消息接口地址 */
- @Value("${wechat.subscribe-url:https://api.weixin.qq.com/cgi-bin/message/subscribe/send}")
- private String subscribeUrl;
- @Resource
- private WechatService wechatService;
- private final RestTemplate restTemplate = new RestTemplate();
- /**
- * 发送微信订阅消息(一次性模板)。
- *
- * @param openid 接收者 openid(必填,空则直接返回 false)
- * @param templateId 消息模板 ID(train_message_template 配置;空则回退 {@code wechat.message-template.<messageKey>} 默认值)
- * @param page 点击跳转的小程序页面路径
- * @param data 模板字段 {字段名: {value: xx}}
- * @param messageKey 消息语义键(audit_log.action 等);用于按配置取模板 ID
- * @return 是否发送成功(test-mode 恒 true,且仅打日志)
- */
- public boolean sendSubscribeMessage(String openid, String templateId, String page, Map<String, Object> data, String messageKey) {
- if (openid == null || openid.trim().isEmpty()) {
- log.debug("订阅消息跳过:openid 为空,templateId={}", templateId);
- return false;
- }
- if (testMode) {
- log.info("【测试模式】模拟订阅消息发送: openid={}, templateId={}, page={}, data={}",
- openid, templateId, page, data);
- return true;
- }
- try {
- // 模板 ID 优先级:显式传入 > 按 messageKey 配置 > 空字符串(返回 false 不误报)
- String realTemplateId = templateId;
- if (!org.springframework.util.StringUtils.hasText(realTemplateId)) {
- realTemplateId = messageKey != null
- ? templateIdByKey(messageKey)
- : "";
- }
- if (!org.springframework.util.StringUtils.hasText(realTemplateId)) {
- log.warn("订阅消息未配置模板 ID,跳过发送: messageKey={}", messageKey);
- return false;
- }
- String accessToken = wechatService.getAccessToken();
- String url = subscribeUrl + "?access_token=" + accessToken;
- JSONObject body = new JSONObject();
- body.put("touser", openid);
- body.put("template_id", realTemplateId);
- body.put("page", page == null ? "pages/mine/index" : page);
- JSONObject dataJson = new JSONObject();
- if (data != null) {
- for (Map.Entry<String, Object> entry : data.entrySet()) {
- JSONObject item = new JSONObject();
- item.put("value", entry.getValue() == null ? "" : String.valueOf(entry.getValue()));
- dataJson.put(entry.getKey(), item);
- }
- }
- body.put("data", dataJson);
- HttpHeaders headers = new HttpHeaders();
- headers.setContentType(MediaType.APPLICATION_JSON);
- ResponseEntity<String> response = restTemplate.postForEntity(
- url, new HttpEntity<>(body.toJSONString(), headers), String.class);
- JSONObject json = JSON.parseObject(response.getBody());
- int errcode = json.getIntValue("errcode");
- if (errcode != 0) {
- log.warn("订阅消息发送失败 errcode={} errmsg={} (openid={}, templateId={})",
- errcode, json.getString("errmsg"), openid, realTemplateId);
- return false;
- }
- return true;
- } catch (RestClientException e) {
- log.error("订阅消息发送异常 (openid={})", openid, e);
- return false;
- }
- }
- /**
- * 从配置读取 messageKey 对应的模板 ID(缺省为空字符串)。
- * 配置形如:wechat.message-template.followup-t1: <templateId>
- */
- @Resource
- private WechatProperties wechatProperties;
- private String templateIdByKey(String messageKey) {
- Map<String, String> messageTemplates = wechatProperties.getMessageTemplate();
- if (messageTemplates == null) {
- return "";
- }
- String templateId = messageTemplates.get(messageKey);
- return templateId == null ? "" : templateId;
- }
- /**
- * 按 openid + messageKey 直接发送(模板 ID 从配置取,page 默认「我的」)。
- *
- * @return 是否发送成功
- */
- public boolean sendToUser(String openid, String messageKey, String page, Map<String, Object> data) {
- return sendSubscribeMessage(openid, null, page, data, messageKey);
- }
- }
|