package com.example.mini_program.service; import com.example.mini_program.config.WxMiniAppConfig; import com.example.mini_program.util.HttpUtil; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.HashMap; import java.util.Map; /** * 微信小程序订阅消息服务 * 调用微信 /cgi-bin/message/subscribe/send 接口推送订阅消息 *

* 模板字段映射: * name1 (姓名) → 访客姓名 * thing3 (事物) → 来访事由 * date8 (日期) → 预约时间 * thing10 (事物) → 到访区域 * phrase18 (短语) → 审批状态 */ @Slf4j @Service @RequiredArgsConstructor public class WxSubscribeMessageService { private static final String GET_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s"; private static final String SEND_MSG_URL = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s"; private final WxMiniAppConfig wxMiniAppConfig; private final ObjectMapper objectMapper; @Value("${wx.miniapp.subscribe-template-id:}") private String templateId; /** * 获取小程序 access_token */ public String getAccessToken() { String url = String.format(GET_TOKEN_URL, wxMiniAppConfig.getAppid(), wxMiniAppConfig.getSecret()); try { Map resp = objectMapper.readValue(HttpUtil.get(url), Map.class); if (resp.containsKey("access_token")) { return (String) resp.get("access_token"); } throw new RuntimeException("获取access_token失败: " + resp); } catch (Exception e) { throw new RuntimeException("获取access_token异常: " + e.getMessage(), e); } } /** * 发送订阅消息 * 所有字段不允许为空字符串(微信返回47003),name类型不接受纯数字 */ public void sendSubscribeMessage(String openid, String visitorName, String reason, String visitTime, String area, String status) { if (templateId == null || templateId.isEmpty()) { log.warn("未配置 subscribe-template-id,跳过订阅消息推送"); return; } String accessToken = getAccessToken(); String url = String.format(SEND_MSG_URL, accessToken); // name1 是 name 类型,纯数字会被微信拒绝,需加前缀兜底 String safeName = blankToDefault(visitorName, "访客"); if (safeName.matches("^\\d+$")) { safeName = "访客" + safeName; } Map data = new HashMap<>(); data.put("name1", Map.of("value", safeName)); data.put("thing3", Map.of("value", blankToDefault(reason, "来访"))); data.put("date8", Map.of("value", blankToDefault(visitTime, "待定"))); data.put("thing10", Map.of("value", blankToDefault(area, "未指定"))); data.put("phrase18", Map.of("value", blankToDefault(status, "待审核"))); Map body = new HashMap<>(); body.put("touser", openid); body.put("template_id", templateId); //点击模板卡片后的跳转页面,仅限本小程序内的页面。 body.put("page", "pages/records/records"); body.put("data", data); //跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 body.put("miniprogram_state", "formal"); body.put("lang", "zh_CN"); try { String json = objectMapper.writeValueAsString(body); Map resp = objectMapper.readValue(HttpUtil.postJson(url, json), Map.class); if ("0".equals(String.valueOf(resp.get("errcode")))) { log.info("订阅消息推送成功, openid={}", openid); } else { log.error("订阅消息推送失败: {}", resp); } } catch (Exception e) { log.error("订阅消息推送异常", e); } } private static String blankToDefault(String val, String def) { return (val != null && !val.isEmpty()) ? val : def; } }