Răsfoiți Sursa

Merge branch 'dev' of VberAdmin/VberAdminPlusV3 into main

YueYunyun 2 ani în urmă
părinte
comite
4a97687730
37 a modificat fișierele cu 794 adăugiri și 98 ștergeri
  1. 1 1
      SERVER/VberAdminPlusV3/pom.xml
  2. 0 1
      SERVER/VberAdminPlusV3/vber-admin/Dockerfile
  3. 1 1
      SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/controller/AuthController.java
  4. 2 0
      SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/constant/CacheNames.java
  5. 1 0
      SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/constant/GlobalConstants.java
  6. 9 1
      SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/service/UserService.java
  7. 15 0
      SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/utils/SetUtils.java
  8. 6 5
      SERVER/VberAdminPlusV3/vber-common/vber-common-redis/src/main/java/com/vber/common/redis/utils/CacheUtils.java
  9. 13 2
      SERVER/VberAdminPlusV3/vber-common/vber-common-tenant/src/main/java/com/vber/common/tenant/helper/TenantHelper.java
  10. 3 1
      SERVER/VberAdminPlusV3/vber-common/vber-common-translation/src/main/java/com/vber/common/translation/core/impl/NicknameTranslationImpl.java
  11. 10 0
      SERVER/VberAdminPlusV3/vber-modules/vber-generator/pom.xml
  12. 35 0
      SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/controller/ProjectReactorController.java
  13. 217 0
      SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/core/ProjectReactor.java
  14. 34 0
      SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/domain/ProjectInfo.java
  15. 21 0
      SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/test/java/com/vber/reactor/ProjectReactorTest.java
  16. 26 2
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/ISysUserService.java
  17. 6 0
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysMenuServiceImpl.java
  18. 72 5
      SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysUserServiceImpl.java
  19. 3 1
      UI/VAP_V3.VUE/src/api/system/_tenant.ts
  20. 8 0
      UI/VAP_V3.VUE/src/api/tool/_gen.ts
  21. 1 0
      UI/VAP_V3.VUE/src/core/services/RequestService.ts
  22. 1 0
      UI/VAP_V3.VUE/src/layouts/main/content/Content.vue
  23. 41 21
      UI/VAP_V3.VUE/src/layouts/main/header/navbar/DynamicTenant.vue
  24. 1 1
      UI/VAP_V3.VUE/src/layouts/main/header/navbar/UserAccountMenu.vue
  25. 1 1
      UI/VAP_V3.VUE/src/layouts/main/toolbar/tag-view/Index.vue
  26. 12 1
      UI/VAP_V3.VUE/src/router/index.ts
  27. 3 28
      UI/VAP_V3.VUE/src/stores/_auth.ts
  28. 8 8
      UI/VAP_V3.VUE/src/stores/_tagView.ts
  29. 105 0
      UI/VAP_V3.VUE/src/stores/_tenant.ts
  30. 3 0
      UI/VAP_V3.VUE/src/stores/index.ts
  31. 11 7
      UI/VAP_V3.VUE/src/views/account/login.vue
  32. 2 2
      UI/VAP_V3.VUE/src/views/account/social/callback.vue
  33. 1 2
      UI/VAP_V3.VUE/src/views/redirect/index.vue
  34. 15 0
      UI/VAP_V3.VUE/src/views/redirect/tenant.vue
  35. 24 0
      UI/VAP_V3.VUE/src/views/system/tenant/index.vue
  36. 4 6
      UI/VAP_V3.VUE/src/views/system/user/index.vue
  37. 78 1
      UI/VAP_V3.VUE/src/views/tool/gen/index.vue

+ 1 - 1
SERVER/VberAdminPlusV3/pom.xml

@@ -9,7 +9,7 @@
     <version>${revision}</version>
     <packaging>pom</packaging>
     <name>${project.artifactId}</name>
-    <description>VberAdminPlus后台管理系统</description>
+    <description>VAP后台管理系统</description>
     <modules>
         <module>vber-common</module>
         <module>vber-modules</module>

+ 0 - 1
SERVER/VberAdminPlusV3/vber-admin/Dockerfile

@@ -1,4 +1,3 @@
-#FROM findepi/graalvm:java17-native
 FROM openjdk:17.0.2-oraclelinux8
 
 MAINTAINER IWB

+ 1 - 1
SERVER/VberAdminPlusV3/vber-admin/src/main/java/com/vber/web/controller/AuthController.java

@@ -100,7 +100,7 @@ public class AuthController {
 
         Long userId = LoginHelper.getUserId();
         scheduledExecutorService.schedule(() -> {
-            WebSocketUtils.sendMessage(userId, "欢迎登录VberAdminPlus后台管理系统");
+            WebSocketUtils.sendMessage(userId, "欢迎登录VAP后台管理系统");
         }, 3, TimeUnit.SECONDS);
         return R.ok(loginVo);
     }

+ 2 - 0
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/constant/CacheNames.java

@@ -45,6 +45,8 @@ public interface CacheNames {
      * 用户名称
      */
     String SYS_NICKNAME = "sys_nickname#30d";
+    String SYS_PHONE = "sys_phone#30d";
+    String SYS_EMAIL = "sys_email#30d";
 
     /**
      * 组织机构

+ 1 - 0
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/constant/GlobalConstants.java

@@ -36,4 +36,5 @@ public interface GlobalConstants {
      * 三方认证 redis key
      */
     String SOCIAL_AUTH_CODE_KEY = GLOBAL_REDIS_KEY + "social_auth_codes:";
+    String TENANT_ID_HEADER = "TenantId";
 }

+ 9 - 1
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/service/UserService.java

@@ -19,8 +19,16 @@ public interface UserService {
      * 通过用户ID查询用户账户
      *
      * @param userId 用户ID
-     * @return 用户账户
+     * @return 用户昵称
      */
     String selectNicknameById(Long userId);
 
+    /**
+     * 通过用户ID查询用户账户
+     *
+     * @param userIds 用户ID  多个用逗号隔开
+     * @return 用户昵称
+     */
+    String selectNicknameByIds(String userIds);
+
 }

+ 15 - 0
SERVER/VberAdminPlusV3/vber-common/vber-common-core/src/main/java/com/vber/common/core/utils/SetUtils.java

@@ -0,0 +1,15 @@
+package com.vber.common.core.utils;
+
+import cn.hutool.core.collection.CollUtil;
+
+import java.util.Set;
+
+/**
+ * @author Yue
+ */
+public class SetUtils {
+    @SafeVarargs
+    public static <T> Set<T> asSet(T... objs) {
+        return CollUtil.newHashSet(objs);
+    }
+}

+ 6 - 5
SERVER/VberAdminPlusV3/vber-common/vber-common-redis/src/main/java/com/vber/common/redis/utils/CacheUtils.java

@@ -7,6 +7,7 @@ import org.redisson.api.RMap;
 import org.springframework.cache.Cache;
 import org.springframework.cache.CacheManager;
 
+import java.util.Objects;
 import java.util.Set;
 
 /**
@@ -26,7 +27,7 @@ public class CacheUtils {
      * @param cacheNames 缓存组名称
      */
     public static Set<Object> keys(String cacheNames) {
-        RMap<Object, Object> rmap = (RMap<Object, Object>) CACHE_MANAGER.getCache(cacheNames).getNativeCache();
+        RMap<Object, Object> rmap = (RMap<Object, Object>) Objects.requireNonNull(CACHE_MANAGER.getCache(cacheNames)).getNativeCache();
         return rmap.keySet();
     }
 
