SubscribeMessageService.java 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. package com.train.service;
  2. import com.alibaba.fastjson.JSON;
  3. import com.alibaba.fastjson.JSONObject;
  4. import com.train.config.WechatProperties;
  5. import lombok.extern.slf4j.Slf4j;
  6. import org.springframework.beans.factory.annotation.Value;
  7. import org.springframework.http.HttpEntity;
  8. import org.springframework.http.HttpHeaders;
  9. import org.springframework.http.MediaType;
  10. import org.springframework.http.ResponseEntity;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.web.client.RestTemplate;
  13. import org.springframework.web.client.RestClientException;
  14. import javax.annotation.Resource;
  15. import java.util.Map;
  16. /**
  17. * 订阅消息服务。
  18. * <p>触发点(设计文档 §4.7):报名成功、课前 T-7/T-3/T-1、成果被退回、投票结果揭晓、T+ 跟进提醒。
  19. * <p>test-mode:直接打日志模拟发送,不依赖微信 access_token 与模板;
  20. * 生产:调用微信 subscribeMessage.send(需 access_token,见 WechatService.getAccessToken),
  21. * 模板 ID 通过 {@code wechat.message-template.<key>} 配置(默认随 messageKey 变化)。
  22. * <p>一次性模板每人每周至多 1 条/模板——发送方需在业务侧保证不触频控。
  23. */
  24. @Slf4j
  25. @Service
  26. public class SubscribeMessageService {
  27. @Value("${wechat.test-mode}")
  28. private boolean testMode;
  29. /** 微信订阅消息接口地址 */
  30. @Value("${wechat.subscribe-url:https://api.weixin.qq.com/cgi-bin/message/subscribe/send}")
  31. private String subscribeUrl;
  32. @Resource
  33. private WechatService wechatService;
  34. private final RestTemplate restTemplate = new RestTemplate();
  35. /**
  36. * 发送微信订阅消息(一次性模板)。
  37. *
  38. * @param openid 接收者 openid(必填,空则直接返回 false)
  39. * @param templateId 消息模板 ID(train_message_template 配置;空则回退 {@code wechat.message-template.<messageKey>} 默认值)
  40. * @param page 点击跳转的小程序页面路径
  41. * @param data 模板字段 {字段名: {value: xx}}
  42. * @param messageKey 消息语义键(audit_log.action 等);用于按配置取模板 ID
  43. * @return 是否发送成功(test-mode 恒 true,且仅打日志)
  44. */
  45. public boolean sendSubscribeMessage(String openid, String templateId, String page, Map<String, Object> data, String messageKey) {
  46. if (openid == null || openid.trim().isEmpty()) {
  47. log.debug("订阅消息跳过:openid 为空,templateId={}", templateId);
  48. return false;
  49. }
  50. if (testMode) {
  51. log.info("【测试模式】模拟订阅消息发送: openid={}, templateId={}, page={}, data={}",
  52. openid, templateId, page, data);
  53. return true;
  54. }
  55. try {
  56. // 模板 ID 优先级:显式传入 > 按 messageKey 配置 > 空字符串(返回 false 不误报)
  57. String realTemplateId = templateId;
  58. if (!org.springframework.util.StringUtils.hasText(realTemplateId)) {
  59. realTemplateId = messageKey != null
  60. ? templateIdByKey(messageKey)
  61. : "";
  62. }
  63. if (!org.springframework.util.StringUtils.hasText(realTemplateId)) {
  64. log.warn("订阅消息未配置模板 ID,跳过发送: messageKey={}", messageKey);
  65. return false;
  66. }
  67. String accessToken = wechatService.getAccessToken();
  68. String url = subscribeUrl + "?access_token=" + accessToken;
  69. JSONObject body = new JSONObject();
  70. body.put("touser", openid);
  71. body.put("template_id", realTemplateId);
  72. body.put("page", page == null ? "pages/mine/index" : page);
  73. JSONObject dataJson = new JSONObject();
  74. if (data != null) {
  75. for (Map.Entry<String, Object> entry : data.entrySet()) {
  76. JSONObject item = new JSONObject();
  77. item.put("value", entry.getValue() == null ? "" : String.valueOf(entry.getValue()));
  78. dataJson.put(entry.getKey(), item);
  79. }
  80. }
  81. body.put("data", dataJson);
  82. HttpHeaders headers = new HttpHeaders();
  83. headers.setContentType(MediaType.APPLICATION_JSON);
  84. ResponseEntity<String> response = restTemplate.postForEntity(
  85. url, new HttpEntity<>(body.toJSONString(), headers), String.class);
  86. JSONObject json = JSON.parseObject(response.getBody());
  87. int errcode = json.getIntValue("errcode");
  88. if (errcode != 0) {
  89. log.warn("订阅消息发送失败 errcode={} errmsg={} (openid={}, templateId={})",
  90. errcode, json.getString("errmsg"), openid, realTemplateId);
  91. return false;
  92. }
  93. return true;
  94. } catch (RestClientException e) {
  95. log.error("订阅消息发送异常 (openid={})", openid, e);
  96. return false;
  97. }
  98. }
  99. /**
  100. * 从配置读取 messageKey 对应的模板 ID(缺省为空字符串)。
  101. * 配置形如:wechat.message-template.followup-t1: <templateId>
  102. */
  103. @Resource
  104. private WechatProperties wechatProperties;
  105. private String templateIdByKey(String messageKey) {
  106. Map<String, String> messageTemplates = wechatProperties.getMessageTemplate();
  107. if (messageTemplates == null) {
  108. return "";
  109. }
  110. String templateId = messageTemplates.get(messageKey);
  111. return templateId == null ? "" : templateId;
  112. }
  113. /**
  114. * 按 openid + messageKey 直接发送(模板 ID 从配置取,page 默认「我的」)。
  115. *
  116. * @return 是否发送成功
  117. */
  118. public boolean sendToUser(String openid, String messageKey, String page, Map<String, Object> data) {
  119. return sendSubscribeMessage(openid, null, page, data, messageKey);
  120. }
  121. }