同步平台业务模块能力
This commit is contained in:
@@ -100,7 +100,7 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("faviconUrl")
|
||||
.description("浏览器地址栏显示图标")
|
||||
.value("https://agent-1251073634.cos.ap-chengdu.myqcloud.com/store/961a8939eb384eafb102008ad045f5de.ico")
|
||||
.value("https://agent-statics-tc.qiming.com/store/961a8939eb384eafb102008ad045f5de.ico")
|
||||
.notice("")
|
||||
.placeholder("请上传浏览器地址栏显示图标")
|
||||
.category(TenantConfig.ConfigCategory.BaseConfig)
|
||||
@@ -609,6 +609,34 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
|
||||
.sort(90)
|
||||
.build());
|
||||
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("oaAppId")
|
||||
.description("公众号 appId(微信内 H5 JSAPI / OAuth,未使用时忽略)")
|
||||
.value("")
|
||||
.notice("公众号 appId")
|
||||
.placeholder("请输入公众号 appId")
|
||||
.category(TenantConfig.ConfigCategory.BaseConfig)
|
||||
.inputType(TenantConfig.InputType.Input)
|
||||
.dataType(TenantConfig.DataType.String)
|
||||
.minHeight(100)
|
||||
.required(false)
|
||||
.sort(91)
|
||||
.build());
|
||||
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("oaAppSecret")
|
||||
.description("公众号 secret(微信内 H5 JSAPI / OAuth,未使用时忽略)")
|
||||
.value("")
|
||||
.notice("公众号 secret")
|
||||
.placeholder("请输入公众号 secret")
|
||||
.category(TenantConfig.ConfigCategory.BaseConfig)
|
||||
.inputType(TenantConfig.InputType.Input)
|
||||
.dataType(TenantConfig.DataType.String)
|
||||
.minHeight(100)
|
||||
.required(false)
|
||||
.sort(91)
|
||||
.build());
|
||||
|
||||
tenantConfigDefaultList.add(TenantConfig.builder()
|
||||
.name("pageFooterText")
|
||||
.description("登录页底部展示信息")
|
||||
@@ -1094,4 +1122,4 @@ public class TenantConfigServiceImpl extends ServiceImpl<TenantConfigMapper, Ten
|
||||
update(updateWrapper);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,13 +13,17 @@ import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WeChatMpService {
|
||||
|
||||
private static final String WECHAT_MP_URL = "https://api.weixin.qq.com/cgi-bin/";
|
||||
private static final String WECHAT_JSCODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
|
||||
private static final String WECHAT_MP_APP_ID_KEY = "mpAppId";
|
||||
private static final String WECHAT_MP_APP_SECRET_KEY = "mpAppSecret";
|
||||
|
||||
@@ -32,18 +36,45 @@ public class WeChatMpService {
|
||||
@Resource
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
public String getAccessToken() {
|
||||
List<TenantConfig> tenantConfigList = tenantConfigService.getTenantConfigList();
|
||||
String mpAppId = "";
|
||||
String mpAppSecret = "";
|
||||
for (TenantConfig tenantConfig : tenantConfigList) {
|
||||
if (WECHAT_MP_APP_ID_KEY.equals(tenantConfig.getName())) {
|
||||
mpAppId = tenantConfig.getValue().toString();
|
||||
}
|
||||
if (WECHAT_MP_APP_SECRET_KEY.equals(tenantConfig.getName())) {
|
||||
mpAppSecret = tenantConfig.getValue().toString();
|
||||
}
|
||||
/**
|
||||
* 小程序登录码 换取 openId
|
||||
*/
|
||||
public String getOpenIdByLoginCode(String jsCode) {
|
||||
if (!StringUtils.hasText(jsCode)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
|
||||
}
|
||||
MpCredentials credentials = resolveMpCredentials();
|
||||
String url = WECHAT_JSCODE2SESSION_URL
|
||||
+ "?appid=" + credentials.appId()
|
||||
+ "&secret=" + credentials.appSecret()
|
||||
+ "&js_code=" + URLEncoder.encode(jsCode.trim(), StandardCharsets.UTF_8)
|
||||
+ "&grant_type=authorization_code";
|
||||
String response = httpClient.get(url);
|
||||
if (!JSON.isValid(response)) {
|
||||
log.warn("微信 jscode2session 响应非 JSON");
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
|
||||
}
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
|
||||
log.warn("微信 jscode2session 失败 errcode={} errmsg={}", jsonObject.getInteger("errcode"), jsonObject.getString("errmsg"));
|
||||
throw BizException.of(
|
||||
ErrorCodeEnum.ERROR_REQUEST,
|
||||
BizExceptionCodeEnum.systemWeChatApiReturnedError,
|
||||
jsonObject.getString("errmsg"));
|
||||
}
|
||||
String openId = jsonObject.getString("openid");
|
||||
if (!StringUtils.hasText(openId)) {
|
||||
log.warn("微信 jscode2session 未返回 openid");
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
|
||||
}
|
||||
log.info("微信 jscode2session 成功 openid={}", maskOpenId(openId));
|
||||
return openId;
|
||||
}
|
||||
|
||||
public String getAccessToken() {
|
||||
MpCredentials credentials = resolveMpCredentials();
|
||||
String mpAppId = credentials.appId();
|
||||
String mpAppSecret = credentials.appSecret();
|
||||
Object accessToken = redisUtil.get("mp.accessToken");
|
||||
if (accessToken != null) {
|
||||
return accessToken.toString();
|
||||
@@ -70,12 +101,14 @@ public class WeChatMpService {
|
||||
if (JSON.isValid(response)) {
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
|
||||
redisUtil.expire("mp.accessToken", 0);
|
||||
|
||||
if (jsonObject.getInteger("errcode") == 40001) {
|
||||
redisUtil.expire("mp.accessToken", -1);
|
||||
if (tryAgainWhenExpired) {
|
||||
return getPhoneNumber(code, false);
|
||||
}
|
||||
}
|
||||
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatApiReturnedError,
|
||||
jsonObject.getString("errmsg"));
|
||||
}
|
||||
@@ -88,4 +121,31 @@ public class WeChatMpService {
|
||||
}
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatPhoneNumberFetchFailed);
|
||||
}
|
||||
|
||||
private MpCredentials resolveMpCredentials() {
|
||||
List<TenantConfig> tenantConfigList = tenantConfigService.getTenantConfigList();
|
||||
String mpAppId = "";
|
||||
String mpAppSecret = "";
|
||||
for (TenantConfig tenantConfig : tenantConfigList) {
|
||||
if (WECHAT_MP_APP_ID_KEY.equals(tenantConfig.getName())) {
|
||||
mpAppId = tenantConfig.getValue().toString();
|
||||
}
|
||||
if (WECHAT_MP_APP_SECRET_KEY.equals(tenantConfig.getName())) {
|
||||
mpAppSecret = tenantConfig.getValue().toString();
|
||||
}
|
||||
}
|
||||
if (!StringUtils.hasText(mpAppId) || !StringUtils.hasText(mpAppSecret)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatMpNotConfigured);
|
||||
}
|
||||
return new MpCredentials(mpAppId, mpAppSecret);
|
||||
}
|
||||
|
||||
private record MpCredentials(String appId, String appSecret) {}
|
||||
|
||||
private static String maskOpenId(String openId) {
|
||||
if (!StringUtils.hasText(openId) || openId.length() <= 8) {
|
||||
return "***";
|
||||
}
|
||||
return openId.substring(0, 4) + "***" + openId.substring(openId.length() - 4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.xspaceagi.system.infra.rpc;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.xspaceagi.system.infra.dao.entity.TenantConfig;
|
||||
import com.xspaceagi.system.infra.dao.service.TenantConfigService;
|
||||
import com.xspaceagi.system.spec.common.RequestContext;
|
||||
import com.xspaceagi.system.spec.enums.ErrorCodeEnum;
|
||||
import com.xspaceagi.system.spec.exception.BizException;
|
||||
import com.xspaceagi.system.spec.exception.BizExceptionCodeEnum;
|
||||
import com.xspaceagi.system.spec.utils.HttpClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/** 微信公众号 OAuth 与 JSAPI 支付凭证(按租户 oaAppId / oaAppSecret)。 */
|
||||
@Slf4j
|
||||
@Service
|
||||
public class WeChatOaService {
|
||||
|
||||
private static final String WECHAT_OAUTH_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
|
||||
private static final String WECHAT_OAUTH_AUTHORIZE_URL = "https://open.weixin.qq.com/connect/oauth2/authorize";
|
||||
|
||||
@Resource
|
||||
private TenantConfigService tenantConfigService;
|
||||
|
||||
@Resource
|
||||
private HttpClient httpClient;
|
||||
|
||||
/**
|
||||
* 公众号网页授权 code 换取 openId(snsapi_base)。
|
||||
*/
|
||||
public String getOpenIdByOAuthCode(String code, long tenantId) {
|
||||
if (!StringUtils.hasText(code)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
|
||||
}
|
||||
OaCredentials credentials = resolveOaCredentials(tenantId);
|
||||
String url = WECHAT_OAUTH_ACCESS_TOKEN_URL
|
||||
+ "?appid=" + credentials.appId()
|
||||
+ "&secret=" + credentials.appSecret()
|
||||
+ "&code=" + URLEncoder.encode(code.trim(), StandardCharsets.UTF_8)
|
||||
+ "&grant_type=authorization_code";
|
||||
String response = httpClient.get(url);
|
||||
if (!JSON.isValid(response)) {
|
||||
log.warn("微信 OAuth access_token 响应非 JSON tenantId={}", tenantId);
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
|
||||
}
|
||||
JSONObject jsonObject = JSONObject.parseObject(response);
|
||||
if (jsonObject.containsKey("errcode") && jsonObject.getInteger("errcode") != 0) {
|
||||
log.warn(
|
||||
"微信 OAuth access_token 失败 tenantId={} errcode={} errmsg={}",
|
||||
tenantId,
|
||||
jsonObject.getInteger("errcode"),
|
||||
jsonObject.getString("errmsg"));
|
||||
throw BizException.of(
|
||||
ErrorCodeEnum.ERROR_REQUEST,
|
||||
BizExceptionCodeEnum.systemWeChatApiReturnedError,
|
||||
jsonObject.getString("errmsg"));
|
||||
}
|
||||
String openId = jsonObject.getString("openid");
|
||||
if (!StringUtils.hasText(openId)) {
|
||||
log.warn("微信 OAuth access_token 未返回 openid tenantId={}", tenantId);
|
||||
throw BizException.of(ErrorCodeEnum.ERROR_REQUEST, BizExceptionCodeEnum.systemWeChatOpenIdFetchFailed);
|
||||
}
|
||||
log.info("微信 OAuth 成功 tenantId={} openid={}", tenantId, maskOpenId(openId));
|
||||
return openId;
|
||||
}
|
||||
|
||||
/** 构建 snsapi_base 授权页 URL,供微信内 H5 跳转。 */
|
||||
public String buildOAuthAuthorizeUrl(String redirectUri, String state, long tenantId) {
|
||||
if (!StringUtils.hasText(redirectUri)) {
|
||||
throw new BizException(ErrorCodeEnum.INVALID_PARAM.getCode(), "redirectUri cannot be blank");
|
||||
}
|
||||
OaCredentials credentials = resolveOaCredentials(tenantId);
|
||||
String normalizedRedirectUri = redirectUri.trim();
|
||||
String encodedRedirect = URLEncoder.encode(normalizedRedirectUri, StandardCharsets.UTF_8);
|
||||
String stateParam = StringUtils.hasText(state) ? state.trim() : "";
|
||||
String redirectUriHost = extractHost(normalizedRedirectUri);
|
||||
log.info(
|
||||
"build wechat oauth authorize url tenantId={} appId={} redirectUriHost={} redirectUri={} state={}",
|
||||
tenantId,
|
||||
credentials.appId(),
|
||||
redirectUriHost,
|
||||
normalizedRedirectUri,
|
||||
stateParam);
|
||||
return WECHAT_OAUTH_AUTHORIZE_URL
|
||||
+ "?appid=" + credentials.appId()
|
||||
+ "&redirect_uri=" + encodedRedirect
|
||||
+ "&response_type=code"
|
||||
+ "&scope=snsapi_base"
|
||||
+ "&state=" + URLEncoder.encode(stateParam, StandardCharsets.UTF_8)
|
||||
+ "#wechat_redirect";
|
||||
}
|
||||
|
||||
private static String extractHost(String uriText) {
|
||||
try {
|
||||
URI uri = URI.create(uriText);
|
||||
return StringUtils.hasText(uri.getHost()) ? uri.getHost() : "";
|
||||
} catch (Exception ignored) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public String resolveOaAppId(long tenantId) {
|
||||
return resolveOaCredentials(tenantId).appId();
|
||||
}
|
||||
|
||||
private OaCredentials resolveOaCredentials(long tenantId) {
|
||||
boolean nullCtx = RequestContext.get() == null;
|
||||
try {
|
||||
if (nullCtx) {
|
||||
RequestContext.setThreadTenantId(tenantId);
|
||||
}
|
||||
List<TenantConfig> tenantConfigList = tenantConfigService.getTenantConfigList();
|
||||
String oaAppId = "";
|
||||
String oaAppSecret = "";
|
||||
for (TenantConfig tenantConfig : tenantConfigList) {
|
||||
if ("oaAppId".equals(tenantConfig.getName()) && tenantConfig.getValue() != null) {
|
||||
oaAppId = tenantConfig.getValue().toString();
|
||||
}
|
||||
if ("oaAppSecret".equals(tenantConfig.getName()) && tenantConfig.getValue() != null) {
|
||||
oaAppSecret = tenantConfig.getValue().toString();
|
||||
}
|
||||
}
|
||||
if (!StringUtils.hasText(oaAppId) || !StringUtils.hasText(oaAppSecret)) {
|
||||
throw BizException.of(ErrorCodeEnum.INVALID_PARAM, BizExceptionCodeEnum.systemWeChatOaNotConfigured);
|
||||
}
|
||||
return new OaCredentials(oaAppId.trim(), oaAppSecret.trim());
|
||||
} finally {
|
||||
if (nullCtx) {
|
||||
RequestContext.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private record OaCredentials(String appId, String appSecret) {}
|
||||
|
||||
private static String maskOpenId(String openId) {
|
||||
if (!StringUtils.hasText(openId) || openId.length() <= 8) {
|
||||
return "***";
|
||||
}
|
||||
return openId.substring(0, 4) + "***" + openId.substring(openId.length() - 4);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user