@@ -37,7 +38,7 @@ public class CacheUtils {
      * @param key        缓存key
      */
     public static <T> T get(String cacheNames, Object key) {
-        Cache.ValueWrapper wrapper = CACHE_MANAGER.getCache(cacheNames).get(key);
+        Cache.ValueWrapper wrapper = Objects.requireNonNull(CACHE_MANAGER.getCache(cacheNames)).get(key);
         return wrapper != null ? (T) wrapper.get() : null;
     }
 
@@ -49,7 +50,7 @@ public class CacheUtils {
      * @param value      缓存值
      */
     public static void put(String cacheNames, Object key, Object value) {
-        CACHE_MANAGER.getCache(cacheNames).put(key, value);
+        Objects.requireNonNull(CACHE_MANAGER.getCache(cacheNames)).put(key, value);
     }
 
     /**
@@ -59,7 +60,7 @@ public class CacheUtils {
      * @param key        缓存key
      */
     public static void evict(String cacheNames, Object key) {
-        CACHE_MANAGER.getCache(cacheNames).evict(key);
+        Objects.requireNonNull(CACHE_MANAGER.getCache(cacheNames)).evict(key);
     }
 
     /**
@@ -68,7 +69,7 @@ public class CacheUtils {
      * @param cacheNames 缓存组名称
      */
     public static void clear(String cacheNames) {
-        CACHE_MANAGER.getCache(cacheNames).clear();
+        Objects.requireNonNull(CACHE_MANAGER.getCache(cacheNames)).clear();
     }
 
 }

+ 13 - 2
SERVER/VberAdminPlusV3/vber-common/vber-common-tenant/src/main/java/com/vber/common/tenant/helper/TenantHelper.java

@@ -7,10 +7,12 @@ import com.alibaba.ttl.TransmittableThreadLocal;
 import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
 import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
 import com.vber.common.core.constant.GlobalConstants;
+import com.vber.common.core.utils.ServletUtils;
 import com.vber.common.core.utils.SpringUtils;
 import com.vber.common.core.utils.StringUtils;
 import com.vber.common.redis.utils.RedisUtils;
 import com.vber.common.satoken.utils.LoginHelper;
+import jakarta.servlet.http.HttpServletRequest;
 import lombok.AccessLevel;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -164,13 +166,22 @@ public class TenantHelper {
     }
 
     /**
-     * 获取当前租户id(动态租户优先)
+     * 获取当前租户id(请求头优先,其次动态租户优先)
      */
     public static String getTenantId() {
         if (!isEnable()) {
             return null;
         }
-        String tenantId = TenantHelper.getDynamic();
+        String tenantId = "";
+        // 请求头获取的租户id优先级最高
+        HttpServletRequest request = ServletUtils.getRequest();
+        if (request != null) {
+            tenantId = request.getHeader(GlobalConstants.TENANT_ID_HEADER);
+        }
+        // 其次动态租户优先
+        if (StringUtils.isBlank(tenantId)) {
+            tenantId = TenantHelper.getDynamic();
+        }
         if (StringUtils.isBlank(tenantId)) {
             tenantId = LoginHelper.getTenantId();
         }

+ 3 - 1
SERVER/VberAdminPlusV3/vber-common/vber-common-translation/src/main/java/com/vber/common/translation/core/impl/NicknameTranslationImpl.java

@@ -1,10 +1,10 @@
 package com.vber.common.translation.core.impl;
 
-import lombok.AllArgsConstructor;
 import com.vber.common.core.service.UserService;
 import com.vber.common.translation.annotation.TranslationType;
 import com.vber.common.translation.constant.TransConstant;
 import com.vber.common.translation.core.TranslationInterface;
+import lombok.AllArgsConstructor;
 
 /**
  * 用户名称翻译实现
@@ -21,6 +21,8 @@ public class NicknameTranslationImpl implements TranslationInterface<String> {
     public String translation(Object key, String other) {
         if (key instanceof Long id) {
             return userService.selectNicknameById(id);
+        } else if (key instanceof String ids) {
+            return userService.selectNicknameByIds(ids);
         }
         return null;
     }

+ 10 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/pom.xml

@@ -45,6 +45,16 @@
             <groupId>org.apache.velocity</groupId>
             <artifactId>velocity-engine-core</artifactId>
         </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>test</scope>
+        </dependency>
     </dependencies>
 
 

+ 35 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/controller/ProjectReactorController.java

@@ -0,0 +1,35 @@
+package com.vber.reactor.controller;
+
+import com.vber.common.core.domain.R;
+import com.vber.common.web.core.BaseController;
+import com.vber.reactor.core.ProjectReactor;
+import com.vber.reactor.domain.ProjectInfo;
+import lombok.RequiredArgsConstructor;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author Yue
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/tool/project")
+public class ProjectReactorController extends BaseController {
+
+    /**
+     * 生成新项目
+     */
+    @PostMapping("/reactor")
+    public R<Void> projectReactor(@Validated @RequestBody ProjectInfo projectInfo) {
+        try {
+            ProjectReactor.build(projectInfo);
+        } catch (Exception e) {
+            return R.fail(e.getMessage());
+        }
+        return R.ok();
+    }
+}

+ 217 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/core/ProjectReactor.java

@@ -0,0 +1,217 @@
+package com.vber.reactor.core;
+
+import cn.hutool.core.io.FileTypeUtil;
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import com.vber.common.core.utils.SetUtils;
+import com.vber.reactor.domain.ProjectInfo;
+import jakarta.validation.constraints.NotNull;
+import lombok.extern.slf4j.Slf4j;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.stream.Collectors;
+
+import static java.io.File.separator;
+
+/**
+ * @author Yue
+ */
+@Slf4j
+public class ProjectReactor {
+    private static final String GROUP_ID = "com.vap";
+    private static final String ARTIFACT_ID = "VberAdminPlus";
+    private static final String VERSION = "V3";
+    private static final String ARTIFACT_ID_VERSION = ARTIFACT_ID + VERSION;
+    private static final String PACKAGE_NAME = "com.vber";
+    private static final String MODULE_PREFIX = "vber-";
+    private static final String MODULE_ARTIFACT_ID_PREFIX = "<artifactId>" + MODULE_PREFIX;
+    private static final String MODULE_ID_PREFIX = "<module>" + MODULE_PREFIX;
+    private static final String TITLE = "VAP后台管理系统";
+    private static final String UI_VUE_FILE = "VAP_V3.VUE";
+    private static final String UI_APP_FILE = "VAP_V3.APP";
+    /**
+     * 白名单文件,不进行重写,避免出问题
+     */
+    private static final Set<String> WHITE_FILE_TYPES = SetUtils.asSet("gif", "jpg", "svg", "png", // 图片
+            "eot", "woff2", "ttf", "woff"); // 字体
+
+    /**
+     * 构建项目。
+     * 该方法使用提供的参数配置项目信息,并调用另一个方法进行实际的项目构建。
+     *
+     * @param groupId      项目的组ID,标识项目的所属组织或组。
+     * @param artifactId   项目的artifact ID,是项目在组内的唯一标识。
+     * @param packageName  项目的包名,用于Java项目的包结构定义。
+     * @param title        项目的标题,可用于描述项目的名称或目的。
+     * @param projectDir   项目目录的路径,指定项目文件和资源所在的物理位置。
+     * @param modulePrefix 项目模块前缀,如果为空则使用默认值"vber-"。用于构建模块名称。
+     */
+    public static void build(@NotNull String groupId, @NotNull String artifactId, @NotNull String packageName, @NotNull String title, @NotNull String projectDir, String modulePrefix) {
+        // 如果modulePrefix为空,则使用默认前缀
+        modulePrefix = StrUtil.isEmpty(modulePrefix) ? "vber-" : modulePrefix;
+        modulePrefix = modulePrefix.endsWith("-") ? modulePrefix : modulePrefix + "-";
+        // 确保projectDir以反斜杠结尾。
+        projectDir = projectDir.endsWith("\\") ? projectDir : projectDir + "\\";
+        build(new ProjectInfo(groupId, artifactId, packageName, title, projectDir, modulePrefix));
+    }
+
+    /**
+     * 构建项目的方法。
+     * 根据提供的项目信息,该方法会创建一个新的项目目录,复制旧项目中的文件到新目录,并根据提供的项目信息修改文件内容。
+     *
+     * @param projectInfo 包含项目的基本信息,如组ID、artifactID、包名等。
+     */
+    public static void build(ProjectInfo projectInfo) {
+        // 记录开始时间
+        long start = System.currentTimeMillis();
+
+        // 分割获取项目的基本目录路径
+        String[] arr = getProjectBaseDir().split("SERVER");
+        String oldProjectDir = arr[0];
+
+        // 使用项目信息中的值,如果为空则使用默认值
+        String groupId = StrUtil.isEmpty(projectInfo.getGroupId()) ? GROUP_ID : projectInfo.getGroupId();
+        String artifactId = StrUtil.isEmpty(projectInfo.getArtifactId()) ? ARTIFACT_ID : projectInfo.getArtifactId();
+        String packageName = StrUtil.isEmpty(projectInfo.getPackageName()) ? PACKAGE_NAME : projectInfo.getPackageName();
+        String modulePrefix = StrUtil.isEmpty(projectInfo.getModulePrefix()) ? MODULE_PREFIX : projectInfo.getModulePrefix();
+        String moduleArtifactIdPrefix = "<artifactId>" + modulePrefix;
+        String moduleIdPrefix = "<module>" + modulePrefix;
+        String uiFile = modulePrefix.replace("-", "").toUpperCase();
+        String title = StrUtil.isEmpty(projectInfo.getTitle()) ? TITLE : projectInfo.getTitle();
+
+        if (StrUtil.isEmpty(projectInfo.getGroupId())) {
+            log.warn("[新项目 GroupId 为空!]");
+        }
+        if (StrUtil.isEmpty(projectInfo.getArtifactId())) {
+            log.warn("[新项目 ArtifactId 为空!]");
+        }
+        if (StrUtil.isEmpty(projectInfo.getPackageName())) {
+            log.warn("[新项目 PackageName 为空!]");
+        }
+        if (StrUtil.isEmpty(projectInfo.getTitle())) {
+            log.warn("[新项目 Title 为空!]");
+        }
+        log.info("[原项目路劲改地址 ({})]", oldProjectDir);
+        String projectDir = StrUtil.isEmpty(projectInfo.getProjectDir()) ? oldProjectDir + "_New\\" :
+                projectInfo.getProjectDir().endsWith("\\") ? projectInfo.getProjectDir() : projectInfo.getProjectDir() + "\\";
+
+        log.info("[检测新项目目录 ({})是否存在]", projectDir);
+        if (FileUtil.exist(projectDir)) {
+            log.error("[新项目目录检测 ({})已存在,请更改新的目录!程序退出]", projectDir);
+            throw new RuntimeException(StrUtil.format("新项目目录检测 ({})已存在,请更改新的目录!", projectDir));
+        }
+        // 如果新目录中存在 PACKAGE_NAME,ARTIFACT_ID 等关键字,路径会被替换,导致生成的文件不在预期目录
+        if (StrUtil.containsAny(projectDir, PACKAGE_NAME, ARTIFACT_ID, StrUtil.upperFirst(ARTIFACT_ID))) {
+            log.error("[新项目目录 `projectBaseDirNew` 检测 ({}) 存在冲突名称「{}」或者「{}」,请更改新的目录!程序退出]",
+                    projectDir, PACKAGE_NAME, ARTIFACT_ID);
+            throw new RuntimeException(StrUtil.format("新项目目录检测 ({})存在冲突名称「{}」或者「{}」,请更改新的目录!", projectDir, PACKAGE_NAME, ARTIFACT_ID));
+        }
+        log.info("[完成新项目目录检测,新项目路径地址 ({})]", projectDir);
+        // 获得需要复制的文件
+        log.info("[开始获得需要重写的文件,预计需要 5-15 秒]");
+        Collection<File> files = listFiles(oldProjectDir);
+        log.info("[需要重写的文件数量:{},预计需要 {}-{} 秒]", files.size(), files.size() / 200, files.size() / 100);
+        // 写入文件
+        files.forEach(file -> {
+            // 如果是白名单的文件类型,不进行重写,直接拷贝
+            String fileType = getFileType(file);
+            if (WHITE_FILE_TYPES.contains(fileType)) {
+                copyFile(file, oldProjectDir, projectDir, packageName, artifactId, modulePrefix, uiFile);
+                return;
+            }
+            // 如果非白名单的文件类型,重写内容,在生成文件
+            String content = replaceFileContent(file, groupId, artifactId, packageName, title, moduleArtifactIdPrefix, moduleIdPrefix);
+            writeFile(file, content, oldProjectDir, projectDir, packageName, artifactId, modulePrefix, uiFile);
+        });
+        log.info("[重写完成]共耗时:{} 秒", (System.currentTimeMillis() - start) / 1000);
+    }
+
+    private static Collection<File> listFiles(String projectBaseDir) {
+        Collection<File> files = FileUtil.loopFiles(projectBaseDir);
+        // 移除 IDEA、Git 自身的文件、Node 编译出来的文件
+        files = files.stream()
+                .filter(file -> !file.getPath().contains(separator + "target" + separator)
+                        && !file.getPath().contains(separator + "node_modules" + separator)
+                        && !file.getPath().contains(separator + ".idea" + separator)
+                        && !file.getPath().contains(separator + ".git" + separator)
+                        && !file.getPath().contains(separator + "dist" + separator)
+                        && !file.getPath().contains(separator + ".vscode" + separator)
+                        && !file.getPath().contains(separator + ".vs" + separator)
+                        && !file.getPath().contains(separator + "vber" + separator + "logs" + separator)
+                        && !file.getPath().contains(separator + "vber" + separator + "profile" + separator)
+                        && !file.getPath().contains(separator + "com" + separator + "vber" + separator + "reactor" + separator)
+                        && !file.getPath().contains(".flattened-pom.xml")
+                        && !file.getPath().contains("dist.zip")
+                        && !file.getPath().contains("dist.rar")
+                        && !file.getPath().contains(".iml")
+                        && !file.getPath().contains(".lock")
+                        && !file.getPath().contains(".html.gz"))
+                .collect(Collectors.toList());
+        return files;
+    }
+
+
+    private static void copyFile(File file, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew, String modulePrefix, String uiFile) {
+        String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew, modulePrefix, uiFile);
+        FileUtil.copyFile(file, new File(newPath));
+    }
+
+    private static String replaceFileContent(File file, String groupIdNew,
+                                             String artifactIdNew, String packageNameNew,
+                                             String titleNew, String moduleArtifactIdPrefix, String moduleIdSuffix) {
+        String content = FileUtil.readString(file, StandardCharsets.UTF_8);
+        // 如果是白名单的文件类型,不进行重写
+        String fileType = getFileType(file);
+        if (WHITE_FILE_TYPES.contains(fileType)) {
+            return content;
+        }
+        // 执行文件内容都重写
+        return content.replaceAll(GROUP_ID, groupIdNew)
+                .replaceAll(PACKAGE_NAME, packageNameNew)
+                .replaceAll(MODULE_ARTIFACT_ID_PREFIX, moduleArtifactIdPrefix)
+                .replaceAll(ARTIFACT_ID_VERSION, artifactIdNew) // 带版本的ARTIFACT_ID也替换成无版本的!
+                .replaceAll(ARTIFACT_ID, artifactIdNew) // 必须放在最后替换,因为 ARTIFACT_ID 太短!
+                .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew))
+                .replaceAll(StrUtil.upperFirst(MODULE_ID_PREFIX), StrUtil.upperFirst(moduleIdSuffix))
+                .replaceAll("permission: \"no_auth\" //NEW PROJECT", "permission: \"\"") //隐藏生成新项目按钮
+                .replaceAll(TITLE, titleNew);
+    }
+
+
+    private static void writeFile(File file, String fileContent, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew, String modulePrefix, String uiFile) {
+        String newPath = buildNewFilePath(file, projectBaseDir, projectBaseDirNew, packageNameNew, artifactIdNew, modulePrefix, uiFile);
+        FileUtil.writeUtf8String(fileContent, newPath);
+    }
+
+
+    private static String buildNewFilePath(File file, String projectBaseDir, String projectBaseDirNew, String packageNameNew, String artifactIdNew, String modulePrefix, String uiFile) {
+        String uiVueFile = uiFile + ".VUE";
+        String uiAppFile = uiFile + ".APP";
+        return file.getPath().replace(projectBaseDir, projectBaseDirNew) // 新目录
+                .replace(PACKAGE_NAME.replaceAll("\\.", Matcher.quoteReplacement(separator)),
+                        packageNameNew.replaceAll("\\.", Matcher.quoteReplacement(separator)))
+                .replace(ARTIFACT_ID, artifactIdNew) //
+                .replace(MODULE_PREFIX, modulePrefix) //
+                .replace(UI_VUE_FILE, uiVueFile) //
+                .replace(UI_APP_FILE, uiAppFile) //
+                .replaceAll(StrUtil.upperFirst(ARTIFACT_ID), StrUtil.upperFirst(artifactIdNew));
+    }
+
+    private static String getFileType(File file) {
+        return file.length() > 0 ? FileTypeUtil.getType(file) : "";
+    }
+
+    private static String getProjectBaseDir() {
+        String baseDir = System.getProperty("user.dir");
+        if (StrUtil.isEmpty(baseDir)) {
+            throw new NullPointerException("项目基础路径不存在");
+        }
+        return baseDir;
+    }
+
+
+}

