package com.sinata.appointment.integration; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.sinata.appointment.config.WechatOfficialAccountProperties; import com.sinata.appointment.constants.BizErrorCode; import com.sinata.appointment.exception.BizException; import java.nio.charset.StandardCharsets; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriUtils; @Slf4j @Service @RequiredArgsConstructor public class WechatOfficialAccountGateway { private static final String OAUTH_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize"; private static final String OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token"; private final WechatOfficialAccountProperties props; @Qualifier("wechatRestTemplate") private final RestTemplate restTemplate; private final ObjectMapper objectMapper; public String buildSilentOAuthUrl(String redirectUri, String state) { if (redirectUri == null || redirectUri.isBlank()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "redirectUri 不能为空"); } if (props.useStub()) { return UriComponentsBuilder.fromHttpUrl(redirectUri) .queryParam("code", "STUB-" + System.currentTimeMillis()) .queryParam("state", state == null ? "" : state.trim()) .build() .toUriString(); } String encodedRedirect = UriUtils.encode(redirectUri, StandardCharsets.UTF_8); String safeState = state == null ? "" : state.trim(); return OAUTH_AUTHORIZE_URL + "?appid=" + props.getAppId() + "&redirect_uri=" + encodedRedirect + "&response_type=code" + "&scope=snsapi_base" + "&state=" + UriUtils.encode(safeState, StandardCharsets.UTF_8) + "#wechat_redirect"; } public WxOfficialOAuthResult exchangeCode(String code) { if (code == null || code.isBlank()) { throw new BizException(BizErrorCode.AUTH_INVALID_CREDENTIALS, "code 不能为空"); } if (props.useStub()) { log.debug("Wechat official account stub: oauth code"); return WxOfficialOAuthResult.stub(code); } String url = UriComponentsBuilder.fromHttpUrl(OAUTH_ACCESS_TOKEN_URL) .queryParam("appid", props.getAppId()) .queryParam("secret", props.getAppSecret()) .queryParam("code", code) .queryParam("grant_type", "authorization_code") .build() .toUriString(); try { String body = restTemplate.getForObject(url, String.class); JsonNode root = objectMapper.readTree(body); int err = root.path("errcode").asInt(0); if (err != 0) { String msg = root.path("errmsg").asText("wx error"); log.warn("official oauth errcode={} msg={}", err, msg); throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "公众号授权失败: " + msg); } String openId = root.path("openid").asText(null); if (openId == null || openId.isBlank()) { throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "公众号未返回 openid"); } return WxOfficialOAuthResult.builder() .openId(openId) .unionId(root.path("unionid").asText(null)) .build(); } catch (BizException e) { throw e; } catch (Exception e) { log.error("official oauth http error", e); throw new BizException(BizErrorCode.MINI_WX_API_ERROR, "公众号授权接口异常"); } } }