同步平台业务模块能力

This commit is contained in:
baiyanyun
2026-07-13 09:13:30 +08:00
parent e612f5e434
commit a1a38f62d8
224 changed files with 22331 additions and 1224 deletions

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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 换取 openIdsnsapi_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);
}
}