mirror of
https://gitee.com/lab1024/smart-admin.git
synced 2026-03-03 22:14:25 +08:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6867198bf6 | ||
|
|
2c661120ca | ||
|
|
a5adcf1cef | ||
|
|
fcbdb9afe0 | ||
|
|
885ad21184 | ||
|
|
36de38fda3 | ||
|
|
efff2dd6d2 | ||
|
|
69ebb583a4 | ||
|
|
843823188b | ||
|
|
97827c4d27 | ||
|
|
38a92824ba | ||
|
|
bf8311c296 | ||
|
|
686f6ac9a3 | ||
|
|
374cc92a79 | ||
|
|
63ef235b95 | ||
|
|
3ceea05ba1 | ||
|
|
9fda0a7bd6 | ||
|
|
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、SqlServer、Oracle】等</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();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ public class DepartmentEntity {
|
||||
/**
|
||||
* 负责人员工 id
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private Long managerId;
|
||||
|
||||
/**
|
||||
|
||||
@@ -274,12 +274,12 @@ public class EmployeeService {
|
||||
if (null == employeeEntity) {
|
||||
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 更新禁用状态
|
||||
employeeDao.updateDisableFlag(employeeId, !employeeEntity.getDisabledFlag());
|
||||
|
||||
if (employeeEntity.getDisabledFlag()) {
|
||||
// 强制退出登录
|
||||
StpUtil.logout(UserTypeEnum.ADMIN_EMPLOYEE.getValue() + StringConst.COLON + employeeId);
|
||||
}
|
||||
// 强制退出登录
|
||||
StpUtil.logout(UserTypeEnum.ADMIN_EMPLOYEE.getValue() + StringConst.COLON + employeeId);
|
||||
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
SELECT * FROM t_goods
|
||||
<where>
|
||||
<if test="query.searchWord != null and query.searchWord !=''">
|
||||
INSTR(goods_name,#{query.searchWord})
|
||||
AND INSTR(goods_name,#{query.searchWord})
|
||||
</if>
|
||||
<if test="query.place != null">
|
||||
AND INSTR(place,#{query.place})
|
||||
|
||||
@@ -20,17 +20,25 @@ public class LongJsonSerializer extends JsonSerializer<Long> {
|
||||
|
||||
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
|
||||
|
||||
/**
|
||||
* JS 安全整数范围
|
||||
* 根据 JS Number.MIN_SAFE_INTEGER 与 Number.MAX_SAFE_INTEGER 得来
|
||||
*/
|
||||
private static final long JS_MIN_SAFE_INTEGER = -9007199254740991L;
|
||||
private static final long JS_MAX_SAFE_INTEGER = 9007199254740991L;
|
||||
|
||||
|
||||
@Override
|
||||
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
|
||||
if (null == value) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
// js中最大安全整数16位 Number.MAX_SAFE_INTEGER
|
||||
String longStr = String.valueOf(value);
|
||||
if (longStr.length() > 16) {
|
||||
gen.writeString(longStr);
|
||||
// 如果超出了 JavaScript 安全整数范围,则序列化为字符串
|
||||
if (value < JS_MIN_SAFE_INTEGER || value > JS_MAX_SAFE_INTEGER) {
|
||||
gen.writeString(Long.toString(value));
|
||||
} else {
|
||||
// 否则,序列化为数字
|
||||
gen.writeNumber(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,48 +5,69 @@ 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 {
|
||||
|
||||
private static final String REDIS_CACHE = "redis";
|
||||
|
||||
private static final String CAFFEINE_CACHE = "caffeine";
|
||||
|
||||
public static final String REDIS_CACHE_PREFIX = "cache";
|
||||
|
||||
|
||||
@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()));
|
||||
.computePrefixWith(name -> REDIS_CACHE_PREFIX + name + ":")
|
||||
// 使用 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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
@@ -31,55 +32,70 @@ import java.net.URI;
|
||||
@Configuration
|
||||
public class FileConfig implements WebMvcConfigurer {
|
||||
|
||||
private static final String HTTPS = "https://";
|
||||
|
||||
private static final String HTTP = "http://";
|
||||
|
||||
private static final String MODE_CLOUD = "cloud";
|
||||
|
||||
private static final String MODE_LOCAL = "local";
|
||||
|
||||
@Value("${file.storage.cloud.region}")
|
||||
private String region;
|
||||
|
||||
@Value("${file.storage.cloud.endpoint}")
|
||||
private String endpoint;
|
||||
|
||||
@Value("${file.storage.cloud.bucket-name}")
|
||||
private String bucketName;
|
||||
|
||||
@Value("${file.storage.cloud.access-key}")
|
||||
private String accessKey;
|
||||
|
||||
@Value("${file.storage.cloud.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${file.storage.cloud.private-url-expire-seconds}")
|
||||
private Long privateUrlExpireSeconds;
|
||||
|
||||
@Value("${file.storage.cloud.url-prefix}")
|
||||
private String urlPrefix;
|
||||
|
||||
@Value("${file.storage.local.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${file.storage.mode}")
|
||||
private String mode;
|
||||
|
||||
@Value("${file.storage.cloud.region}")
|
||||
private String cloudRegion;
|
||||
|
||||
@Value("${file.storage.cloud.endpoint}")
|
||||
private String cloudEndpoint;
|
||||
|
||||
@Value("${file.storage.cloud.bucket-name}")
|
||||
private String cloudBucketName;
|
||||
|
||||
@Value("${file.storage.cloud.access-key}")
|
||||
private String cloudAccessKey;
|
||||
|
||||
@Value("${file.storage.cloud.secret-key}")
|
||||
private String cloudSecretKey;
|
||||
|
||||
@Value("${file.storage.cloud.private-url-expire-seconds}")
|
||||
private Long cloudPrivateUrlExpireSeconds;
|
||||
|
||||
@Value("${file.storage.cloud.public-url-prefix}")
|
||||
private String cloudPublicUrlPrefix;
|
||||
|
||||
@Value("${file.storage.local.upload-path}")
|
||||
private String localUploadPath;
|
||||
|
||||
|
||||
/**
|
||||
* 初始化 云oss client 配置
|
||||
*
|
||||
* @return
|
||||
* 初始化 s3 client 配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
|
||||
public S3Client initAmazonS3() {
|
||||
public S3Client initS3Client() {
|
||||
return S3Client.builder()
|
||||
.region(Region.AWS_GLOBAL)
|
||||
.endpointOverride(URI.create((urlPrefix.startsWith(HTTPS) ? HTTPS : HTTP) + endpoint))
|
||||
.region(Region.of(cloudRegion))
|
||||
.endpointOverride(URI.create(cloudEndpoint))
|
||||
.credentialsProvider(
|
||||
StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(accessKey, secretKey)))
|
||||
AwsBasicCredentials.create(cloudAccessKey, cloudSecretKey)))
|
||||
.serviceConfiguration(S3Configuration.builder()
|
||||
.pathStyleAccessEnabled(false)
|
||||
.chunkedEncodingEnabled(false)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 s3 预签名
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
|
||||
public S3Presigner initS3Presigner() {
|
||||
return S3Presigner
|
||||
.builder()
|
||||
.region(Region.of(cloudRegion))
|
||||
.endpointOverride(URI.create(cloudEndpoint))
|
||||
.credentialsProvider(
|
||||
StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(cloudAccessKey, cloudSecretKey)))
|
||||
.serviceConfiguration(S3Configuration.builder()
|
||||
.pathStyleAccessEnabled(false)
|
||||
.chunkedEncodingEnabled(false)
|
||||
@@ -102,7 +118,7 @@ public class FileConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
if (MODE_LOCAL.equals(mode)) {
|
||||
String path = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||
String path = localUploadPath.endsWith("/") ? localUploadPath : localUploadPath + "/";
|
||||
registry.addResourceHandler(FileStorageLocalServiceImpl.UPLOAD_MAPPING + "/**").addResourceLocations("file:" + path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.lab1024.sa.base.config;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
@@ -13,6 +14,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
@@ -37,6 +40,9 @@ public class JsonConfig {
|
||||
builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter()));
|
||||
builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
|
||||
builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
|
||||
builder.serializerByType(Long.TYPE, LongJsonSerializer.INSTANCE);
|
||||
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
|
||||
builder.serializerByType(BigDecimal.class, ToStringSerializer.instance);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package net.lab1024.sa.base.module.support.cache;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import jakarta.annotation.Resource;
|
||||
import net.lab1024.sa.base.config.CacheConfig;
|
||||
import net.lab1024.sa.base.constant.ReloadConst;
|
||||
import net.lab1024.sa.base.module.support.reload.core.annoation.SmartReload;
|
||||
import org.springframework.data.redis.cache.RedisCache;
|
||||
@@ -49,7 +50,7 @@ public class RedisCacheServiceImpl implements CacheService {
|
||||
// 获取 Redis 连接
|
||||
RedisConnection connection = redisConnectionFactory.getConnection();
|
||||
// 根据指定的 key 模式获取所有匹配的键
|
||||
Set<byte[]> keys = connection.keyCommands().keys((cacheName + ":*").getBytes());
|
||||
Set<byte[]> keys = connection.keyCommands().keys((CacheConfig.REDIS_CACHE_PREFIX + ":" + cacheName + "*").getBytes());
|
||||
|
||||
if (keys != null) {
|
||||
return keys.stream().map(key -> {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.lab1024.sa.base.module.support.dict.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
/**
|
||||
* 字典回显样式 枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum DictDataStyleEnum implements BaseEnum {
|
||||
|
||||
DEFAULT("默认", "default"),
|
||||
PRIMARY("主要", "primary"),
|
||||
SUCCESS("成功", "success"),
|
||||
INFO("信息", "info"),
|
||||
WARNING("警告", "warning"),
|
||||
DANGER("危险", "danger");
|
||||
|
||||
private final String value;
|
||||
|
||||
private final String desc;
|
||||
|
||||
DictDataStyleEnum(String desc, String value) {
|
||||
this.desc = desc;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@ public class DictDataEntity {
|
||||
*/
|
||||
private String dataLabel;
|
||||
|
||||
/**
|
||||
* 字典项样式
|
||||
*/
|
||||
private String dataStyle;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
|
||||
@@ -4,6 +4,10 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
|
||||
import net.lab1024.sa.base.module.support.dict.constant.DictDataStyleEnum;
|
||||
|
||||
|
||||
/**
|
||||
* 字典数据表 新建表单
|
||||
@@ -28,6 +32,10 @@ public class DictDataAddForm {
|
||||
@NotBlank(message = "字典项显示名称 不能为空")
|
||||
private String dataLabel;
|
||||
|
||||
@SchemaEnum(value = DictDataStyleEnum.class, desc = "数据样式")
|
||||
@CheckEnum(message = "样式参数错误", value = DictDataStyleEnum.class)
|
||||
private String dataStyle;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -38,6 +38,9 @@ public class DictDataVO implements Serializable {
|
||||
@Schema(description = "字典项显示名称")
|
||||
private String dataLabel;
|
||||
|
||||
@Schema(description = "字典项回显")
|
||||
private String dataStyle;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DictVO {
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "禁用状态")
|
||||
private Integer disabledFlag;
|
||||
private Boolean disabledFlag;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@@ -20,22 +20,22 @@ public enum FileFolderTypeEnum implements BaseEnum {
|
||||
/**
|
||||
* 通用
|
||||
*/
|
||||
COMMON(1, FileFolderTypeEnum.FOLDER_PUBLIC + "/common/", "通用"),
|
||||
COMMON(1, FileFolderTypeEnum.FOLDER_PRIVATE + "/common/", "通用"),
|
||||
|
||||
/**
|
||||
* 公告
|
||||
*/
|
||||
NOTICE(2, FileFolderTypeEnum.FOLDER_PUBLIC + "/notice/", "公告"),
|
||||
NOTICE(2, FileFolderTypeEnum.FOLDER_PRIVATE + "/notice/", "公告"),
|
||||
|
||||
/**
|
||||
* 帮助中心
|
||||
*/
|
||||
HELP_DOC(3, FileFolderTypeEnum.FOLDER_PUBLIC + "/help-doc/", "帮助中心"),
|
||||
HELP_DOC(3, FileFolderTypeEnum.FOLDER_PRIVATE + "/help-doc/", "帮助中心"),
|
||||
|
||||
/**
|
||||
* 意见反馈
|
||||
*/
|
||||
FEEDBACK(4, FileFolderTypeEnum.FOLDER_PUBLIC + "/feedback/", "意见反馈"),
|
||||
FEEDBACK(4, FileFolderTypeEnum.FOLDER_PRIVATE + "/feedback/", "意见反馈"),
|
||||
|
||||
;
|
||||
|
||||
|
||||
@@ -71,6 +71,9 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
@Resource
|
||||
private S3Client s3Client;
|
||||
|
||||
@Resource
|
||||
private S3Presigner s3Presigner;
|
||||
|
||||
@Resource
|
||||
private FileConfig cloudConfig;
|
||||
|
||||
@@ -101,7 +104,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.getCloudBucketName())
|
||||
.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,16 +126,12 @@ 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);
|
||||
uploadVO.setFileType(fileType);
|
||||
// 根据 访问权限 返回不同的 URL
|
||||
String url = cloudConfig.getUrlPrefix() + fileKey;
|
||||
String url = cloudConfig.getCloudPublicUrlPrefix() + fileKey;
|
||||
if (ObjectCannedACL.PRIVATE.equals(acl)) {
|
||||
// 获取临时访问的URL
|
||||
url = this.getFileUrl(fileKey).getData();
|
||||
@@ -146,11 +156,10 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
|
||||
if (!fileKey.startsWith(FileFolderTypeEnum.FOLDER_PRIVATE)) {
|
||||
// 不是私有的 都公共读
|
||||
return ResponseDTO.ok(cloudConfig.getUrlPrefix() + fileKey);
|
||||
return ResponseDTO.ok(cloudConfig.getCloudPublicUrlPrefix() + fileKey);
|
||||
}
|
||||
|
||||
// 如果是私有的,则规定时间内可以访问,超过规定时间,则连接失效
|
||||
|
||||
String fileRedisKey = RedisKeyConst.Support.FILE_PRIVATE_VO + fileKey;
|
||||
FileVO fileVO = redisService.getObject(fileRedisKey, FileVO.class);
|
||||
if (fileVO == null) {
|
||||
@@ -158,15 +167,22 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
if (fileVO == null) {
|
||||
return ResponseDTO.userErrorParam("文件不存在");
|
||||
}
|
||||
GetObjectRequest getUrlRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).build();
|
||||
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder().signatureDuration(Duration.ofSeconds(cloudConfig.getPrivateUrlExpireSeconds())).getObjectRequest(getUrlRequest).build();
|
||||
GetObjectRequest getUrlRequest = GetObjectRequest
|
||||
.builder()
|
||||
.bucket(cloudConfig.getCloudBucketName())
|
||||
.key(fileKey)
|
||||
.build();
|
||||
|
||||
S3Presigner presigner = S3Presigner.builder().region(Region.of(cloudConfig.getRegion())).build();
|
||||
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest
|
||||
.builder()
|
||||
.signatureDuration(Duration.ofSeconds(cloudConfig.getCloudPrivateUrlExpireSeconds()))
|
||||
.getObjectRequest(getUrlRequest)
|
||||
.build();
|
||||
|
||||
PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest);
|
||||
PresignedGetObjectRequest presignedGetObjectRequest = s3Presigner.presignGetObject(getObjectPresignRequest);
|
||||
String url = presignedGetObjectRequest.url().toString();
|
||||
fileVO.setFileUrl(url);
|
||||
redisService.set(fileRedisKey, fileVO, cloudConfig.getPrivateUrlExpireSeconds() - 5);
|
||||
redisService.set(fileRedisKey, fileVO, cloudConfig.getCloudPrivateUrlExpireSeconds() - 5);
|
||||
}
|
||||
|
||||
return ResponseDTO.ok(fileVO.getFileUrl());
|
||||
@@ -180,7 +196,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
public ResponseDTO<FileDownloadVO> download(String key) {
|
||||
|
||||
// 获取文件 meta
|
||||
HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getBucketName()).key(key).build();
|
||||
HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getCloudBucketName()).key(key).build();
|
||||
HeadObjectResponse headObjectResponse = s3Client.headObject(objectRequest);
|
||||
Map<String, String> userMetadata = headObjectResponse.metadata();
|
||||
FileMetadataVO metadataDTO = null;
|
||||
@@ -194,7 +210,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
}
|
||||
|
||||
//获取oss对象
|
||||
GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(key).build();
|
||||
GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getCloudBucketName()).key(key).build();
|
||||
ResponseBytes<GetObjectResponse> s3ClientObject = s3Client.getObject(getObjectRequest, ResponseTransformer.toBytes());
|
||||
|
||||
// 输入流转换为字节流
|
||||
@@ -229,7 +245,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
*/
|
||||
@Override
|
||||
public ResponseDTO<String> delete(String fileKey) {
|
||||
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).build();
|
||||
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(cloudConfig.getCloudBucketName()).key(fileKey).build();
|
||||
s3Client.deleteObject(deleteObjectRequest);
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ public class FileStorageLocalServiceImpl implements IFileStorageService {
|
||||
// 目录不存在,新建
|
||||
directory.mkdirs();
|
||||
}
|
||||
if (!path.endsWith("/")) {
|
||||
path = path + "/";
|
||||
if (!path.endsWith(File.separator)) {
|
||||
path = path + File.separator;
|
||||
}
|
||||
FileUploadVO fileUploadVO = new FileUploadVO();
|
||||
//原文件名
|
||||
|
||||
@@ -3,8 +3,10 @@ package net.lab1024.sa.base.module.support.loginlog.domain;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -20,6 +22,8 @@ import java.time.LocalDateTime;
|
||||
@TableName("t_login_log")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoginLogEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package net.lab1024.sa.base.module.support.securityprotect.domain;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -21,6 +23,8 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Builder
|
||||
@TableName("t_login_fail")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoginFailEntity {
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -103,12 +103,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -103,12 +103,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -103,12 +103,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -103,12 +103,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,6 @@ public class DepartmentEntity {
|
||||
/**
|
||||
* 负责人员工 id
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.NEVER)
|
||||
private Long managerId;
|
||||
|
||||
/**
|
||||
|
||||
@@ -275,12 +275,12 @@ public class EmployeeService {
|
||||
if (null == employeeEntity) {
|
||||
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
|
||||
}
|
||||
|
||||
// 更新禁用状态
|
||||
employeeDao.updateDisableFlag(employeeId, !employeeEntity.getDisabledFlag());
|
||||
|
||||
if (employeeEntity.getDisabledFlag()) {
|
||||
// 强制退出登录
|
||||
StpUtil.logout(UserTypeEnum.ADMIN_EMPLOYEE.getValue() + StringConst.COLON + employeeId);
|
||||
}
|
||||
// 强制退出登录
|
||||
StpUtil.logout(UserTypeEnum.ADMIN_EMPLOYEE.getValue() + StringConst.COLON + employeeId);
|
||||
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
SELECT * FROM t_goods
|
||||
<where>
|
||||
<if test="query.searchWord != null and query.searchWord !=''">
|
||||
INSTR(goods_name,#{query.searchWord})
|
||||
AND INSTR(goods_name,#{query.searchWord})
|
||||
</if>
|
||||
<if test="query.place != null">
|
||||
AND INSTR(place,#{query.place})
|
||||
|
||||
@@ -20,17 +20,25 @@ public class LongJsonSerializer extends JsonSerializer<Long> {
|
||||
|
||||
public static final LongJsonSerializer INSTANCE = new LongJsonSerializer();
|
||||
|
||||
/**
|
||||
* JS 安全整数范围
|
||||
* 根据 JS Number.MIN_SAFE_INTEGER 与 Number.MAX_SAFE_INTEGER 得来
|
||||
*/
|
||||
private static final long JS_MIN_SAFE_INTEGER = -9007199254740991L;
|
||||
private static final long JS_MAX_SAFE_INTEGER = 9007199254740991L;
|
||||
|
||||
|
||||
@Override
|
||||
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
|
||||
if (null == value) {
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
// js中最大安全整数16位 Number.MAX_SAFE_INTEGER
|
||||
String longStr = String.valueOf(value);
|
||||
if (longStr.length() > 16) {
|
||||
gen.writeString(longStr);
|
||||
// 如果超出了 JavaScript 安全整数范围,则序列化为字符串
|
||||
if (value < JS_MIN_SAFE_INTEGER || value > JS_MAX_SAFE_INTEGER) {
|
||||
gen.writeString(Long.toString(value));
|
||||
} else {
|
||||
// 否则,序列化为数字
|
||||
gen.writeNumber(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,28 +19,46 @@ import javax.annotation.Resource;
|
||||
/**
|
||||
* 缓存配置
|
||||
*
|
||||
* @author zhoumingfa
|
||||
* @date 2025/03/28
|
||||
*/
|
||||
@Configuration
|
||||
public class CacheConfig {
|
||||
|
||||
private static final String REDIS_CACHE = "redis";
|
||||
|
||||
private static final String CAFFEINE_CACHE = "caffeine";
|
||||
|
||||
public static final String REDIS_CACHE_PREFIX = "cache";
|
||||
|
||||
|
||||
@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()));
|
||||
.computePrefixWith(name -> REDIS_CACHE_PREFIX + name + ":")
|
||||
// 使用 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() {
|
||||
|
||||
@@ -15,6 +15,7 @@ import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.S3Configuration;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
|
||||
import java.net.URI;
|
||||
|
||||
@@ -31,55 +32,70 @@ import java.net.URI;
|
||||
@Configuration
|
||||
public class FileConfig implements WebMvcConfigurer {
|
||||
|
||||
private static final String HTTPS = "https://";
|
||||
|
||||
private static final String HTTP = "http://";
|
||||
|
||||
private static final String MODE_CLOUD = "cloud";
|
||||
|
||||
private static final String MODE_LOCAL = "local";
|
||||
|
||||
@Value("${file.storage.cloud.region}")
|
||||
private String region;
|
||||
|
||||
@Value("${file.storage.cloud.endpoint}")
|
||||
private String endpoint;
|
||||
|
||||
@Value("${file.storage.cloud.bucket-name}")
|
||||
private String bucketName;
|
||||
|
||||
@Value("${file.storage.cloud.access-key}")
|
||||
private String accessKey;
|
||||
|
||||
@Value("${file.storage.cloud.secret-key}")
|
||||
private String secretKey;
|
||||
|
||||
@Value("${file.storage.cloud.private-url-expire-seconds}")
|
||||
private Long privateUrlExpireSeconds;
|
||||
|
||||
@Value("${file.storage.cloud.url-prefix}")
|
||||
private String urlPrefix;
|
||||
|
||||
@Value("${file.storage.local.upload-path}")
|
||||
private String uploadPath;
|
||||
|
||||
@Value("${file.storage.mode}")
|
||||
private String mode;
|
||||
|
||||
@Value("${file.storage.cloud.region}")
|
||||
private String cloudRegion;
|
||||
|
||||
@Value("${file.storage.cloud.endpoint}")
|
||||
private String cloudEndpoint;
|
||||
|
||||
@Value("${file.storage.cloud.bucket-name}")
|
||||
private String cloudBucketName;
|
||||
|
||||
@Value("${file.storage.cloud.access-key}")
|
||||
private String cloudAccessKey;
|
||||
|
||||
@Value("${file.storage.cloud.secret-key}")
|
||||
private String cloudSecretKey;
|
||||
|
||||
@Value("${file.storage.cloud.private-url-expire-seconds}")
|
||||
private Long cloudPrivateUrlExpireSeconds;
|
||||
|
||||
@Value("${file.storage.cloud.public-url-prefix}")
|
||||
private String cloudPublicUrlPrefix;
|
||||
|
||||
@Value("${file.storage.local.upload-path}")
|
||||
private String localUploadPath;
|
||||
|
||||
|
||||
/**
|
||||
* 初始化 云oss client 配置
|
||||
*
|
||||
* @return
|
||||
* 初始化 s3 client 配置
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
|
||||
public S3Client initAmazonS3() {
|
||||
public S3Client initS3Client() {
|
||||
return S3Client.builder()
|
||||
.region(Region.AWS_GLOBAL)
|
||||
.endpointOverride(URI.create((urlPrefix.startsWith(HTTPS) ? HTTPS : HTTP) + endpoint))
|
||||
.region(Region.of(cloudRegion))
|
||||
.endpointOverride(URI.create(cloudEndpoint))
|
||||
.credentialsProvider(
|
||||
StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(accessKey, secretKey)))
|
||||
AwsBasicCredentials.create(cloudAccessKey, cloudSecretKey)))
|
||||
.serviceConfiguration(S3Configuration.builder()
|
||||
.pathStyleAccessEnabled(false)
|
||||
.chunkedEncodingEnabled(false)
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化 s3 预签名
|
||||
*/
|
||||
@Bean
|
||||
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
|
||||
public S3Presigner initS3Presigner() {
|
||||
return S3Presigner
|
||||
.builder()
|
||||
.region(Region.of(cloudRegion))
|
||||
.endpointOverride(URI.create(cloudEndpoint))
|
||||
.credentialsProvider(
|
||||
StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(cloudAccessKey, cloudSecretKey)))
|
||||
.serviceConfiguration(S3Configuration.builder()
|
||||
.pathStyleAccessEnabled(false)
|
||||
.chunkedEncodingEnabled(false)
|
||||
@@ -102,7 +118,7 @@ public class FileConfig implements WebMvcConfigurer {
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
if (MODE_LOCAL.equals(mode)) {
|
||||
String path = uploadPath.endsWith("/") ? uploadPath : uploadPath + "/";
|
||||
String path = localUploadPath.endsWith("/") ? localUploadPath : localUploadPath + "/";
|
||||
registry.addResourceHandler(FileStorageLocalServiceImpl.UPLOAD_MAPPING + "/**").addResourceLocations("file:" + path);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.lab1024.sa.base.config;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
@@ -13,6 +14,8 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeParseException;
|
||||
@@ -37,6 +40,9 @@ public class JsonConfig {
|
||||
builder.serializers(new LocalDateSerializer(DatePattern.NORM_DATE_FORMAT.getDateTimeFormatter()));
|
||||
builder.serializers(new LocalDateTimeSerializer(DatePattern.NORM_DATETIME_FORMAT.getDateTimeFormatter()));
|
||||
builder.serializerByType(Long.class, LongJsonSerializer.INSTANCE);
|
||||
builder.serializerByType(Long.TYPE, LongJsonSerializer.INSTANCE);
|
||||
builder.serializerByType(BigInteger.class, ToStringSerializer.instance);
|
||||
builder.serializerByType(BigDecimal.class, ToStringSerializer.instance);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package net.lab1024.sa.base.module.support.cache;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.google.common.collect.Lists;
|
||||
import net.lab1024.sa.base.config.CacheConfig;
|
||||
import net.lab1024.sa.base.constant.ReloadConst;
|
||||
import net.lab1024.sa.base.module.support.reload.core.annoation.SmartReload;
|
||||
import org.springframework.data.redis.cache.RedisCache;
|
||||
@@ -49,7 +50,7 @@ public class RedisCacheServiceImpl implements CacheService {
|
||||
// 获取 Redis 连接
|
||||
RedisConnection connection = redisConnectionFactory.getConnection();
|
||||
// 根据指定的 key 模式获取所有匹配的键
|
||||
Set<byte[]> keys = connection.keyCommands().keys((cacheName + ":*").getBytes());
|
||||
Set<byte[]> keys = connection.keyCommands().keys((CacheConfig.REDIS_CACHE_PREFIX + ":" + cacheName + "*").getBytes());
|
||||
|
||||
if (keys != null) {
|
||||
return keys.stream().map(key -> {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package net.lab1024.sa.base.module.support.dict.constant;
|
||||
|
||||
import lombok.Getter;
|
||||
import net.lab1024.sa.base.common.enumeration.BaseEnum;
|
||||
|
||||
/**
|
||||
* 字典回显样式 枚举
|
||||
*/
|
||||
@Getter
|
||||
public enum DictDataStyleEnum implements BaseEnum {
|
||||
|
||||
DEFAULT("默认", "default"),
|
||||
PRIMARY("主要", "primary"),
|
||||
SUCCESS("成功", "success"),
|
||||
INFO("信息", "info"),
|
||||
WARNING("警告", "warning"),
|
||||
DANGER("危险", "danger");
|
||||
|
||||
private final String value;
|
||||
|
||||
private final String desc;
|
||||
|
||||
DictDataStyleEnum(String desc, String value) {
|
||||
this.desc = desc;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
@@ -40,6 +40,11 @@ public class DictDataEntity {
|
||||
*/
|
||||
private String dataLabel;
|
||||
|
||||
/**
|
||||
* 字典项样式
|
||||
*/
|
||||
private String dataStyle;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,9 @@ package net.lab1024.sa.base.module.support.dict.domain.form;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import net.lab1024.sa.base.common.swagger.SchemaEnum;
|
||||
import net.lab1024.sa.base.common.validator.enumeration.CheckEnum;
|
||||
import net.lab1024.sa.base.module.support.dict.constant.DictDataStyleEnum;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
@@ -29,6 +32,10 @@ public class DictDataAddForm {
|
||||
@NotBlank(message = "字典项显示名称 不能为空")
|
||||
private String dataLabel;
|
||||
|
||||
@SchemaEnum(value = DictDataStyleEnum.class, desc = "数据样式")
|
||||
@CheckEnum(message = "样式参数错误", value = DictDataStyleEnum.class)
|
||||
private String dataStyle;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -38,6 +38,9 @@ public class DictDataVO implements Serializable {
|
||||
@Schema(description = "字典项显示名称")
|
||||
private String dataLabel;
|
||||
|
||||
@Schema(description = "字典项回显")
|
||||
private String dataStyle;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ public class DictVO {
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "禁用状态")
|
||||
private Integer disabledFlag;
|
||||
private Boolean disabledFlag;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
@@ -20,22 +20,22 @@ public enum FileFolderTypeEnum implements BaseEnum {
|
||||
/**
|
||||
* 通用
|
||||
*/
|
||||
COMMON(1, FileFolderTypeEnum.FOLDER_PUBLIC + "/common/", "通用"),
|
||||
COMMON(1, FileFolderTypeEnum.FOLDER_PRIVATE + "/common/", "通用"),
|
||||
|
||||
/**
|
||||
* 公告
|
||||
*/
|
||||
NOTICE(2, FileFolderTypeEnum.FOLDER_PUBLIC + "/notice/", "公告"),
|
||||
NOTICE(2, FileFolderTypeEnum.FOLDER_PRIVATE + "/notice/", "公告"),
|
||||
|
||||
/**
|
||||
* 帮助中心
|
||||
*/
|
||||
HELP_DOC(3, FileFolderTypeEnum.FOLDER_PUBLIC + "/help-doc/", "帮助中心"),
|
||||
HELP_DOC(3, FileFolderTypeEnum.FOLDER_PRIVATE + "/help-doc/", "帮助中心"),
|
||||
|
||||
/**
|
||||
* 意见反馈
|
||||
*/
|
||||
FEEDBACK(4, FileFolderTypeEnum.FOLDER_PUBLIC + "/feedback/", "意见反馈"),
|
||||
FEEDBACK(4, FileFolderTypeEnum.FOLDER_PRIVATE + "/feedback/", "意见反馈"),
|
||||
|
||||
;
|
||||
|
||||
|
||||
@@ -24,7 +24,6 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
import software.amazon.awssdk.core.ResponseBytes;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.core.sync.ResponseTransformer;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.*;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
@@ -72,6 +71,9 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
@Resource
|
||||
private S3Client s3Client;
|
||||
|
||||
@Resource
|
||||
private S3Presigner s3Presigner;
|
||||
|
||||
@Resource
|
||||
private FileConfig cloudConfig;
|
||||
|
||||
@@ -106,7 +108,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.getCloudBucketName())
|
||||
.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,16 +130,12 @@ 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);
|
||||
uploadVO.setFileType(fileType);
|
||||
// 根据 访问权限 返回不同的 URL
|
||||
String url = cloudConfig.getUrlPrefix() + fileKey;
|
||||
String url = cloudConfig.getCloudPublicUrlPrefix() + fileKey;
|
||||
if (ObjectCannedACL.PRIVATE.equals(acl)) {
|
||||
// 获取临时访问的URL
|
||||
url = this.getFileUrl(fileKey).getData();
|
||||
@@ -151,11 +160,10 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
|
||||
if (!fileKey.startsWith(FileFolderTypeEnum.FOLDER_PRIVATE)) {
|
||||
// 不是私有的 都公共读
|
||||
return ResponseDTO.ok(cloudConfig.getUrlPrefix() + fileKey);
|
||||
return ResponseDTO.ok(cloudConfig.getCloudPublicUrlPrefix() + fileKey);
|
||||
}
|
||||
|
||||
// 如果是私有的,则规定时间内可以访问,超过规定时间,则连接失效
|
||||
|
||||
String fileRedisKey = RedisKeyConst.Support.FILE_PRIVATE_VO + fileKey;
|
||||
FileVO fileVO = redisService.getObject(fileRedisKey, FileVO.class);
|
||||
if (fileVO == null) {
|
||||
@@ -163,15 +171,22 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
if (fileVO == null) {
|
||||
return ResponseDTO.userErrorParam("文件不存在");
|
||||
}
|
||||
GetObjectRequest getUrlRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).build();
|
||||
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder().signatureDuration(Duration.ofSeconds(cloudConfig.getPrivateUrlExpireSeconds())).getObjectRequest(getUrlRequest).build();
|
||||
GetObjectRequest getUrlRequest = GetObjectRequest
|
||||
.builder()
|
||||
.bucket(cloudConfig.getCloudBucketName())
|
||||
.key(fileKey)
|
||||
.build();
|
||||
|
||||
S3Presigner presigner = S3Presigner.builder().region(Region.of(cloudConfig.getRegion())).build();
|
||||
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest
|
||||
.builder()
|
||||
.signatureDuration(Duration.ofSeconds(cloudConfig.getCloudPrivateUrlExpireSeconds()))
|
||||
.getObjectRequest(getUrlRequest)
|
||||
.build();
|
||||
|
||||
PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest);
|
||||
PresignedGetObjectRequest presignedGetObjectRequest = s3Presigner.presignGetObject(getObjectPresignRequest);
|
||||
String url = presignedGetObjectRequest.url().toString();
|
||||
fileVO.setFileUrl(url);
|
||||
redisService.set(fileRedisKey, fileVO, cloudConfig.getPrivateUrlExpireSeconds() - 5);
|
||||
redisService.set(fileRedisKey, fileVO, cloudConfig.getCloudPrivateUrlExpireSeconds() - 5);
|
||||
}
|
||||
|
||||
return ResponseDTO.ok(fileVO.getFileUrl());
|
||||
@@ -185,7 +200,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
public ResponseDTO<FileDownloadVO> download(String key) {
|
||||
|
||||
// 获取文件 meta
|
||||
HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getBucketName()).key(key).build();
|
||||
HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getCloudBucketName()).key(key).build();
|
||||
HeadObjectResponse headObjectResponse = s3Client.headObject(objectRequest);
|
||||
Map<String, String> userMetadata = headObjectResponse.metadata();
|
||||
FileMetadataVO metadataDTO = null;
|
||||
@@ -199,7 +214,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
}
|
||||
|
||||
//获取oss对象
|
||||
GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(key).build();
|
||||
GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getCloudBucketName()).key(key).build();
|
||||
ResponseBytes<GetObjectResponse> s3ClientObject = s3Client.getObject(getObjectRequest, ResponseTransformer.toBytes());
|
||||
|
||||
// 输入流转换为字节流
|
||||
@@ -234,7 +249,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
|
||||
*/
|
||||
@Override
|
||||
public ResponseDTO<String> delete(String fileKey) {
|
||||
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).build();
|
||||
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(cloudConfig.getCloudBucketName()).key(fileKey).build();
|
||||
s3Client.deleteObject(deleteObjectRequest);
|
||||
return ResponseDTO.ok();
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ public class FileStorageLocalServiceImpl implements IFileStorageService {
|
||||
// 目录不存在,新建
|
||||
directory.mkdirs();
|
||||
}
|
||||
if (!path.endsWith("/")) {
|
||||
path = path + "/";
|
||||
if (!path.endsWith(File.separator)) {
|
||||
path = path + File.separator;
|
||||
}
|
||||
FileUploadVO fileUploadVO = new FileUploadVO();
|
||||
//原文件名
|
||||
|
||||
@@ -3,8 +3,10 @@ package net.lab1024.sa.base.module.support.loginlog.domain;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -20,6 +22,8 @@ import java.time.LocalDateTime;
|
||||
@TableName("t_login_log")
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoginLogEntity {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ package net.lab1024.sa.base.module.support.securityprotect.domain;
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@@ -21,6 +23,8 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Builder
|
||||
@TableName("t_login_fail")
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoginFailEntity {
|
||||
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -102,12 +102,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -102,12 +102,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -102,12 +102,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -102,12 +102,13 @@ file:
|
||||
url-prefix:
|
||||
cloud:
|
||||
region: oss-cn-hangzhou
|
||||
endpoint: oss-cn-hangzhou.aliyuncs.com
|
||||
endpoint: https://oss-cn-hangzhou.aliyuncs.com
|
||||
bucket-name: 1024lab-smart-admin
|
||||
access-key:
|
||||
secret-key:
|
||||
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
|
||||
private-url-expire-seconds: 3600
|
||||
# 云计算厂商支持公开的文件访问模式;minio默认是不支持的,对于minio用户可以配置为空
|
||||
public-url-prefix: https://1024lab-smart-admin.oss-cn-hangzhou.aliyuncs.com/
|
||||
|
||||
# open api配置
|
||||
springdoc:
|
||||
|
||||
@@ -17,14 +17,14 @@ module.exports = {
|
||||
ecmaVersion: 12, // 默认情况下,ESLint使用的是ECMAScript5语法,此处我们设置的选项是 es12
|
||||
sourceType: 'module', // 指定js导入的方式
|
||||
},
|
||||
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', 'plugin:vue/base'],
|
||||
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', 'plugin:vue/base', 'prettier'],
|
||||
globals: {
|
||||
defineProps: 'readonly',
|
||||
defineEmits: 'readonly',
|
||||
defineExpose: 'readonly',
|
||||
withDefaults: 'readonly',
|
||||
},
|
||||
plugins: ['vue'],
|
||||
plugins: ['vue', 'prettier'],
|
||||
rules: {
|
||||
'no-unused-vars': [
|
||||
'error',
|
||||
@@ -62,5 +62,6 @@ module.exports = {
|
||||
],
|
||||
// Enable vue/script-setup-uses-vars rule
|
||||
'vue/script-setup-uses-vars': 'error',
|
||||
'prettier/prettier': 'error',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"axios": "1.6.8",
|
||||
"clipboard": "2.0.11",
|
||||
"crypto-js": "4.1.1",
|
||||
"dayjs": "1.10.5",
|
||||
"dayjs": "1.11.13",
|
||||
"decimal.js": "10.3.1",
|
||||
"diff": "5.2.0",
|
||||
"diff2html": "3.4.47",
|
||||
@@ -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",
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
|
||||
import { useSpinStore } from '/@/store/modules/system/spin';
|
||||
import { Popover, theme } from 'ant-design-vue';
|
||||
import { themeColors } from '/@/theme/color.js';
|
||||
import { themeColors } from '/@/theme/color';
|
||||
import SmartCopyIcon from '/@/components/framework/smart-copy-icon/index.vue';
|
||||
|
||||
const antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale);
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useDictStore } from '/@/store/modules/system/dict.js';
|
||||
import { useDictStore } from '/@/store/modules/system/dict';
|
||||
|
||||
const props = defineProps({
|
||||
value: [Array, String, Number],
|
||||
|
||||
@@ -1,16 +1,46 @@
|
||||
<template>
|
||||
<span>{{ dataLabels }}</span>
|
||||
<template v-if="props.showStyle">
|
||||
<template v-for="(item, index) in dataList" :key="item.dictDataId">
|
||||
<span v-if="item.color" :style="{ color: token[item.color] }"> {{ item.dataLabel }} </span>
|
||||
<span v-else>{{ item.dataLabel }}</span>
|
||||
{{ index < dataList.length - 1 ? ',' : '' }}
|
||||
</template>
|
||||
</template>
|
||||
<!--不显示回显的颜色演示-->
|
||||
<template v-else>
|
||||
<span>{{ dataList }}</span>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import { useDictStore } from '/@/store/modules/system/dict.js';
|
||||
import { useDictStore } from '/@/store/modules/system/dict';
|
||||
import { theme } from 'ant-design-vue';
|
||||
import { DICT_DATA_STYLE_ENUM } from '/@/constants/support/dict-const';
|
||||
|
||||
const props = defineProps({
|
||||
dictCode: String,
|
||||
dataValue: [String, Number],
|
||||
showStyle: { type: Boolean, default: false },
|
||||
});
|
||||
const dataLabels = computed(() => {
|
||||
return useDictStore().getDataLabels(props.dictCode, props.dataValue);
|
||||
|
||||
const dataList = computed(() => {
|
||||
if (!props.showStyle) {
|
||||
return useDictStore().getDataLabels(props.dictCode, props.dataValue);
|
||||
}
|
||||
|
||||
let dictDataList = useDictStore().getDataList(props.dictCode, props.dataValue);
|
||||
let dataList = [];
|
||||
for (const item of dictDataList) {
|
||||
if (item.dataStyle) {
|
||||
dataList.push(Object.assign({}, item, { color: DICT_DATA_STYLE_ENUM[item.dataStyle.toUpperCase()].color }));
|
||||
} else {
|
||||
dataList.push(Object.assign({}, item));
|
||||
}
|
||||
}
|
||||
return dataList;
|
||||
});
|
||||
|
||||
const { useToken } = theme;
|
||||
const { token } = useToken();
|
||||
</script>
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useDictStore } from '/@/store/modules/system/dict.js';
|
||||
import { useDictStore } from '/@/store/modules/system/dict';
|
||||
|
||||
const props = defineProps({
|
||||
dictCode: String,
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
import { message } from 'ant-design-vue';
|
||||
import { mergeColumn } from './smart-table-column-merge';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { useAppConfigStore } from '/@/store/modules/system/app-config.js';
|
||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
|
||||
|
||||
const props = defineProps({
|
||||
// 表格列数组
|
||||
|
||||
@@ -71,7 +71,6 @@
|
||||
v-model:pageSize="params.pageSize"
|
||||
:total="total"
|
||||
@change="queryEmployee"
|
||||
@showSizeChange="queryEmployee"
|
||||
:show-total="(total) => `共${total}条`"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
<script setup>
|
||||
import { onMounted, ref, watch } from 'vue';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { positionApi } from '/@/api/system/position-api.js';
|
||||
import { positionApi } from '/@/api/system/position-api';
|
||||
|
||||
// =========== 属性定义 和 事件方法暴露 =============
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ export const appDefaultConfig = {
|
||||
// 圆角
|
||||
borderRadius: 6,
|
||||
// 菜单展开模式
|
||||
flatPattern: false,
|
||||
menuSingleExpandFlag: true,
|
||||
// 标签页
|
||||
pageTagFlag: true,
|
||||
// 标签页样式: default、 antd、chrome
|
||||
|
||||
@@ -21,6 +21,7 @@ import message from './business/message/message-const';
|
||||
import codeGeneratorConst from './support/code-generator-const';
|
||||
import changeLogConst from './support/change-log-const';
|
||||
import jobConst from './support/job-const';
|
||||
import dictConst from './support/dict-const';
|
||||
|
||||
export default {
|
||||
FLAG_NUMBER_ENUM,
|
||||
@@ -39,4 +40,5 @@ export default {
|
||||
...codeGeneratorConst,
|
||||
...changeLogConst,
|
||||
...jobConst,
|
||||
...dictConst,
|
||||
};
|
||||
|
||||
@@ -16,6 +16,40 @@ export const DICT_CODE_ENUM = {
|
||||
GOODS_PLACE: 'GOODS_PLACE',
|
||||
};
|
||||
|
||||
export const DICT_DATA_STYLE_ENUM = {
|
||||
DEFAULT: {
|
||||
value: 'default',
|
||||
desc: '默认',
|
||||
color: 'colorText',
|
||||
},
|
||||
PRIMARY: {
|
||||
value: 'primary',
|
||||
desc: '主要',
|
||||
color: 'colorPrimary',
|
||||
},
|
||||
SUCCESS: {
|
||||
value: 'success',
|
||||
desc: '成功',
|
||||
color: 'colorSuccess',
|
||||
},
|
||||
INFO: {
|
||||
value: 'info',
|
||||
desc: '信息',
|
||||
color: 'colorInfo',
|
||||
},
|
||||
WARN: {
|
||||
value: 'warn',
|
||||
desc: '警告',
|
||||
color: 'colorWarning',
|
||||
},
|
||||
DANGER: {
|
||||
value: 'danger',
|
||||
desc: '危险',
|
||||
color: 'colorError',
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
DICT_CODE_ENUM,
|
||||
DICT_DATA_STYLE_ENUM,
|
||||
};
|
||||
|
||||
@@ -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': '页脚',
|
||||
|
||||
@@ -16,10 +16,10 @@
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
import Password from '/@/views/system/account/components/password/index.vue';
|
||||
import { useUserStore } from '/@/store/modules/system/user.js';
|
||||
import { loginApi } from '/@/api/system/login-api.js';
|
||||
import { smartSentry } from '/@/lib/smart-sentry.js';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading/index.js';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import { loginApi } from '/@/api/system/login-api';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { SmartLoading } from '/@/components/framework/smart-loading/index';
|
||||
|
||||
/**
|
||||
* 修改密码弹窗
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import HeaderResetPassword from './header-reset-password-modal/index.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { ACCOUNT_MENU } from '/@/views/system/account/account-menu.js';
|
||||
import { ACCOUNT_MENU } from '/@/views/system/account/account-menu';
|
||||
|
||||
// 头像背景颜色
|
||||
const AVATAR_BACKGROUND_COLOR_ARRAY = ['#87d068', '#00B853', '#f56a00', '#1890ff'];
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { reactive, ref } from 'vue';
|
||||
import { messageApi } from '/@/api/support/message-api.js';
|
||||
import { messageApi } from '/@/api/support/message-api';
|
||||
|
||||
const emit = defineEmits(['refresh']);
|
||||
|
||||
|
||||
@@ -74,9 +74,9 @@
|
||||
<script setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { BellOutlined } from '@ant-design/icons-vue';
|
||||
import { useUserStore } from '/@/store/modules/system/user.js';
|
||||
import { smartSentry } from '/@/lib/smart-sentry.js';
|
||||
import { messageApi } from '/@/api/support/message-api.js';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import { smartSentry } from '/@/lib/smart-sentry';
|
||||
import { messageApi } from '/@/api/support/message-api';
|
||||
import dayjs from 'dayjs';
|
||||
import { theme } from 'ant-design-vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
@@ -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="隐藏" />
|
||||
@@ -137,7 +137,7 @@
|
||||
import { useAppConfigStore } from '/@/store/modules/system/app-config';
|
||||
import { Modal } from 'ant-design-vue';
|
||||
import { appDefaultConfig } from '/@/config/app-config';
|
||||
import { themeColors } from '/@/theme/color.js';
|
||||
import { themeColors } from '/@/theme/color';
|
||||
|
||||
// ----------------- modal 显示与隐藏 -----------------
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
import DefaultTab from './components/default-tab.vue';
|
||||
import AntdTab from './components/antd-tab.vue';
|
||||
import ChromeTab from './components/chrome-tab.vue';
|
||||
import { PAGE_TAG_ENUM } from '/@/constants/layout-const.js';
|
||||
import { PAGE_TAG_ENUM } from '/@/constants/layout-const';
|
||||
import { theme } from 'ant-design-vue';
|
||||
|
||||
const pageTagStyle = computed(() => useAppConfigStore().$state.pageTagStyle);
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -104,7 +104,7 @@
|
||||
import SideHelpDoc from './components/side-help-doc/index.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
|
||||
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
|
||||
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const';
|
||||
import { theme as antDesignTheme } from 'ant-design-vue';
|
||||
const appConfigStore = useAppConfigStore();
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
import SideHelpDoc from './components/side-help-doc/index.vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
|
||||
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
|
||||
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const';
|
||||
import { theme as antDesignTheme } from 'ant-design-vue';
|
||||
|
||||
const appConfigStore = useAppConfigStore();
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
|
||||
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
|
||||
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const';
|
||||
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
|
||||
import { theme as antDesignTheme } from 'ant-design-vue';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useUserStore } from '/@/store/modules/system/user';
|
||||
import { decryptData, encryptData } from './encrypt';
|
||||
import { DATA_TYPE_ENUM } from '../constants/common-const';
|
||||
import _ from 'lodash';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const';
|
||||
|
||||
// token的消息头
|
||||
const TOKEN_HEADER = 'Authorization';
|
||||
|
||||
@@ -28,11 +28,11 @@ import { store } from '/@/store';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import 'ant-design-vue/dist/reset.css';
|
||||
import '/@/theme/index.less';
|
||||
import { localRead } from '/@/utils/local-util.js';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
|
||||
import { localRead } from '/@/utils/local-util';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const';
|
||||
import '/@/utils/ployfill';
|
||||
import { useDictStore } from '/@/store/modules/system/dict.js';
|
||||
import { dictApi } from '/@/api/support/dict-api.js';
|
||||
import { useDictStore } from '/@/store/modules/system/dict';
|
||||
import { dictApi } from '/@/api/support/dict-api';
|
||||
|
||||
/*
|
||||
* -------------------- ※ 着重 解释说明下main.js的初始化逻辑 begin ※ --------------------
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
* @Email: lab1024@163.com
|
||||
* @Copyright 1024创新实验室 ( https://1024lab.net ),Since 2012
|
||||
*/
|
||||
import { useDictStore } from '/@/store/modules/system/dict.js';
|
||||
import { useDictStore } from '/@/store/modules/system/dict';
|
||||
|
||||
export default {
|
||||
install: (app) => {
|
||||
|
||||
@@ -18,7 +18,7 @@ import SmartLayout from '../layout/index.vue';
|
||||
import { useUserStore } from '/@/store/modules/system/user';
|
||||
import { localClear, localRead } from '/@/utils/local-util';
|
||||
import _ from 'lodash';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
|
||||
import LocalStorageKeyConst from '/@/constants/local-storage-key-const';
|
||||
|
||||
export const router = createRouter({
|
||||
history: createWebHashHistory(),
|
||||
|
||||
@@ -12,7 +12,7 @@ export const loginRouters = [
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: () => import('/@/views/system/login3/login.vue'),
|
||||
component: () => import('/@/views/system/login/login.vue'),
|
||||
meta: {
|
||||
title: '登录',
|
||||
hideInMenu: true,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user