+ 34 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/main/java/com/vber/reactor/domain/ProjectInfo.java

@@ -0,0 +1,34 @@
+package com.vber.reactor.domain;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * @author Yue
+ */
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+public class ProjectInfo implements Serializable {
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotBlank(message = "新项目GroupId不能为空")
+    private String groupId;
+    @NotBlank(message = "新项目ArtifactId不能为空")
+    private String artifactId;
+    @NotBlank(message = "新项目包名称不能为空")
+    private String packageName;
+    @NotBlank(message = "新项目系统标题不能为空")
+    private String title;
+    @NotBlank(message = "新项目存放路径不能为空")
+    private String projectDir;
+
+    private String modulePrefix;
+}

+ 21 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-generator/src/test/java/com/vber/reactor/ProjectReactorTest.java

@@ -0,0 +1,21 @@
+package com.vber.reactor;
+
+
+import com.vber.reactor.core.ProjectReactor;
+import org.junit.Test;
+
+public class ProjectReactorTest {
+    @Test
+    public void reactor() {
+        // ========== 手动修改配置后运行 ==========
+        String groupId = "com.va";
+        String artifactId = "VberAdmin";
+        String packageName = "cn.vber";
+        String title = "VberAdmin管理平台";
+        String modulePrefix = "vb-";
+        String projectDir = "D:\\Project\\VberPlus\\";
+        // ========== 手动修改配置后运行 ==========
+        ProjectReactor.build(groupId, artifactId, packageName, title, projectDir, modulePrefix);
+
+    }
+}

