mirror of
				https://gitee.com/lab1024/smart-admin.git
				synced 2025-11-04 10:23:43 +08:00 
			
		
		
		
	Compare commits
	
		
			16 Commits
		
	
	
		
			v3.25.0
			...
			9361097097
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9361097097 | ||
| 
						 | 
					942c628cc6 | ||
| 
						 | 
					521b98746f | ||
| 
						 | 
					4229ec80b0 | ||
| 
						 | 
					4582656e27 | ||
| 
						 | 
					fff0120058 | ||
| 
						 | 
					b8f2200af6 | ||
| 
						 | 
					0a56497b51 | ||
| 
						 | 
					2eb3742063 | ||
| 
						 | 
					74aa2da89b | ||
| 
						 | 
					3dcad0b78a | ||
| 
						 | 
					6ba6b18849 | ||
| 
						 | 
					d2c55e35ff | ||
| 
						 | 
					8135e0ec10 | ||
| 
						 | 
					2a545117fa | ||
| 
						 | 
					d8baf9dba7 | 
@@ -4,9 +4,9 @@
 | 
			
		||||
 | 
			
		||||
**<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</font>** 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。
 | 
			
		||||
 | 
			
		||||
**<font color="#DC143C">支持国产数据库:达梦、金仓、南大通用、OceanBase、GaussDB 高斯、阿里PolarDB、GoldenDB。 </font>**
 | 
			
		||||
**<font color="#DC143C">支持国产数据库:【达梦、金仓、南大通用、OceanBase、GaussDB 高斯、阿里PolarDB、GoldenDB】等,主流数据库:【Mysql, PostgreSQL】等</font>**
 | 
			
		||||
 | 
			
		||||
前端提供 **<font color="#DC143C">JavaScript和TypeScript双版本</font>**,后端提供 **<font color="#DC143C">Java8+SpringBoot2.X和Java17+SpringBoot3.X 双版本</font>**。
 | 
			
		||||
 **<font color="#DC143C">前端提供JavaScript和TypeScript双版本,后端提供Java8+SpringBoot2.X和Java17+SpringBoot3.X 双版本</font>**。
 | 
			
		||||
 | 
			
		||||
同时 **<font color="#DC143C">重磅开源</font>** 开源六年来 **<font color="#DC143C">千余家企业验证过且正在使用</font>** 的代码规范: **<font color="#DC143C">《高质量代码思想》、《Vue3规范》、《Java规范》</font>** ,让大家在这浮躁的世界里感受到一股把代码写好的清流!同时又能节省大量时间,减少加班,快乐工作,保持谦逊,保持学习,**<font color="#DC143C">热爱代码,更热爱生活</font>** !
 | 
			
		||||
