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.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 客户端构造微信认证客户端。
|
*/
|
public AppWechatAuthClient(AppWechatAuthProperties properties, ObjectMapper objectMapper) {
|
this(properties, objectMapper, new RestTemplate());
|
}
|
|
/**
|
* 注入微信配置、JSON 解析器与 HTTP 客户端。
|
*/
|
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;
|
}
|
}
|
}
|