+ 26 - 2
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/ISysUserService.java

@@ -205,8 +205,32 @@ public interface ISysUserService {
     /**
      * 通过组织机构id查询当前组织机构所有用户
      *
-     * @param orgId
-     * @return
+     * @param orgId 组织机构id
+     * @return 用户列表
      */
     List<SysUserVo> selectUserListByOrg(Long orgId);
+
+    /**
+     * 通过用户ID查询用户账户
+     *
+     * @param userIds 用户ID  多个用逗号隔开
+     * @return 用户昵称
+     */
+    String selectNicknameByIds(String userIds);
+
+    /**
+     * 通过用户ID查询用户手机号
+     *
+     * @param userId 用户id
+     * @return 用户手机号
+     */
+    String selectPhonenumberById(Long userId);
+
+    /**
+     * 通过用户ID查询用户邮箱
+     *
+     * @param userId 用户id
+     * @return 用户邮箱
+     */
+    String selectEmailById(Long userId);
 }

+ 6 - 0
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysMenuServiceImpl.java

@@ -7,6 +7,7 @@ import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.vber.common.core.config.VbConfig;
 import com.vber.common.core.constant.UserConstants;
 import com.vber.common.core.utils.MapstructUtils;
 import com.vber.common.core.utils.StreamUtils;
@@ -44,6 +45,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
     private final SysRoleMapper roleMapper;
     private final SysRoleMenuMapper roleMenuMapper;
     private final SysTenantPackageMapper tenantPackageMapper;
+    private final VbConfig vbConfig;
 
     /**
      * 根据用户查询系统菜单列表
@@ -202,6 +204,10 @@ public class SysMenuServiceImpl implements ISysMenuService {
     public List<RouterVo> buildMenus(List<SysMenu> menus) {
         List<RouterVo> routers = new LinkedList<>();
         for (SysMenu menu : menus) {
+            // 不是生产环境跳过开发工具的菜单
+            if (!"dev".equals(vbConfig.getMode()) && "tool".equals(menu.getPath())) {
+                continue;
+            }
             RouterVo router = new RouterVo();
             router.setHidden("1".equals(menu.getVisible()));
             router.setName(menu.getRouteName());

+ 72 - 5
SERVER/VberAdminPlusV3/vber-modules/vber-system/src/main/java/com/vber/system/service/impl/SysUserServiceImpl.java

@@ -1,6 +1,7 @@
 package com.vber.system.service.impl;
 
 import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
@@ -9,18 +10,18 @@ import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
 import com.vber.common.core.constant.CacheNames;
 import com.vber.common.core.constant.UserConstants;
 import com.vber.common.core.exception.ServiceException;
 import com.vber.common.core.service.UserService;
 import com.vber.common.core.utils.MapstructUtils;
+import com.vber.common.core.utils.SpringUtils;
 import com.vber.common.core.utils.StreamUtils;
 import com.vber.common.core.utils.StringUtils;
 import com.vber.common.mybatis.core.page.PageQuery;
 import com.vber.common.mybatis.core.page.TableDataInfo;
 import com.vber.common.mybatis.helper.DataBaseHelper;
+import com.vber.common.redis.utils.CacheUtils;
 import com.vber.common.satoken.utils.LoginHelper;
 import com.vber.system.domain.SysOrg;
 import com.vber.system.domain.SysUser;
@@ -32,10 +33,16 @@ import com.vber.system.domain.vo.SysRoleVo;
 import com.vber.system.domain.vo.SysUserVo;
 import com.vber.system.mapper.*;
 import com.vber.system.service.ISysUserService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.cache.CacheManager;
+import org.springframework.cache.annotation.CacheEvict;
 import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.annotation.Caching;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
+import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 
@@ -55,6 +62,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
     private final SysPostMapper postMapper;
     private final SysUserRoleMapper userRoleMapper;
     private final SysUserPostMapper userPostMapper;
+    private final CacheManager cacheManager;
 
     @Override
     public TableDataInfo<SysUserVo> selectPageUserList(SysUserBo user, PageQuery pageQuery) {
@@ -309,6 +317,11 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
      */
     @Override
     @Transactional(rollbackFor = Exception.class)