### **技术体系**
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ public class AdminInterceptor implements HandlerInterceptor {
 | 
			
		||||
            Method method = ((HandlerMethod) handler).getMethod();
 | 
			
		||||
            NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
 | 
			
		||||
            if (noNeedLogin != null) {
 | 
			
		||||
                checkActiveTimeout(requestEmployee);
 | 
			
		||||
                updateActiveTimeout(requestEmployee);
 | 
			
		||||
                SmartRequestUtil.setRequestUser(requestEmployee);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
@@ -77,8 +77,8 @@ public class AdminInterceptor implements HandlerInterceptor {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 检测token 活跃频率
 | 
			
		||||
            checkActiveTimeout(requestEmployee);
 | 
			
		||||
            // 更新活跃
 | 
			
		||||
            updateActiveTimeout(requestEmployee);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // --------------- 第三步: 校验 权限 ---------------
 | 
			
		||||
@@ -123,15 +123,12 @@ public class AdminInterceptor implements HandlerInterceptor {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 检测:token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结
 | 
			
		||||
     * 更新活跃时间
 | 
			
		||||
     */
 | 
			
		||||
    private void checkActiveTimeout(RequestEmployee requestEmployee) {
 | 
			
		||||
        // 用户不在线,也不用检测
 | 
			
		||||
    private void updateActiveTimeout(RequestEmployee requestEmployee) {
 | 
			
		||||
        if (requestEmployee == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        StpUtil.checkActiveTimeout();
 | 
			
		||||
        StpUtil.updateLastActiveToNow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -153,8 +153,19 @@ public class LoginManager {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @CacheEvict(value = {AdminCacheConst.Login.USER_PERMISSION, AdminCacheConst.Login.REQUEST_EMPLOYEE}, allEntries = true)
 | 
			
		||||
    public void clear(){
 | 
			
		||||
    /**
 | 
			
		||||
     * 清除用户权限
 | 
			
		||||
     */
 | 
			
		||||
    @CacheEvict(value = AdminCacheConst.Login.USER_PERMISSION)
 | 
			
		||||
    public void clearUserPermission(Long employeeId) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 清除用户登录信息
 | 
			
		||||
     */
 | 
			
		||||
    @CacheEvict(value = AdminCacheConst.Login.REQUEST_EMPLOYEE)
 | 
			
		||||
    public void clearUserLoginInfo(Long employeeId) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -317,8 +317,8 @@ public class LoginService implements StpInterface {
 | 
			
		||||
        // sa token 登出
 | 
			
		||||
        StpUtil.logout();
 | 
			
		||||
 | 
			
		||||
        // 清空登录信息缓存
 | 
			
		||||
        loginManager.clear();
 | 
			
		||||
        // 清除用户登录信息缓存和权限信息
 | 
			
		||||
        this.clearLoginEmployeeCache(requestUser.getUserId());
 | 
			
		||||
 | 
			
		||||
        //保存登出日志
 | 
			
		||||
        LoginLogEntity loginEntity = LoginLogEntity.builder()
 | 
			
		||||
@@ -474,6 +474,7 @@ public class LoginService implements StpInterface {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void clearLoginEmployeeCache(Long employeeId) {
 | 
			
		||||
        loginManager.clear();
 | 
			
		||||
        loginManager.clearUserPermission(employeeId);
 | 
			
		||||
        loginManager.clearUserLoginInfo(employeeId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,18 +5,19 @@ import jakarta.annotation.Resource;
 | 
			
		||||
import net.lab1024.sa.base.module.support.cache.CacheService;
 | 
			
		||||
import net.lab1024.sa.base.module.support.cache.CaffeineCacheServiceImpl;
 | 
			
		||||
import net.lab1024.sa.base.module.support.cache.RedisCacheServiceImpl;
 | 
			
		||||
import net.lab1024.sa.base.module.support.redis.CustomRedisCacheManager;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
import org.springframework.cache.CacheManager;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
 | 
			
		||||
import org.springframework.data.redis.cache.RedisCacheWriter;
 | 
			
		||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
 | 
			
		||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 缓存配置
 | 
			
		||||
 *
 | 
			
		||||
 * @author zhoumingfa
 | 
			
		||||
 * @date 2025/03/28
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
public class CacheConfig {
 | 
			
		||||
@@ -26,27 +27,44 @@ public class CacheConfig {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private RedisConnectionFactory factory;
 | 
			
		||||
    private RedisConnectionFactory redisConnectionFactory;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建自定义Redis缓存管理器Bean 整合spring-cache
 | 
			
		||||
     * Redis连接工厂,用于建立与Redis服务器的连接
 | 
			
		||||
     *
 | 
			
		||||
     * @return CacheManager Redis缓存管理器实例
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
 | 
			
		||||
    public RedisCacheConfiguration redisCacheConfiguration() {
 | 
			
		||||
        return RedisCacheConfiguration.defaultCacheConfig()
 | 
			
		||||
    public CacheManager cacheManager() {
 | 
			
		||||
        // 使用非阻塞模式的缓存写入器,适用于大多数高并发场景
 | 
			
		||||
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
 | 
			
		||||
 | 
			
		||||
        // 构建默认缓存配置
 | 
			
		||||
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
 | 
			
		||||
                // 禁止缓存 null 值,避免缓存穿透
 | 
			
		||||
                .disableCachingNullValues()
 | 
			
		||||
                .computePrefixWith(name -> "cache:" + name + ":")
 | 
			
		||||
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
 | 
			
		||||
                // 使用 FastJSON 序列化缓存值,支持复杂对象
 | 
			
		||||
                .serializeValuesWith(RedisSerializationContext.SerializationPair
 | 
			
		||||
                        .fromSerializer(new GenericFastJsonRedisSerializer()));
 | 
			
		||||
 | 
			
		||||
        // 返回自定义缓存管理器,支持 cacheName#ttl 格式与永久缓存(#-1)
 | 
			
		||||
        return new CustomRedisCacheManager(redisCacheWriter, defaultCacheConfig);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
 | 
			
		||||
    public CacheService redisCacheService() {
 | 
			
		||||
        return new RedisCacheServiceImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE)
 | 
			
		||||
    public CacheService caffeineCacheService() {
 | 
			
		||||
        return new CaffeineCacheServiceImpl();
 | 
			
		||||
    }
 | 
			
		||||
@Bean
 | 
			
		||||
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
 | 
			
		||||
public CacheService redisCacheService() {
 | 
			
		||||
    return new RedisCacheServiceImpl();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Bean
 | 
			
		||||
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE)
 | 
			
		||||
public CacheService caffeineCacheService() {
 | 
			
		||||
    return new CaffeineCacheServiceImpl();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -81,7 +81,7 @@ public class FileConfig implements WebMvcConfigurer {
 | 
			
		||||
                        StaticCredentialsProvider.create(
 | 
			
		||||
                                AwsBasicCredentials.create(accessKey, secretKey)))
 | 
			
		||||
                .serviceConfiguration(S3Configuration.builder()
 | 
			
		||||
                        .pathStyleAccessEnabled(false)
 | 
			
		||||
                        .pathStyleAccessEnabled(true)
 | 
			
		||||
                        .chunkedEncodingEnabled(false)
 | 
			
		||||
                        .build())
 | 
			
		||||
                .build();
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import org.springframework.data.redis.cache.RedisCacheManager;
 | 
			
		||||
import org.springframework.data.redis.connection.RedisConnection;
 | 
			
		||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
 | 
			
		||||
 | 
			
		||||
import java.nio.charset.StandardCharsets;
 | 
			
		||||
import java.util.Collection;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.Set;
 | 
			
		||||
@@ -53,7 +54,7 @@ public class RedisCacheServiceImpl implements CacheService {
 | 
			
		||||
 | 
			
		||||
        if (keys != null) {
 | 
			
		||||
            return keys.stream().map(key -> {
 | 
			
		||||
                String redisKey = StrUtil.str(key, "utf-8");
 | 
			
		||||
                String redisKey = StrUtil.str(key, StandardCharsets.UTF_8);
 | 
			
		||||
                // 从 Redis 键中提取出最后一个冒号后面的字符串作为真正的键
 | 
			
		||||
                return redisKey.substring(redisKey.lastIndexOf(":") + 1);
 | 
			
		||||
            }).collect(Collectors.toList());
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,144 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.cache.manager;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.boot.convert.DurationStyle;
 | 
			
		||||
import org.springframework.data.redis.cache.*;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
 | 
			
		||||
import static net.lab1024.sa.base.common.constant.StringConst.COLON;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 自定义 RedisCacheManager,支持在 cacheName 中通过 '#' 指定 TTL(过期时间)。
 | 
			
		||||
 * @Author CoderKK
 | 
			
		||||
 * @Date 2025-08-15 13:01:01
 | 
			
		||||
 * <p>
 | 
			
		||||
 * 支持格式:{@code cacheName#ttl},其中 ttl 支持 Spring 的 Duration 格式。
 | 
			
		||||
 * 特殊值:{@code -1} 表示永久缓存(永不过期)。
 | 
			
		||||
 * </p>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>使用示例:</h3>
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * // 10 秒后过期
 | 
			
		||||
 * @Cacheable(value = "user#10s", key = "#id")
 | 
			
		||||
 * // 2 小时后过期
 | 
			
		||||
 * @Cacheable(value = "report#2h", key = "#date")
 | 
			
		||||
 * // 30 分钟后过期
 | 
			
		||||
 * @Cacheable(value = "session#30m", key = "#token")
 | 
			
		||||
 * // 永不过期(永久缓存),适用于极少变化的配置数据
 | 
			
		||||
 * @Cacheable(value = "appConfig#-1", key = "'globalSettings'")
 | 
			
		||||
 * // 无 TTL,使用全局默认过期时间(如 7 天)
 | 
			
		||||
 * @Cacheable(value = "product", key = "#productId")
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>生成的 Redis Key 格式:</h3>
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * cache:cacheName:key
 | 
			
		||||
 * 例如:cache:user:123
 | 
			
		||||
 *      cache:appConfig:globalSettings
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>支持的 TTL 单位:</h3>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>{@code ms} / {@code millis} / {@code milliseconds} - 毫秒</li>
 | 
			
		||||
 *   <li>{@code s} / {@code secs} / {@code seconds} - 秒</li>
 | 
			
		||||
 *   <li>{@code m} / {@code mins} / {@code minutes} - 分钟</li>
 | 
			
		||||
 *   <li>{@code h} / {@code hrs} / {@code hours} - 小时</li>
 | 
			
		||||
 *   <li>{@code d} / {@code days} - 天</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>注意事项:</h3>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>不写单位默认为毫秒</li>
 | 
			
		||||
 *   <li>永久缓存(#-1)不会自动过期,请配合 @CacheEvict 手动清理。</li>
 | 
			
		||||
 *   <li>避免对频繁更新的数据使用永久缓存,防止数据陈旧。</li>
 | 
			
		||||
 *   <li>cacheName 中的 '#' 只解析第一个,后续字符将作为 TTL 处理。</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class CustomRedisCacheManager extends RedisCacheManager {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 缓存全局前缀
 | 
			
		||||
     */
 | 
			
		||||
    private static final String CACHE_PREFIX = "cache";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义 TTL 分隔符,用于在 cacheName 后附加过期时间
 | 
			
		||||
     */
 | 
			
		||||
    private static final String CUSTOM_TTL_SEPARATOR = "#";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认缓存过期时间:7 天
 | 
			
		||||
     */
 | 
			
		||||
    private static final Duration DEFAULT_TTL = Duration.ofDays(7);
 | 
			
		||||
 | 
			
		||||
    public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
 | 
			
		||||
        super(cacheWriter, defaultCacheConfiguration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建 RedisCache 实例,支持从 cacheName 解析 TTL
 | 
			
		||||
     *
 | 
			
		||||
     * @param name        缓存名称(支持 name#ttl 格式)
 | 
			
		||||
     * @param cacheConfig 默认缓存配置
 | 
			
		||||
     * @return RedisCache
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
 | 
			
		||||
        Duration ttl = parseTtlFromCacheName(name);
 | 
			
		||||
        if (ttl == null) {
 | 
			
		||||
            ttl = DEFAULT_TTL;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CacheKeyPrefix keyPrefix = cacheName -> {
 | 
			
		||||
            if (StrUtil.isBlank(cacheName)) {
 | 
			
		||||
                return CACHE_PREFIX + COLON;
 | 
			
		||||
            }
 | 
			
		||||
            String[] parts = cacheName.split(CUSTOM_TTL_SEPARATOR, 2);
 | 
			
		||||
            String cleanName = StrUtil.trim(parts[0]);
 | 
			
		||||
            return CACHE_PREFIX + COLON + cleanName + COLON;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 构建最终缓存配置:设置 key 前缀 + TTL
 | 
			
		||||
        RedisCacheConfiguration config = cacheConfig.computePrefixWith(keyPrefix).entryTtl(ttl);
 | 
			
		||||
 | 
			
		||||
        return super.createRedisCache(name, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 从 cacheName 中解析 TTL
 | 
			
		||||
     *
 | 
			
		||||
     * @param name 缓存名称,格式如:users#10m, products#2h, config#-1(永久)
 | 
			
		||||
     * @return 解析出的 Duration若无效则返回 null;若为 -1,则返回 Duration.ofMillis(-1) 表示永久缓存
 | 
			
		||||
     */
 | 
			
		||||
    private Duration parseTtlFromCacheName(String name) {
 | 
			
		||||
        if (StrUtil.isBlank(name)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String[] parts = name.split(CUSTOM_TTL_SEPARATOR, 2);
 | 
			
		||||
        if (parts.length < 2) {
 | 
			
		||||
            return null; // 无 TTL 部分
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String ttlStr = StrUtil.trim(parts[1]);
 | 
			
		||||
        if (StrUtil.isBlank(ttlStr)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 特殊处理:-1 表示永久缓存
 | 
			
		||||
        if ("-1".equals(ttlStr)) {
 | 
			
		||||
            return Duration.ofMillis(-1); // Spring Redis 中负数 Duration 表示永不过期
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            Duration ttl = DurationStyle.detectAndParse(ttlStr);
 | 
			
		||||
            return ttl.toSeconds() > 0 ? ttl : null;
 | 
			
		||||
        } catch (IllegalArgumentException e) {
 | 
			
		||||
            log.debug("解析缓存 TTL 失败,cacheName='{}', ttl='{}', 错误: {}", name, ttlStr, e.getMessage());
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -30,7 +30,7 @@ public class DictDataVO implements Serializable {
 | 
			
		||||
    private String dictName;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "字典禁用状态")
 | 
			
		||||
    private Integer dictDisabledFlag;
 | 
			
		||||
    private Boolean dictDisabledFlag;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "字典项值")
 | 
			
		||||
    private String dataValue;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public class DictVO {
 | 
			
		||||
    private String remark;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "禁用状态")
 | 
			
		||||
    private Integer disabledFlag;
 | 
			
		||||
    private Boolean disabledFlag;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "创建时间")
 | 
			
		||||
    private LocalDateTime createTime;
 | 
			
		||||
 
 | 
			
		||||
@@ -101,7 +101,18 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
 | 
			
		||||
        userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
 | 
			
		||||
        userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
 | 
			
		||||
 | 
			
		||||
        PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).metadata(userMetadata).contentLength(file.getSize()).contentType(this.getContentType(fileType)).contentEncoding(StandardCharsets.UTF_8.name()).contentDisposition("attachment;filename=" + urlEncoderFilename).build();
 | 
			
		||||
        // 根据文件路径获取并设置访问权限
 | 
			
		||||
        ObjectCannedACL acl = this.getACL(path);
 | 
			
		||||
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
 | 
			
		||||
                .bucket(cloudConfig.getBucketName())
 | 
			
		||||
                .key(fileKey)
 | 
			
		||||
                .metadata(userMetadata)
 | 
			
		||||
                .contentLength(file.getSize())
 | 
			
		||||
                .contentType(this.getContentType(fileType))
 | 
			
		||||
                .contentEncoding(StandardCharsets.UTF_8.name())
 | 
			
		||||
                .contentDisposition("attachment;filename=" + urlEncoderFilename)
 | 
			
		||||
                .acl(acl)
 | 
			
		||||
                .build();
 | 
			
		||||
        InputStream inputStream = null;
 | 
			
		||||
        try {
 | 
			
		||||
            inputStream = file.getInputStream();
 | 
			
		||||
@@ -112,10 +123,6 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
 | 
			
		||||
        } finally {
 | 
			
		||||
            IOUtils.closeQuietly(inputStream);
 | 
			
		||||
        }
 | 
			
		||||
        // 根据文件路径获取并设置访问权限
 | 
			
		||||
        ObjectCannedACL acl = this.getACL(path);
 | 
			
		||||
        PutObjectAclRequest aclRequest = PutObjectAclRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).acl(this.getACL(path)).build();
 | 
			
		||||
        s3Client.putObjectAcl(aclRequest);
 | 
			
		||||
        // 返回上传结果
 | 
			
		||||
        FileUploadVO uploadVO = new FileUploadVO();
 | 
			
		||||
        uploadVO.setFileName(originalFileName);
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ import jakarta.servlet.http.HttpServletResponse;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import net.lab1024.sa.base.common.constant.StringConst;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.RequestUser;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.ResponseDTO;
 | 
			
		||||
import net.lab1024.sa.base.common.util.SmartIpUtil;
 | 
			
		||||
import net.lab1024.sa.base.common.util.SmartRequestUtil;
 | 
			
		||||
import net.lab1024.sa.base.module.support.operatelog.OperateLogDao;
 | 
			
		||||
@@ -46,7 +47,7 @@ import java.util.concurrent.ThreadPoolExecutor;
 | 
			
		||||
 * @Date 2021-12-08 20:48:52
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Aspect
 | 
			
		||||
@@ -71,14 +72,14 @@ public abstract class OperateLogAspect {
 | 
			
		||||
    public void logPointCut() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @AfterReturning(pointcut = "logPointCut()")
 | 
			
		||||
    public void doAfterReturning(JoinPoint joinPoint) {
 | 
			
		||||
        handleLog(joinPoint, null);
 | 
			
		||||
    @AfterReturning(pointcut = "logPointCut()", returning = "responseDTO")
 | 
			
		||||
    public void doAfterReturning(JoinPoint joinPoint, Object responseDTO) {
 | 
			
		||||
        handleLog(joinPoint, null, responseDTO);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @AfterThrowing(value = "logPointCut()", throwing = "e")
 | 
			
		||||
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
 | 
			
		||||
        handleLog(joinPoint, e);
 | 
			
		||||
        handleLog(joinPoint, e, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -109,16 +110,15 @@ public abstract class OperateLogAspect {
 | 
			
		||||
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void handleLog(final JoinPoint joinPoint, final Exception e) {
 | 
			
		||||
    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object responseDTO) {
 | 
			
		||||
        try {
 | 
			
		||||
            OperateLog operateLog = this.getAnnotationLog(joinPoint);
 | 
			
		||||
            if (operateLog == null) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            this.submitLog(joinPoint, e);
 | 
			
		||||
            this.submitLog(joinPoint, e, responseDTO);
 | 
			
		||||
        } catch (Exception exp) {
 | 
			
		||||
            log.error("保存操作日志异常:{}", exp.getMessage());
 | 
			
		||||
            exp.printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -173,11 +173,8 @@ public abstract class OperateLogAspect {
 | 
			
		||||
    /**
 | 
			
		||||
     * 提交存储操作日志
 | 
			
		||||
     *
 | 
			
		||||
     * @param joinPoint
 | 
			
		||||
     * @param e
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    private void submitLog(final JoinPoint joinPoint, final Throwable e) throws Exception {
 | 
			
		||||
    private void submitLog(final JoinPoint joinPoint, final Throwable e, Object responseDTO) {
 | 
			
		||||
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
 | 
			
		||||
        //设置用户信息
 | 
			
		||||
        RequestUser user = SmartRequestUtil.getRequestUser();
 | 
			
		||||
@@ -191,7 +188,7 @@ public abstract class OperateLogAspect {
 | 
			
		||||
        String methodName = joinPoint.getSignature().getName();
 | 
			
		||||
        String operateMethod = className + "." + methodName;
 | 
			
		||||
        String failReason = null;
 | 
			
		||||
        Boolean successFlag = true;
 | 
			
		||||
        boolean successFlag = true;
 | 
			
		||||
        if (e != null) {
 | 
			
		||||
            successFlag = false;
 | 
			
		||||
            failReason = getExceptionString(e);
 | 
			
		||||
@@ -210,15 +207,32 @@ public abstract class OperateLogAspect {
 | 
			
		||||
                        .userAgent(user.getUserAgent())
 | 
			
		||||
                        .failReason(failReason)
 | 
			
		||||
                        .successFlag(successFlag).build();
 | 
			
		||||
 | 
			
		||||
        Operation apiOperation = this.getApiOperation(joinPoint);
 | 
			
		||||
        if (apiOperation != null) {
 | 
			
		||||
            operateLogEntity.setContent(apiOperation.summary());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Tag api = this.getApi(joinPoint);
 | 
			
		||||
        if (api != null) {
 | 
			
		||||
            String name = api.name();
 | 
			
		||||
            operateLogEntity.setModule(StrUtil.join(",", name));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 处理返回值 ResponseDTO
 | 
			
		||||
        if(responseDTO instanceof ResponseDTO) {
 | 
			
		||||
            ResponseDTO response = (ResponseDTO) responseDTO;
 | 
			
		||||
            ResponseDTO logResponseDTO = new ResponseDTO(
 | 
			
		||||
                    response.getCode(),
 | 
			
		||||
                    response.getLevel(),
 | 
			
		||||
                    response.getOk(),
 | 
			
		||||
                    response.getMsg(),
 | 
			
		||||
                    null
 | 
			
		||||
            );
 | 
			
		||||
            logResponseDTO.setDataType(response.getDataType());
 | 
			
		||||
            operateLogEntity.setResponse(JSON.toJSONString(logResponseDTO));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        taskExecutor.execute(() -> {
 | 
			
		||||
            this.saveLog(operateLogEntity);
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,11 @@ public class OperateLogEntity {
 | 
			
		||||
     */
 | 
			
		||||
    private String param;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回值
 | 
			
		||||
     */
 | 
			
		||||
    private String response;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 客户ip
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,9 @@ public class OperateLogVO {
 | 
			
		||||
    @Schema(description = "请求参数")
 | 
			
		||||
    private String param;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "返回值")
 | 
			
		||||
    private String response;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "客户ip")
 | 
			
		||||
    private String ip;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,145 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.redis;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.boot.convert.DurationStyle;
 | 
			
		||||
import org.springframework.data.redis.cache.*;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
 | 
			
		||||
import static net.lab1024.sa.base.common.constant.StringConst.COLON;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 自定义 RedisCacheManager,支持在 cacheName 中通过 '#' 指定 TTL(过期时间)。
 | 
			
		||||
 *
 | 
			
		||||
 * @Author CoderKK
 | 
			
		||||
 * @Date 2025-08-15 13:01:01
 | 
			
		||||
 * <p>
 | 
			
		||||
 * 支持格式:{@code cacheName#ttl},其中 ttl 支持 Spring 的 Duration 格式。
 | 
			
		||||
 * 特殊值:{@code -1} 表示永久缓存(永不过期)。
 | 
			
		||||
 * </p>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>使用示例:</h3>
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * // 10 秒后过期
 | 
			
		||||
 * @Cacheable(value = "user#10s", key = "#id")
 | 
			
		||||
 * // 2 小时后过期
 | 
			
		||||
 * @Cacheable(value = "report#2h", key = "#date")
 | 
			
		||||
 * // 30 分钟后过期
 | 
			
		||||
 * @Cacheable(value = "session#30m", key = "#token")
 | 
			
		||||
 * // 永不过期(永久缓存),适用于极少变化的配置数据
 | 
			
		||||
 * @Cacheable(value = "appConfig#-1", key = "'globalSettings'")
 | 
			
		||||
 * // 无 TTL,使用全局默认过期时间(如 7 天)
 | 
			
		||||
 * @Cacheable(value = "product", key = "#productId")
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>生成的 Redis Key 格式:</h3>
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * cache:cacheName:key
 | 
			
		||||
 * 例如:cache:user:123
 | 
			
		||||
 *      cache:appConfig:globalSettings
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>支持的 TTL 单位:</h3>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>{@code ms} / {@code millis} / {@code milliseconds} - 毫秒</li>
 | 
			
		||||
 *   <li>{@code s} / {@code secs} / {@code seconds} - 秒</li>
 | 
			
		||||
 *   <li>{@code m} / {@code mins} / {@code minutes} - 分钟</li>
 | 
			
		||||
 *   <li>{@code h} / {@code hrs} / {@code hours} - 小时</li>
 | 
			
		||||
 *   <li>{@code d} / {@code days} - 天</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>注意事项:</h3>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>不写单位默认为毫秒</li>
 | 
			
		||||
 *   <li>永久缓存(#-1)不会自动过期,请配合 @CacheEvict 手动清理。</li>
 | 
			
		||||
 *   <li>避免对频繁更新的数据使用永久缓存,防止数据陈旧。</li>
 | 
			
		||||
 *   <li>cacheName 中的 '#' 只解析第一个,后续字符将作为 TTL 处理。</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class CustomRedisCacheManager extends RedisCacheManager {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 缓存全局前缀
 | 
			
		||||
     */
 | 
			
		||||
    private static final String CACHE_PREFIX = "cache";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义 TTL 分隔符,用于在 cacheName 后附加过期时间
 | 
			
		||||
     */
 | 
			
		||||
    private static final String CUSTOM_TTL_SEPARATOR = "#";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认缓存过期时间:7 天
 | 
			
		||||
     */
 | 
			
		||||
    private static final Duration DEFAULT_TTL = Duration.ofDays(7);
 | 
			
		||||
 | 
			
		||||
    public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
 | 
			
		||||
        super(cacheWriter, defaultCacheConfiguration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建 RedisCache 实例,支持从 cacheName 解析 TTL
 | 
			
		||||
     *
 | 
			
		||||
     * @param name        缓存名称(支持 name#ttl 格式)
 | 
			
		||||
     * @param cacheConfig 默认缓存配置
 | 
			
		||||
     * @return RedisCache
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
 | 
			
		||||
        Duration ttl = parseTtlFromCacheName(name);
 | 
			
		||||
        if (ttl == null) {
 | 
			
		||||
            ttl = DEFAULT_TTL;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CacheKeyPrefix keyPrefix = cacheName -> {
 | 
			
		||||
            if (StrUtil.isBlank(cacheName)) {
 | 
			
		||||
                return CACHE_PREFIX + COLON;
 | 
			
		||||
            }
 | 
			
		||||
            String[] parts = cacheName.split(CUSTOM_TTL_SEPARATOR, 2);
 | 
			
		||||
            String cleanName = StrUtil.trim(parts[0]);
 | 
			
		||||
            return CACHE_PREFIX + COLON + cleanName + COLON;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 构建最终缓存配置:设置 key 前缀 + TTL
 | 
			
		||||
        RedisCacheConfiguration config = cacheConfig.computePrefixWith(keyPrefix).entryTtl(ttl);
 | 
			
		||||
 | 
			
		||||
        return super.createRedisCache(name, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 从 cacheName 中解析 TTL
 | 
			
		||||
     *
 | 
			
		||||
     * @param name 缓存名称,格式如:users#10m, products#2h, config#-1(永久)
 | 
			
		||||
     * @return 解析出的 Duration若无效则返回 null;若为 -1,则返回 Duration.ofMillis(-1) 表示永久缓存
 | 
			
		||||
     */
 | 
			
		||||
    private Duration parseTtlFromCacheName(String name) {
 | 
			
		||||
        if (StrUtil.isBlank(name)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String[] parts = name.split(CUSTOM_TTL_SEPARATOR, 2);
 | 
			
		||||
        if (parts.length < 2) {
 | 
			
		||||
            return null; // 无 TTL 部分
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String ttlStr = StrUtil.trim(parts[1]);
 | 
			
		||||
        if (StrUtil.isBlank(ttlStr)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 特殊处理:-1 表示永久缓存
 | 
			
		||||
        if ("-1".equals(ttlStr)) {
 | 
			
		||||
            return Duration.ofMillis(-1); // Spring Redis 中负数 Duration 表示永不过期
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            Duration ttl = DurationStyle.detectAndParse(ttlStr);
 | 
			
		||||
            return ttl.getSeconds() > 0 ? ttl : null;
 | 
			
		||||
        } catch (IllegalArgumentException e) {
 | 
			
		||||
            log.error("解析缓存 TTL 失败,cacheName='{}', ttl='{}', 错误: {}", name, ttlStr, e);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "DictSelect")
 | 
			
		||||
        <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "Date")
 | 
			
		||||
@@ -106,7 +106,7 @@
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "DictSelect")
 | 
			
		||||
          <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          </a-form-item>
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "Date")
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Dict")
 | 
			
		||||
            <a-form-item label="${field.label}" class="smart-query-form-item">
 | 
			
		||||
              <DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
              <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
            </a-form-item>
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Enum")
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "DictSelect")
 | 
			
		||||
        <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "Date")
 | 
			
		||||
@@ -106,7 +106,7 @@
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "DictSelect")
 | 
			
		||||
          <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          </a-form-item>
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "Date")
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Dict")
 | 
			
		||||
            <a-form-item label="${field.label}" class="smart-query-form-item">
 | 
			
		||||
              <DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
              <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
            </a-form-item>
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Enum")
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
                AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords}))
 | 
			
		||||
            </if>
 | 
			
		||||
            <if test="query.requestKeywords != null and query.requestKeywords != ''">
 | 
			
		||||
                AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}))
 | 
			
		||||
                AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}) OR INSTR(response,#{query.requestKeywords}))
 | 
			
		||||
            </if>
 | 
			
		||||
            <if test="query.successFlag != null">
 | 
			
		||||
                AND success_flag = #{query.successFlag}
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
        <ip2region.version>2.7.0</ip2region.version>
 | 
			
		||||
        <bcprov.version>1.80</bcprov.version>
 | 
			
		||||
        <smartdb.version>1.2.0</smartdb.version>
 | 
			
		||||
        <redisson.version>3.50.0</redisson.version>
 | 
			
		||||
        <redisson.version>3.25.0</redisson.version>
 | 
			
		||||
        <snakeyaml.version>2.4</snakeyaml.version>
 | 
			
		||||
        <freemarker.version>2.3.34</freemarker.version>
 | 
			
		||||
        <jsoup.version>1.21.1</jsoup.version>
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,7 @@ public class AdminInterceptor implements HandlerInterceptor {
 | 
			
		||||
            Method method = ((HandlerMethod) handler).getMethod();
 | 
			
		||||
            NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
 | 
			
		||||
            if (noNeedLogin != null) {
 | 
			
		||||
                checkActiveTimeout(requestEmployee);
 | 
			
		||||
                updateActiveTimeout(requestEmployee);
 | 
			
		||||
                SmartRequestUtil.setRequestUser(requestEmployee);
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
@@ -77,8 +77,8 @@ public class AdminInterceptor implements HandlerInterceptor {
 | 
			
		||||
                return false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 检测token 活跃频率
 | 
			
		||||
            checkActiveTimeout(requestEmployee);
 | 
			
		||||
            // 更新活跃
 | 
			
		||||
            updateActiveTimeout(requestEmployee);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            // --------------- 第三步: 校验 权限 ---------------
 | 
			
		||||
@@ -123,15 +123,12 @@ public class AdminInterceptor implements HandlerInterceptor {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 检测:token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结
 | 
			
		||||
     * 更新活跃时间
 | 
			
		||||
     */
 | 
			
		||||
    private void checkActiveTimeout(RequestEmployee requestEmployee) {
 | 
			
		||||
        // 用户不在线,也不用检测
 | 
			
		||||
    private void updateActiveTimeout(RequestEmployee requestEmployee) {
 | 
			
		||||
        if (requestEmployee == null) {
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        StpUtil.checkActiveTimeout();
 | 
			
		||||
        StpUtil.updateLastActiveToNow();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -153,8 +153,19 @@ public class LoginManager {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @CacheEvict(value = {AdminCacheConst.Login.USER_PERMISSION, AdminCacheConst.Login.REQUEST_EMPLOYEE}, allEntries = true)
 | 
			
		||||
    public void clear(){
 | 
			
		||||
    /**
 | 
			
		||||
     * 清除用户权限
 | 
			
		||||
     */
 | 
			
		||||
    @CacheEvict(value = AdminCacheConst.Login.USER_PERMISSION)
 | 
			
		||||
    public void clearUserPermission(Long employeeId) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 清除用户登录信息
 | 
			
		||||
     */
 | 
			
		||||
    @CacheEvict(value = AdminCacheConst.Login.REQUEST_EMPLOYEE)
 | 
			
		||||
    public void clearUserLoginInfo(Long employeeId) {
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -319,8 +319,8 @@ public class LoginService implements StpInterface {
 | 
			
		||||
        // sa token 登出
 | 
			
		||||
        StpUtil.logout();
 | 
			
		||||
 | 
			
		||||
        // 清空登录信息缓存
 | 
			
		||||
        loginManager.clear();
 | 
			
		||||
        // 清除用户登录信息缓存和权限信息
 | 
			
		||||
        this.clearLoginEmployeeCache(requestUser.getUserId());
 | 
			
		||||
 | 
			
		||||
        //保存登出日志
 | 
			
		||||
        LoginLogEntity loginEntity = LoginLogEntity.builder()
 | 
			
		||||
@@ -476,6 +476,7 @@ public class LoginService implements StpInterface {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public void clearLoginEmployeeCache(Long employeeId) {
 | 
			
		||||
        loginManager.clear();
 | 
			
		||||
        loginManager.clearUserPermission(employeeId);
 | 
			
		||||
        loginManager.clearUserLoginInfo(employeeId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -4,10 +4,13 @@ import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
 | 
			
		||||
import net.lab1024.sa.base.module.support.cache.CacheService;
 | 
			
		||||
import net.lab1024.sa.base.module.support.cache.CaffeineCacheServiceImpl;
 | 
			
		||||
import net.lab1024.sa.base.module.support.cache.RedisCacheServiceImpl;
 | 
			
		||||
import net.lab1024.sa.base.module.support.redis.CustomRedisCacheManager;
 | 
			
		||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
 | 
			
		||||
import org.springframework.cache.CacheManager;
 | 
			
		||||
import org.springframework.context.annotation.Bean;
 | 
			
		||||
import org.springframework.context.annotation.Configuration;
 | 
			
		||||
import org.springframework.data.redis.cache.RedisCacheConfiguration;
 | 
			
		||||
import org.springframework.data.redis.cache.RedisCacheWriter;
 | 
			
		||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
 | 
			
		||||
import org.springframework.data.redis.serializer.RedisSerializationContext;
 | 
			
		||||
 | 
			
		||||
@@ -16,8 +19,6 @@ import javax.annotation.Resource;
 | 
			
		||||
/**
 | 
			
		||||
 * 缓存配置
 | 
			
		||||
 *
 | 
			
		||||
 * @author zhoumingfa
 | 
			
		||||
 * @date 2025/03/28
 | 
			
		||||
 */
 | 
			
		||||
@Configuration
 | 
			
		||||
public class CacheConfig {
 | 
			
		||||
@@ -27,27 +28,44 @@ public class CacheConfig {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    @Resource
 | 
			
		||||
    private RedisConnectionFactory factory;
 | 
			
		||||
    private RedisConnectionFactory redisConnectionFactory;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建自定义Redis缓存管理器Bean 整合spring-cache
 | 
			
		||||
     * Redis连接工厂,用于建立与Redis服务器的连接
 | 
			
		||||
     *
 | 
			
		||||
     * @return CacheManager Redis缓存管理器实例
 | 
			
		||||
     */
 | 
			
		||||
    @Bean
 | 
			
		||||
    @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
 | 
			
		||||
    public RedisCacheConfiguration redisCacheConfiguration() {
 | 
			
		||||
        return RedisCacheConfiguration.defaultCacheConfig()
 | 
			
		||||
    public CacheManager cacheManager() {
 | 
			
		||||
        // 使用非阻塞模式的缓存写入器,适用于大多数高并发场景
 | 
			
		||||
        RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory);
 | 
			
		||||
 | 
			
		||||
        // 构建默认缓存配置
 | 
			
		||||
        RedisCacheConfiguration defaultCacheConfig = RedisCacheConfiguration.defaultCacheConfig()
 | 
			
		||||
                // 禁止缓存 null 值,避免缓存穿透
 | 
			
		||||
                .disableCachingNullValues()
 | 
			
		||||
                .computePrefixWith(name -> "cache:" + name + ":")
 | 
			
		||||
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));
 | 
			
		||||
                // 使用 FastJSON 序列化缓存值,支持复杂对象
 | 
			
		||||
                .serializeValuesWith(RedisSerializationContext.SerializationPair
 | 
			
		||||
                        .fromSerializer(new GenericFastJsonRedisSerializer()));
 | 
			
		||||
 | 
			
		||||
        // 返回自定义缓存管理器,支持 cacheName#ttl 格式与永久缓存(#-1)
 | 
			
		||||
        return new CustomRedisCacheManager(redisCacheWriter, defaultCacheConfig);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
 | 
			
		||||
    public CacheService redisCacheService() {
 | 
			
		||||
        return new RedisCacheServiceImpl();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @Bean
 | 
			
		||||
    @ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE)
 | 
			
		||||
    public CacheService caffeineCacheService() {
 | 
			
		||||
        return new CaffeineCacheServiceImpl();
 | 
			
		||||
    }
 | 
			
		||||
@Bean
 | 
			
		||||
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = REDIS_CACHE)
 | 
			
		||||
public CacheService redisCacheService() {
 | 
			
		||||
    return new RedisCacheServiceImpl();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Bean
 | 
			
		||||
@ConditionalOnProperty(prefix = "spring.cache", name = {"type"}, havingValue = CAFFEINE_CACHE)
 | 
			
		||||
public CacheService caffeineCacheService() {
 | 
			
		||||
    return new CaffeineCacheServiceImpl();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
@@ -81,7 +81,7 @@ public class FileConfig implements WebMvcConfigurer {
 | 
			
		||||
                        StaticCredentialsProvider.create(
 | 
			
		||||
                                AwsBasicCredentials.create(accessKey, secretKey)))
 | 
			
		||||
                .serviceConfiguration(S3Configuration.builder()
 | 
			
		||||
                        .pathStyleAccessEnabled(false)
 | 
			
		||||
                        .pathStyleAccessEnabled(true)
 | 
			
		||||
                        .chunkedEncodingEnabled(false)
 | 
			
		||||
                        .build())
 | 
			
		||||
                .build();
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public class DictDataVO implements Serializable {
 | 
			
		||||
    private String dictName;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "字典禁用状态")
 | 
			
		||||
    private Integer dictDisabledFlag;
 | 
			
		||||
    private Boolean dictDisabledFlag;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "字典项值")
 | 
			
		||||
    private String dataValue;
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ public class DictVO {
 | 
			
		||||
    private String remark;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "禁用状态")
 | 
			
		||||
    private Integer disabledFlag;
 | 
			
		||||
    private Boolean disabledFlag;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "创建时间")
 | 
			
		||||
    private LocalDateTime createTime;
 | 
			
		||||
 
 | 
			
		||||
@@ -106,7 +106,18 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
 | 
			
		||||
        userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
 | 
			
		||||
        userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
 | 
			
		||||
 | 
			
		||||
        PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).metadata(userMetadata).contentLength(file.getSize()).contentType(this.getContentType(fileType)).contentEncoding(StandardCharsets.UTF_8.name()).contentDisposition("attachment;filename=" + urlEncoderFilename).build();
 | 
			
		||||
        // 根据文件路径获取并设置访问权限
 | 
			
		||||
        ObjectCannedACL acl = this.getACL(path);
 | 
			
		||||
        PutObjectRequest putObjectRequest = PutObjectRequest.builder()
 | 
			
		||||
                .bucket(cloudConfig.getBucketName())
 | 
			
		||||
                .key(fileKey)
 | 
			
		||||
                .metadata(userMetadata)
 | 
			
		||||
                .contentLength(file.getSize())
 | 
			
		||||
                .contentType(this.getContentType(fileType))
 | 
			
		||||
                .contentEncoding(StandardCharsets.UTF_8.name())
 | 
			
		||||
                .contentDisposition("attachment;filename=" + urlEncoderFilename)
 | 
			
		||||
                .acl(acl)
 | 
			
		||||
                .build();
 | 
			
		||||
        InputStream inputStream = null;
 | 
			
		||||
        try {
 | 
			
		||||
            inputStream = file.getInputStream();
 | 
			
		||||
@@ -117,10 +128,6 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
 | 
			
		||||
        } finally {
 | 
			
		||||
            IOUtils.closeQuietly(inputStream);
 | 
			
		||||
        }
 | 
			
		||||
        // 根据文件路径获取并设置访问权限
 | 
			
		||||
        ObjectCannedACL acl = this.getACL(path);
 | 
			
		||||
        PutObjectAclRequest aclRequest = PutObjectAclRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).acl(this.getACL(path)).build();
 | 
			
		||||
        s3Client.putObjectAcl(aclRequest);
 | 
			
		||||
        // 返回上传结果
 | 
			
		||||
        FileUploadVO uploadVO = new FileUploadVO();
 | 
			
		||||
        uploadVO.setFileName(originalFileName);
 | 
			
		||||
 
 | 
			
		||||
@@ -3,11 +3,12 @@ package net.lab1024.sa.base.module.support.operatelog.core;
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import com.alibaba.fastjson.JSON;
 | 
			
		||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 | 
			
		||||
import io.swagger.v3.oas.annotations.tags.Tag;
 | 
			
		||||
import io.swagger.v3.oas.annotations.Operation;
 | 
			
		||||
import io.swagger.v3.oas.annotations.tags.Tag;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import net.lab1024.sa.base.common.constant.StringConst;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.RequestUser;
 | 
			
		||||
import net.lab1024.sa.base.common.domain.ResponseDTO;
 | 
			
		||||
import net.lab1024.sa.base.common.util.SmartIpUtil;
 | 
			
		||||
import net.lab1024.sa.base.common.util.SmartRequestUtil;
 | 
			
		||||
import net.lab1024.sa.base.module.support.operatelog.OperateLogDao;
 | 
			
		||||
@@ -20,7 +21,6 @@ import org.aspectj.lang.annotation.AfterThrowing;
 | 
			
		||||
import org.aspectj.lang.annotation.Aspect;
 | 
			
		||||
import org.aspectj.lang.annotation.Pointcut;
 | 
			
		||||
import org.aspectj.lang.reflect.MethodSignature;
 | 
			
		||||
import org.springframework.beans.factory.annotation.Autowired;
 | 
			
		||||
import org.springframework.boot.context.properties.bind.BindResult;
 | 
			
		||||
import org.springframework.context.ApplicationContext;
 | 
			
		||||
import org.springframework.core.annotation.AnnotationUtils;
 | 
			
		||||
@@ -36,7 +36,6 @@ import javax.servlet.http.HttpServletResponse;
 | 
			
		||||
import java.io.PrintWriter;
 | 
			
		||||
import java.io.StringWriter;
 | 
			
		||||
import java.lang.reflect.Method;
 | 
			
		||||
import java.net.BindException;
 | 
			
		||||
import java.util.ArrayList;
 | 
			
		||||
import java.util.List;
 | 
			
		||||
import java.util.concurrent.ThreadPoolExecutor;
 | 
			
		||||
@@ -48,7 +47,7 @@ import java.util.concurrent.ThreadPoolExecutor;
 | 
			
		||||
 * @Date 2021-12-08 20:48:52
 | 
			
		||||
 * @Wechat zhuoda1024
 | 
			
		||||
 * @Email lab1024@163.com
 | 
			
		||||
 * @Copyright  <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
@Aspect
 | 
			
		||||
@@ -73,14 +72,14 @@ public abstract class OperateLogAspect {
 | 
			
		||||
    public void logPointCut() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @AfterReturning(pointcut = "logPointCut()")
 | 
			
		||||
    public void doAfterReturning(JoinPoint joinPoint) {
 | 
			
		||||
        handleLog(joinPoint, null);
 | 
			
		||||
    @AfterReturning(pointcut = "logPointCut()", returning = "responseDTO")
 | 
			
		||||
    public void doAfterReturning(JoinPoint joinPoint, Object responseDTO) {
 | 
			
		||||
        handleLog(joinPoint, null, responseDTO);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    @AfterThrowing(value = "logPointCut()", throwing = "e")
 | 
			
		||||
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
 | 
			
		||||
        handleLog(joinPoint, e);
 | 
			
		||||
        handleLog(joinPoint, e, null);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
@@ -111,16 +110,15 @@ public abstract class OperateLogAspect {
 | 
			
		||||
        taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    protected void handleLog(final JoinPoint joinPoint, final Exception e) {
 | 
			
		||||
    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object responseDTO) {
 | 
			
		||||
        try {
 | 
			
		||||
            OperateLog operateLog = this.getAnnotationLog(joinPoint);
 | 
			
		||||
            if (operateLog == null) {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
            this.submitLog(joinPoint, e);
 | 
			
		||||
            this.submitLog(joinPoint, e, responseDTO);
 | 
			
		||||
        } catch (Exception exp) {
 | 
			
		||||
            log.error("保存操作日志异常:{}", exp.getMessage());
 | 
			
		||||
            exp.printStackTrace();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -175,11 +173,8 @@ public abstract class OperateLogAspect {
 | 
			
		||||
    /**
 | 
			
		||||
     * 提交存储操作日志
 | 
			
		||||
     *
 | 
			
		||||
     * @param joinPoint
 | 
			
		||||
     * @param e
 | 
			
		||||
     * @throws Exception
 | 
			
		||||
     */
 | 
			
		||||
    private void submitLog(final JoinPoint joinPoint, final Throwable e) throws Exception {
 | 
			
		||||
    private void submitLog(final JoinPoint joinPoint, final Throwable e, Object responseDTO) {
 | 
			
		||||
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
 | 
			
		||||
        //设置用户信息
 | 
			
		||||
        RequestUser user = SmartRequestUtil.getRequestUser();
 | 
			
		||||
@@ -193,7 +188,7 @@ public abstract class OperateLogAspect {
 | 
			
		||||
        String methodName = joinPoint.getSignature().getName();
 | 
			
		||||
        String operateMethod = className + "." + methodName;
 | 
			
		||||
        String failReason = null;
 | 
			
		||||
        Boolean successFlag = true;
 | 
			
		||||
        boolean successFlag = true;
 | 
			
		||||
        if (e != null) {
 | 
			
		||||
            successFlag = false;
 | 
			
		||||
            failReason = getExceptionString(e);
 | 
			
		||||
@@ -212,15 +207,32 @@ public abstract class OperateLogAspect {
 | 
			
		||||
                        .userAgent(user.getUserAgent())
 | 
			
		||||
                        .failReason(failReason)
 | 
			
		||||
                        .successFlag(successFlag).build();
 | 
			
		||||
 | 
			
		||||
        Operation apiOperation = this.getApiOperation(joinPoint);
 | 
			
		||||
        if (apiOperation != null) {
 | 
			
		||||
            operateLogEntity.setContent(apiOperation.summary());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        Tag api = this.getApi(joinPoint);
 | 
			
		||||
        if (api != null) {
 | 
			
		||||
            String name = api.name();
 | 
			
		||||
            operateLogEntity.setModule(StrUtil.join(",", name));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 处理返回值 ResponseDTO
 | 
			
		||||
        if(responseDTO instanceof ResponseDTO) {
 | 
			
		||||
            ResponseDTO response = (ResponseDTO) responseDTO;
 | 
			
		||||
            ResponseDTO logResponseDTO = new ResponseDTO(
 | 
			
		||||
                    response.getCode(),
 | 
			
		||||
                    response.getLevel(),
 | 
			
		||||
                    response.getOk(),
 | 
			
		||||
                    response.getMsg(),
 | 
			
		||||
                    null
 | 
			
		||||
            );
 | 
			
		||||
            logResponseDTO.setDataType(response.getDataType());
 | 
			
		||||
            operateLogEntity.setResponse(JSON.toJSONString(logResponseDTO));
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        taskExecutor.execute(() -> {
 | 
			
		||||
            this.saveLog(operateLogEntity);
 | 
			
		||||
        });
 | 
			
		||||
 
 | 
			
		||||
@@ -71,6 +71,11 @@ public class OperateLogEntity {
 | 
			
		||||
     */
 | 
			
		||||
    private String param;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 返回值
 | 
			
		||||
     */
 | 
			
		||||
    private String response;
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 客户ip
 | 
			
		||||
     */
 | 
			
		||||
 
 | 
			
		||||
@@ -47,6 +47,9 @@ public class OperateLogVO {
 | 
			
		||||
    @Schema(description = "请求参数")
 | 
			
		||||
    private String param;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "返回值")
 | 
			
		||||
    private String response;
 | 
			
		||||
 | 
			
		||||
    @Schema(description = "客户ip")
 | 
			
		||||
    private String ip;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,145 @@
 | 
			
		||||
package net.lab1024.sa.base.module.support.redis;
 | 
			
		||||
 | 
			
		||||
import cn.hutool.core.util.StrUtil;
 | 
			
		||||
import lombok.extern.slf4j.Slf4j;
 | 
			
		||||
import org.springframework.boot.convert.DurationStyle;
 | 
			
		||||
import org.springframework.data.redis.cache.*;
 | 
			
		||||
 | 
			
		||||
import java.time.Duration;
 | 
			
		||||
 | 
			
		||||
import static net.lab1024.sa.base.common.constant.StringConst.COLON;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 自定义 RedisCacheManager,支持在 cacheName 中通过 '#' 指定 TTL(过期时间)。
 | 
			
		||||
 *
 | 
			
		||||
 * @Author CoderKK
 | 
			
		||||
 * @Date 2025-08-15 13:01:01
 | 
			
		||||
 * <p>
 | 
			
		||||
 * 支持格式:{@code cacheName#ttl},其中 ttl 支持 Spring 的 Duration 格式。
 | 
			
		||||
 * 特殊值:{@code -1} 表示永久缓存(永不过期)。
 | 
			
		||||
 * </p>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>使用示例:</h3>
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * // 10 秒后过期
 | 
			
		||||
 * @Cacheable(value = "user#10s", key = "#id")
 | 
			
		||||
 * // 2 小时后过期
 | 
			
		||||
 * @Cacheable(value = "report#2h", key = "#date")
 | 
			
		||||
 * // 30 分钟后过期
 | 
			
		||||
 * @Cacheable(value = "session#30m", key = "#token")
 | 
			
		||||
 * // 永不过期(永久缓存),适用于极少变化的配置数据
 | 
			
		||||
 * @Cacheable(value = "appConfig#-1", key = "'globalSettings'")
 | 
			
		||||
 * // 无 TTL,使用全局默认过期时间(如 7 天)
 | 
			
		||||
 * @Cacheable(value = "product", key = "#productId")
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>生成的 Redis Key 格式:</h3>
 | 
			
		||||
 * <pre>
 | 
			
		||||
 * cache:cacheName:key
 | 
			
		||||
 * 例如:cache:user:123
 | 
			
		||||
 *      cache:appConfig:globalSettings
 | 
			
		||||
 * </pre>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>支持的 TTL 单位:</h3>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>{@code ms} / {@code millis} / {@code milliseconds} - 毫秒</li>
 | 
			
		||||
 *   <li>{@code s} / {@code secs} / {@code seconds} - 秒</li>
 | 
			
		||||
 *   <li>{@code m} / {@code mins} / {@code minutes} - 分钟</li>
 | 
			
		||||
 *   <li>{@code h} / {@code hrs} / {@code hours} - 小时</li>
 | 
			
		||||
 *   <li>{@code d} / {@code days} - 天</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 *
 | 
			
		||||
 * <h3>注意事项:</h3>
 | 
			
		||||
 * <ul>
 | 
			
		||||
 *   <li>不写单位默认为毫秒</li>
 | 
			
		||||
 *   <li>永久缓存(#-1)不会自动过期,请配合 @CacheEvict 手动清理。</li>
 | 
			
		||||
 *   <li>避免对频繁更新的数据使用永久缓存,防止数据陈旧。</li>
 | 
			
		||||
 *   <li>cacheName 中的 '#' 只解析第一个,后续字符将作为 TTL 处理。</li>
 | 
			
		||||
 * </ul>
 | 
			
		||||
 */
 | 
			
		||||
@Slf4j
 | 
			
		||||
public class CustomRedisCacheManager extends RedisCacheManager {
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 缓存全局前缀
 | 
			
		||||
     */
 | 
			
		||||
    private static final String CACHE_PREFIX = "cache";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 自定义 TTL 分隔符,用于在 cacheName 后附加过期时间
 | 
			
		||||
     */
 | 
			
		||||
    private static final String CUSTOM_TTL_SEPARATOR = "#";
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 默认缓存过期时间:7 天
 | 
			
		||||
     */
 | 
			
		||||
    private static final Duration DEFAULT_TTL = Duration.ofDays(7);
 | 
			
		||||
 | 
			
		||||
    public CustomRedisCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
 | 
			
		||||
        super(cacheWriter, defaultCacheConfiguration);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 创建 RedisCache 实例,支持从 cacheName 解析 TTL
 | 
			
		||||
     *
 | 
			
		||||
     * @param name        缓存名称(支持 name#ttl 格式)
 | 
			
		||||
     * @param cacheConfig 默认缓存配置
 | 
			
		||||
     * @return RedisCache
 | 
			
		||||
     */
 | 
			
		||||
    @Override
 | 
			
		||||
    protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
 | 
			
		||||
        Duration ttl = parseTtlFromCacheName(name);
 | 
			
		||||
        if (ttl == null) {
 | 
			
		||||
            ttl = DEFAULT_TTL;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        CacheKeyPrefix keyPrefix = cacheName -> {
 | 
			
		||||
            if (StrUtil.isBlank(cacheName)) {
 | 
			
		||||
                return CACHE_PREFIX + COLON;
 | 
			
		||||
            }
 | 
			
		||||
            String[] parts = cacheName.split(CUSTOM_TTL_SEPARATOR, 2);
 | 
			
		||||
            String cleanName = StrUtil.trim(parts[0]);
 | 
			
		||||
            return CACHE_PREFIX + COLON + cleanName + COLON;
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        // 构建最终缓存配置:设置 key 前缀 + TTL
 | 
			
		||||
        RedisCacheConfiguration config = cacheConfig.computePrefixWith(keyPrefix).entryTtl(ttl);
 | 
			
		||||
 | 
			
		||||
        return super.createRedisCache(name, config);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
     * 从 cacheName 中解析 TTL
 | 
			
		||||
     *
 | 
			
		||||
     * @param name 缓存名称,格式如:users#10m, products#2h, config#-1(永久)
 | 
			
		||||
     * @return 解析出的 Duration若无效则返回 null;若为 -1,则返回 Duration.ofMillis(-1) 表示永久缓存
 | 
			
		||||
     */
 | 
			
		||||
    private Duration parseTtlFromCacheName(String name) {
 | 
			
		||||
        if (StrUtil.isBlank(name)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String[] parts = name.split(CUSTOM_TTL_SEPARATOR, 2);
 | 
			
		||||
        if (parts.length < 2) {
 | 
			
		||||
            return null; // 无 TTL 部分
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        String ttlStr = StrUtil.trim(parts[1]);
 | 
			
		||||
        if (StrUtil.isBlank(ttlStr)) {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // 特殊处理:-1 表示永久缓存
 | 
			
		||||
        if ("-1".equals(ttlStr)) {
 | 
			
		||||
            return Duration.ofMillis(-1); // Spring Redis 中负数 Duration 表示永不过期
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try {
 | 
			
		||||
            Duration ttl = DurationStyle.detectAndParse(ttlStr);
 | 
			
		||||
            return ttl.getSeconds() > 0 ? ttl : null;
 | 
			
		||||
        } catch (IllegalArgumentException e) {
 | 
			
		||||
            log.error("解析缓存 TTL 失败,cacheName='{}', ttl='{}', 错误: {}", name, ttlStr, e);
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "DictSelect")
 | 
			
		||||
        <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "Date")
 | 
			
		||||
@@ -106,7 +106,7 @@
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "DictSelect")
 | 
			
		||||
          <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          </a-form-item>
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "Date")
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Dict")
 | 
			
		||||
            <a-form-item label="${field.label}" class="smart-query-form-item">
 | 
			
		||||
              <DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
              <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
            </a-form-item>
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Enum")
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "DictSelect")
 | 
			
		||||
        <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
        </a-form-item>
 | 
			
		||||
      #end
 | 
			
		||||
      #if($field.frontComponent == "Date")
 | 
			
		||||
@@ -106,7 +106,7 @@
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "DictSelect")
 | 
			
		||||
          <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})"  name="${field.fieldName}">
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
            <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
 | 
			
		||||
          </a-form-item>
 | 
			
		||||
        #end
 | 
			
		||||
        #if($field.frontComponent == "Date")
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Dict")
 | 
			
		||||
            <a-form-item label="${field.label}" class="smart-query-form-item">
 | 
			
		||||
              <DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
              <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
 | 
			
		||||
            </a-form-item>
 | 
			
		||||
#end
 | 
			
		||||
#if($field.queryTypeEnum == "Enum")
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@
 | 
			
		||||
                AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords}))
 | 
			
		||||
            </if>
 | 
			
		||||
            <if test="query.requestKeywords != null and query.requestKeywords != ''">
 | 
			
		||||
                AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}))
 | 
			
		||||
                AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}) OR INSTR(response,#{query.requestKeywords}))
 | 
			
		||||
            </if>
 | 
			
		||||
            <if test="query.successFlag != null">
 | 
			
		||||
                AND success_flag = #{query.successFlag}
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@
 | 
			
		||||
    "vue": "3.4.27",
 | 
			
		||||
    "vue-i18n": "9.13.1",
 | 
			
		||||
    "vue-router": "4.3.2",
 | 
			
		||||
    "vue3-json-viewer": "2.2.2"
 | 
			
		||||
    "vue3-json-viewer": "2.3.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@vitejs/plugin-vue": "5.0.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,6 @@
 | 
			
		||||
      v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      @change="ajaxQuery"
 | 
			
		||||
      @showSizeChange="ajaxQuery"
 | 
			
		||||
      :show-total="(total) => `共${total}条`"
 | 
			
		||||
    />
 | 
			
		||||
    <a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null">
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,6 @@
 | 
			
		||||
        v-model:pageSize="params.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryEmployee"
 | 
			
		||||
        @showSizeChange="queryEmployee"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ export const appDefaultConfig = {
 | 
			
		||||
  // 圆角
 | 
			
		||||
  borderRadius: 6,
 | 
			
		||||
  // 菜单展开模式
 | 
			
		||||
  flatPattern: false,
 | 
			
		||||
  menuSingleExpandFlag: true,
 | 
			
		||||
  // 标签页
 | 
			
		||||
  pageTagFlag: true,
 | 
			
		||||
  // 标签页样式: default、 antd、chrome
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,11 @@ export default {
 | 
			
		||||
  'setting.menu.layout': 'Menu Layout',
 | 
			
		||||
  'setting.menu.width': 'Menu Width',
 | 
			
		||||
  'setting.menu.theme': 'Menu Theme',
 | 
			
		||||
  'setting.menu.expand': 'Menu Expand',
 | 
			
		||||
  'setting.page.width': 'Page Width',
 | 
			
		||||
  'setting.border.radius': 'Border Radius',
 | 
			
		||||
  'setting.compact': 'Page Compact',
 | 
			
		||||
  'setting.bread': 'Show Bread',
 | 
			
		||||
  'setting.flatPattern': 'Flat Pattern',
 | 
			
		||||
  'setting.pagetag': 'Show PageTag',
 | 
			
		||||
  'setting.pagetag.style': 'PageTag Style',
 | 
			
		||||
  'setting.footer': 'Show Footer',
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,11 @@ export default {
 | 
			
		||||
  'setting.menu.layout': '菜单布局',
 | 
			
		||||
  'setting.menu.width': '菜单宽度',
 | 
			
		||||
  'setting.menu.theme': '菜单主题',
 | 
			
		||||
  'setting.menu.expand': '菜单展开',
 | 
			
		||||
  'setting.compact': '页面紧凑',
 | 
			
		||||
  'setting.border.radius': '页面圆角',
 | 
			
		||||
  'setting.page.width': '页面宽度',
 | 
			
		||||
  'setting.bread': '面包屑',
 | 
			
		||||
  'setting.flatPattern': '菜单展开模式',
 | 
			
		||||
  'setting.pagetag': '标签页',
 | 
			
		||||
  'setting.pagetag.style': '标签页样式',
 | 
			
		||||
  'setting.footer': '页脚',
 | 
			
		||||
 
 | 
			
		||||
@@ -83,8 +83,8 @@
 | 
			
		||||
          <a-radio-button value="chrome">Chrome</a-radio-button>
 | 
			
		||||
        </a-radio-group>
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item :label="$t('setting.flatPattern')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
 | 
			
		||||
        <a-switch @change="changeFlatPattern" v-model:checked="formState.flatPattern" checked-children="多个" un-checked-children="单个" />
 | 
			
		||||
      <a-form-item :label="$t('setting.menu.expand')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
 | 
			
		||||
        <a-switch @change="changeMenuExpandFlag" v-model:checked="formState.menuSingleExpandFlag" checked-children="单个" un-checked-children="多个" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item :label="$t('setting.pagetag')">
 | 
			
		||||
        <a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" />
 | 
			
		||||
@@ -213,8 +213,8 @@
 | 
			
		||||
    borderRadius: appConfigStore.borderRadius,
 | 
			
		||||
    // 标签页
 | 
			
		||||
    pageTagFlag: appConfigStore.pageTagFlag,
 | 
			
		||||
    // 标签页
 | 
			
		||||
    flatPattern: appConfigStore.flatPattern,
 | 
			
		||||
    // 菜单展开方式
 | 
			
		||||
    menuSingleExpandFlag: appConfigStore.menuSingleExpandFlag,
 | 
			
		||||
    // 标签页 样式
 | 
			
		||||
    pageTagStyle: appConfigStore.pageTagStyle,
 | 
			
		||||
    // 面包屑
 | 
			
		||||
@@ -303,9 +303,9 @@
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function changeFlatPattern(e) {
 | 
			
		||||
  function changeMenuExpandFlag(e) {
 | 
			
		||||
    appConfigStore.$patch({
 | 
			
		||||
      flatPattern: e,
 | 
			
		||||
      menuSingleExpandFlag: e,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
  import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
 | 
			
		||||
  const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
  const flatPattern = computed(() => useAppConfigStore().$state.flatPattern);
 | 
			
		||||
  const menuSingleExpandFlag = computed(() => useAppConfigStore().$state.menuSingleExpandFlag);
 | 
			
		||||
 | 
			
		||||
  const props = defineProps({
 | 
			
		||||
    collapsed: {
 | 
			
		||||
@@ -46,8 +46,7 @@
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const menuTree = computed(() => useUserStore().getMenuTree || []);
 | 
			
		||||
  const rootSubmenuKeys = computed(()=>menuTree.value.map(item=>item.menuId));
 | 
			
		||||
 | 
			
		||||
  const rootSubmenuKeys = computed(() => menuTree.value.map((item) => item.menuId));
 | 
			
		||||
 | 
			
		||||
  //展开的菜单
 | 
			
		||||
  let currentRoute = useRoute();
 | 
			
		||||
@@ -76,9 +75,15 @@
 | 
			
		||||
    let parentList = menuParentIdListMap.get(currentRoute.name) || [];
 | 
			
		||||
 | 
			
		||||
    // 如果是折叠菜单的话,则不需要设置openkey
 | 
			
		||||
    if (!props.collapsed) {
 | 
			
		||||
    if (props.collapsed) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let needOpenKeys = _.map(parentList, 'name').map(Number);
 | 
			
		||||
    if (menuSingleExpandFlag.value) {
 | 
			
		||||
      openKeys.value = [...needOpenKeys];
 | 
			
		||||
    } else {
 | 
			
		||||
      // 使用lodash的union函数,进行 去重合并两个数组
 | 
			
		||||
      let needOpenKeys = _.map(parentList, 'name').map(Number);
 | 
			
		||||
      openKeys.value = _.union(openKeys.value, needOpenKeys);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -92,17 +97,18 @@
 | 
			
		||||
      immediate: true,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  function onOpenChange(openKeysParams){
 | 
			
		||||
  if(flatPattern.value){
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  function onOpenChange(openKeysParams) {
 | 
			
		||||
    if (!menuSingleExpandFlag.value) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const latestOpenKey = openKeysParams.find((key) => openKeys.value.indexOf(key) === -1);
 | 
			
		||||
    if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
 | 
			
		||||
      openKeys.value = openKeysParams;
 | 
			
		||||
    } else {
 | 
			
		||||
      openKeys.value = latestOpenKey ? [latestOpenKey] : [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const latestOpenKey = openKeysParams.find(key => openKeys.value.indexOf(key) === -1);
 | 
			
		||||
  if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
 | 
			
		||||
    openKeys.value = openKeysParams;
 | 
			
		||||
  } else {
 | 
			
		||||
    openKeys.value = latestOpenKey ? [latestOpenKey] : [];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
  defineExpose({
 | 
			
		||||
    updateOpenKeysAndSelectKeys,
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,6 @@
 | 
			
		||||
          v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
          :total="total"
 | 
			
		||||
          @change="queryEmployee"
 | 
			
		||||
          @showSizeChange="queryEmployee"
 | 
			
		||||
          :show-total="showTableTotal"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryViewRecord"
 | 
			
		||||
        @showSizeChange="queryViewRecord"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryNoticeList"
 | 
			
		||||
        @showSizeChange="queryNoticeList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -122,7 +122,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryNoticeList"
 | 
			
		||||
        @showSizeChange="queryNoticeList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -74,7 +74,6 @@
 | 
			
		||||
          v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
          :total="total"
 | 
			
		||||
          @change="ajaxQuery"
 | 
			
		||||
          @showSizeChange="ajaxQuery"
 | 
			
		||||
          :show-total="(total) => `共${total}条`"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,16 @@ import { convertUpperCamel } from '/@/utils/str-util';
 | 
			
		||||
// -------------------------------- java 类型 --------------------------------
 | 
			
		||||
export const JavaTypeMap = new Map();
 | 
			
		||||
JavaTypeMap.set('bit', 'Boolean');
 | 
			
		||||
JavaTypeMap.set('bool', 'Boolean');
 | 
			
		||||
JavaTypeMap.set('int', 'Integer');
 | 
			
		||||
JavaTypeMap.set('int2', 'Integer');
 | 
			
		||||
JavaTypeMap.set('int4', 'Integer');
 | 
			
		||||
JavaTypeMap.set('tinyint', 'Integer');
 | 
			
		||||
JavaTypeMap.set('smallint', 'Integer');
 | 
			
		||||
JavaTypeMap.set('integer', 'Integer');
 | 
			
		||||
JavaTypeMap.set('year', 'Integer');
 | 
			
		||||
JavaTypeMap.set('bigint', 'Long');
 | 
			
		||||
JavaTypeMap.set('int8', 'Long');
 | 
			
		||||
JavaTypeMap.set('float', 'BigDecimal');
 | 
			
		||||
JavaTypeMap.set('double', 'BigDecimal');
 | 
			
		||||
JavaTypeMap.set('decimal', 'BigDecimal');
 | 
			
		||||
@@ -20,6 +24,7 @@ JavaTypeMap.set('longtext', 'String');
 | 
			
		||||
JavaTypeMap.set('blob', 'String');
 | 
			
		||||
JavaTypeMap.set('date', 'LocalDate');
 | 
			
		||||
JavaTypeMap.set('datetime', 'LocalDateTime');
 | 
			
		||||
JavaTypeMap.set('timestamp', 'LocalDateTime');
 | 
			
		||||
 | 
			
		||||
export const JavaTypeList = [
 | 
			
		||||
  'Boolean', //
 | 
			
		||||
@@ -39,7 +44,11 @@ export function getJavaType(dataType) {
 | 
			
		||||
// -------------------------------- js 类型 --------------------------------
 | 
			
		||||
export const JsTypeMap = new Map();
 | 
			
		||||
JsTypeMap.set('bit', 'Boolean');
 | 
			
		||||
JsTypeMap.set('bool', 'Boolean');
 | 
			
		||||
JsTypeMap.set('int', 'Number');
 | 
			
		||||
JsTypeMap.set('int2', 'Number');
 | 
			
		||||
JsTypeMap.set('int4', 'Number');
 | 
			
		||||
JsTypeMap.set('int8', 'Number');
 | 
			
		||||
JsTypeMap.set('tinyint', 'Number');
 | 
			
		||||
JsTypeMap.set('smallint', 'Number');
 | 
			
		||||
JsTypeMap.set('integer', 'Number');
 | 
			
		||||
@@ -50,12 +59,14 @@ JsTypeMap.set('double', 'Number');
 | 
			
		||||
JsTypeMap.set('decimal', 'Number');
 | 
			
		||||
JsTypeMap.set('char', 'String');
 | 
			
		||||
JsTypeMap.set('varchar', 'String');
 | 
			
		||||
JsTypeMap.set('character', 'String');
 | 
			
		||||
JsTypeMap.set('tinytext', 'String');
 | 
			
		||||
JsTypeMap.set('text', 'String');
 | 
			
		||||
JsTypeMap.set('longtext', 'String');
 | 
			
		||||
JsTypeMap.set('blob', 'String');
 | 
			
		||||
JsTypeMap.set('date', 'Date');
 | 
			
		||||
JsTypeMap.set('datetime', 'Date');
 | 
			
		||||
JsTypeMap.set('timestamp', 'Date');
 | 
			
		||||
 | 
			
		||||
export const JsTypeList = [
 | 
			
		||||
  'Boolean', //
 | 
			
		||||
@@ -72,17 +83,23 @@ export function getJsType(dataType) {
 | 
			
		||||
 | 
			
		||||
export const FrontComponentMap = new Map();
 | 
			
		||||
FrontComponentMap.set('bit', 'BooleanSelect');
 | 
			
		||||
FrontComponentMap.set('bool', 'BooleanSelect');
 | 
			
		||||
FrontComponentMap.set('int', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('int2', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('int4', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('int8', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('tinyint', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('smallint', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('integer', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('year', 'Date');
 | 
			
		||||
FrontComponentMap.set('timestamp', 'Date');
 | 
			
		||||
FrontComponentMap.set('bigint', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('float', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('double', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('decimal', 'InputNumber');
 | 
			
		||||
FrontComponentMap.set('char', 'Input');
 | 
			
		||||
FrontComponentMap.set('varchar', 'Input');
 | 
			
		||||
FrontComponentMap.set('character', 'Input');
 | 
			
		||||
FrontComponentMap.set('tinytext', 'Input');
 | 
			
		||||
FrontComponentMap.set('text', 'Textarea');
 | 
			
		||||
FrontComponentMap.set('longtext', 'Textarea');
 | 
			
		||||
 
 | 
			
		||||
@@ -66,7 +66,6 @@
 | 
			
		||||
          v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
          :total="total"
 | 
			
		||||
          @change="ajaxQuery"
 | 
			
		||||
          @showSizeChange="ajaxQuery"
 | 
			
		||||
          :show-total="(total) => `共${total}条`"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -98,7 +98,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryList"
 | 
			
		||||
        @showSizeChange="queryList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -79,7 +79,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -88,7 +88,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryHelpDocList"
 | 
			
		||||
        @showSizeChange="queryHelpDocList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryViewRecord"
 | 
			
		||||
        @showSizeChange="queryViewRecord"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -117,7 +117,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryJobList"
 | 
			
		||||
        @showSizeChange="queryJobList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,6 @@
 | 
			
		||||
          v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
          :total="total"
 | 
			
		||||
          @change="queryLogList"
 | 
			
		||||
          @showSizeChange="queryLogList"
 | 
			
		||||
          :show-total="(total) => `共${total}条`"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -136,7 +136,6 @@
 | 
			
		||||
              v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
              :total="total"
 | 
			
		||||
              @change="queryJobList"
 | 
			
		||||
              @showSizeChange="queryJobList"
 | 
			
		||||
              :show-total="(total) => `共${total}条`"
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="onSearch"
 | 
			
		||||
        @showSizeChange="onSearch"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -89,7 +89,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,6 @@
 | 
			
		||||
        v-model:pageSize="queryParam.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryList"
 | 
			
		||||
        @showSizeChange="queryList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -12,6 +12,10 @@
 | 
			
		||||
    <div class="info-box">
 | 
			
		||||
      <a-row class="smart-margin-top10">
 | 
			
		||||
        <a-col :span="16">
 | 
			
		||||
          <a-row class="detail-info">
 | 
			
		||||
            <a-col :span="12"> 用户id:{{ detail.operateUserId }}</a-col>
 | 
			
		||||
            <a-col :span="12"> 用户名称: {{ detail.operateUserName }}</a-col>
 | 
			
		||||
          </a-row>
 | 
			
		||||
          <a-row class="detail-info">
 | 
			
		||||
            <a-col :span="12"> 请求url: {{ detail.url }}</a-col>
 | 
			
		||||
            <a-col :span="12"> 请求日期: {{ detail.createTime }}</a-col>
 | 
			
		||||
@@ -21,8 +25,7 @@
 | 
			
		||||
            <a-col :span="12"> IP地区: {{ detail.ipRegion }}</a-col>
 | 
			
		||||
          </a-row>
 | 
			
		||||
          <a-row class="detail-info">
 | 
			
		||||
            <a-col :span="12"> 用户id:{{ detail.operateUserId }}</a-col>
 | 
			
		||||
            <a-col :span="12"> 用户名称: {{ detail.operateUserName }}</a-col>
 | 
			
		||||
            <a-col :span="12"> 客户端: {{ detail.os }} / {{ detail.browser }}  {{ detail.device ? '/' + detail.device : detail.device }}</a-col>
 | 
			
		||||
          </a-row>
 | 
			
		||||
        </a-col>
 | 
			
		||||
        <a-col :span="8">
 | 
			
		||||
@@ -32,21 +35,26 @@
 | 
			
		||||
          </a-typography-text>
 | 
			
		||||
        </a-col>
 | 
			
		||||
      </a-row>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="info-box">
 | 
			
		||||
      <h4>请求明细:</h4>
 | 
			
		||||
      <a-col :span="24"> 方法: {{ detail.method }}</a-col>
 | 
			
		||||
      <a-col :span="24"> 说明: {{ detail.module }} - {{ detail.content }}</a-col>
 | 
			
		||||
      <a-row class="detail-info">
 | 
			
		||||
        <a-col :span="24"> 方法: {{ detail.method }}</a-col>
 | 
			
		||||
      </a-row>
 | 
			
		||||
      <a-row class="detail-info">
 | 
			
		||||
        <a-col :span="24"> 说明: {{ detail.module }} - {{ detail.content }}</a-col>
 | 
			
		||||
      </a-row>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="info-box">
 | 
			
		||||
      <h4>请求参数:</h4>
 | 
			
		||||
      <JsonViewer :value="detail.param ? JSON.parse(detail.param) : ''" theme="jv-dark" copyable boxed sort />
 | 
			
		||||
      <JsonViewer :value="detail.param ? JSON.parse(detail.param) : ''" :expanded="true" :expandDepth="10" copyable boxed sort theme="light" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="info-box" v-if="detail.successFlag">
 | 
			
		||||
      <h4>返回结果:</h4>
 | 
			
		||||
      <JsonViewer :value="detail.response ? JSON.parse(detail.response) : ''" :expanded="true" :expandDepth="10" copyable boxed sort theme="light" />
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="info-box" v-if="detail.failReason">
 | 
			
		||||
      <h4>请求失败原因:</h4>
 | 
			
		||||
      <div>
 | 
			
		||||
      <a-card>
 | 
			
		||||
        {{ detail.failReason }}
 | 
			
		||||
      </div>
 | 
			
		||||
      </a-card>
 | 
			
		||||
    </div>
 | 
			
		||||
  </a-modal>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -57,6 +65,7 @@
 | 
			
		||||
  import { operateLogApi } from '/@/api/support/operate-log-api';
 | 
			
		||||
  import { smartSentry } from '/@/lib/smart-sentry';
 | 
			
		||||
  import { SmartLoading } from '/@/components/framework/smart-loading';
 | 
			
		||||
  import uaparser from 'ua-parser-js';
 | 
			
		||||
 | 
			
		||||
  defineExpose({
 | 
			
		||||
    show,
 | 
			
		||||
@@ -87,11 +96,16 @@
 | 
			
		||||
    param: '',
 | 
			
		||||
    url: '',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  async function getDetail(operateLogId) {
 | 
			
		||||
    try {
 | 
			
		||||
      SmartLoading.show();
 | 
			
		||||
      let res = await operateLogApi.detail(operateLogId);
 | 
			
		||||
      detail = Object.assign(detail, res.data);
 | 
			
		||||
      let ua = uaparser(res.data.userAgent);
 | 
			
		||||
      detail.browser = ua.browser.name;
 | 
			
		||||
      detail.os = ua.os.name;
 | 
			
		||||
      detail.device = ua.device.vendor ? ua.device.vendor + ua.device.model : '';
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      smartSentry.captureError(e);
 | 
			
		||||
    } finally {
 | 
			
		||||
@@ -107,10 +121,11 @@
 | 
			
		||||
    font-size: 20px;
 | 
			
		||||
    font-weight: bold;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .info-box {
 | 
			
		||||
    border-bottom: 1px solid #f0f0f0;
 | 
			
		||||
    padding: 10px 8px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .detail-info {
 | 
			
		||||
    .ant-col {
 | 
			
		||||
      line-height: 1.46;
 | 
			
		||||
@@ -118,6 +133,7 @@
 | 
			
		||||
      padding-right: 5px;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .detail-right-title {
 | 
			
		||||
    text-align: right;
 | 
			
		||||
    color: grey;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@
 | 
			
		||||
        <a-input style="width: 150px" v-model:value="queryForm.keywords" placeholder="模块/操作内容" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item label="请求关键字" class="smart-query-form-item">
 | 
			
		||||
        <a-input style="width: 220px" v-model:value="queryForm.requestKeywords" placeholder="请求地址/请求方法/请求参数" />
 | 
			
		||||
        <a-input style="width: 270px" v-model:value="queryForm.requestKeywords" placeholder="请求地址/请求方法/请求参数/返回结果" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item label="用户名称" class="smart-query-form-item">
 | 
			
		||||
        <a-input style="width: 100px" v-model:value="queryForm.userName" placeholder="用户名称" />
 | 
			
		||||
@@ -24,7 +24,7 @@
 | 
			
		||||
        <a-range-picker @change="changeCreateDate" v-model:value="createDateRange" :presets="defaultChooseTimeRange" style="width: 240px" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
 | 
			
		||||
      <a-form-item label="快速筛选" class="smart-query-form-item">
 | 
			
		||||
      <a-form-item label="状态:" class="smart-query-form-item">
 | 
			
		||||
        <a-radio-group v-model:value="queryForm.successFlag" @change="onSearch">
 | 
			
		||||
          <a-radio-button :value="undefined">全部</a-radio-button>
 | 
			
		||||
          <a-radio-button :value="true">成功</a-radio-button>
 | 
			
		||||
@@ -51,18 +51,23 @@
 | 
			
		||||
    </a-row>
 | 
			
		||||
  </a-form>
 | 
			
		||||
 | 
			
		||||
  <a-card size="small" :bordered="false" :hoverable="true" style="height: 100%">
 | 
			
		||||
  <a-card size="small" :bordered="false" :hoverable="true" >
 | 
			
		||||
    <a-row justify="end">
 | 
			
		||||
      <TableOperator class="smart-margin-bottom5" v-model="columns" :tableId="TABLE_ID_CONST.SUPPORT.CONFIG" :refresh="ajaxQuery" />
 | 
			
		||||
    </a-row>
 | 
			
		||||
    <a-table size="small" :loading="tableLoading" :dataSource="tableData" :columns="columns" bordered rowKey="operateLogId" :pagination="false">
 | 
			
		||||
      <template #bodyCell="{ text, record, column }">
 | 
			
		||||
        <template v-if="column.dataIndex === 'response'">
 | 
			
		||||
          <a-typography-text v-if="text && text.ok">{{ text ? text.msg : '-' }}</a-typography-text>
 | 
			
		||||
          <a-typography-text v-else type="warning">{{ text ? text.msg : '-' }}</a-typography-text>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-if="column.dataIndex === 'successFlag'">
 | 
			
		||||
          <a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '失败' }}</a-tag>
 | 
			
		||||
          <a-tag :color="text ? 'success' : 'error'">{{ text ? '成功' : '报错' }}</a-tag>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-if="column.dataIndex === 'userAgent'">
 | 
			
		||||
          <div>{{ record.browser }} / {{ record.os }} / {{ record.device }}</div>
 | 
			
		||||
          <div>{{ record.os }} / {{ record.browser }}  {{ record.device ? '/' + record.device : record.device }}</div>
 | 
			
		||||
        </template>
 | 
			
		||||
 | 
			
		||||
        <template v-if="column.dataIndex === 'operateUserType'">
 | 
			
		||||
@@ -88,7 +93,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
@@ -135,14 +139,15 @@
 | 
			
		||||
      ellipsis: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: 'IP',
 | 
			
		||||
      dataIndex: 'ip',
 | 
			
		||||
      title: '返回结果',
 | 
			
		||||
      dataIndex: 'response',
 | 
			
		||||
      ellipsis: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: 'IP地区',
 | 
			
		||||
      dataIndex: 'ipRegion',
 | 
			
		||||
      ellipsis: true,
 | 
			
		||||
      width: 150,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '客户端',
 | 
			
		||||
@@ -150,20 +155,15 @@
 | 
			
		||||
      ellipsis: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '请求方法',
 | 
			
		||||
      dataIndex: 'method',
 | 
			
		||||
      ellipsis: true,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '请求结果',
 | 
			
		||||
      dataIndex: 'successFlag',
 | 
			
		||||
      width: 80,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '时间',
 | 
			
		||||
      title: '操作时间',
 | 
			
		||||
      dataIndex: 'createTime',
 | 
			
		||||
      width: 150,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '状态',
 | 
			
		||||
      dataIndex: 'successFlag',
 | 
			
		||||
      width: 60,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '操作',
 | 
			
		||||
      dataIndex: 'action',
 | 
			
		||||
@@ -212,6 +212,10 @@
 | 
			
		||||
      let responseModel = await operateLogApi.queryList(queryForm);
 | 
			
		||||
 | 
			
		||||
      for (const e of responseModel.data.list) {
 | 
			
		||||
        if(e.response){
 | 
			
		||||
          e.response = JSON.parse(e.response);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (!e.userAgent) {
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,6 @@
 | 
			
		||||
      v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      @change="ajaxQuery"
 | 
			
		||||
      @showSizeChange="ajaxQuery"
 | 
			
		||||
      :show-total="(total) => `共${total}条`"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,6 @@
 | 
			
		||||
      v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      @change="ajaxQuery"
 | 
			
		||||
      @showSizeChange="ajaxQuery"
 | 
			
		||||
      :show-total="(total) => `共${total}条`"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@
 | 
			
		||||
      v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      @change="ajaxQuery"
 | 
			
		||||
      @showSizeChange="ajaxQuery"
 | 
			
		||||
      :show-total="(total) => `共${total}条`"
 | 
			
		||||
    />
 | 
			
		||||
  </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -94,7 +94,6 @@
 | 
			
		||||
        v-model:pageSize="params.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryEmployee"
 | 
			
		||||
        @showSizeChange="queryEmployee"
 | 
			
		||||
        :show-total="showTableTotal"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -8,23 +8,24 @@
 | 
			
		||||
  * @Copyright  1024创新实验室 ( https://1024lab.net ),Since 2012
 | 
			
		||||
-->
 | 
			
		||||
<template>
 | 
			
		||||
  <a-modal :open="visible" width="600px" :bodyStyle="{height:'480px'}"   title="" :closable="false" :maskClosable="true">
 | 
			
		||||
    <a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px">助力卓大抖音1000个粉丝,开播写代码🎉🎉</div> </a-row>
 | 
			
		||||
    <a-row><div style="font-weight:bolder;margin: 20px auto;font-size: 15px">和1024创新实验室一起,热爱代码,热爱生活,永远年轻,永远前行🎉🎉</div> </a-row>
 | 
			
		||||
  <a-modal :open="visible" width="600px" :bodyStyle="{height:'360px'}"   title="" :closable="false" :maskClosable="true">
 | 
			
		||||
    <a-row><div style="font-weight:bolder;margin: 0 auto;font-size: 16px;color: red;">重磅更新:国产数据库支持🎉🎉</div> </a-row>
 | 
			
		||||
    <a-row><div style="font-weight:bolder;margin: 10px auto;font-size: 16px;color: red;">支持:达梦、人大金仓、华为高斯GaussDB 等🎉🎉</div> </a-row>
 | 
			
		||||
    <br />
 | 
			
		||||
    <div class="app-qr-box">
 | 
			
		||||
      <div class="app-qr">
 | 
			
		||||
        <a-image
 | 
			
		||||
            :width="300"
 | 
			
		||||
            :width="200"
 | 
			
		||||
            style="border-radius: 15px;"
 | 
			
		||||
            src="https://img.smartadmin.1024lab.net/wechat/douyin.png"
 | 
			
		||||
            src="https://img.smartadmin.1024lab.net/wechat/zhuoda-wechat.jpg"
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
        <span class="qr-desc strong"> 打开【抖音APP】-点击【左上角侧边栏】-【点击扫一扫】-【进行关注】</span>
 | 
			
		||||
        <span class="qr-desc strong"> 添加“卓大”微信,备注对应数据库,如 达梦</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
    <template #footer>
 | 
			
		||||
      <a-button type="primary" @click="hide">知道了</a-button>
 | 
			
		||||
      <a-button type="default" @click="hide">知道了</a-button>
 | 
			
		||||
      <a-button danger type="default" @click="goto" target="_blank" href="https://smartadmin.vip/views/other/china-db/">去看看国产数据库,了解一下</a-button>
 | 
			
		||||
    </template>
 | 
			
		||||
  </a-modal>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -36,6 +37,9 @@ defineExpose({
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const visible = ref(true);
 | 
			
		||||
  function goto(){
 | 
			
		||||
 | 
			
		||||
  }
 | 
			
		||||
  function show() {
 | 
			
		||||
    visible.value = true;
 | 
			
		||||
  }
 | 
			
		||||
@@ -64,8 +68,7 @@ defineExpose({
 | 
			
		||||
        display: flex;
 | 
			
		||||
        margin-top: 20px;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        font-size: 13px;
 | 
			
		||||
        color: red;
 | 
			
		||||
        font-size: 15px;
 | 
			
		||||
        text-align: center;
 | 
			
		||||
        overflow-x: hidden;
 | 
			
		||||
        > img {
 | 
			
		||||
 
 | 
			
		||||
@@ -108,7 +108,7 @@
 | 
			
		||||
    let lunarMonth = lunar.getMonthInChinese();
 | 
			
		||||
    let lunarDay = lunar.getDayInChinese();
 | 
			
		||||
    //节气
 | 
			
		||||
    let jieqi = lunar.getPrevJieQi().getName();
 | 
			
		||||
    let jieqi = lunar.getJieQi();
 | 
			
		||||
    let next = lunar.getNextJieQi();
 | 
			
		||||
    let nextJieqi = next.getName() + ' ' + next.getSolar().toYmd();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -52,14 +52,14 @@
 | 
			
		||||
        <a-col :span="24">
 | 
			
		||||
          <OfficialAccountCard />
 | 
			
		||||
        </a-col>
 | 
			
		||||
        <!--待办、已办-->
 | 
			
		||||
        <a-col :span="24">
 | 
			
		||||
          <ChangelogCard />
 | 
			
		||||
        </a-col>
 | 
			
		||||
        <!--更新日志-->
 | 
			
		||||
        <a-col :span="24">
 | 
			
		||||
          <ToBeDoneCard />
 | 
			
		||||
        </a-col>
 | 
			
		||||
        <!--待办、已办-->
 | 
			
		||||
        <a-col :span="24">
 | 
			
		||||
          <ChangelogCard />
 | 
			
		||||
        </a-col>
 | 
			
		||||
      </a-row>
 | 
			
		||||
    </a-col>
 | 
			
		||||
<!--    <AdModal/>-->
 | 
			
		||||
 
 | 
			
		||||
@@ -86,7 +86,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryRoleEmployee"
 | 
			
		||||
        @showSizeChange="queryRoleEmployee"
 | 
			
		||||
        :show-total="showTableTotal"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@
 | 
			
		||||
    "vue": "3.4.27",
 | 
			
		||||
    "vue-i18n": "9.13.1",
 | 
			
		||||
    "vue-router": "4.3.2",
 | 
			
		||||
    "vue3-json-viewer": "2.2.2"
 | 
			
		||||
    "vue3-json-viewer": "2.3.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@vitejs/plugin-vue": "5.1.4",
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,6 @@
 | 
			
		||||
      v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
      :total="total"
 | 
			
		||||
      @change="ajaxQuery"
 | 
			
		||||
      @showSizeChange="ajaxQuery"
 | 
			
		||||
      :show-total="(total) => `共${total}条`"
 | 
			
		||||
    />
 | 
			
		||||
    <a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null">
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,6 @@
 | 
			
		||||
        v-model:pageSize="params.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryEmployee"
 | 
			
		||||
        @showSizeChange="queryEmployee"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ export const appDefaultConfig = {
 | 
			
		||||
  // 圆角
 | 
			
		||||
  borderRadius: 6,
 | 
			
		||||
  // 菜单展开模式
 | 
			
		||||
  flatPattern: false,
 | 
			
		||||
  menuSingleExpandFlag: true,
 | 
			
		||||
  // 标签页
 | 
			
		||||
  pageTagFlag: true,
 | 
			
		||||
  // 标签页样式: default、 antd、chrome
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,11 @@ export default {
 | 
			
		||||
  'setting.menu.layout': 'Menu Layout',
 | 
			
		||||
  'setting.menu.width': 'Menu Width',
 | 
			
		||||
  'setting.menu.theme': 'Menu Theme',
 | 
			
		||||
  'setting.menu.expand': 'Menu Expand',
 | 
			
		||||
  'setting.page.width': 'Page Width',
 | 
			
		||||
  'setting.border.radius': 'Border Radius',
 | 
			
		||||
  'setting.compact': 'Page Compact',
 | 
			
		||||
  'setting.bread': 'Show Bread',
 | 
			
		||||
  'setting.flatPattern': 'Flat Pattern',
 | 
			
		||||
  'setting.pagetag': 'Show PageTag',
 | 
			
		||||
  'setting.pagetag.style': 'PageTag Style',
 | 
			
		||||
  'setting.footer': 'Show Footer',
 | 
			
		||||
 
 | 
			
		||||
@@ -21,11 +21,11 @@ export default {
 | 
			
		||||
  'setting.menu.layout': '菜单布局',
 | 
			
		||||
  'setting.menu.width': '菜单宽度',
 | 
			
		||||
  'setting.menu.theme': '菜单主题',
 | 
			
		||||
  'setting.menu.expand': '菜单展开',
 | 
			
		||||
  'setting.compact': '页面紧凑',
 | 
			
		||||
  'setting.border.radius': '页面圆角',
 | 
			
		||||
  'setting.page.width': '页面宽度',
 | 
			
		||||
  'setting.bread': '面包屑',
 | 
			
		||||
  'setting.flatPattern': '菜单展开模式',
 | 
			
		||||
  'setting.pagetag': '标签页',
 | 
			
		||||
  'setting.pagetag.style': '标签页样式',
 | 
			
		||||
  'setting.footer': '页脚',
 | 
			
		||||
 
 | 
			
		||||
@@ -83,8 +83,8 @@
 | 
			
		||||
          <a-radio-button value="chrome">Chrome</a-radio-button>
 | 
			
		||||
        </a-radio-group>
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item :label="$t('setting.flatPattern')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
 | 
			
		||||
        <a-switch @change="changeFlatPattern" v-model:checked="formState.flatPattern" checked-children="多个" un-checked-children="单个" />
 | 
			
		||||
      <a-form-item :label="$t('setting.menu.expand')" v-if="formState.layout === LAYOUT_ENUM.SIDE.value">
 | 
			
		||||
        <a-switch @change="changeMenuExpandFlag" v-model:checked="formState.menuSingleExpandFlag" checked-children="单个" un-checked-children="多个" />
 | 
			
		||||
      </a-form-item>
 | 
			
		||||
      <a-form-item :label="$t('setting.pagetag')">
 | 
			
		||||
        <a-switch @change="changePageTagFlag" v-model:checked="formState.pageTagFlag" checked-children="显示" un-checked-children="隐藏" />
 | 
			
		||||
@@ -213,8 +213,8 @@
 | 
			
		||||
    borderRadius: appConfigStore.borderRadius,
 | 
			
		||||
    // 标签页
 | 
			
		||||
    pageTagFlag: appConfigStore.pageTagFlag,
 | 
			
		||||
    // 标签页
 | 
			
		||||
    flatPattern: appConfigStore.flatPattern,
 | 
			
		||||
    // 菜单展开方式
 | 
			
		||||
    menuSingleExpandFlag: appConfigStore.menuSingleExpandFlag,
 | 
			
		||||
    // 标签页 样式
 | 
			
		||||
    pageTagStyle: appConfigStore.pageTagStyle,
 | 
			
		||||
    // 面包屑
 | 
			
		||||
@@ -303,9 +303,9 @@
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function changeFlatPattern(e) {
 | 
			
		||||
  function changeMenuExpandFlag(e) {
 | 
			
		||||
    appConfigStore.$patch({
 | 
			
		||||
      flatPattern: e,
 | 
			
		||||
      menuSingleExpandFlag: e,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,7 @@
 | 
			
		||||
  import { useUserStore } from '/@/store/modules/system/user';
 | 
			
		||||
 | 
			
		||||
  const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
 | 
			
		||||
  const flatPattern = computed(() => useAppConfigStore().$state.flatPattern);
 | 
			
		||||
  const menuSingleExpandFlag = computed(() => useAppConfigStore().$state.menuSingleExpandFlag);
 | 
			
		||||
 | 
			
		||||
  const props = defineProps({
 | 
			
		||||
    collapsed: {
 | 
			
		||||
@@ -46,8 +46,7 @@
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const menuTree = computed(() => useUserStore().getMenuTree || []);
 | 
			
		||||
  const rootSubmenuKeys = computed(()=>menuTree.value.map(item=>item.menuId));
 | 
			
		||||
 | 
			
		||||
  const rootSubmenuKeys = computed(() => menuTree.value.map((item) => item.menuId));
 | 
			
		||||
 | 
			
		||||
  //展开的菜单
 | 
			
		||||
  let currentRoute = useRoute();
 | 
			
		||||
@@ -76,9 +75,15 @@
 | 
			
		||||
    let parentList = menuParentIdListMap.get(currentRoute.name) || [];
 | 
			
		||||
 | 
			
		||||
    // 如果是折叠菜单的话,则不需要设置openkey
 | 
			
		||||
    if (!props.collapsed) {
 | 
			
		||||
    if (props.collapsed) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    let needOpenKeys = _.map(parentList, 'name').map(Number);
 | 
			
		||||
    if (menuSingleExpandFlag.value) {
 | 
			
		||||
      openKeys.value = [...needOpenKeys];
 | 
			
		||||
    } else {
 | 
			
		||||
      // 使用lodash的union函数,进行 去重合并两个数组
 | 
			
		||||
      let needOpenKeys = _.map(parentList, 'name').map(Number);
 | 
			
		||||
      openKeys.value = _.union(openKeys.value, needOpenKeys);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -92,17 +97,18 @@
 | 
			
		||||
      immediate: true,
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
  function onOpenChange(openKeysParams){
 | 
			
		||||
  if(flatPattern.value){
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  function onOpenChange(openKeysParams) {
 | 
			
		||||
    if (!menuSingleExpandFlag.value) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const latestOpenKey = openKeysParams.find((key) => openKeys.value.indexOf(key) === -1);
 | 
			
		||||
    if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
 | 
			
		||||
      openKeys.value = openKeysParams;
 | 
			
		||||
    } else {
 | 
			
		||||
      openKeys.value = latestOpenKey ? [latestOpenKey] : [];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const latestOpenKey = openKeysParams.find(key => openKeys.value.indexOf(key) === -1);
 | 
			
		||||
  if (rootSubmenuKeys.value.indexOf(latestOpenKey) === -1) {
 | 
			
		||||
    openKeys.value = openKeysParams;
 | 
			
		||||
  } else {
 | 
			
		||||
    openKeys.value = latestOpenKey ? [latestOpenKey] : [];
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
  defineExpose({
 | 
			
		||||
    updateOpenKeysAndSelectKeys,
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -78,7 +78,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -63,7 +63,6 @@
 | 
			
		||||
          v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
          :total="total"
 | 
			
		||||
          @change="queryEmployee"
 | 
			
		||||
          @showSizeChange="queryEmployee"
 | 
			
		||||
          :show-total="showTableTotal"
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -102,7 +102,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="ajaxQuery"
 | 
			
		||||
        @showSizeChange="ajaxQuery"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryViewRecord"
 | 
			
		||||
        @showSizeChange="queryViewRecord"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -72,7 +72,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryNoticeList"
 | 
			
		||||
        @showSizeChange="queryNoticeList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -122,7 +122,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryNoticeList"
 | 
			
		||||
        @showSizeChange="queryNoticeList"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -107,7 +107,6 @@
 | 
			
		||||
        v-model:pageSize="queryForm.pageSize"
 | 
			
		||||
        :total="total"
 | 
			
		||||
        @change="queryData"
 | 
			
		||||
        @showSizeChange="queryData"
 | 
			
		||||
        :show-total="(total) => `共${total}条`"
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user