package com.tcm.training.admin.app.auth.service; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.tcm.training.admin.app.auth.config.AppWechatAuthProperties; import com.tcm.training.admin.app.auth.dto.AppWechatLoginRequest; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.client.RestOperations; import org.springframework.web.client.RestTemplate; import org.springframework.web.util.UriComponentsBuilder; /** * 微信登录接口客户端,负责换取授权信息并拉取微信头像昵称。 */ @Service public class AppWechatAuthClient { private static final Logger log = LoggerFactory.getLogger(AppWechatAuthClient.class); private final AppWechatAuthProperties properties; private final ObjectMapper objectMapper; private final RestOperations restOperations; /** * 使用默认 HTTP 客户端构造微信认证客户端。 */ @Autowired public AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper) { this(properties, objectMapper, new RestTemplate()); } /** * 注入微信配置、JSON 解析器与 HTTP 客户端。 * 存在多个构造器时,这个扩展构造器仅供测试或内部定制使用,避免 Spring 误判默认注入入口。 */ AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper, RestOperations restOperations) { this.properties = properties; this.objectMapper = objectMapper; this.restOperations = restOperations; } /** * 按当前请求中的授权信息换取微信用户资料。 */ public AppWechatUserProfile resolveUserProfile(AppWechatLoginRequest request) { if (request == null) { throw new IllegalArgumentException("微信授权信息不完整"); } String accessToken = trimToNull(request.getAccessToken()); String openid = trimToNull(request.getOpenid()); String unionid = trimToNull(request.getUnionid()); if (accessToken != null && openid != null) { AppWechatUserProfile profile = fetchUserInfo(accessToken, openid); if (profile.getUnionid() == null) { profile.setUnionid(unionid); } return profile; } String code = trimToNull(request.getCode()); if (code == null) { throw new IllegalArgumentException("微信授权信息不完整"); } if (!properties.isServerAuthConfigured()) { throw new IllegalArgumentException("微信登录暂未配置,请稍后重试"); } WechatAccessTokenSnapshot tokenSnapshot = exchangeCode(code); AppWechatUserProfile profile = fetchUserInfo(tokenSnapshot.getAccessToken(), tokenSnapshot.getOpenid()); if (profile.getUnionid() == null) { profile.setUnionid(tokenSnapshot.getUnionid()); } return profile; } /** * 使用临时 code 向微信换取 accessToken 与 openid。 */ private WechatAccessTokenSnapshot exchangeCode(String code) { String url = UriComponentsBuilder.fromHttpUrl(properties.getAccessTokenUrl()) .queryParam("appid", properties.getAppId()) .queryParam("secret", properties.getAppSecret()) .queryParam("code", code) .queryParam("grant_type", "authorization_code") .build(true) .toUriString(); JsonNode payload = getWechatPayload(url, "微信登录授权失败"); ensureWechatSuccess(payload, "微信登录授权失败"); String accessToken = trimToNull(readText(payload, "access_token")); String openid = trimToNull(readText(payload, "openid")); if (accessToken == null || openid == null) { throw new IllegalArgumentException("微信登录授权失败"); } WechatAccessTokenSnapshot snapshot = new WechatAccessTokenSnapshot(); snapshot.setAccessToken(accessToken); snapshot.setOpenid(openid); snapshot.setUnionid(trimToNull(readText(payload, "unionid"))); return snapshot; } /** * 使用微信 accessToken 与 openid 拉取昵称、头像和性别。 */ private AppWechatUserProfile fetchUserInfo(String accessToken, String openid) { String url = UriComponentsBuilder.fromHttpUrl(properties.getUserInfoUrl()) .queryParam("access_token", accessToken) .queryParam("openid", openid) .queryParam("lang", "zh_CN") .build(true) .toUriString(); JsonNode payload = getWechatPayload(url, "获取微信用户信息失败"); ensureWechatSuccess(payload, "获取微信用户信息失败"); AppWechatUserProfile profile = new AppWechatUserProfile(); profile.setOpenid(openid); profile.setUnionid(trimToNull(readText(payload, "unionid"))); profile.setNickname(trimToNull(readText(payload, "nickname"))); profile.setAvatar(trimToNull(readText(payload, "headimgurl"))); profile.setSex(readInteger(payload, "sex")); return profile; } /** * 发起微信接口调用并将响应解析为 JSON。 */ private JsonNode getWechatPayload(String url, String failureMessage) { try { String responseBody = restOperations.getForObject(url, String.class); if (responseBody == null || responseBody.trim().isEmpty()) { throw new IllegalArgumentException(failureMessage); } return objectMapper.readTree(responseBody); } catch (IllegalArgumentException ex) { throw ex; } catch (Exception ex) { log.error("调用微信接口失败: url={}", url, ex); throw new IllegalArgumentException(failureMessage); } } /** * 校验微信接口返回是否为成功态。 */ private void ensureWechatSuccess(JsonNode payload, String failureMessage) { Integer errcode = readInteger(payload, "errcode"); if (errcode == null || errcode == 0) { return; } String errmsg = trimToNull(readText(payload, "errmsg")); if (errmsg == null) { errmsg = failureMessage; } log.warn("微信接口返回异常: errcode={}, errmsg={}", errcode, errmsg); throw new IllegalArgumentException(errmsg); } /** * 读取字符串字段。 */ private String readText(JsonNode payload, String fieldName) { JsonNode valueNode = payload.get(fieldName); if (valueNode == null || valueNode.isNull()) { return null; } return valueNode.asText(); } /** * 读取整数字段。 */ private Integer readInteger(JsonNode payload, String fieldName) { String value = trimToNull(readText(payload, fieldName)); if (value == null) { return null; } try { return Integer.valueOf(value); } catch (NumberFormatException ex) { return null; } } /** * 将空白字符串归一化为 null。 */ private String trimToNull(String value) { if (value == null) { return null; } String trimmed = value.trim(); return trimmed.isEmpty() ? null : trimmed; } /** * 保存微信换票结果,避免在多个私有方法之间散落参数。 */ private static class WechatAccessTokenSnapshot { private String accessToken; private String openid; private String unionid; /** * 获取 accessToken。 */ public String getAccessToken() { return accessToken; } /** * 设置 accessToken。 */ public void setAccessToken(String accessToken) { this.accessToken = accessToken; } /** * 获取 openid。 */ public String getOpenid() { return openid; } /** * 设置 openid。 */ public void setOpenid(String openid) { this.openid = openid; } /** * 获取 unionid。 */ public String getUnionid() { return unionid; } /** * 设置 unionid。 */ public void setUnionid(String unionid) { this.unionid = unionid; } } }