+    @Caching(evict = {
+            @CacheEvict(value = CacheNames.SYS_NICKNAME, key = "#user.userId"),
+            @CacheEvict(value = CacheNames.SYS_PHONE, key = "#user.userId"),
+            @CacheEvict(value = CacheNames.SYS_EMAIL, key = "#user.userId"),
+    })
     public int updateUser(SysUserBo user) {
         // 新增用户与角色管理
         insertUserRole(user, true);
@@ -357,6 +370,11 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
      * @return 结果
      */
     @Override
+    @Caching(evict = {
+            @CacheEvict(value = CacheNames.SYS_NICKNAME, key = "#user.userId"),
+            @CacheEvict(value = CacheNames.SYS_PHONE, key = "#user.userId"),
+            @CacheEvict(value = CacheNames.SYS_EMAIL, key = "#user.userId"),
+    })
     public int updateUserProfile(SysUserBo user) {
         return baseMapper.update(null,
                 new LambdaUpdateWrapper<SysUser>()
@@ -503,6 +521,15 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
             checkUserDataScope(userId);
         }
         List<Long> ids = List.of(userIds);
+        if (CollUtil.isEmpty(ids)) {
+            return 0;
+        }
+        ids.forEach(userId -> {
+            CacheUtils.evict(CacheNames.SYS_USER_NAME, userId);
+            CacheUtils.evict(CacheNames.SYS_NICKNAME, userId);
+            CacheUtils.evict(CacheNames.SYS_PHONE, userId);
+            CacheUtils.evict(CacheNames.SYS_EMAIL, userId);
+        });
         // 删除用户与角色关联
         userRoleMapper.delete(new LambdaQueryWrapper<SysUserRole>().in(SysUserRole::getUserId, ids));
         // 删除用户与岗位表
@@ -518,8 +545,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
     /**
      * 通过组织机构id查询当前组织机构所有用户
      *
-     * @param orgId
-     * @return
+     * @param orgId 组织结构ID
+     * @return 用户信息集合
      */
     @Override
     public List<SysUserVo> selectUserListByOrg(Long orgId) {
@@ -529,8 +556,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
         return baseMapper.selectVoList(lqw);
     }
 
-    @Cacheable(cacheNames = CacheNames.SYS_USER_NAME, key = "#userId")
     @Override
+    @Cacheable(cacheNames = CacheNames.SYS_USER_NAME, key = "#userId")
     public String selectUserNameById(Long userId) {
         SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper<SysUser>()
                 .select(SysUser::getUserName).eq(SysUser::getUserId, userId));
@@ -544,4 +571,44 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
                 .select(SysUser::getNickName).eq(SysUser::getUserId, userId));
         return ObjectUtil.isNull(sysUser) ? null : sysUser.getNickName();
     }
+
+    @Override
+    public String selectNicknameByIds(String userIds) {
+        List<String> list = new ArrayList<>();
+        for (Long id : StringUtils.splitTo(userIds, Convert::toLong)) {
+            String nickname = SpringUtils.getAopProxy(this).selectNicknameById(id);
+            if (StringUtils.isNotBlank(nickname)) {
+                list.add(nickname);
+            }
+        }
+        return String.join(StringUtils.SEPARATOR, list);
+    }
+
+    /**
+     * 通过用户ID查询用户手机号
+     *
+     * @param userId 用户id
+     * @return 用户手机号
+     */
+    @Override
+    @Cacheable(cacheNames = CacheNames.SYS_PHONE, key = "#userId")
+    public String selectPhonenumberById(Long userId) {
+        SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper<SysUser>()
+                .select(SysUser::getPhonenumber).eq(SysUser::getUserId, userId));
+        return ObjectUtil.isNull(sysUser) ? null : sysUser.getPhonenumber();
+    }
+
+    /**
+     * 通过用户ID查询用户邮箱
+     *
+     * @param userId 用户id
+     * @return 用户邮箱
+     */
+    @Override
+    @Cacheable(cacheNames = CacheNames.SYS_EMAIL, key = "#userId")
+    public String selectEmailById(Long userId) {
+        SysUser sysUser = baseMapper.selectOne(new LambdaQueryWrapper<SysUser>()
+                .select(SysUser::getEmail).eq(SysUser::getUserId, userId));
+        return ObjectUtil.isNull(sysUser) ? null : sysUser.getEmail();
+    }
 }

+ 3 - 1
UI/VAP_V3.VUE/src/api/system/_tenant.ts

@@ -72,9 +72,11 @@ class tenantApi {
 	}
 	// 同步租户套餐
 	syncTenantPackage = (tenantId: string | number, packageId: string | number) => {
+		console.log("----R`", tenantId, packageId)
+
 		return Rs.get({
 			url: "/system/tenant/syncTenantPackage",
-			data: { tenantId, packageId }
+			params: { tenantId, packageId }
 		})
 	}
 	// 动态切换租户

+ 8 - 0
UI/VAP_V3.VUE/src/api/tool/_gen.ts

@@ -86,6 +86,14 @@ class genApi {
 			url: "/tool/gen/getDataNames"
 		})
 	}
+
+	// 获取数据源名称
+	projectReactor = (data: any) => {
+		return Rs.post({
+			url: "/tool/project/reactor",
+			data
+		})
+	}
 }
 
 export default genApi

+ 1 - 0
UI/VAP_V3.VUE/src/core/services/RequestService.ts

