longz
2026-04-27 25df88d0ab9d7444ed1d162b9efa1259ac2f6f66
backend/src/main/java/com/sinata/appointment/integration/WechatMiniGateway.java
@@ -5,6 +5,7 @@
import com.sinata.appointment.config.WechatMiniAppProperties;
import com.sinata.appointment.constants.BizErrorCode;
import com.sinata.appointment.exception.BizException;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
@@ -30,6 +31,8 @@
            "https://api.weixin.qq.com/cgi-bin/token";
    private static final String GET_USER_PHONE =
            "https://api.weixin.qq.com/wxa/business/getuserphonenumber";
    private static final String SEND_SUBSCRIBE_MESSAGE =
            "https://api.weixin.qq.com/cgi-bin/message/subscribe/send";
    private final WechatMiniAppProperties props;
    private final RestTemplate restTemplate;
@@ -138,6 +141,65 @@
        }
    }
    public void sendSubscribeMessage(
            String openId,
            String templateId,
            String page,
            Map<String, String> templateData) {
        if (openId == null || openId.isBlank()) {
            throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "openId 不能为空");
        }
        if (templateId == null || templateId.isBlank()) {
            throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "templateId 不能为空");
        }
        if (templateData == null || templateData.isEmpty()) {
            throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "订阅消息参数不能为空");
        }
        if (props.useStub()) {
            log.info(
                    "Wechat mini stub: subscribe message openId={} templateId={} page={} data={}",
                    openId,
                    templateId,
                    page,
                    templateData);
            return;
        }
        String token = getAccessToken();
        String url =
                UriComponentsBuilder.fromHttpUrl(SEND_SUBSCRIBE_MESSAGE)
                        .queryParam("access_token", token)
                        .build()
                        .toUriString();
        try {
            var root = objectMapper.createObjectNode();
            root.put("touser", openId);
            root.put("template_id", templateId);
            if (page != null && !page.isBlank()) {
                root.put("page", page);
            }
            var dataNode = root.putObject("data");
            templateData.forEach(
                    (key, value) -> dataNode.putObject(key).put("value", value == null ? "" : value));
            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            String body =
                    restTemplate.postForObject(
                            url, new HttpEntity<>(root.toString(), headers), String.class);
            JsonNode response = objectMapper.readTree(body);
            int err = response.path("errcode").asInt(-1);
            if (err != 0) {
                String msg = response.path("errmsg").asText("wx error");
                log.warn("sendSubscribeMessage errcode={} msg={}", err, msg);
                throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "发送订阅消息失败: " + msg);
            }
        } catch (BizException e) {
            throw e;
        } catch (Exception e) {
            log.error("sendSubscribeMessage http error", e);
            throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "微信订阅消息接口异常");
        }
    }
    private String getAccessToken() {
        tokenLock.lock();
        try {