Compare commits
11 Commits
644188288a
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| e77353b478 | |||
| e487f021dc | |||
| 5ca3280ddc | |||
| b9ec1fcdb9 | |||
| a6a2e14abf | |||
| 18696f91a6 | |||
| 49e6cd2197 | |||
| f7f7b30813 | |||
| b043270e52 | |||
| f720adc2a8 | |||
| 2e51710e3a |
32
pom.xml
32
pom.xml
@@ -26,7 +26,31 @@
|
|||||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
<dependencyManagement>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<!-- 强制 okhttp/okio 版本,避免 minio-java(要求 okhttp>=4.11.0) 与微信/支付 SDK 引入的 okhttp3 旧版冲突 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okio</groupId>
|
||||||
|
<artifactId>okio</artifactId>
|
||||||
|
<version>3.6.0</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</dependencyManagement>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- 强制 okhttp 4.12.0:minio-java 要求 okhttp>=4.11.0,放在 dependencies 首位确保胜出,
|
||||||
|
避免运行时 NoSuchMethodError(okhttp3.RequestBody.create 等) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
|
<artifactId>okhttp</artifactId>
|
||||||
|
<version>4.12.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- spring-boot-devtools -->
|
<!-- spring-boot-devtools -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
@@ -297,6 +321,14 @@
|
|||||||
<version>3.17.4</version>
|
<version>3.17.4</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MinIO 官方 Java SDK:用于对接 MinIO 兼容 S3 存储。
|
||||||
|
注意 aliyun OSS SDK 无 AWS4-HMAC-SHA256 签名器,无法对接 MinIO,故 minio 分支单独用此 SDK。 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.minio</groupId>
|
||||||
|
<artifactId>minio</artifactId>
|
||||||
|
<version>8.5.7</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 阿里云 内容安全审核 -->
|
<!-- 阿里云 内容安全审核 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.aliyun</groupId>
|
<groupId>com.aliyun</groupId>
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
|||||||
"/api/sendSmsCaptcha",
|
"/api/sendSmsCaptcha",
|
||||||
"/api/sendSmsCaptchaByAdmin",
|
"/api/sendSmsCaptchaByAdmin",
|
||||||
"/api/sendEmailCaptcha",
|
"/api/sendEmailCaptcha",
|
||||||
|
"/api/verifyEmailCaptcha",
|
||||||
"/api/loginBySms",
|
"/api/loginBySms",
|
||||||
"/api/loginBySuperAdminSms",
|
"/api/loginBySuperAdminSms",
|
||||||
"/api/loginBySelectTenant",
|
"/api/loginBySelectTenant",
|
||||||
|
|||||||
151
src/main/java/com/gxwebsoft/common/core/utils/MinioUtil.java
Normal file
151
src/main/java/com/gxwebsoft/common/core/utils/MinioUtil.java
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
package com.gxwebsoft.common.core.utils;
|
||||||
|
|
||||||
|
import io.minio.BucketExistsArgs;
|
||||||
|
import io.minio.GetBucketPolicyArgs;
|
||||||
|
import io.minio.MakeBucketArgs;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.SetBucketPolicyArgs;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO 对象存储工具类
|
||||||
|
*
|
||||||
|
* <p>说明:aliyun-sdk-oss 只实现了阿里云自家的 OSS 签名(OSSV1/V2/V4Signer),
|
||||||
|
* 不含 AWS4-HMAC-SHA256,因此无法对接只接受 AWS4 的 MinIO。
|
||||||
|
* 所有 MinIO 操作统一走官方 minio-java SDK。
|
||||||
|
*
|
||||||
|
* @author WebSoft
|
||||||
|
* @since 2026-08-07
|
||||||
|
*/
|
||||||
|
public class MinioUtil {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MinIO 默认 region。
|
||||||
|
* 必须显式指定,否则 minio-java 会先发 {@code GET /{bucket}?location=} 做 region 自动探测,
|
||||||
|
* 该探测请求在 MinIO + 反向代理场景下容易触发 SignatureDoesNotMatch。
|
||||||
|
*/
|
||||||
|
public static final String DEFAULT_REGION = "us-east-1";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 已确保「存在 + 已配置匿名只读策略」的 bucket 缓存(endpoint|bucket)。
|
||||||
|
* 进程内缓存,避免每次上传都多发两次 HTTP 请求。
|
||||||
|
*/
|
||||||
|
private static final Set<String> READY_BUCKETS = ConcurrentHashMap.newKeySet();
|
||||||
|
|
||||||
|
private MinioUtil() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 规范化 endpoint:去掉结尾斜杠,避免 minio-java 拼接出双斜杠导致签名不匹配
|
||||||
|
*/
|
||||||
|
public static String normalizeEndpoint(String endpoint) {
|
||||||
|
if (endpoint == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String result = endpoint.trim();
|
||||||
|
while (result.endsWith("/")) {
|
||||||
|
result = result.substring(0, result.length() - 1);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 MinioClient(统一带上 region,跳过 getBucketLocation 探测)
|
||||||
|
*
|
||||||
|
* @param endpoint S3 API 地址,如 https://minio.websoft.top
|
||||||
|
* @param accessKeyId AK
|
||||||
|
* @param accessKeySecret SK
|
||||||
|
*/
|
||||||
|
public static MinioClient buildClient(String endpoint, String accessKeyId, String accessKeySecret) {
|
||||||
|
return MinioClient.builder()
|
||||||
|
.endpoint(normalizeEndpoint(endpoint))
|
||||||
|
.credentials(accessKeyId, accessKeySecret)
|
||||||
|
.region(DEFAULT_REGION)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成 bucket 的匿名只读策略。
|
||||||
|
* 只放开读取对象,<b>故意不给 s3:ListBucket</b>,避免匿名用户列出桶内全部文件清单。
|
||||||
|
*/
|
||||||
|
public static String buildPublicReadPolicy(String bucketName) {
|
||||||
|
return "{"
|
||||||
|
+ "\"Version\":\"2012-10-17\","
|
||||||
|
+ "\"Statement\":["
|
||||||
|
+ "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},"
|
||||||
|
+ "\"Action\":[\"s3:GetBucketLocation\"],"
|
||||||
|
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "\"]},"
|
||||||
|
+ "{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},"
|
||||||
|
+ "\"Action\":[\"s3:GetObject\"],"
|
||||||
|
+ "\"Resource\":[\"arn:aws:s3:::" + bucketName + "/*\"]}"
|
||||||
|
+ "]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保 bucket 可用:
|
||||||
|
* <ol>
|
||||||
|
* <li>bucket 不存在 → 自动创建,并套上匿名只读策略</li>
|
||||||
|
* <li>bucket 已存在但没有任何策略 → 补上匿名只读策略(兼容历史手动建的裸桶)</li>
|
||||||
|
* <li>bucket 已有自定义策略 → 不覆盖,尊重人工配置</li>
|
||||||
|
* </ol>
|
||||||
|
* 结果会缓存在进程内,同一个 endpoint+bucket 只检查一次。
|
||||||
|
*
|
||||||
|
* <p>本方法不会抛异常:即使当前 AK 没有建桶/改策略权限,也只打日志,
|
||||||
|
* 后续的上传动作照常执行(真正失败会由上传自身报错)。
|
||||||
|
*
|
||||||
|
* @return true 表示 bucket 已就绪
|
||||||
|
*/
|
||||||
|
public static boolean ensureBucketReady(MinioClient client, String endpoint, String bucketName) {
|
||||||
|
if (client == null || bucketName == null || bucketName.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String cacheKey = normalizeEndpoint(endpoint) + "|" + bucketName;
|
||||||
|
if (READY_BUCKETS.contains(cacheKey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
boolean exists = client.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build());
|
||||||
|
if (!exists) {
|
||||||
|
client.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
|
||||||
|
client.setBucketPolicy(SetBucketPolicyArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.config(buildPublicReadPolicy(bucketName))
|
||||||
|
.build());
|
||||||
|
System.out.println("[MinIO] 自动创建 bucket 并配置匿名只读策略: " + bucketName);
|
||||||
|
} else {
|
||||||
|
// minio-java 在无策略时返回空串而非抛错
|
||||||
|
String policy = client.getBucketPolicy(GetBucketPolicyArgs.builder().bucket(bucketName).build());
|
||||||
|
if (policy == null || policy.trim().isEmpty()) {
|
||||||
|
client.setBucketPolicy(SetBucketPolicyArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.config(buildPublicReadPolicy(bucketName))
|
||||||
|
.build());
|
||||||
|
System.out.println("[MinIO] 已为存量 bucket 补充匿名只读策略: " + bucketName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
READY_BUCKETS.add(cacheKey);
|
||||||
|
return true;
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 不阻断上传:权限不足或网络异常时仅告警
|
||||||
|
System.out.println("[MinIO] ensureBucketReady 失败(不影响上传), bucket=" + bucketName
|
||||||
|
+ ", reason=" + e.getMessage());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 手动失效缓存(例如后台修改了存储配置后调用)
|
||||||
|
*/
|
||||||
|
public static void evictBucketCache(String endpoint, String bucketName) {
|
||||||
|
READY_BUCKETS.remove(normalizeEndpoint(endpoint) + "|" + bucketName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空全部缓存
|
||||||
|
*/
|
||||||
|
public static void clearBucketCache() {
|
||||||
|
READY_BUCKETS.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,7 @@ import com.aliyuncs.profile.IClientProfile;
|
|||||||
import com.gxwebsoft.common.core.annotation.OperationLog;
|
import com.gxwebsoft.common.core.annotation.OperationLog;
|
||||||
import com.gxwebsoft.common.core.config.ConfigProperties;
|
import com.gxwebsoft.common.core.config.ConfigProperties;
|
||||||
import com.gxwebsoft.common.core.utils.FileServerUtil;
|
import com.gxwebsoft.common.core.utils.FileServerUtil;
|
||||||
|
import com.gxwebsoft.common.core.utils.MinioUtil;
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
import com.gxwebsoft.common.core.web.BaseController;
|
import com.gxwebsoft.common.core.web.BaseController;
|
||||||
@@ -37,6 +38,8 @@ import io.swagger.v3.oas.annotations.tags.Tag;
|
|||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.UploadObjectArgs;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
@@ -100,6 +103,9 @@ public class AliOssController extends BaseController {
|
|||||||
String accessKeySecret = settingInfo.getString("accessKeySecret");
|
String accessKeySecret = settingInfo.getString("accessKeySecret");
|
||||||
String uploadMethod = settingInfo.getString("uploadMethod");
|
String uploadMethod = settingInfo.getString("uploadMethod");
|
||||||
|
|
||||||
|
// 规范化 endpoint:去掉结尾斜杠,避免 minio-java 拼接出双斜杠导致签名不匹配
|
||||||
|
endpoint = MinioUtil.normalizeEndpoint(endpoint);
|
||||||
|
|
||||||
// 判断是否登录
|
// 判断是否登录
|
||||||
String authorization = getAuthorization();
|
String authorization = getAuthorization();
|
||||||
|
|
||||||
@@ -115,18 +121,19 @@ public class AliOssController extends BaseController {
|
|||||||
|
|
||||||
// 上传文件结果
|
// 上传文件结果
|
||||||
FileRecord result;
|
FileRecord result;
|
||||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
boolean isMinio = "minio".equals(uploadMethod);
|
||||||
// 创建OSSClient实例。
|
// MinIO 用官方 minio-java SDK(原生 AWS4-HMAC-SHA256 + path-style);
|
||||||
// MinIO 兼容 S3 协议,需开启 PathStyle 访问(endpoint/bucket/key)并使用 V4 签名
|
// aliyun OSS SDK 无 AWS4 签名器,对接不了 MinIO,因此 minio 分支独立使用 MinioClient。
|
||||||
OSS ossClient;
|
OSS ossClient = null;
|
||||||
if ("minio".equals(uploadMethod)) {
|
MinioClient minioClient = null;
|
||||||
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration();
|
if (isMinio) {
|
||||||
// 3.17 已移除 setPathStyleAccess;用 setExtractSettingFromEndpoint 让 SDK 对 MinIO 这类
|
// 显式指定 region=us-east-1,跳过 minio-java 的 getBucketLocation 自动探测
|
||||||
// 非阿里云 endpoint 自动按 path-style 访问,并从 endpoint 推导 region
|
// (该探测 GET 在 MinIO + 反代下会触发 SignatureDoesNotMatch)
|
||||||
minioConfig.setSignatureVersion(SignVersion.V4);
|
minioClient = MinioUtil.buildClient(endpoint, accessKeyId, accessKeySecret);
|
||||||
minioConfig.setExtractSettingFromEndpoint(true);
|
// bucket 不存在则自动创建,并套上匿名只读策略(存量裸桶也会自动补策略)
|
||||||
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider, minioConfig);
|
MinioUtil.ensureBucketReady(minioClient, endpoint, bucketName);
|
||||||
} else {
|
} else {
|
||||||
|
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||||
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,16 +144,18 @@ public class AliOssController extends BaseController {
|
|||||||
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
|
String path = upload.getAbsolutePath().replace("\\", "/").substring(dir.length());
|
||||||
String originalName = file.getOriginalFilename();
|
String originalName = file.getOriginalFilename();
|
||||||
|
|
||||||
// 创建PutObjectRequest对象。
|
// 上传文件到对象存储。
|
||||||
|
if (isMinio) {
|
||||||
|
// minio-java:path 此时无前导 "/",作为对象名(如 2026/08/07/xxx.png),默认 region=us-east-1
|
||||||
|
minioClient.uploadObject(UploadObjectArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(path)
|
||||||
|
.filename(upload.getAbsolutePath())
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
|
PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, path, upload);
|
||||||
// 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。
|
ossClient.putObject(putObjectRequest);
|
||||||
// ObjectMetadata metadata = new ObjectMetadata();
|
}
|
||||||
// metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString());
|
|
||||||
// metadata.setObjectAcl(CannedAccessControlList.Private);
|
|
||||||
// putObjectRequest.setMetadata(metadata);
|
|
||||||
|
|
||||||
// 上传文件。
|
|
||||||
PutObjectResult ossResult = ossClient.putObject(putObjectRequest);
|
|
||||||
|
|
||||||
// 保存记录并返回
|
// 保存记录并返回
|
||||||
result = new FileRecord();
|
result = new FileRecord();
|
||||||
@@ -177,7 +186,7 @@ public class AliOssController extends BaseController {
|
|||||||
|
|
||||||
// 根据文件类型设置缩略图和预览图
|
// 根据文件类型设置缩略图和预览图
|
||||||
// MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL
|
// MinIO 不支持阿里云 x-oss-process 图片处理参数,统一返回原图 URL
|
||||||
boolean isMinio = "minio".equals(uploadMethod);
|
isMinio = "minio".equals(uploadMethod);
|
||||||
if (FileServerUtil.isImage(contentType)) {
|
if (FileServerUtil.isImage(contentType)) {
|
||||||
if (isMinio) {
|
if (isMinio) {
|
||||||
result.setThumbnail(bucketDomain + path);
|
result.setThumbnail(bucketDomain + path);
|
||||||
@@ -228,10 +237,15 @@ public class AliOssController extends BaseController {
|
|||||||
+ "a serious internal problem while trying to communicate with OSS, "
|
+ "a serious internal problem while trying to communicate with OSS, "
|
||||||
+ "such as not being able to access the network.");
|
+ "such as not being able to access the network.");
|
||||||
System.out.println("Error Message:" + ce.getMessage());
|
System.out.println("Error Message:" + ce.getMessage());
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 兼容 minio-java 抛出的异常(MinioException / IOException 等)
|
||||||
|
System.out.println("Caught an upload Exception: " + e.getMessage());
|
||||||
|
e.printStackTrace();
|
||||||
} finally {
|
} finally {
|
||||||
if (ossClient != null) {
|
if (ossClient != null) {
|
||||||
ossClient.shutdown();
|
ossClient.shutdown();
|
||||||
}
|
}
|
||||||
|
// MinioClient 内部复用 HttpClient 连接池,无需显式关闭
|
||||||
}
|
}
|
||||||
return fail("上传失败", null);
|
return fail("上传失败", null);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -877,6 +877,31 @@ public class MainController extends BaseController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "校验邮箱验证码")
|
||||||
|
@PostMapping("/verifyEmailCaptcha")
|
||||||
|
public ApiResult<?> verifyEmailCaptcha(@RequestBody EmailCaptchaParam param) {
|
||||||
|
if (param == null || StrUtil.isBlank(param.getEmail())) {
|
||||||
|
return fail("邮箱不能为空");
|
||||||
|
}
|
||||||
|
if (!CommonUtil.isValidEmail(param.getEmail())) {
|
||||||
|
return fail("请输入有效的邮箱地址");
|
||||||
|
}
|
||||||
|
if (StrUtil.isBlank(param.getCode())) {
|
||||||
|
return fail("验证码不能为空");
|
||||||
|
}
|
||||||
|
// 与 sendEmailCaptcha 写入的 key 保持一致:emailCode:邮箱
|
||||||
|
String key = RedisConstants.EMAIL_CODE_KEY + ":" + param.getEmail();
|
||||||
|
String cachedCode = redisUtil.get(key);
|
||||||
|
String devCode = redisUtil.get(CACHE_KEY_VERIFICATION_CODE_BY_DEV_SMS);
|
||||||
|
if (cachedCode == null
|
||||||
|
|| (!param.getCode().equals(cachedCode) && !param.getCode().equals(devCode))) {
|
||||||
|
return fail("邮箱验证码不正确");
|
||||||
|
}
|
||||||
|
// 校验通过即删除,防止重复使用
|
||||||
|
redisUtil.delete(key);
|
||||||
|
return success("邮箱验证通过");
|
||||||
|
}
|
||||||
|
|
||||||
@OperationLog
|
@OperationLog
|
||||||
@Operation(summary = "重置密码")
|
@Operation(summary = "重置密码")
|
||||||
@PutMapping("/password")
|
@PutMapping("/password")
|
||||||
@@ -1184,6 +1209,13 @@ public class MainController extends BaseController {
|
|||||||
if (current == null) {
|
if (current == null) {
|
||||||
return fail("未登录或登录已过期", null);
|
return fail("未登录或登录已过期", null);
|
||||||
}
|
}
|
||||||
|
// 切换到当前登录用户所属租户(如注册成功后刷新权限令牌)直接放行:
|
||||||
|
// 注册即时场景下可切换租户列表校验可能未能命中刚创建的新超管,自己切自己一定合法。
|
||||||
|
if (targetTenantId.equals(current.getTenantId())) {
|
||||||
|
String access_token = generateToken(current);
|
||||||
|
loginRecordService.saveAsync(current.getUsername(), LoginRecord.TYPE_LOGIN, null, current.getTenantId(), request);
|
||||||
|
return success("切换成功", new LoginResult(access_token, current));
|
||||||
|
}
|
||||||
// 校验目标租户在可切换范围内(与 switchableTenants 一致)
|
// 校验目标租户在可切换范围内(与 switchableTenants 一致)
|
||||||
User target = findSwitchableUser(current, targetTenantId);
|
User target = findSwitchableUser(current, targetTenantId);
|
||||||
if (target == null) {
|
if (target == null) {
|
||||||
@@ -1571,6 +1603,7 @@ public class MainController extends BaseController {
|
|||||||
company.setEmail(email);
|
company.setEmail(email);
|
||||||
company.setPhone(phone);
|
company.setPhone(phone);
|
||||||
company.setPassword(password);
|
company.setPassword(password);
|
||||||
|
company.setAppCode(user.getAppCode());
|
||||||
company.setTid(tenant.getTenantId());
|
company.setTid(tenant.getTenantId());
|
||||||
company.setShortName(tenantName);
|
company.setShortName(tenantName);
|
||||||
company.setCategoryId(661);
|
company.setCategoryId(661);
|
||||||
|
|||||||
@@ -294,6 +294,10 @@ public class Company implements Serializable {
|
|||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String password;
|
private String password;
|
||||||
|
|
||||||
|
@Schema(description = "应用代号(非数据库字段,注册时由前端传入,用于生成开通邮件中的域名,如 mp/shop/site)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String appCode;
|
||||||
|
|
||||||
@Schema(description = "手机号(脱敏)")
|
@Schema(description = "手机号(脱敏)")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String mobile;
|
private String mobile;
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ public class User implements UserDetails {
|
|||||||
@Schema(description = "邮箱")
|
@Schema(description = "邮箱")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "应用代号(非数据库字段,注册时由前端传入,用于生成开通邮件中的域名,如 mp/shop/site)")
|
||||||
|
@TableField(exist = false)
|
||||||
|
private String appCode;
|
||||||
|
|
||||||
@Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)")
|
@Schema(description = "邮箱验证码(非数据库字段,仅用于绑定/修改邮箱时校验)")
|
||||||
@TableField(exist = false)
|
@TableField(exist = false)
|
||||||
private String emailCode;
|
private String emailCode;
|
||||||
|
|||||||
@@ -36,4 +36,13 @@ public interface RoleMenuMapper extends BaseMapper<RoleMenu> {
|
|||||||
@InterceptorIgnore(tenantLine = "true")
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
List<Menu> listMenuByRoleIds(@Param("roleIds") List<Integer> roleIds, @Param("menuType") Integer menuType);
|
List<Menu> listMenuByRoleIds(@Param("roleIds") List<Integer> roleIds, @Param("menuType") Integer menuType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨租户查询角色菜单(忽略租户拦截器)
|
||||||
|
*
|
||||||
|
* @param tenantId 租户id
|
||||||
|
* @return List<RoleMenu>
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
List<RoleMenu> selectListAll(@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,4 +62,13 @@ public interface UserRoleMapper extends BaseMapper<UserRole> {
|
|||||||
*/
|
*/
|
||||||
List<UserRole> selectListRel(@Param("param") UserRoleParam param);
|
List<UserRole> selectListRel(@Param("param") UserRoleParam param);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 跨租户查询用户角色(忽略租户拦截器)
|
||||||
|
*
|
||||||
|
* @param tenantId 租户id
|
||||||
|
* @return List<UserRole>
|
||||||
|
*/
|
||||||
|
@InterceptorIgnore(tenantLine = "true")
|
||||||
|
List<UserRole> selectListAll(@Param("tenantId") Integer tenantId);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,9 @@
|
|||||||
ORDER BY a.sort_number
|
ORDER BY a.sort_number
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 跨租户查询角色菜单 -->
|
||||||
|
<select id="selectListAll" resultType="com.gxwebsoft.common.system.entity.RoleMenu">
|
||||||
|
SELECT * FROM sys_role_menu WHERE tenant_id = #{tenantId}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -64,4 +64,9 @@
|
|||||||
<include refid="selectSql"></include>
|
<include refid="selectSql"></include>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 跨租户查询用户角色 -->
|
||||||
|
<select id="selectListAll" resultType="com.gxwebsoft.common.system.entity.UserRole">
|
||||||
|
SELECT * FROM sys_user_role WHERE tenant_id = #{tenantId}
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ public class EmailCaptchaParam implements Serializable {
|
|||||||
@Schema(description = "邮箱")
|
@Schema(description = "邮箱")
|
||||||
private String email;
|
private String email;
|
||||||
|
|
||||||
|
@Schema(description = "邮箱验证码(校验时使用)")
|
||||||
|
private String code;
|
||||||
|
|
||||||
@Schema(description = "租户ID")
|
@Schema(description = "租户ID")
|
||||||
private String tenantId;
|
private String tenantId;
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import com.aliyun.oss.common.auth.DefaultCredentialProvider;
|
|||||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.gxwebsoft.common.core.utils.CommonUtil;
|
import com.gxwebsoft.common.core.utils.CommonUtil;
|
||||||
|
import com.gxwebsoft.common.core.utils.MinioUtil;
|
||||||
import com.gxwebsoft.common.core.web.PageParam;
|
import com.gxwebsoft.common.core.web.PageParam;
|
||||||
import com.gxwebsoft.common.core.web.PageResult;
|
import com.gxwebsoft.common.core.web.PageResult;
|
||||||
import com.gxwebsoft.common.system.entity.Company;
|
import com.gxwebsoft.common.system.entity.Company;
|
||||||
@@ -22,6 +23,8 @@ import com.gxwebsoft.common.system.param.FileRecordParam;
|
|||||||
import com.gxwebsoft.common.system.service.CompanyService;
|
import com.gxwebsoft.common.system.service.CompanyService;
|
||||||
import com.gxwebsoft.common.system.service.FileRecordService;
|
import com.gxwebsoft.common.system.service.FileRecordService;
|
||||||
import com.gxwebsoft.common.system.service.SettingService;
|
import com.gxwebsoft.common.system.service.SettingService;
|
||||||
|
import io.minio.MinioClient;
|
||||||
|
import io.minio.RemoveObjectArgs;
|
||||||
import org.springframework.scheduling.annotation.Async;
|
import org.springframework.scheduling.annotation.Async;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@@ -96,24 +99,31 @@ public class FileRecordServiceImpl extends ServiceImpl<FileRecordMapper, FileRec
|
|||||||
String accessKeyId = uploadConfig.getString("accessKeyId");
|
String accessKeyId = uploadConfig.getString("accessKeyId");
|
||||||
String accessKeySecret = uploadConfig.getString("accessKeySecret");
|
String accessKeySecret = uploadConfig.getString("accessKeySecret");
|
||||||
String uploadMethod = uploadConfig.getString("uploadMethod");
|
String uploadMethod = uploadConfig.getString("uploadMethod");
|
||||||
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
// 规范化 endpoint:去掉结尾斜杠,避免 minio-java 拼接出双斜杠导致签名不匹配
|
||||||
// MinIO 兼容 S3 协议,需开启 PathStyle 访问 + V4 签名
|
endpoint = MinioUtil.normalizeEndpoint(endpoint);
|
||||||
OSS ossClient;
|
boolean isMinio = "minio".equals(uploadMethod);
|
||||||
if ("minio".equals(uploadMethod)) {
|
OSS ossClient = null;
|
||||||
ClientBuilderConfiguration minioConfig = new ClientBuilderConfiguration();
|
MinioClient minioClient = null;
|
||||||
// 3.17 已移除 setPathStyleAccess;用 setExtractSettingFromEndpoint 让 SDK 对 MinIO 这类
|
if (isMinio) {
|
||||||
// 非阿里云 endpoint 自动按 path-style 访问,并从 endpoint 推导 region
|
// MinIO 用官方 minio-java SDK(原生 AWS4 + path-style),aliyun OSS SDK 无法对接 MinIO
|
||||||
minioConfig.setSignatureVersion(SignVersion.V4);
|
// 显式指定 region=us-east-1,跳过 getBucketLocation 自动探测(该探测 GET 会触发 SignatureDoesNotMatch)
|
||||||
minioConfig.setExtractSettingFromEndpoint(true);
|
minioClient = MinioUtil.buildClient(endpoint, accessKeyId, accessKeySecret);
|
||||||
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider, minioConfig);
|
|
||||||
} else {
|
} else {
|
||||||
|
CredentialsProvider credentialsProvider = new DefaultCredentialProvider(accessKeyId, accessKeySecret);
|
||||||
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider);
|
||||||
}
|
}
|
||||||
for (FileRecord fileRecord : fileRecords) {
|
for (FileRecord fileRecord : fileRecords) {
|
||||||
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
|
fileRecord.setPath(StrUtil.replace(fileRecord.getPath(), bucketDomain.concat("/"), ""));
|
||||||
try {
|
try {
|
||||||
// 删除远程文件
|
// 删除远程文件
|
||||||
|
if (isMinio) {
|
||||||
|
minioClient.removeObject(RemoveObjectArgs.builder()
|
||||||
|
.bucket(bucketName)
|
||||||
|
.object(fileRecord.getPath())
|
||||||
|
.build());
|
||||||
|
} else {
|
||||||
ossClient.deleteObject(bucketName, fileRecord.getPath());
|
ossClient.deleteObject(bucketName, fileRecord.getPath());
|
||||||
|
}
|
||||||
// 释放空间大小
|
// 释放空间大小
|
||||||
if (fileRecord.getCompanyId() > 0) {
|
if (fileRecord.getCompanyId() > 0) {
|
||||||
Company company = companyService.getById(fileRecord.getCompanyId());
|
Company company = companyService.getById(fileRecord.getCompanyId());
|
||||||
|
|||||||
@@ -10,8 +10,12 @@ import com.gxwebsoft.common.core.utils.DomainUtil;
|
|||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||||
import com.gxwebsoft.common.system.entity.*;
|
import com.gxwebsoft.common.system.entity.*;
|
||||||
import com.gxwebsoft.common.system.mapper.MenuMapper;
|
import com.gxwebsoft.common.system.mapper.MenuMapper;
|
||||||
|
import com.gxwebsoft.common.system.mapper.RoleMapper;
|
||||||
|
import com.gxwebsoft.common.system.mapper.RoleMenuMapper;
|
||||||
|
import com.gxwebsoft.common.system.mapper.UserRoleMapper;
|
||||||
import com.gxwebsoft.common.system.mapper.TenantMapper;
|
import com.gxwebsoft.common.system.mapper.TenantMapper;
|
||||||
import com.gxwebsoft.common.system.param.MenuParam;
|
import com.gxwebsoft.common.system.param.MenuParam;
|
||||||
|
import com.gxwebsoft.common.system.param.RoleParam;
|
||||||
import com.gxwebsoft.common.system.service.*;
|
import com.gxwebsoft.common.system.service.*;
|
||||||
import com.gxwebsoft.common.system.param.TenantParam;
|
import com.gxwebsoft.common.system.param.TenantParam;
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
import com.gxwebsoft.common.core.web.ApiResult;
|
||||||
@@ -26,6 +30,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -39,8 +44,6 @@ import java.util.stream.Collectors;
|
|||||||
*/
|
*/
|
||||||
@Service
|
@Service
|
||||||
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> implements TenantService {
|
||||||
// cms建站权限标识前缀
|
|
||||||
private static final String CMS_AUTHORITY_PREFIX = "cms:";
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
private CompanyService companyService;
|
private CompanyService companyService;
|
||||||
@@ -49,6 +52,12 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
@Resource
|
@Resource
|
||||||
private MenuMapper menuMapper;
|
private MenuMapper menuMapper;
|
||||||
@Resource
|
@Resource
|
||||||
|
private RoleMapper roleMapper;
|
||||||
|
@Resource
|
||||||
|
private RoleMenuMapper roleMenuMapper;
|
||||||
|
@Resource
|
||||||
|
private UserRoleMapper userRoleMapper;
|
||||||
|
@Resource
|
||||||
private RoleMenuService roleMenuService;
|
private RoleMenuService roleMenuService;
|
||||||
@Resource
|
@Resource
|
||||||
private RoleService roleService;
|
private RoleService roleService;
|
||||||
@@ -610,17 +619,38 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
// 发送邮件通知
|
// 发送邮件通知(按应用代号区分域名:前端注册时传入 appCode,未知默认 site)
|
||||||
String title = "恭喜!您的应用已创建成功";
|
// mp -> https://mp-{tid}.shoplnk.cn 后台 https://mp.websoft.top
|
||||||
String appUrl = "\r\n应用地址:" + DomainUtil.getSiteUrl(company.getTid().toString());
|
// shop -> https://shop-{tid}.shoplnk.cn 后台 https://shop.websoft.top
|
||||||
String appName = "\r\n应用名称:" + company.getShortName();
|
// site -> https://site-{tid}.sitelnk.cn 后台 https://site.websoft.top
|
||||||
String adminUrl = "\r\n后台管理:" + DomainUtil.getAdminUrl(company.getTid().toString());
|
final Map<String, String[]> appDomainMap = new HashMap<>(3);
|
||||||
String account = "\r\n账号:admin";
|
appDomainMap.put("mp", new String[]{"mp", "shoplnk.cn", "mp.websoft.top"});
|
||||||
String password = "\r\n密码:" + company.getPassword();
|
appDomainMap.put("shop", new String[]{"shop", "shoplnk.cn", "shop.websoft.top"});
|
||||||
String content = title + appUrl + appName + adminUrl + account + password;
|
appDomainMap.put("site", new String[]{"site", "sitelnk.cn", "site.websoft.top"});
|
||||||
|
final String appCode = StrUtil.isBlank(company.getAppCode()) ? "site" : company.getAppCode();
|
||||||
|
final String[] domainCfg = appDomainMap.getOrDefault(appCode, appDomainMap.get("site"));
|
||||||
|
final String sitePrefix = domainCfg[0];
|
||||||
|
final String siteDomain = domainCfg[1];
|
||||||
|
final String adminHost = domainCfg[2];
|
||||||
|
final String siteUrl = "https://" + sitePrefix + "-" + company.getTid() + "." + siteDomain;
|
||||||
|
final String adminUrl = "https://" + adminHost;
|
||||||
|
final String title = "您的企业官网已开通成功";
|
||||||
|
final StringBuilder content = new StringBuilder();
|
||||||
|
content.append("尊敬的用户:\r\n\r\n");
|
||||||
|
content.append("恭喜!您的企业官网「").append(company.getShortName()).append("」已开通成功。\r\n\r\n");
|
||||||
|
content.append("您可以使用以下信息登录管理后台,继续完善站点内容:\r\n\r\n");
|
||||||
|
content.append("• 官网地址:").append(siteUrl).append("\r\n");
|
||||||
|
content.append("• 后台地址:").append(adminUrl).append("\r\n");
|
||||||
|
content.append("• 登录账号:").append(company.getEmail()).append("\r\n");
|
||||||
|
content.append("• 登录密码:").append(company.getPassword()).append("\r\n\r\n");
|
||||||
|
content.append("安全提示:\r\n");
|
||||||
|
content.append("1. 请妥善保管登录密码,不要通过邮件、聊天工具发送给他人。\r\n");
|
||||||
|
content.append("2. 建议首次登录后立即进入「个人中心 > 修改密码」重置密码。\r\n\r\n");
|
||||||
|
content.append("点击登录后台:").append(adminUrl).append("\r\n\r\n");
|
||||||
|
content.append("如有疑问,请联系客服或访问帮助中心。");
|
||||||
// 发送邮件通知
|
// 发送邮件通知
|
||||||
if (company.getEmail() != null) {
|
if (company.getEmail() != null) {
|
||||||
emailRecordService.sendEmail(title, content, company.getEmail(), company.getTid());
|
emailRecordService.sendEmail(title, content.toString(), company.getEmail(), company.getTid());
|
||||||
}
|
}
|
||||||
return company;
|
return company;
|
||||||
}
|
}
|
||||||
@@ -654,21 +684,70 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = {Exception.class})
|
@Transactional(rollbackFor = {Exception.class})
|
||||||
public ApiResult<?> grantCmsPermission(Integer refTenant, Integer curTenant) {
|
public ApiResult<?> grantCmsPermission(Integer refTenant, Integer curTenant) {
|
||||||
// 1.当前租户的超级管理员角色, 必须带租户条件
|
if (refTenant == null || curTenant == null) {
|
||||||
final Role superAdmin = roleService.getOne(new LambdaQueryWrapper<Role>()
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户或当前租户为空");
|
||||||
.eq(Role::getRoleCode, "superAdmin")
|
|
||||||
.eq(Role::getTenantId, curTenant)
|
|
||||||
.last("limit 1"));
|
|
||||||
if (superAdmin == null) {
|
|
||||||
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "当前租户没有superAdmin角色, 无法授权");
|
|
||||||
}
|
}
|
||||||
final Integer roleId = superAdmin.getRoleId();
|
if (refTenant.equals(curTenant)) {
|
||||||
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户不能与当前租户相同");
|
||||||
|
}
|
||||||
|
final Map<String, Object> data = new HashMap<>();
|
||||||
|
data.put("refTenantId", refTenant);
|
||||||
|
data.put("tenantId", curTenant);
|
||||||
|
|
||||||
// 2.跨租户读取参考租户的全部未删除菜单
|
// ===== 1. 克隆角色 sys_role(按 role_code 去重,superAdmin/admin/user 已存在则复用)=====
|
||||||
final MenuParam refParam = new MenuParam();
|
final RoleParam refRoleParam = new RoleParam();
|
||||||
refParam.setTenantId(refTenant);
|
refRoleParam.setTenantId(refTenant);
|
||||||
refParam.setDeleted(0);
|
final List<Role> refRoles = roleMapper.selectListAll(refRoleParam);
|
||||||
final List<Menu> refMenus = menuMapper.getMenuByClone(refParam);
|
if (CollectionUtils.isEmpty(refRoles)) {
|
||||||
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的角色");
|
||||||
|
}
|
||||||
|
final RoleParam curRoleParam = new RoleParam();
|
||||||
|
curRoleParam.setTenantId(curTenant);
|
||||||
|
// role_code -> Role(当前租户已存在的角色)
|
||||||
|
final Map<String, Role> curRoleByCode = roleMapper.selectListAll(curRoleParam).stream()
|
||||||
|
.collect(Collectors.toMap(Role::getRoleCode, r -> r, (a, b) -> a));
|
||||||
|
// oldRoleId -> newRoleId 映射
|
||||||
|
final Map<Integer, Integer> roleIdMapping = new HashMap<>();
|
||||||
|
// 待新增角色(按 role_code 去重)
|
||||||
|
final Map<String, Role> toCreateByCode = new LinkedHashMap<>();
|
||||||
|
for (Role r : refRoles) {
|
||||||
|
final Role exist = curRoleByCode.get(r.getRoleCode());
|
||||||
|
if (exist != null) {
|
||||||
|
roleIdMapping.put(r.getRoleId(), exist.getRoleId());
|
||||||
|
} else if (!toCreateByCode.containsKey(r.getRoleCode())) {
|
||||||
|
final Role copy = new Role();
|
||||||
|
copy.setRoleCode(r.getRoleCode());
|
||||||
|
copy.setRoleName(r.getRoleName());
|
||||||
|
copy.setComments(r.getComments());
|
||||||
|
copy.setSortNumber(r.getSortNumber());
|
||||||
|
copy.setTenantId(curTenant);
|
||||||
|
toCreateByCode.put(r.getRoleCode(), copy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int roleCreated = 0;
|
||||||
|
if (!toCreateByCode.isEmpty()) {
|
||||||
|
roleService.saveBatch(new ArrayList<>(toCreateByCode.values()));
|
||||||
|
// 重新读取当前租户角色,回填 oldRoleId -> newRoleId 映射
|
||||||
|
final Map<String, Role> refreshed = roleMapper.selectListAll(curRoleParam).stream()
|
||||||
|
.collect(Collectors.toMap(Role::getRoleCode, r -> r, (a, b) -> a));
|
||||||
|
for (Role r : refRoles) {
|
||||||
|
final Role nr = refreshed.get(r.getRoleCode());
|
||||||
|
if (nr != null) {
|
||||||
|
roleIdMapping.put(r.getRoleId(), nr.getRoleId());
|
||||||
|
curRoleByCode.putIfAbsent(r.getRoleCode(), nr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
roleCreated = toCreateByCode.size();
|
||||||
|
}
|
||||||
|
// 当前租户全部角色:roleId -> Role(含新克隆的),供后续 role_code 对齐使用
|
||||||
|
final Map<Integer, Role> newRoleById = new HashMap<>();
|
||||||
|
curRoleByCode.values().forEach(r -> newRoleById.put(r.getRoleId(), r));
|
||||||
|
|
||||||
|
// ===== 2. 克隆菜单 sys_menu(按层级复制,父先于子,标题/权限去重)=====
|
||||||
|
final MenuParam refMenuParam = new MenuParam();
|
||||||
|
refMenuParam.setTenantId(refTenant);
|
||||||
|
refMenuParam.setDeleted(0);
|
||||||
|
final List<Menu> refMenus = menuMapper.getMenuByClone(refMenuParam);
|
||||||
if (CollectionUtils.isEmpty(refMenus)) {
|
if (CollectionUtils.isEmpty(refMenus)) {
|
||||||
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的菜单");
|
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有可复制的菜单");
|
||||||
}
|
}
|
||||||
@@ -676,42 +755,19 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
for (Menu menu : refMenus) {
|
for (Menu menu : refMenus) {
|
||||||
refMenuMap.put(menu.getMenuId(), menu);
|
refMenuMap.put(menu.getMenuId(), menu);
|
||||||
}
|
}
|
||||||
|
final MenuParam curMenuParam = new MenuParam();
|
||||||
// 3.筛选cms:*子树, 权限标识以cms:开头的节点 + 其全部祖先, 保证父级完整
|
curMenuParam.setTenantId(curTenant);
|
||||||
final Set<Integer> cmsMenuIds = new HashSet<>();
|
curMenuParam.setDeleted(0);
|
||||||
for (Menu menu : refMenus) {
|
|
||||||
if (menu.getAuthority() == null || !menu.getAuthority().startsWith(CMS_AUTHORITY_PREFIX)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
Menu current = menu;
|
|
||||||
// add返回false说明该节点及其祖先已收录, 无需继续向上追溯
|
|
||||||
while (current != null && cmsMenuIds.add(current.getMenuId())) {
|
|
||||||
final Integer parentId = current.getParentId();
|
|
||||||
current = (parentId == null || parentId == 0) ? null : refMenuMap.get(parentId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (cmsMenuIds.isEmpty()) {
|
|
||||||
return new ApiResult<>(Constants.RESULT_ERROR_CODE, "参考租户[" + refTenant + "]没有cms权限菜单");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4.当前租户已有菜单, 用于去重, 保证接口幂等
|
|
||||||
final MenuParam curParam = new MenuParam();
|
|
||||||
curParam.setTenantId(curTenant);
|
|
||||||
curParam.setDeleted(0);
|
|
||||||
final Map<String, Integer> curMenuKeys = new HashMap<>();
|
final Map<String, Integer> curMenuKeys = new HashMap<>();
|
||||||
for (Menu menu : menuMapper.getMenuByClone(curParam)) {
|
for (Menu menu : menuMapper.getMenuByClone(curMenuParam)) {
|
||||||
curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId());
|
curMenuKeys.putIfAbsent(getMenuKey(menu), menu.getMenuId());
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5.按层级复制菜单, 父菜单先于子菜单插入
|
|
||||||
final List<Menu> sources = refMenus.stream()
|
final List<Menu> sources = refMenus.stream()
|
||||||
.filter(d -> cmsMenuIds.contains(d.getMenuId()))
|
|
||||||
.sorted(Comparator.comparingInt((Menu d) -> getMenuDepth(d, refMenuMap)))
|
.sorted(Comparator.comparingInt((Menu d) -> getMenuDepth(d, refMenuMap)))
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
final Map<Integer, Integer> menuIdMapping = new HashMap<>();
|
final Map<Integer, Integer> menuIdMapping = new HashMap<>();
|
||||||
int menuCount = 0;
|
int menuCount = 0;
|
||||||
for (Menu source : sources) {
|
for (Menu source : sources) {
|
||||||
// 已存在的菜单不重复插入, 直接复用其菜单id作为子菜单的父级
|
|
||||||
final Integer existsMenuId = curMenuKeys.get(getMenuKey(source));
|
final Integer existsMenuId = curMenuKeys.get(getMenuKey(source));
|
||||||
if (existsMenuId != null) {
|
if (existsMenuId != null) {
|
||||||
menuIdMapping.put(source.getMenuId(), existsMenuId);
|
menuIdMapping.put(source.getMenuId(), existsMenuId);
|
||||||
@@ -737,32 +793,88 @@ public class TenantServiceImpl extends ServiceImpl<TenantMapper, Tenant> impleme
|
|||||||
menuCount++;
|
menuCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6.绑定cms菜单到超级管理员角色, 已绑定的跳过
|
// ===== 3. 克隆角色菜单 sys_role_menu(roleId/menuId 用映射转换,去重)=====
|
||||||
final Set<Integer> boundMenuIds = roleMenuService.list(new LambdaQueryWrapper<RoleMenu>()
|
final Set<String> curRmKeys = roleMenuMapper.selectListAll(curTenant).stream()
|
||||||
.eq(RoleMenu::getRoleId, roleId))
|
.map(rm -> rm.getRoleId() + "_" + rm.getMenuId())
|
||||||
.stream().map(RoleMenu::getMenuId).collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
final List<RoleMenu> roleMenus = new ArrayList<>();
|
final List<RoleMenu> rmsToSave = new ArrayList<>();
|
||||||
for (Integer menuId : menuIdMapping.values()) {
|
for (RoleMenu rm : roleMenuMapper.selectListAll(refTenant)) {
|
||||||
if (menuId == null || boundMenuIds.contains(menuId)) {
|
final Integer newRoleId = roleIdMapping.get(rm.getRoleId());
|
||||||
|
final Integer newMenuId = menuIdMapping.get(rm.getMenuId());
|
||||||
|
if (newRoleId == null || newMenuId == null) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final RoleMenu roleMenu = new RoleMenu();
|
final String key = newRoleId + "_" + newMenuId;
|
||||||
roleMenu.setRoleId(roleId);
|
if (curRmKeys.contains(key)) {
|
||||||
roleMenu.setMenuId(menuId);
|
continue;
|
||||||
roleMenu.setTenantId(curTenant);
|
|
||||||
roleMenus.add(roleMenu);
|
|
||||||
}
|
}
|
||||||
if (!roleMenus.isEmpty()) {
|
final RoleMenu copy = new RoleMenu();
|
||||||
roleMenuService.saveBatch(roleMenus);
|
copy.setRoleId(newRoleId);
|
||||||
|
copy.setMenuId(newMenuId);
|
||||||
|
copy.setTenantId(curTenant);
|
||||||
|
rmsToSave.add(copy);
|
||||||
|
curRmKeys.add(key);
|
||||||
|
}
|
||||||
|
int roleMenuCreated = 0;
|
||||||
|
if (!rmsToSave.isEmpty()) {
|
||||||
|
roleMenuService.saveBatch(rmsToSave);
|
||||||
|
roleMenuCreated = rmsToSave.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
final Map<String, Object> data = new HashMap<>();
|
// ===== 4. 克隆用户角色 sys_user_role(按 role_code 对齐当前租户用户,去重)=====
|
||||||
data.put("refTenantId", refTenant);
|
final List<UserRole> curUserRoles = userRoleMapper.selectListAll(curTenant);
|
||||||
data.put("tenantId", curTenant);
|
final Set<String> curUrKeys = curUserRoles.stream()
|
||||||
data.put("roleId", roleId);
|
.map(ur -> ur.getUserId() + "_" + ur.getRoleId())
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
// 当前租户 userId -> 其 role_code 集合
|
||||||
|
final Map<Integer, Set<String>> userRoleCodes = new HashMap<>();
|
||||||
|
for (UserRole ur : curUserRoles) {
|
||||||
|
final Role role = newRoleById.get(ur.getRoleId());
|
||||||
|
if (role == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
userRoleCodes.computeIfAbsent(ur.getUserId(), k -> new HashSet<>()).add(role.getRoleCode());
|
||||||
|
}
|
||||||
|
final List<UserRole> ursToSave = new ArrayList<>();
|
||||||
|
for (UserRole ur : userRoleMapper.selectListAll(refTenant)) {
|
||||||
|
final Integer newRoleId = roleIdMapping.get(ur.getRoleId());
|
||||||
|
if (newRoleId == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final Role newRole = newRoleById.get(newRoleId);
|
||||||
|
if (newRole == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String roleCode = newRole.getRoleCode();
|
||||||
|
// 找当前租户中 role_code 相同的用户,建立绑定
|
||||||
|
for (Map.Entry<Integer, Set<String>> e : userRoleCodes.entrySet()) {
|
||||||
|
if (!e.getValue().contains(roleCode)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final String key = e.getKey() + "_" + newRoleId;
|
||||||
|
if (curUrKeys.contains(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final UserRole copy = new UserRole();
|
||||||
|
copy.setUserId(e.getKey());
|
||||||
|
copy.setRoleId(newRoleId);
|
||||||
|
copy.setTenantId(curTenant);
|
||||||
|
ursToSave.add(copy);
|
||||||
|
curUrKeys.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int userRoleCreated = 0;
|
||||||
|
if (!ursToSave.isEmpty()) {
|
||||||
|
userRoleService.saveBatch(ursToSave);
|
||||||
|
userRoleCreated = ursToSave.size();
|
||||||
|
}
|
||||||
|
|
||||||
|
data.put("roleTotal", refRoles.size());
|
||||||
|
data.put("roleCreated", roleCreated);
|
||||||
data.put("menuTotal", menuIdMapping.size());
|
data.put("menuTotal", menuIdMapping.size());
|
||||||
data.put("menuCreated", menuCount);
|
data.put("menuCreated", menuCount);
|
||||||
data.put("roleMenuCreated", roleMenus.size());
|
data.put("roleMenuCreated", roleMenuCreated);
|
||||||
|
data.put("userRoleCreated", userRoleCreated);
|
||||||
return new ApiResult<>(Constants.RESULT_OK_CODE, "授权成功", data);
|
return new ApiResult<>(Constants.RESULT_OK_CODE, "授权成功", data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user