@@ -49,6 +49,7 @@ Rs.interceptors.request.use(
 	(config) => {
 		config = Object.assign({}, defaultOption, config)
 		config.headers["Content-Language"] = getLanguage()
+		config.headers["TenantId"] = appStore.tenantStore.getTenantId()
 		if (!config.url) {
 			console.log("请求URL不能为空", config)
 			throw new Error("请求URL不能为空")

+ 1 - 0
UI/VAP_V3.VUE/src/layouts/main/content/Content.vue

@@ -16,6 +16,7 @@ const cachedViews = appStore.tagViewStore.getCachedViews()
 			}">
 			<router-view v-slot="{ Component, route }">
 				<transition name="vb-fade-router" mode="out-in">
+					<!-- 在同一页面打开路由组件,缓存组件会与切换租户冲突 -->
 					<keep-alive :include="cachedViews">
 						<component v-if="!route.meta.link" :is="Component" :key="route.path" />
 					</keep-alive>

+ 41 - 21
UI/VAP_V3.VUE/src/layouts/main/header/navbar/DynamicTenant.vue

@@ -2,41 +2,61 @@
 import apis from "@a"
 import router from "@r"
 import appStore from "@s"
+const route = useRoute()
 
-const tenantEnabled = ref(true)
 const tenantId = ref("")
+const tenantEnabled = ref(true)
 const tenantOptions = ref<any>([])
 const show = computed(() => {
-	return appStore.authStore.canChangeTenant()
+	return appStore.tenantStore.canChangeTenant()
+})
+const companyName = computed(() => {
+	return (
+		tenantOptions.value.find((item: any) => {
+			return item.tenantId === tenantId.value
+		})?.companyName ?? ""
+	)
 })
-
-const initTenant = async () => {
-	apis.loginApi.getTenantList().then((res: any) => {
-		tenantEnabled.value = res.data.tenantEnabled === undefined ? true : res.data.tenantEnabled
-		if (tenantEnabled.value) {
-			tenantOptions.value = res.data.voList
-		}
-	})
-}
 
 function onDynamicTenant() {
 	if (tenantId.value != null && tenantId.value !== "") {
-		apis.system.tenantApi.dynamicTenant(tenantId.value).then((res: any) => {
-			appStore.authStore.setDynamic(true)
-			appStore.tagViewStore.delAllViews()
-			router.push({ path: "/" })
-		})
+		window.open("/redirect-tenant/" + tenantId.value + "/" + route.path)
+		// appStore.tenantStore.setDynamicTenant(tenantId.value).then(() => {
+		// 	router.push({
+		// 		//path: "/redirect-tenant/" + tenantId.value + "/" + route.path,
+		// 		name: "redirect-tenant",
+		// 		params: {
+		// 			tenant: tenantId.value,
+		// 			path: route.path
+		// 		},
+		// 		query: route.query
+		// 	})
+		// })
 	}
 }
 function onDynamicClear() {
-	apis.system.tenantApi.dynamicClear().then((res: any) => {
-		appStore.authStore.setDynamic(false)
-		appStore.tagViewStore.delAllViews()
-		router.push({ path: "/" })
+	appStore.tenantStore.clearDynamicTenant().then(() => {
+		// router.push({
+		// 	name: "redirect-tenant",
+		// 	params: {
+		// 		tenant: "0",
+		// 		path: route.path
+		// 	},
+		// 	query: route.query
+		// })
 	})
 }
 function init() {
-	initTenant()
+	appStore.tenantStore
+		.initTenant()
+		.then((res: any) => {
+			tenantEnabled.value = true
+			tenantOptions.value = res
+		})
+		.catch(() => {
+			tenantOptions.value = []
+			tenantEnabled.value = false
+		})
 }
 
 onMounted(init)

+ 1 - 1
UI/VAP_V3.VUE/src/layouts/main/header/navbar/UserAccountMenu.vue

@@ -4,7 +4,7 @@ import apis from "@a"
 const router = useRouter()
 const user = appStore.authStore.user
 const dynamic = computed(() => {
-	return appStore.authStore.isDynamic()
+	return appStore.tenantStore.isDynamic()
 })
 function clearCache() {
 	appStore.dictStore.cleanDict().then(() => {

+ 1 - 1
UI/VAP_V3.VUE/src/layouts/main/toolbar/tag-view/Index.vue

@@ -84,7 +84,7 @@ function initTags() {
 }
 function addTags() {
 	const { name } = route
-	if (name) {
+	if (name && name !== "redirect" && name !== "redirect-tenant") {
 		tagViewStore.addView(route)
 		if (route.meta.link) {
 			tagViewStore.addIframeView(route)

+ 12 - 1
UI/VAP_V3.VUE/src/router/index.ts

@@ -42,10 +42,21 @@ export const constantRoutes: RouteRecordRaw[] = [
 				}
 			},
 			{
+				name: "redirect",
 				path: "/redirect/:path(.*)",
 				component: () => import("@/views/redirect/index.vue"),
 				meta: {
-					title: "加载中..."
+					title: "加载中...",
+					noCache: true
+				}
+			},
+			{
+				name: "redirect-tenant",
+				path: "/redirect-tenant/:tenant/:path(.*)",
+				component: () => import("@/views/redirect/tenant.vue"),
+				meta: {
+					title: "加载中...",
+					noCache: true
 				}
 			}
 		]

+ 3 - 28
UI/VAP_V3.VUE/src/stores/_auth.ts

@@ -17,7 +17,7 @@ export const useAuthStore = defineStore("auth", () => {
 	const setUser = (data: any) => {
 		const u = data.user
 		user.value.tenantId = u.tenantId
-		setTenantId(u.tenantId)
+		appStore.tenantStore.setTenantId(u.tenantId)
 		user.value.userId = u.userId
 		user.value.userName = u.userName
 		user.value.nickName = u.nickName
@@ -50,6 +50,7 @@ export const useAuthStore = defineStore("auth", () => {
 		isAuthenticated.value = false
 		appStore.tagViewStore.delAllViews()
 		appStore.bodyConfigStore.resetLayoutConfig()
+		appStore.tenantStore.setTenantId("")
 		user.value = {} as User
 		localCache.remove("tenantId")
 		JwtService.destroyToken()
@@ -114,27 +115,6 @@ export const useAuthStore = defineStore("auth", () => {
 		})
 	}
 
-	function setTenantId(tenantId: string) {
-		localCache.set("tenantId", tenantId)
-	}
-	function getTenantId() {
-		return localCache.get("tenantId") || "000000"
-	}
-
-	function canChangeTenant() {
-		return (
-			user.value.roles.includes("super_admin") || user.value.permissions.includes("system:tenant")
-		)
-	}
-
-	const dynamic = ref(false)
-	function setDynamic(isDynamic: boolean) {
-		dynamic.value = isDynamic
-	}
-	function isDynamic() {
-		return dynamic.value
-	}
-
 	return {
 		errors,
 		user,
@@ -144,11 +124,6 @@ export const useAuthStore = defineStore("auth", () => {
 		callback,
 		logout,
 		getInfo,
-		changeAvatar,
-		setTenantId,
-		getTenantId,
-		canChangeTenant,
-		setDynamic,
-		isDynamic
+		changeAvatar
 	}
 })

+ 8 - 8
UI/VAP_V3.VUE/src/stores/_tagView.ts

@@ -82,15 +82,15 @@ export const useTagViewStore = defineStore("tagView", () => {
 	}
 	function delOthersViews(view: RouteLocationNormalizedLoaded) {
 		return new Promise((resolve) => {
-			delOthersvisitedViews(view)
-			delOtherscachedViews(view)
+			delOthersVisitedViews(view)
+			delOthersCachedViews(view)
 			resolve({
 				visitedViews: [...visitedViews.value],
 				cachedViews: [...cachedViews.value]
 			})
 		})
 	}
-	function delOthersvisitedViews(view: RouteLocationNormalizedLoaded) {
+	function delOthersVisitedViews(view: RouteLocationNormalizedLoaded) {
 		return new Promise((resolve) => {
 			visitedViews.value = visitedViews.value.filter((v) => {
 				return v.meta?.affix || v.path === view.path
@@ -99,7 +99,7 @@ export const useTagViewStore = defineStore("tagView", () => {
 			resolve([...visitedViews.value])
 		})
 	}
-	function delOtherscachedViews(view: RouteLocationNormalizedLoaded) {
+	function delOthersCachedViews(view: RouteLocationNormalizedLoaded) {
 		return new Promise((resolve) => {
 			const index = cachedViews.value.indexOf(view.name as string)
 			if (index > -1) {
@@ -112,15 +112,15 @@ export const useTagViewStore = defineStore("tagView", () => {
 	}
 	function delAllViews() {
 		return new Promise((resolve) => {
-			delAllvisitedViews()
-			delAllcachedViews()
+			delAllVisitedViews()
+			delAllCachedViews()
 			resolve({
 				visitedViews: [...visitedViews.value],
 				cachedViews: [...cachedViews.value]
 			})
 		})
 	}
-	function delAllvisitedViews() {
+	function delAllVisitedViews() {
 		return new Promise((resolve) => {
 			const affixTags = visitedViews.value.filter((tag) => tag.meta?.affix)
 			visitedViews.value = affixTags
@@ -128,7 +128,7 @@ export const useTagViewStore = defineStore("tagView", () => {
 			resolve([...visitedViews.value])
 		})
 	}
-	function delAllcachedViews() {
+	function delAllCachedViews() {
 		return new Promise((resolve) => {
 			cachedViews.value = []
 			resolve([...cachedViews.value])

+ 105 - 0
UI/VAP_V3.VUE/src/stores/_tenant.ts

@@ -0,0 +1,105 @@
+import apis from "@a"
+import appStore from "@s"
+
+export const useTenantStore = defineStore("tenant", () => {
+	const dynamicTenant = ref("")
+	const isNewPage = ref(false)
+	const tenantEnabled = ref(true)
+	const tenantList = ref<any>([])
+
+	const getCompanyName = () => {
+		return (
+			tenantList.value.find((item: any) => {
+				return item.tenantId === getTenantId()
+			})?.companyName ?? ""
+		)
+	}
+	const initTenant = async () => {
+		return new Promise((resolve, reject) => {
+			apis.loginApi.getTenantList().then((res: any) => {
+				tenantEnabled.value = res.data.tenantEnabled === undefined ? true : res.data.tenantEnabled
+				if (tenantEnabled.value) {
+					tenantList.value = res.data.voList
+					resolve(res.data.voList)
+				} else {
+					reject(false)
+				}
+			})
+		})
+	}
+	function getTenantList() {
+		return tenantList.value
+	}
+	function enable() {
+		return tenantEnabled.value
+	}
+	function setTenantId(tenantId: string) {
+		localCache.set("tenantId", tenantId)
+	}
+	function setTenantNewPage(tenantId: string) {
+		return new Promise((resolve, reject) => {
+			dynamicTenant.value = tenantId
+			isNewPage.value = true
+			appStore.tagViewStore.delAllViews().then(() => {
+				resolve(tenantId)
+			})
+		})
+	}
+
+	function setDynamicTenant(tenantId: string) {
+		return new Promise((resolve, reject) => {
+			dynamicTenant.value = tenantId
+			appStore.tagViewStore.delAllViews().then(() => {
+				resolve(tenantId)
+			})
+		})
+	}
+	function getTenantId() {
+		return dynamicTenant.value || localCache.get("tenantId")
+	}
+	function getTenantName() {
+		return getCompanyName() || ""
+	}
+
+	function canChangeTenant() {
+		return (
+			!isNewPage.value &&
+			(appStore.authStore.user.roles.includes("super_admin") ||
+				appStore.authStore.user.permissions.includes("system:tenant"))
+		)
+	}
+
+	function isDynamic() {
+		return getTenantId() != appStore.authStore.user.tenantId
+	}
+
+	function clearDynamicTenant() {
+		return new Promise((resolve, reject) => {
+			apis.system.tenantApi
+				.dynamicClear()
+				.then((res: any) => {
+					setDynamicTenant("")
+					appStore.tagViewStore.delAllViews().then(() => {
+						resolve(res)
+					})
+				})
+				.catch((err) => {
+					reject(err)
+				})
+		})
+	}
+
+	return {
+		initTenant,
+		getTenantList,
+		enable,
+		canChangeTenant,
+		setDynamicTenant,
+		setTenantNewPage,
+		clearDynamicTenant,
+		setTenantId,
+		getTenantId,
+		getTenantName,
+		isDynamic
+	}
+})

+ 3 - 0
UI/VAP_V3.VUE/src/stores/index.ts

@@ -3,6 +3,7 @@ import { createPinia } from "pinia"
 
 import { useBodyConfigStore } from "./_bodyConfig"
 import { useAuthStore } from "./_auth"
+import { useTenantStore } from "./_tenant"
 import { useMenuStore } from "./_menu"
 import { useDictStore } from "./_dict"
 import { useTagViewStore } from "./_tagView"
@@ -12,6 +13,7 @@ const store = createPinia()
 export interface IAppStore {
 	bodyConfigStore: ReturnType<typeof useBodyConfigStore>
 	authStore: ReturnType<typeof useAuthStore>
+	tenantStore: ReturnType<typeof useTenantStore>
 	menuStore: ReturnType<typeof useMenuStore>
 	dictStore: ReturnType<typeof useDictStore>
 	tagViewStore: ReturnType<typeof useTagViewStore>
@@ -24,6 +26,7 @@ export const appStore: IAppStore = {} as IAppStore
 export const registerStore = () => {
 	appStore.bodyConfigStore = useBodyConfigStore()
 	appStore.authStore = useAuthStore()
+	appStore.tenantStore = useTenantStore()
 	appStore.menuStore = useMenuStore()
 	appStore.dictStore = useDictStore()
 	appStore.tagViewStore = useTagViewStore()

+ 11 - 7
UI/VAP_V3.VUE/src/views/account/login.vue

@@ -80,7 +80,7 @@ const doSocialLogin = (type: string) => {
 		message.msgError("请选择租户")
 		return
 	}
-	appStore.authStore.setTenantId(loginForm.value.tenantId)
+	appStore.tenantStore.setTenantId(loginForm.value.tenantId)
 	appStore.authStore.socialLogin(type).then((res: any) => {
 		if (res.code === HttpStatus.SUCCESS) {
 			// 获取授权地址跳转
@@ -121,15 +121,19 @@ function getCookie() {
 }
 
 const initTenant = async () => {
-	apis.loginApi.getTenantList().then((res: any) => {
-		tenantEnabled.value = res.data.tenantEnabled === undefined ? true : res.data.tenantEnabled
-		if (tenantEnabled.value) {
-			tenantOptions.value = res.data.voList
+	appStore.tenantStore
+		.initTenant()
+		.then((res: any) => {
+			tenantEnabled.value = true
+			tenantOptions.value = res
 			if (tenantOptions.value != null && tenantOptions.value.length !== 0) {
 				loginForm.value.tenantId = tenantOptions.value[0].tenantId
 			}
-		}
-	})
+		})
+		.catch(() => {
+			tenantOptions.value = []
+			tenantEnabled.value = false
+		})
 }
 function init() {
 	getCode()

+ 2 - 2
UI/VAP_V3.VUE/src/views/account/social/callback.vue

@@ -9,7 +9,7 @@ const loading = ref(true)
 const code = route.query.code as string
 const state = route.query.state as string
 const source = route.query.source as string
-const tenantId = appStore.authStore.getTenantId()
+const tenantId = appStore.tenantStore.getTenantId()
 
 const callbackByCode = async (data: LoginData) => {
 	appStore.authStore
@@ -39,7 +39,7 @@ function init() {
 	const data: LoginData = {
 		socialCode: code,
 		socialState: state,
-		tenantId: tenantId,
+		tenantId: tenantId || "",
 		source: source,
 		clientId: import.meta.env.VITE_APP_CLIENT_ID,
 		grantType: "social"

+ 1 - 2
UI/VAP_V3.VUE/src/views/redirect/index.vue

@@ -7,6 +7,5 @@ const route = useRoute()
 const router = useRouter()
 const { params, query } = route
 const { path } = params
-// console.log("PATH", path)
-router.replace({ path: "/" + path, query })
+router.replace({ path: "" + path, query })
 </script>

+ 15 - 0
UI/VAP_V3.VUE/src/views/redirect/tenant.vue

@@ -0,0 +1,15 @@
+<template>
+	<div></div>
+</template>
+
+<script setup lang="ts">
+import appStore from "@s"
+
+const route = useRoute()
+const router = useRouter()
+const { params, query } = route
+const { path, tenant } = params
+const tenantId = tenant == "0" ? "" : tenant + ""
+appStore.tenantStore.setTenantNewPage(tenantId)
+router.replace({ path: "" + path, query })
+</script>

+ 24 - 0
UI/VAP_V3.VUE/src/views/system/tenant/index.vue

@@ -326,6 +326,12 @@ function handleStatusChange(row: any) {
 			row.status = row.status === "0" ? "1" : "0"
 		})
 }
+
+function handleSyncTenantPackage(row: any) {
+	apis.system.tenantApi.syncTenantPackage(row.tenantId, row.packageId).then(() => {
+		message.msgSuccess("同步成功")
+	})
+}
 /** 提交按钮 */
 function submitForm() {
 	apis.system.tenantApi.addOrUpdate(form.value).then(() => {
@@ -362,6 +368,24 @@ function getPackages() {
 			:check-multiple="true"
 			:reset-search-form-fun="resetQuery"
 			:custom-search-fun="handleQuery">
+			<template #packageName="{ row }">
+				<span v-if="row.packageName" class="w-100 position-relative d-block">
+					{{ row.packageName }}
+					<vb-tooltip content="同步套餐" placement="top">
+						<el-button
+							style="right: 0px; position: absolute; top: 50%; transform: translateY(-50%)"
+							link
+							type="primary"
+							@click="handleSyncTenantPackage(row)"
+							v-hasPermission="'system:tenant:edit'">
+							<template #icon>
+								<VbIcon icon-name="arrows-loop" icon-type="solid" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+				</span>
+				<span class="text-muted" v-else>未配置套餐</span>
+			</template>
 			<template #expireTime="{ row }">
 				{{ row.expireTime ? dayjs(row.expireTime).format("YYYY-MM-DD") : "永久" }}
 			</template>

+ 4 - 6
UI/VAP_V3.VUE/src/views/system/user/index.vue

@@ -272,7 +272,7 @@ const modalOpts = reactive({
 	}
 })
 const { title: modalTitle, emptyFormData, formItems } = toRefs(modalOpts)
-const form = ref(emptyFormData)
+const form = ref(emptyFormData.value)
 function reset() {
 	form.value = emptyFormData.value
 }
@@ -336,7 +336,6 @@ function submitForm() {
 	if (form.value.roleId) {
 		form.value.roleIds = [form.value.roleId]
 	}
-	console.log("---", form.value)
 	if (form.value.userId != undefined) {
 		apis.system.userApi.updateUser(form.value).then(() => {
 			message.msgSuccess("修改成功")
@@ -433,11 +432,10 @@ function submitAuthRole() {
 	const roleIds = authRoleRef.value.getSelectedRoleIds()
 	console.log("ROLEIDS", roleIds)
 }
-loadOrg()
-// onMounted(() => {
 
-//   //console.log("PROXY", proxy?.$refs)
-// })
+onMounted(() => {
+	loadOrg()
+})
 </script>
 
 <template>

+ 78 - 1
UI/VAP_V3.VUE/src/views/tool/gen/index.vue

@@ -10,6 +10,7 @@ const importTableRef = ref()
 const modalRef = ref()
 const importModalRef = ref()
 const previewModalRef = ref()
+
 const opts = reactive<any>({
 	columns: [
 		{ field: "tableId", name: `表ID`, width: "auto", visible: false },
@@ -58,7 +59,18 @@ const opts = reactive<any>({
 	handleFuns: {
 		handleImport
 	},
-
+	customBtns: [
+		{
+			show: true,
+			key: "handleProject",
+			name: "生成新项目",
+			btnClass: "btn btn-light-info",
+			clickFun: handleProject,
+			iconType: "class",
+			icon: "bi bi-bookmark-plus",
+			permission: "no_auth" //NEW PROJECT
+		}
+	],
 	tableListFun: apis.tool.genApi.listTable,
 	getEntityFun: apis.tool.genApi.getGenTable,
 	deleteEntityFun: apis.tool.genApi.delTable,
@@ -205,6 +217,63 @@ function getTemplateName(templateName: string) {
 	}
 	return templateName
 }
+
+const newProjectModalRef = ref()
+const newProjectForm = ref<any>({
+	groupId: "com.vap",
+	artifactId: "VberAdminPlus",
+	packageName: "com.vber",
+	modulePrefix: "vber-",
+	title: "VAP后台管理系统",
+	projectDir: "D:\\Project\\" + dayjs().format("YYYYMMDDHHmmss") + "\\"
+})
+const newProjectFormItem: any = [
+	{
+		field: "groupId",
+		label: "GROUP ID",
+		placeholder: "请输入GROUP ID",
+		required: true
+	},
+	{
+		field: "artifactId",
+		label: "ARTIFACT ID",
+		placeholder: "请输入ARTIFACT ID",
+		required: true
+	},
+	{
+		field: "packageName",
+		label: "PACKAGE NAME",
+		placeholder: "请输入PACKAGE NAME",
+		required: true
+	},
+	{
+		field: "modulePrefix",
+		label: "MODULE PREFIX",
+		placeholder: "请输入MODULE PREFIX",
+		required: true
+	},
+	{
+		field: "title",
+		label: "TITLE",
+		placeholder: "请输入TITLE",
+		required: true
+	},
+	{
+		field: "projectDir",
+		label: "PROJECT DIR",
+		placeholder: "请输入PROJECT DIR",
+		required: true
+	}
+]
+function handleProject() {
+	newProjectModalRef.value?.show()
+}
+
+function submitProjectForm() {
+	apis.tool.genApi.projectReactor(newProjectForm.value).then(() => {
+		message.msgSuccess("新项目生成成功:", newProjectForm.value.projectDir)
+	})
+}
 </script>
 
 <template>
@@ -332,5 +401,13 @@ function getTemplateName(templateName: string) {
 				</div>
 			</template>
 		</VbModal>
+		<VbModal
+			v-model:modal="newProjectModalRef"
+			title="生成新项目"
+			:form-items="newProjectFormItem"
+			:form-data="newProjectForm"
+			@confirm="submitProjectForm"
+			:form-label-width="140"
+			append-to-body></VbModal>
 	</div>
 </template>