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; /** * 订阅消息服务。 *

触发点(设计文档 §4.7):报名成功、课前 T-7/T-3/T-1、成果被退回、投票结果揭晓、T+ 跟进提醒。 *

test-mode:直接打日志模拟发送,不依赖微信 access_token 与模板; * 生产:调用微信 subscribeMessage.send(需 access_token,见 WechatService.getAccessToken), * 模板 ID 通过 {@code wechat.message-template.} 配置(默认随 messageKey 变化)。 *

一次性模板每人每周至多 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.} 默认值) * @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 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 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 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: */ @Resource private WechatProperties wechatProperties; private String templateIdByKey(String messageKey) { Map 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 data) { return sendSubscribeMessage(openid, null, page, data, messageKey); } }