Compare commits

...

7 Commits

Author SHA1 Message Date
CoderKK
addfaeb0de
Pre Merge pull request !86 from CoderKK/master 2025-08-08 07:55:21 +00:00
zhuoda
8135e0ec10 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 21:00:12 +08:00
zhuoda
2a545117fa v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:46:21 +08:00
zhuoda
d8baf9dba7 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:44:51 +08:00
zhuoda
2f8a0b6153 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:15:40 +08:00
zhuoda
a97afd4683 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:12:40 +08:00
zhuoda
075e7ad134 v3.25.0【优化】密码加密随机盐;【优化】java依赖版本;【优化】后端依赖库;【优化】单号生成器;【优化】防重复提交;【优化】sa-base.yaml健康检查邮箱;【新增】前端夜间模式;【优化】标签页issue;【优化】字典int回显bug 2025-08-05 20:10:13 +08:00
146 changed files with 2408 additions and 1765 deletions

View File

@ -4,6 +4,8 @@
**<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</font>** 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。
**<font color="#DC143C">支持国产数据库达梦、金仓、南大通用、OceanBase、GaussDB 高斯、阿里PolarDB、GoldenDB。 </font>**
前端提供 **<font color="#DC143C">JavaScript和TypeScript双版本</font>**,后端提供 **<font color="#DC143C">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>**

View File

@ -16,52 +16,49 @@
</modules>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springboot.version>3.3.1</springboot.version>
<spring-mock.version>2.0.8</spring-mock.version>
<spring-security-crypto.version>6.4.3</spring-security-crypto.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<java.version>17</java.version>
<springboot.version>3.5.4</springboot.version>
<spring-security-crypto.version>6.5.1</spring-security-crypto.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version>
<knife4j.version>4.4.0</knife4j.version>
<fastjson.version>2.0.52</fastjson.version>
<druid.version>1.2.23</druid.version>
<springdoc-openapi.version>2.8.9</springdoc-openapi.version>
<knife4j.version>4.6.0</knife4j.version>
<fastjson.version>2.0.57</fastjson.version>
<druid.version>1.2.25</druid.version>
<google-linkedhashmap.version>1.4.2</google-linkedhashmap.version>
<google-guava.version>20.0</google-guava.version>
<reflections.version>0.9.11</reflections.version>
<commons-io.version>2.15.0</commons-io.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<commons-collections4.version>4.4</commons-collections4.version>
<commons-compress.version>1.26.0</commons-compress.version>
<commons-codec.version>1.13</commons-codec.version>
<commons-text.version>1.9</commons-text.version>
<xerces.version>2.12.0</xerces.version>
<fast-excel.version>1.0.0</fast-excel.version>
<poi.version>5.2.4</poi.version>
<ooxml-schemas.version>1.4</ooxml-schemas.version>
<aws-java-sdk.version>1.11.842</aws-java-sdk.version>
<log4j-spring-boot.version>2.23.1</log4j-spring-boot.version>
<hutool.version>5.8.29</hutool.version>
<velocity-engine-core.version>2.3</velocity-engine-core.version>
<reflections.version>0.10.2</reflections.version>
<commons-io.version>2.19.0</commons-io.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-collections4.version>4.5.0</commons-collections4.version>
<commons-compress.version>1.27.1</commons-compress.version>
<commons-codec.version>1.18.0</commons-codec.version>
<commons-text.version>1.13.1</commons-text.version>
<fast-excel.version>1.2.0</fast-excel.version>
<poi.version>5.4.1</poi.version>
<awssdk-s3.version>2.31.78</awssdk-s3.version>
<mysql-connector-j.version>9.3.0</mysql-connector-j.version>
<hutool.version>5.8.39</hutool.version>
<velocity-engine-core.version>2.4.1</velocity-engine-core.version>
<velocity-tools.version>3.1</velocity-tools.version>
<sa-token.version>1.41.0</sa-token.version>
<sa-token.version>1.44.0</sa-token.version>
<ip2region.version>2.7.0</ip2region.version>
<bcprov.version>1.80</bcprov.version>
<jackson-datatype-jsr310.version>2.13.4</jackson-datatype-jsr310.version>
<jackson-dataformat-yaml.version>2.16.1</jackson-dataformat-yaml.version>
<smartdb.version>1.2.0</smartdb.version>
<redisson.version>3.25.0</redisson.version>
<snakeyaml.version>2.2</snakeyaml.version>
<freemarker.version>2.3.33</freemarker.version>
<jsoup.version>1.18.1</jsoup.version>
<redisson.version>3.50.0</redisson.version>
<snakeyaml.version>2.4</snakeyaml.version>
<freemarker.version>2.3.34</freemarker.version>
<jsoup.version>1.21.1</jsoup.version>
<tika.version>3.1.0</tika.version>
<httpcomponents.version>5.5</httpcomponents.version>
</properties>
<dependencyManagement>
<dependencies>
<!--BOM begin-->
<!--SpringBoot BOM begin-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
@ -69,19 +66,16 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!--BOM end-->
<!--SpringBoot BOM end-->
<!--mybatis-plus BOM begin-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus BOM end-->
<dependency>
<groupId>org.springframework.security</groupId>
@ -89,6 +83,12 @@
<version>${spring-security-crypto.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector-j.version}</version>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
@ -96,7 +96,7 @@
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
@ -107,7 +107,6 @@
<version>${fastjson.version}</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
@ -169,9 +168,9 @@
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-java-sdk.version}</version>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${awssdk-s3.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
@ -217,6 +216,7 @@
<artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- sa-token end -->
<!--ip 地址-->
@ -254,12 +254,6 @@
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
@ -268,20 +262,8 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>${ooxml-schemas.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson-datatype-jsr310.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson-dataformat-yaml.version}</version>
<artifactId>poi-ooxml-full</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>
@ -295,6 +277,25 @@
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</exclusion>
<exclusion>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-32</artifactId>
</exclusion>
<exclusion>
<artifactId>objenesis</artifactId>
<groupId>org.objenesis</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-data-27</artifactId>
<version>${redisson.version}</version>
</dependency>
<dependency>
@ -321,6 +322,12 @@
<version>${tika.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpcomponents.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -68,6 +68,7 @@ public class AdminInterceptor implements HandlerInterceptor {
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee);
SmartRequestUtil.setRequestUser(requestEmployee);
return true;
}

View File

@ -36,7 +36,7 @@ public class DepartmentEntity {
/**
* 负责人员工 id
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
@TableField(updateStrategy = FieldStrategy.NEVER)
private Long managerId;
/**

View File

@ -23,6 +23,12 @@ public class EmployeeEntity {
@TableId(type = IdType.AUTO)
private Long employeeId;
/**
* 唯一id
*/
private String employeeUid;
/**
* 登录账号
*/

View File

@ -138,16 +138,20 @@ public class EmployeeService {
}
EmployeeEntity entity = SmartBeanUtil.copy(employeeAddForm, EmployeeEntity.class);
// 员工uid
String employeeUid = cn.hutool.core.lang.UUID.randomUUID(true).toString(true);
entity.setEmployeeUid(employeeUid);
// 设置密码 默认密码
String password = securityPasswordService.randomPassword();
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(password));
// 设置密码 随机密码
String randomPassword = securityPasswordService.randomPassword();
String generateSaltPassword = this.generateSaltPassword(randomPassword, employeeUid);
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(generateSaltPassword));
// 保存数据
entity.setDeletedFlag(Boolean.FALSE);
employeeManager.saveEmployee(entity, employeeAddForm.getRoleIdList());
return ResponseDTO.ok(password);
return ResponseDTO.ok(randomPassword);
}
/**
@ -241,7 +245,6 @@ public class EmployeeService {
/**
* 更新登录人头像
*
*/
public ResponseDTO<String> updateAvatar(EmployeeUpdateAvatarForm employeeUpdateAvatarForm) {
Long employeeId = employeeUpdateAvatarForm.getEmployeeId();
@ -343,12 +346,12 @@ public class EmployeeService {
}
// 校验原始密码
if (!SecurityPasswordService.matchesPwd(updatePasswordForm.getOldPassword(),employeeEntity.getLoginPwd()) ) {
if (!SecurityPasswordService.matchesPwd(this.generateSaltPassword(updatePasswordForm.getOldPassword(), employeeEntity.getEmployeeUid()), employeeEntity.getLoginPwd())) {
return ResponseDTO.userErrorParam("原密码有误,请重新输入");
}
// 新旧密码相同
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword()) ){
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword())) {
return ResponseDTO.userErrorParam("新密码与原始密码相同,请重新输入");
}
@ -359,14 +362,13 @@ public class EmployeeService {
}
// 根据三级等保规则校验密码是否重复
ResponseDTO<String> passwordRepeatTimes = securityPasswordService.validatePasswordRepeatTimes(requestUser, updatePasswordForm.getNewPassword());
ResponseDTO<String> passwordRepeatTimes = securityPasswordService.validatePasswordRepeatTimes(requestUser, this.generateSaltPassword(updatePasswordForm.getNewPassword(), employeeEntity.getEmployeeUid()));
if (!passwordRepeatTimes.getOk()) {
return ResponseDTO.error(passwordRepeatTimes);
}
// 更新密码
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getNewPassword());
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(this.generateSaltPassword(updatePasswordForm.getNewPassword(), employeeEntity.getEmployeeUid()));
EmployeeEntity updateEntity = new EmployeeEntity();
updateEntity.setEmployeeId(employeeId);
updateEntity.setLoginPwd(newEncryptPassword);
@ -405,8 +407,14 @@ public class EmployeeService {
* 重置密码
*/
public ResponseDTO<String> resetPassword(Long employeeId) {
EmployeeEntity employeeEntity = employeeDao.selectById(employeeId);
if (employeeEntity == null) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
String password = securityPasswordService.randomPassword();
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(password));
String saltPassword = this.generateSaltPassword(password, employeeEntity.getEmployeeUid());
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(saltPassword));
return ResponseDTO.ok(password);
}
@ -426,4 +434,14 @@ public class EmployeeService {
return employeeDao.getByLoginName(loginName, false);
}
/**
* 生成加盐密码
* 格式为[password]_[uid大写]_[uid小写]
*/
public String generateSaltPassword(String password, String employeeUid) {
return password + StringConst.UNDERLINE +
employeeUid.toUpperCase() +
StringConst.UNDERLINE +
employeeUid.toLowerCase();
}
}

View File

@ -9,7 +9,6 @@ import cn.hutool.extra.servlet.JakartaServletUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.admin.module.system.department.service.DepartmentService;
import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity;
import net.lab1024.sa.admin.module.system.employee.service.EmployeeService;
import net.lab1024.sa.admin.module.system.login.domain.LoginForm;
@ -38,7 +37,6 @@ import net.lab1024.sa.base.module.support.captcha.CaptchaService;
import net.lab1024.sa.base.module.support.captcha.domain.CaptchaVO;
import net.lab1024.sa.base.module.support.config.ConfigKeyEnum;
import net.lab1024.sa.base.module.support.config.ConfigService;
import net.lab1024.sa.base.module.support.file.service.IFileStorageService;
import net.lab1024.sa.base.module.support.loginlog.LoginLogResultEnum;
import net.lab1024.sa.base.module.support.loginlog.LoginLogService;
import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogEntity;
@ -79,9 +77,6 @@ public class LoginService implements StpInterface {
@Resource
private EmployeeService employeeService;
@Resource
private DepartmentService departmentService;
@Resource
private CaptchaService captchaService;
@ -103,9 +98,6 @@ public class LoginService implements StpInterface {
@Resource
private SecurityPasswordService protectPasswordService;
@Resource
private IFileStorageService fileStorageService;
@Resource
private ApiEncryptService apiEncryptService;
@ -193,7 +185,7 @@ public class LoginService implements StpInterface {
}
// 密码错误
if (!SecurityPasswordService.matchesPwd(requestPassword, employeeEntity.getLoginPwd())) {
if (!SecurityPasswordService.matchesPwd(employeeService.generateSaltPassword(requestPassword, employeeEntity.getEmployeeUid()), employeeEntity.getLoginPwd())) {
// 记录登录失败
saveLoginLog(employeeEntity, ip, userAgent, "密码错误", LoginLogResultEnum.LOGIN_FAIL, loginDeviceEnum);
// 记录等级保护次数

View File

@ -64,7 +64,6 @@
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
</dependency>
<!-- sa-token end -->
<dependency>
@ -118,19 +117,25 @@
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
</dependency>
<!-- knife4j & SpringDoc -->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
</dependency>
<dependency>
@ -159,10 +164,11 @@
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
@ -217,11 +223,6 @@
<artifactId>poi</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId>
@ -229,12 +230,7 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<artifactId>poi-ooxml-full</artifactId>
</dependency>
<dependency>

View File

@ -52,11 +52,7 @@ public class SmartPageUtil {
log.error("《存在SQL注入》 : {}", sortItem.getColumn());
throw new BusinessException("存在SQL注入风险请联系技术工作人员");
}
OrderItem orderItem = new OrderItem();
orderItem.setColumn(sortItem.getColumn());
orderItem.setAsc(sortItem.getIsAsc());
orderItemList.add(orderItem);
orderItemList.add(sortItem.getIsAsc() ? OrderItem.asc(sortItem.getColumn()) : OrderItem.desc(sortItem.getColumn()));
}
page.setOrders(orderItemList);
return page;

View File

@ -18,6 +18,9 @@ public class SmartRequestUtil {
private static final ThreadLocal<RequestUser> REQUEST_THREAD_LOCAL = new ThreadLocal<>();
public static void setRequestUser(RequestUser requestUser) {
if(requestUser == null){
return;
}
REQUEST_THREAD_LOCAL.set(requestUser);
}

View File

@ -1,12 +1,5 @@
package net.lab1024.sa.base.config;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.Data;
import net.lab1024.sa.base.module.support.file.service.FileStorageCloudServiceImpl;
import net.lab1024.sa.base.module.support.file.service.FileStorageLocalServiceImpl;
@ -17,6 +10,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
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 java.net.URI;
/**
* 文件上传 配置
@ -31,6 +31,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@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";
@ -69,15 +73,17 @@ public class FileConfig implements WebMvcConfigurer {
*/
@Bean
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
public AmazonS3 initAmazonS3() {
ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setProtocol(Protocol.HTTPS);
return AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.withClientConfiguration(clientConfig)
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
.withPathStyleAccessEnabled(false)
.withChunkedEncodingDisabled(true)
public S3Client initAmazonS3() {
return S3Client.builder()
.region(Region.AWS_GLOBAL)
.endpointOverride(URI.create((urlPrefix.startsWith(HTTPS) ? HTTPS : HTTP) + endpoint))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(false)
.chunkedEncodingEnabled(false)
.build())
.build();
}

View File

@ -1,13 +1,14 @@
package net.lab1024.sa.base.config;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.module.support.repeatsubmit.RepeatSubmitAspect;
import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitRedisTicket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
/**
* 重复提交配置
@ -22,22 +23,22 @@ import org.springframework.data.redis.core.ValueOperations;
public class RepeatSubmitConfig {
@Resource
private ValueOperations<String, String> valueOperations;
private RedisTemplate<String, Object> redisTemplate;
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
RepeatSubmitRedisTicket caffeineTicket = new RepeatSubmitRedisTicket(valueOperations, this::ticket);
return new RepeatSubmitAspect(caffeineTicket);
RepeatSubmitRedisTicket ticket = new RepeatSubmitRedisTicket(redisTemplate, this::ticket);
return new RepeatSubmitAspect(ticket);
}
/**
* 获取指明某个用户的凭证
*/
private String ticket(String servletPath) {
private String ticket(HttpServletRequest request) {
Long userId = SmartRequestUtil.getRequestUserId();
if (null == userId) {
return StringConst.EMPTY;
}
return servletPath + "_" + userId;
return request.getServletPath() + "_" + userId;
}
}

View File

@ -0,0 +1,92 @@
package net.lab1024.sa.base.config;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import org.apache.hc.client5.http.classic.HttpClient;
import org.apache.hc.client5.http.config.TlsConfig;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
import org.apache.hc.core5.util.TimeValue;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestClient;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* http请求配置
*
* @Author 1024创新实验室: 卓大
* @Date 2025-07-26 21:22:12
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Configuration
public class RestClientConfig {
@Value("${http.pool.max-total}")
private Integer maxTotal;
@Value("${http.pool.connect-timeout}")
private Integer connectTimeout;
@Value("${http.pool.read-timeout}")
private Integer readTimeout;
@Value("${http.pool.write-timeout}")
private Integer writeTimeout;
@Value("${http.pool.keep-alive}")
private Integer keepAlive;
@Bean
public RestClient restClient() {
HttpComponentsClientHttpRequestFactory factory =
new HttpComponentsClientHttpRequestFactory();
factory.setConnectTimeout(connectTimeout);
factory.setConnectionRequestTimeout(connectTimeout);
factory.setReadTimeout(readTimeout);
PoolingHttpClientConnectionManager cm =
new PoolingHttpClientConnectionManager();
cm.setMaxTotal(this.maxTotal);
cm.setDefaultTlsConfig(TlsConfig.DEFAULT);
HttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.setKeepAliveStrategy((response, context) -> TimeValue.of(this.keepAlive, TimeUnit.MICROSECONDS))
.build();
factory.setHttpClient(httpClient);
return RestClient.builder()
.requestFactory(factory)
.messageConverters(converters())
.build();
}
public List<HttpMessageConverter<?>> converters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
HttpMessageConverter<?> converter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
fastMediaTypes.add(MediaType.APPLICATION_JSON);
fastConverter.setSupportedMediaTypes(fastMediaTypes);
converters.add(converter);
converters.add(fastConverter);
return converters;
}
}

View File

@ -1,130 +0,0 @@
package net.lab1024.sa.base.config;
import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* http请求配置
*
* @Author 1024创新实验室: 罗伊
* @Date 2022-05-30 21:22:12
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Configuration
public class RestTemplateConfig {
@Value("${http.pool.max-total}")
private Integer maxTotal;
@Value("${http.pool.connect-timeout}")
private Integer connectTimeout;
@Value("${http.pool.read-timeout}")
private Integer readTimeout;
@Value("${http.pool.write-timeout}")
private Integer writeTimeout;
@Value("${http.pool.keep-alive}")
private Integer keepAlive;
@Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
restTemplate.setRequestFactory(this.clientHttpRequestFactory());
List<HttpMessageConverter<?>> messageConverterList = restTemplate.getMessageConverters();
messageConverterList.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
messageConverterList.addAll(this.converters());
return restTemplate;
}
public List<HttpMessageConverter<?>> converters() {
List<HttpMessageConverter<?>> converters = new ArrayList<>();
HttpMessageConverter<?> converter = new StringHttpMessageConverter(StandardCharsets.UTF_8);
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_FORM_URLENCODED);
fastMediaTypes.add(MediaType.APPLICATION_JSON);
fastConverter.setSupportedMediaTypes(fastMediaTypes);
converters.add(converter);
converters.add(fastConverter);
return converters;
}
public OkHttp3ClientHttpRequestFactory clientHttpRequestFactory() {
return new OkHttp3ClientHttpRequestFactory(httpClientBuilder());
}
public OkHttpClient httpClientBuilder() {
return new OkHttpClient.Builder()
.retryOnConnectionFailure(true)
.connectionPool(this.pool())
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
.readTimeout(readTimeout, TimeUnit.MILLISECONDS)
.writeTimeout(writeTimeout, TimeUnit.MILLISECONDS)
.build();
}
public ConnectionPool pool() {
return new ConnectionPool(maxTotal, keepAlive, TimeUnit.MILLISECONDS);
}
@Bean
public X509TrustManager x509TrustManager() {
return new X509TrustManager() {
@Override
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
};
}
@Bean
public SSLSocketFactory sslSocketFactory() {
try {
//信任任何链接
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[]{x509TrustManager()}, new SecureRandom());
return sslContext.getSocketFactory();
} catch (NoSuchAlgorithmException | KeyManagementException e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -12,6 +12,7 @@ import net.lab1024.sa.base.common.constant.RequestHeaderConst;
import net.lab1024.sa.base.common.swagger.SmartOperationCustomizer;
import net.lab1024.sa.base.constant.SwaggerTagConst;
import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.models.GroupedOpenApi;
@ -24,6 +25,8 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import java.util.List;
import java.util.Optional;
@ -80,6 +83,21 @@ public class SwaggerConfig {
.addSecuritySchemes(RequestHeaderConst.TOKEN, new SecurityScheme().scheme("Bearer").description("请输入token,格式为[Bearer xxxxxxxx]").type(SecurityScheme.Type.APIKEY).in(SecurityScheme.In.HEADER).name(RequestHeaderConst.TOKEN));
}
@Bean
public GlobalOpenApiCustomizer orderGlobalOpenApiCustomizer() {
return openApi -> {
// 全局添加鉴权参数
if(openApi.getPaths()!=null){
openApi.getPaths().forEach((s, pathItem) -> {
// 为所有接口添加鉴权
pathItem.readOperations().forEach(operation -> {
operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION));
});
});
}
};
}
@Bean
public GroupedOpenApi businessApi() {
return GroupedOpenApi.builder()
@ -122,11 +140,11 @@ public class SwaggerConfig {
Optional<JavadocProvider> javadocProvider) {
List<ServerBaseUrlCustomizer> list = Lists.newArrayList(new ServerBaseUrlCustomizer() {
@Override
public String customize(String baseUrl) {
public String customize(String serverBaseUrl, HttpRequest request) {
if (StringUtils.isNotBlank(serverBaseUrl)) {
return serverBaseUrl;
}
return baseUrl;
return serverBaseUrl;
}
});
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,

View File

@ -40,7 +40,7 @@ import java.util.Optional;
@Service
public class CodeGeneratorService {
private static final String COLUMN_NULLABLE_IDENTIFY = "NO";
private static final String COLUMN_NO_NULLABLE_IDENTIFY = "NO";
private static final String COLUMN_PRIMARY_KEY = "PRI";
@ -65,7 +65,7 @@ public class CodeGeneratorService {
public List<TableColumnVO> getTableColumns(String tableName) {
List<TableColumnVO> tableColumns = codeGeneratorDao.selectTableColumn(tableName);
for (TableColumnVO tableColumn : tableColumns) {
tableColumn.setNullableFlag(!COLUMN_NULLABLE_IDENTIFY.equalsIgnoreCase(tableColumn.getIsNullable()));
tableColumn.setNullableFlag(!COLUMN_NO_NULLABLE_IDENTIFY.equalsIgnoreCase(tableColumn.getIsNullable()));
tableColumn.setPrimaryKeyFlag(COLUMN_PRIMARY_KEY.equalsIgnoreCase(tableColumn.getColumnKey()));
tableColumn.setAutoIncreaseFlag(SmartStringUtil.isNotEmpty(tableColumn.getExtra()) && COLUMN_AUTO_INCREASE.equalsIgnoreCase(tableColumn.getExtra()));
}

View File

@ -55,9 +55,6 @@ public class FileService {
@Resource
private FileDao fileDao;
@Resource
private RedisService redisService;
@Resource
private SecurityFileService securityFileService;

View File

@ -3,10 +3,6 @@ package net.lab1024.sa.base.module.support.file.service;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.util.IdUtil;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.SystemErrorCode;
@ -23,18 +19,25 @@ import net.lab1024.sa.base.module.support.file.domain.vo.FileVO;
import net.lab1024.sa.base.module.support.redis.RedisService;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.FileCopyUtils;
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;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@ -45,7 +48,7 @@ import java.util.Map;
* @Date 2019年10月11日 15:34:47
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Slf4j
public class FileStorageCloudServiceImpl implements IFileStorageService {
@ -66,7 +69,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
private static final String USER_METADATA_FILE_SIZE = "file-size";
@Resource
private AmazonS3 amazonS3;
private S3Client s3Client;
@Resource
private FileConfig cloudConfig;
@ -88,42 +91,38 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
String fileType = FilenameUtils.getExtension(originalFileName);
String uuid = IdUtil.fastSimpleUUID();
String time = LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_FORMATTER);
String fileKey = path + uuid + "_" + time+ "." + fileType;
String fileKey = path + uuid + "_" + time + "." + fileType;
// 文件名称 URL 编码
String urlEncoderFilename;
try {
urlEncoderFilename = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name());
} catch (UnsupportedEncodingException e) {
log.error("文件上传服务URL ENCODE-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败");
}
ObjectMetadata meta = new ObjectMetadata();
meta.setContentEncoding(StandardCharsets.UTF_8.name());
meta.setContentDisposition("attachment;filename=" + urlEncoderFilename);
urlEncoderFilename = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8);
Map<String, String> userMetadata = new HashMap<>(10);
userMetadata.put(USER_METADATA_FILE_NAME, urlEncoderFilename);
userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
meta.setUserMetadata(userMetadata);
meta.setContentLength(file.getSize());
meta.setContentType(this.getContentType(fileType));
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();
InputStream inputStream = null;
try {
amazonS3.putObject(cloudConfig.getBucketName(), fileKey, file.getInputStream(), meta);
inputStream = file.getInputStream();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
} catch (IOException e) {
log.error("文件上传-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
IOUtils.closeQuietly(inputStream);
}
// 根据文件路径获取并设置访问权限
CannedAccessControlList acl = this.getACL(path);
amazonS3.setObjectAcl(cloudConfig.getBucketName(), fileKey, acl);
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;
if (CannedAccessControlList.Private.equals(acl)) {
if (ObjectCannedACL.PRIVATE.equals(acl)) {
// 获取临时访问的URL
url = this.getFileUrl(fileKey).getData();
}
@ -136,8 +135,8 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
/**
* 获取文件url
*
* @param fileKey
* @return
* @param fileKey 文件key
* @return url
*/
@Override
public ResponseDTO<String> getFileUrl(String fileKey) {
@ -159,10 +158,14 @@ 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();
Date expiration = new Date(System.currentTimeMillis() + cloudConfig.getPrivateUrlExpireSeconds() * 1000L);
URL url = amazonS3.generatePresignedUrl(cloudConfig.getBucketName(), fileKey, expiration);
fileVO.setFileUrl(url.toString());
S3Presigner presigner = S3Presigner.builder().region(Region.of(cloudConfig.getRegion())).build();
PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest);
String url = presignedGetObjectRequest.url().toString();
fileVO.setFileUrl(url);
redisService.set(fileRedisKey, fileVO, cloudConfig.getPrivateUrlExpireSeconds() - 5);
}
@ -175,11 +178,11 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
*/
@Override
public ResponseDTO<FileDownloadVO> download(String key) {
//获取oss对象
S3Object s3Object = amazonS3.getObject(cloudConfig.getBucketName(), key);
// 获取文件 meta
ObjectMetadata metadata = s3Object.getObjectMetadata();
Map<String, String> userMetadata = metadata.getUserMetadata();
HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getBucketName()).key(key).build();
HeadObjectResponse headObjectResponse = s3Client.headObject(objectRequest);
Map<String, String> userMetadata = headObjectResponse.metadata();
FileMetadataVO metadataDTO = null;
if (MapUtils.isNotEmpty(userMetadata)) {
metadataDTO = new FileMetadataVO();
@ -190,43 +193,31 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
metadataDTO.setFileSize(fileSize);
}
// 获得输入流
InputStream objectContent = s3Object.getObjectContent();
try {
// 输入流转换为字节流
byte[] buffer = FileCopyUtils.copyToByteArray(objectContent);
//获取oss对象
GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(key).build();
ResponseBytes<GetObjectResponse> s3ClientObject = s3Client.getObject(getObjectRequest, ResponseTransformer.toBytes());
FileDownloadVO fileDownloadVO = new FileDownloadVO();
fileDownloadVO.setData(buffer);
fileDownloadVO.setMetadata(metadataDTO);
return ResponseDTO.ok(fileDownloadVO);
} catch (IOException e) {
log.error("文件下载-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "下载失败");
} finally {
try {
// 关闭输入流
objectContent.close();
s3Object.close();
} catch (IOException e) {
log.error("文件下载-发生异常:", e);
}
}
// 输入流转换为字节流
byte[] buffer = s3ClientObject.asByteArray();
FileDownloadVO fileDownloadVO = new FileDownloadVO();
fileDownloadVO.setData(buffer);
fileDownloadVO.setMetadata(metadataDTO);
return ResponseDTO.ok(fileDownloadVO);
}
/**
* 根据文件夹路径 返回对应的访问权限
*
* @param fileKey
* @return
* @param fileKey 文件key
* @return 权限
*/
private CannedAccessControlList getACL(String fileKey) {
private ObjectCannedACL getACL(String fileKey) {
// 公用读
if (fileKey.contains(FileFolderTypeEnum.FOLDER_PUBLIC)) {
return CannedAccessControlList.PublicRead;
return ObjectCannedACL.PUBLIC_READ;
}
// 其他默认私有读写
return CannedAccessControlList.Private;
return ObjectCannedACL.PRIVATE;
}
/**
@ -235,11 +226,11 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
* ps不能删除fileKey不为空的文件夹
*
* @param fileKey 文件or文件夹
* @return
*/
@Override
public ResponseDTO<String> delete(String fileKey) {
amazonS3.deleteObject(cloudConfig.getBucketName(), fileKey);
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).build();
s3Client.deleteObject(deleteObjectRequest);
return ResponseDTO.ok();
}

View File

@ -3,7 +3,6 @@ package net.lab1024.sa.base.module.support.file.service;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.net.NetUtil;
import cn.hutool.core.util.IdUtil;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.SystemErrorCode;
@ -24,6 +23,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.time.LocalDateTime;
import java.util.UUID;
/**
* 本地存储 实现
@ -85,7 +85,7 @@ public class FileStorageLocalServiceImpl implements IFileStorageService {
//原文件名
String originalFileName = multipartFile.getOriginalFilename();
//新文件名
String uuid = IdUtil.fastSimpleUUID();
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String time = LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_FORMATTER);
String newFileName = uuid + "_" + time;
String fileType = FilenameUtils.getExtension(originalFileName);

View File

@ -1,5 +1,6 @@
package net.lab1024.sa.base.module.support.repeatsubmit;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.UserErrorCode;
import net.lab1024.sa.base.common.domain.ResponseDTO;
@ -16,10 +17,19 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
/**
* 重复提交 aop切口
* 重复提交 aop切口 <br>
* -------------------------<br>
* 着重说明<br>
* 注解属性 intervalMilliSecond 是指 一段时间内只允许有一次请求<br>
* intervalMilliSecond = 0: 表示只有上个请求执行完以后才可以执行<br>
* intervalMilliSecond > 0: 表示指定时间内只才能执行特别提醒<br>
* ------------------------<br>
* 特殊说明 intervalMilliSecond > 0 <br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 50 同一时间内可能会有2个请求同时在执行<br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 200 同一时间内只能有1请求执行且执行完后100ms才会执行下一次请求<br>
*
* @Author 1024创新实验室: 胡克
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -43,36 +53,46 @@ public class RepeatSubmitAspect {
@Around("@annotation(net.lab1024.sa.base.module.support.repeatsubmit.annoation.RepeatSubmit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ticketToken = attributes.getRequest().getServletPath();
String ticket = this.repeatSubmitTicket.getTicket(ticketToken);
if (attributes == null) {
return point.proceed();
}
/**
* 第一步生成防重复提交的 ticket凭证
* ticket 是根据 Request对象 自定义 生成的可以加入请求user相关属性作为生成要素
*/
HttpServletRequest request = attributes.getRequest();
String ticket = this.repeatSubmitTicket.generateTicket(request);
if (StringUtils.isEmpty(ticket)) {
return point.proceed();
}
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
int limit = annotation.value();
Long intervalMilliSecond = (long) annotation.intervalMilliSecond();
// 获取上一次请求时间
Long lastRequestTime = this.repeatSubmitTicket.getTicketTimestamp(ticket);
// 校验是否限制时间内重复提交
if (lastRequestTime != null && System.currentTimeMillis() < lastRequestTime + limit) {
/**
* 第二步根据 ticket 凭证进行 加锁
* 能加锁则可以执行
* 若不能加锁则证明还是时间间隔interval中
*/
boolean lockSuccessFlag = this.repeatSubmitTicket.tryLock(ticket, System.currentTimeMillis(), intervalMilliSecond);
if (!lockSuccessFlag) {
return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT);
}
// 执行
Object obj = null;
try {
// ticket 设置在执行中
this.repeatSubmitTicket.putTicket(ticket);
// 执行
obj = point.proceed();
return point.proceed();
} catch (Throwable throwable) {
log.error("", throwable);
log.error(throwable.getMessage(), throwable);
throw throwable;
} finally {
this.repeatSubmitTicket.unLock(ticket, intervalMilliSecond);
}
return obj;
}
}

View File

@ -7,10 +7,19 @@ import java.lang.annotation.Target;
/**
* 标记 需要防止重复提交 的注解<br>
* 单位毫秒
* 单位毫秒<br>
* -------------------------<br>
* 着重说明<br>
* 注解属性 intervalMilliSecond 是指 一段时间内只允许有一次请求<br>
* intervalMilliSecond = 0: 表示只有上个请求执行完以后才可以执行<br>
* intervalMilliSecond > 0: 表示指定时间内只才能执行特别提醒<br>
* ------------------------<br>
* 特殊说明 intervalMilliSecond > 0 <br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 50 同一时间内可能会有2个请求同时在执行<br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 200 同一时间内只能有1请求执行且执行完后100ms才会执行下一次请求<br>
*
* @Author 1024创新实验室: 胡克
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -20,8 +29,8 @@ import java.lang.annotation.Target;
public @interface RepeatSubmit {
/**
* 重复提交间隔时间/毫秒
* 间隔时间/毫秒
*/
int value() default 300;
int intervalMilliSecond() default 0;
}

View File

@ -1,42 +1,43 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import jakarta.servlet.http.HttpServletRequest;
import java.util.function.Function;
/**
* 凭证用于校验重复提交的东西
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public abstract class AbstractRepeatSubmitTicket {
private final Function<String, String> ticketFunction;
private final Function<HttpServletRequest, String> generateTicketFunction;
public AbstractRepeatSubmitTicket(Function<String, String> ticketFunction) {
this.ticketFunction = ticketFunction;
public AbstractRepeatSubmitTicket(Function<HttpServletRequest, String> generateTicketFunction) {
this.generateTicketFunction = generateTicketFunction;
}
/**
* 获取凭证
* 生成 加锁的 凭证
*/
public String getTicket(String ticketToken) {
return this.ticketFunction.apply(ticketToken);
public String generateTicket(HttpServletRequest request) {
return this.generateTicketFunction.apply(request);
}
/**
* 获取凭证 时间戳
* 加锁
*/
public abstract Long getTicketTimestamp(String ticket);
public abstract boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond);
/**
* 设置本次请求时间
* 移除锁
*/
public abstract void putTicket(String ticket);
public abstract void unLock(String ticket, Long intervalMilliSecond);
}

View File

@ -1,44 +0,0 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 凭证内存实现
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class RepeatSubmitCaffeineTicket extends AbstractRepeatSubmitTicket {
/**
* 限制缓存最大数量 超过后先放入的会自动移除
* 默认缓存时间
* 初始大小为100万
*/
private static final Cache<String, Long> cache = Caffeine.newBuilder()
.maximumSize(100 * 10000)
.expireAfterWrite(300 * 1000L, TimeUnit.MILLISECONDS).build();
public RepeatSubmitCaffeineTicket(Function<String, String> ticketFunction) {
super(ticketFunction);
}
@Override
public Long getTicketTimestamp(String ticket) {
return cache.getIfPresent(ticket);
}
@Override
public void putTicket(String ticket) {
cache.put(ticket, System.currentTimeMillis());
}
}

View File

@ -0,0 +1,60 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.common.collect.Maps;
import jakarta.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
/**
* 凭证内存实现
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class RepeatSubmitMemoryTicket extends AbstractRepeatSubmitTicket {
private Interner<String> pool = Interners.newStrongInterner();
private ConcurrentMap<String, Long> ticketMap = Maps.newConcurrentMap();
public RepeatSubmitMemoryTicket(Function<HttpServletRequest, String> ticketFunction) {
super(ticketFunction);
}
@Override
public boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond) {
synchronized (pool.intern(ticket)) {
Long lastTime = ticketMap.putIfAbsent(ticket, currentTimestamp);
if (lastTime == null) {
return true;
}
if (intervalMilliSecond <= 0) {
return false;
}
if (currentTimestamp - lastTime < intervalMilliSecond) {
return false;
}
ticketMap.put(ticket, currentTimestamp);
return true;
}
}
@Override
public void unLock(String ticket, Long intervalMilliSecond) {
if (intervalMilliSecond > 0) {
return;
}
ticketMap.remove(ticket);
}
}

View File

@ -1,38 +1,47 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import org.springframework.data.redis.core.ValueOperations;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 凭证redis实现
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket {
private final ValueOperations<String, String> redisValueOperations;
private final RedisTemplate<String, Object> redisTemplate;
public RepeatSubmitRedisTicket(ValueOperations<String, String> redisValueOperations,
Function<String, String> ticketFunction) {
public RepeatSubmitRedisTicket(RedisTemplate<String, Object> redisTemplate,
Function<HttpServletRequest, String> ticketFunction) {
super(ticketFunction);
this.redisValueOperations = redisValueOperations;
this.redisTemplate = redisTemplate;
}
@Override
public Long getTicketTimestamp(String ticket) {
String ticketLastTime = redisValueOperations.get(ticket);
return ticketLastTime == null ? null : Long.valueOf(ticketLastTime);
public boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond) {
if (intervalMilliSecond > 0) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(ticket, String.valueOf(currentTimestamp), intervalMilliSecond, TimeUnit.MILLISECONDS));
} else {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(ticket, String.valueOf(currentTimestamp)));
}
}
@Override
public void putTicket(String ticket) {
redisValueOperations.getOperations().delete(ticket);
this.getTicketTimestamp(ticket);
public void unLock(String ticket, Long intervalMilliSecond) {
if (intervalMilliSecond > 0) {
return;
}
redisTemplate.delete(ticket);
}
}

View File

@ -3,6 +3,7 @@ package net.lab1024.sa.base.module.support.securityprotect.service;
import lombok.extern.slf4j.Slf4j;
import jakarta.annotation.Resource;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.apache.commons.io.IOUtils;
import org.apache.tika.config.TikaConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
@ -35,27 +37,9 @@ public class SecurityFileService {
private Level3ProtectConfigService level3ProtectConfigService;
// 定义白名单MIME类型
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList(
"application/json",
"application/zip",
"application/x-7z-compressed",
"application/pdf",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-works",
"text/csv",
"audio/*",
"video/*",
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList("application/json", "application/zip", "application/x-7z-compressed", "application/pdf", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-works", "text/csv", "audio/*", "video/*",
// 图片类型 svg有安全隐患所以不使用"image/*"
"image/jpeg",
"image/png",
"image/gif",
"image/bmp"
);
"image/jpeg", "image/png", "image/gif", "image/bmp");
/**
* 检测文件安全类型
@ -73,8 +57,7 @@ public class SecurityFileService {
// 文件类型安全检测
if (level3ProtectConfigService.isFileDetectFlag()) {
String fileType = getFileMimeType(file);
if (ALLOWED_MIME_TYPES.stream()
.noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
if (ALLOWED_MIME_TYPES.stream().noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
return ResponseDTO.userErrorParam("禁止上传此文件类型");
}
}
@ -89,16 +72,20 @@ public class SecurityFileService {
* @return 文件的 MIME 类型
*/
public static String getFileMimeType(MultipartFile file) {
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
TikaConfig tika = new TikaConfig();
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
TikaInputStream stream = TikaInputStream.get(file.getInputStream());
TikaInputStream stream = TikaInputStream.get(inputStream);
MediaType mimetype = tika.getDetector().detect(stream, metadata);
return mimetype.toString();
} catch (IOException | TikaException e) {
log.error(e.getMessage(), e);
return MimeTypes.OCTET_STREAM;
} finally {
IOUtils.closeQuietly(inputStream);
}
}

View File

@ -171,4 +171,5 @@ public class SecurityLoginService {
return ResponseDTO.ok();
}
}
}
;

View File

@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
* @Date 2022-03-25 21:46:07
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public abstract class SerialNumberBaseService implements SerialNumberService {
@ -37,7 +37,7 @@ public abstract class SerialNumberBaseService implements SerialNumberService {
@Resource
protected SerialNumberDao serialNumberDao;
private ConcurrentHashMap<Integer, SerialNumberInfoBO> serialNumberMap = new ConcurrentHashMap<>();
protected ConcurrentHashMap<Integer, SerialNumberInfoBO> serialNumberMap = new ConcurrentHashMap<>();
public abstract List<String> generateSerialNumberList(SerialNumberInfoBO serialNumber, int count);

View File

@ -1,23 +1,34 @@
package net.lab1024.sa.base.module.support.serialnumber.service.impl;
import cn.hutool.core.util.RandomUtil;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.exception.BusinessException;
import net.lab1024.sa.base.common.util.SmartDateFormatterEnum;
import net.lab1024.sa.base.common.util.SmartEnumUtil;
import net.lab1024.sa.base.common.util.SmartLocalDateUtil;
import net.lab1024.sa.base.common.util.SmartStringUtil;
import net.lab1024.sa.base.constant.RedisKeyConst;
import net.lab1024.sa.base.module.support.redis.RedisService;
import net.lab1024.sa.base.module.support.serialnumber.constant.SerialNumberRuleTypeEnum;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberEntity;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberGenerateResultBO;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberInfoBO;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberLastGenerateBO;
import net.lab1024.sa.base.module.support.serialnumber.service.SerialNumberBaseService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 单据序列号 基于redis锁实现
* 单据序列号 基于redis key-value increase 实现
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2022-03-25 21:46:07
* @Date 2025-08-03 22:46:07
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -25,86 +36,128 @@ import java.util.List;
@Slf4j
public class SerialNumberRedisService extends SerialNumberBaseService {
private static final int MAX_GET_LOCK_COUNT = 5;
private static final long SLEEP_MILLISECONDS = 200L;
@Resource
private RedisService redisService;
@Resource
private RedisTemplate redisTemplate;
@Override
public void initLastGenerateData(List<SerialNumberEntity> serialNumberEntityList) {
if (serialNumberEntityList == null) {
return;
}
//删除之前的
redisService.delete(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO);
// 设置redis的上次值
for (SerialNumberEntity serialNumberEntity : serialNumberEntityList) {
SerialNumberLastGenerateBO lastGenerateBO = SerialNumberLastGenerateBO
.builder()
.serialNumberId(serialNumberEntity.getSerialNumberId())
.lastNumber(serialNumberEntity.getLastNumber())
.lastTime(serialNumberEntity.getLastTime())
.build();
if (serialNumberEntity.getLastTime() == null) {
continue;
}
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberEntity.getSerialNumberId()),
lastGenerateBO
);
String redisKey = generateRedisKeyByDate(serialNumberEntity.getSerialNumberId(),
SmartEnumUtil.getEnumByName(serialNumberEntity.getRuleType().toUpperCase(), SerialNumberRuleTypeEnum.class),
serialNumberEntity.getLastTime().toLocalDate());
Object o = redisTemplate.opsForValue().get(redisKey);
if (o == null) {
redisTemplate.opsForValue().set(redisKey, serialNumberEntity.getLastNumber());
}
}
}
/**
* 每天凌晨一点进行检测
* 检测单位数量为3; 3天前3月前3年前
*/
@Scheduled(cron = "0 0 1 * * ?")
public void tryDeleteUnusedRedisKey() {
for (SerialNumberInfoBO serialNumberInfoBO : serialNumberMap.values()) {
SerialNumberRuleTypeEnum typeEnum = serialNumberInfoBO.getSerialNumberRuleTypeEnum();
String dateStr = "";
switch (typeEnum) {
case DAY -> {
dateStr = SmartLocalDateUtil.format(LocalDate.now().minusDays(3), SmartDateFormatterEnum.YMD);
}
case MONTH -> {
dateStr = SmartLocalDateUtil.format(LocalDate.now().minusMonths(3), SmartDateFormatterEnum.YM);
}
case YEAR -> {
dateStr = String.valueOf(LocalDate.now().minusYears(3));
}
}
if (SmartStringUtil.isNotEmpty(dateStr)) {
String redisKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfoBO.getSerialNumberId() + ":" + dateStr;
redisService.delete(redisKey);
}
}
}
@Override
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
SerialNumberGenerateResultBO serialNumberGenerateResult = null;
String lockKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfo.getSerialNumberId();
boolean lock = false;
for (int i = 0; i < MAX_GET_LOCK_COUNT; i++) {
try {
lock = redisService.getLock(lockKey, 60 * 1000L);
if (lock) {
break;
}
Thread.sleep(SLEEP_MILLISECONDS);
} catch (Throwable e) {
log.error(e.getMessage(), e);
// 根据步长计算 redis 增加值
ArrayList<Integer> list = new ArrayList<>(count);
int redisIncrease = 0;
for (int i = 0; i < count; i++) {
int stepIncrease = 1;
Integer stepRandomRange = serialNumberInfo.getStepRandomRange();
if (stepRandomRange > 1) {
stepIncrease = RandomUtil.getSecureRandom().nextInt(1, serialNumberInfo.getStepRandomRange() + 1);
}
redisIncrease += stepIncrease;
list.add(stepIncrease);
}
if (!lock) {
throw new BusinessException("SerialNumber 尝试5次未能生成单号");
}
try {
// 获取上次的生成结果
SerialNumberLastGenerateBO lastGenerateBO = (SerialNumberLastGenerateBO) redisService.mget(
RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()));
String redisKey = generateRedisKeyByDate(serialNumberInfo.getSerialNumberId(), serialNumberInfo.getSerialNumberRuleTypeEnum(), LocalDate.now());
Long increaseResult = redisTemplate.opsForValue().increment(redisKey, redisIncrease);
ArrayList<Long> numberList = new ArrayList<>(count);
Long number = increaseResult;
for (Integer i : list) {
number = number - i;
numberList.add((number + 1));
}
Collections.reverse(numberList);
SerialNumberGenerateResultBO serialNumberGenerateResult = SerialNumberGenerateResultBO
.builder()
.serialNumberId(serialNumberInfo.getSerialNumberId())
.lastNumber(increaseResult)
.lastTime(LocalDateTime.now())
.numberList(numberList)
.isReset(false)
.build();
// 生成
serialNumberGenerateResult = super.loopNumberList(lastGenerateBO, serialNumberInfo, count);
// 将生成信息保存的内存和数据库
lastGenerateBO.setLastNumber(serialNumberGenerateResult.getLastNumber());
lastGenerateBO.setLastTime(serialNumberGenerateResult.getLastTime());
serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime());
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()), lastGenerateBO);
// 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult);
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
} catch (Throwable e) {
log.error(e.getMessage(), e);
throw e;
} finally {
redisService.unLock(lockKey);
}
}
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
private String generateRedisKeyByDate(Integer serialNumberId, SerialNumberRuleTypeEnum serialNumberRuleTypeEnum, LocalDate localDate) {
return switch (serialNumberRuleTypeEnum) {
case DAY -> {
String dayStr = SmartLocalDateUtil.format(localDate, SmartDateFormatterEnum.YMD);
yield RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId + ":" + dayStr;
}
case MONTH -> {
String monthStr = SmartLocalDateUtil.format(localDate, SmartDateFormatterEnum.YM);
yield RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId + ":" + monthStr;
}
case YEAR -> {
String yearStr = String.valueOf(localDate.getYear());
yield RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId + ":" + yearStr;
}
case NONE -> RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId;
};
}
}

View File

@ -65,10 +65,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -110,7 +126,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 20

View File

@ -65,10 +65,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -110,7 +126,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 20
@ -134,7 +150,7 @@ reload:
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 前缀 例如:Bear
# token 前缀 例如:Bearer
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天2592000秒-1 代表永久有效
timeout: 2592000
@ -168,4 +184,4 @@ smart:
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60
db-refresh-interval: 60

View File

@ -7,14 +7,14 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 10
min-idle: 10
max-active: 100
max-active: 200
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
filters: stat
druid:
username: druid
password: 1024lab
password: 1024
login:
enabled: false
method:
@ -34,7 +34,6 @@ spring:
min-idle: 10
max-idle: 50
max-wait: 30000ms
# 邮件置以SSL的方式发送, 这个需要使用这种方式并且端口是465
mail:
host: smtp.163.com
@ -65,10 +64,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -95,14 +110,13 @@ file:
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
private-url-expire-seconds: 3600
# open api配置
springdoc:
swagger-ui:
enabled: true # 开关
doc-expansion: none #关闭展开
tags-sorter: alpha
server-base-url: https://preview.smartadmin.vip/smart-admin-api
server-base-url:
api-docs:
enabled: true # 开关
knife4j:
@ -112,7 +126,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 100
@ -133,7 +147,7 @@ reload:
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 前缀 例如:Bear
# token 前缀 例如:Bearer
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天2592000秒-1 代表永久有效
timeout: 2592000

View File

@ -65,10 +65,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -110,7 +126,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 20
@ -134,7 +150,7 @@ reload:
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 前缀 例如:Bear
# token 前缀 例如:Bearer
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天2592000秒-1 代表永久有效
timeout: 2592000
@ -168,4 +184,4 @@ smart:
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60
db-refresh-interval: 60

View File

@ -22,48 +22,43 @@
<springboot.version>2.7.18</springboot.version>
<spring-mock.version>2.0.8</spring-mock.version>
<spring-security-crypto.version>5.8.16</spring-security-crypto.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
<mysql-connector-j.version>8.0.33</mysql-connector-j.version>
<mybatis-plus.version>3.5.12</mybatis-plus.version>
<mysql-connector-j.version>9.3.0</mysql-connector-j.version>
<p6spy.version>3.9.1</p6spy.version>
<springdoc-openapi-ui.version>1.7.0</springdoc-openapi-ui.version>
<knife4j.version>4.3.0</knife4j.version>
<fastjson.version>2.0.48</fastjson.version>
<druid.version>1.2.14</druid.version>
<knife4j.version>4.5.0</knife4j.version>
<fastjson.version>2.0.57</fastjson.version>
<druid.version>1.2.25</druid.version>
<google-linkedhashmap.version>1.4.2</google-linkedhashmap.version>
<google-guava.version>20.0</google-guava.version>
<reflections.version>0.9.11</reflections.version>
<commons-io.version>2.15.0</commons-io.version>
<commons-lang3.version>3.12.0</commons-lang3.version>
<commons-collections4.version>4.4</commons-collections4.version>
<commons-compress.version>1.26.0</commons-compress.version>
<commons-codec.version>1.13</commons-codec.version>
<commons-text.version>1.9</commons-text.version>
<xerces.version>2.12.0</xerces.version>
<fast-excel.version>1.0.0</fast-excel.version>
<poi.version>5.2.4</poi.version>
<ooxml-schemas.version>1.4</ooxml-schemas.version>
<aws-java-sdk.version>1.11.842</aws-java-sdk.version>
<log4j-spring-boot.version>2.23.1</log4j-spring-boot.version>
<hutool.version>5.7.22</hutool.version>
<velocity-engine-core.version>2.3</velocity-engine-core.version>
<reflections.version>0.10.2</reflections.version>
<commons-io.version>2.19.0</commons-io.version>
<commons-lang3.version>3.18.0</commons-lang3.version>
<commons-collections4.version>4.5.0</commons-collections4.version>
<commons-compress.version>1.27.1</commons-compress.version>
<commons-codec.version>1.18.0</commons-codec.version>
<commons-text.version>1.13.1</commons-text.version>
<fast-excel.version>1.2.0</fast-excel.version>
<poi.version>5.4.1</poi.version>
<awssdk-s3.version>2.31.78</awssdk-s3.version>
<hutool.version>5.8.39</hutool.version>
<velocity-engine-core.version>2.4.1</velocity-engine-core.version>
<velocity-tools.version>3.1</velocity-tools.version>
<sa-token.version>1.41.0</sa-token.version>
<sa-token.version>1.44.0</sa-token.version>
<ip2region.version>2.7.0</ip2region.version>
<bcprov.version>1.80</bcprov.version>
<jackson-datatype-jsr310.version>2.13.4</jackson-datatype-jsr310.version>
<jackson-dataformat-yaml.version>2.16.1</jackson-dataformat-yaml.version>
<smartdb.version>1.2.0</smartdb.version>
<redisson.version>3.25.0</redisson.version>
<snakeyaml.version>2.2</snakeyaml.version>
<freemarker.version>2.3.33</freemarker.version>
<jsoup.version>1.18.1</jsoup.version>
<tika.version>2.9.3</tika.version>
<snakeyaml.version>2.4</snakeyaml.version>
<freemarker.version>2.3.34</freemarker.version>
<jsoup.version>1.21.1</jsoup.version>
<tika.version>2.9.4</tika.version>
</properties>
<dependencyManagement>
<dependencies>
<!--BOM begin-->
<!--SpringBoot BOM begin-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
@ -71,7 +66,16 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!--BOM end-->
<!--SpringBoot BOM end-->
<!--mybatis-plus BOM begin-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus BOM end-->
<dependency>
<groupId>org.springframework</groupId>
@ -97,18 +101,6 @@
<version>${mysql-connector-j.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>p6spy</groupId>
<artifactId>p6spy</artifactId>
@ -194,9 +186,9 @@
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<version>${aws-java-sdk.version}</version>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${awssdk-s3.version}</version>
<exclusions>
<exclusion>
<artifactId>commons-logging</artifactId>
@ -294,20 +286,8 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<version>${ooxml-schemas.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson-datatype-jsr310.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<version>${jackson-dataformat-yaml.version}</version>
<artifactId>poi-ooxml-full</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency>

View File

@ -4,7 +4,6 @@ import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaAnnotationStrategy;
import cn.dev33.satoken.strategy.SaStrategy;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.admin.module.system.login.domain.RequestEmployee;
import net.lab1024.sa.admin.module.system.login.service.LoginService;
@ -69,6 +68,7 @@ public class AdminInterceptor implements HandlerInterceptor {
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee);
SmartRequestUtil.setRequestUser(requestEmployee);
return true;
}

View File

@ -36,7 +36,7 @@ public class DepartmentEntity {
/**
* 负责人员工 id
*/
@TableField(updateStrategy = FieldStrategy.IGNORED)
@TableField(updateStrategy = FieldStrategy.NEVER)
private Long managerId;
/**

View File

@ -23,6 +23,12 @@ public class EmployeeEntity {
@TableId(type = IdType.AUTO)
private Long employeeId;
/**
* 唯一id
*/
private String employeeUid;
/**
* 登录账号
*/

View File

@ -1,6 +1,7 @@
package net.lab1024.sa.admin.module.system.employee.service;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.UUID;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import net.lab1024.sa.admin.module.system.department.dao.DepartmentDao;
@ -138,16 +139,20 @@ public class EmployeeService {
}
EmployeeEntity entity = SmartBeanUtil.copy(employeeAddForm, EmployeeEntity.class);
// 员工uid
String employeeUid = UUID.randomUUID(true).toString(true);
entity.setEmployeeUid(employeeUid);
// 设置密码 默认密码
String password = securityPasswordService.randomPassword();
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(password));
// 设置密码 随机密码
String randomPassword = securityPasswordService.randomPassword();
String generateSaltPassword = this.generateSaltPassword(randomPassword, employeeUid);
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(generateSaltPassword));
// 保存数据
entity.setDeletedFlag(Boolean.FALSE);
employeeManager.saveEmployee(entity, employeeAddForm.getRoleIdList());
return ResponseDTO.ok(password);
return ResponseDTO.ok(randomPassword);
}
/**
@ -241,7 +246,6 @@ public class EmployeeService {
/**
* 更新登录人头像
*
*/
public ResponseDTO<String> updateAvatar(EmployeeUpdateAvatarForm employeeUpdateAvatarForm) {
Long employeeId = employeeUpdateAvatarForm.getEmployeeId();
@ -343,12 +347,12 @@ public class EmployeeService {
}
// 校验原始密码
if (!SecurityPasswordService.matchesPwd(updatePasswordForm.getOldPassword(),employeeEntity.getLoginPwd()) ) {
if (!SecurityPasswordService.matchesPwd(this.generateSaltPassword(updatePasswordForm.getOldPassword(), employeeEntity.getEmployeeUid()), employeeEntity.getLoginPwd())) {
return ResponseDTO.userErrorParam("原密码有误,请重新输入");
}
// 新旧密码相同
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword()) ){
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword())) {
return ResponseDTO.userErrorParam("新密码与原始密码相同,请重新输入");
}
@ -359,14 +363,13 @@ public class EmployeeService {
}
// 根据三级等保规则校验密码是否重复
ResponseDTO<String> passwordRepeatTimes = securityPasswordService.validatePasswordRepeatTimes(requestUser, updatePasswordForm.getNewPassword());
ResponseDTO<String> passwordRepeatTimes = securityPasswordService.validatePasswordRepeatTimes(requestUser, this.generateSaltPassword(updatePasswordForm.getNewPassword(), employeeEntity.getEmployeeUid()));
if (!passwordRepeatTimes.getOk()) {
return ResponseDTO.error(passwordRepeatTimes);
}
// 更新密码
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getNewPassword());
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(this.generateSaltPassword(updatePasswordForm.getNewPassword(), employeeEntity.getEmployeeUid()));
EmployeeEntity updateEntity = new EmployeeEntity();
updateEntity.setEmployeeId(employeeId);
updateEntity.setLoginPwd(newEncryptPassword);
@ -405,8 +408,14 @@ public class EmployeeService {
* 重置密码
*/
public ResponseDTO<String> resetPassword(Long employeeId) {
EmployeeEntity employeeEntity = employeeDao.selectById(employeeId);
if (employeeEntity == null) {
return ResponseDTO.error(UserErrorCode.DATA_NOT_EXIST);
}
String password = securityPasswordService.randomPassword();
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(password));
String saltPassword = this.generateSaltPassword(password, employeeEntity.getEmployeeUid());
employeeDao.updatePassword(employeeId, SecurityPasswordService.getEncryptPwd(saltPassword));
return ResponseDTO.ok(password);
}
@ -426,4 +435,14 @@ public class EmployeeService {
return employeeDao.getByLoginName(loginName, false);
}
/**
* 生成加盐密码
* 格式为[password]_[uid大写]_[uid小写]
*/
public String generateSaltPassword(String password, String employeeUid) {
return password + StringConst.UNDERLINE +
employeeUid.toUpperCase() +
StringConst.UNDERLINE +
employeeUid.toLowerCase();
}
}

View File

@ -58,7 +58,7 @@ public class LoginController {
return ResponseDTO.ok(loginResult);
}
@Operation(summary = "退出登 @author 卓大")
@Operation(summary = "退出登 @author 卓大")
@GetMapping("/login/logout")
public ResponseDTO<String> logout() {
return loginService.logout(SmartRequestUtil.getRequestUser());

View File

@ -7,7 +7,6 @@ import cn.hutool.core.util.NumberUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.admin.module.system.department.domain.vo.DepartmentVO;
import net.lab1024.sa.admin.module.system.department.service.DepartmentService;
import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity;
import net.lab1024.sa.admin.module.system.employee.service.EmployeeService;
@ -49,8 +48,6 @@ import net.lab1024.sa.base.module.support.securityprotect.domain.LoginFailEntity
import net.lab1024.sa.base.module.support.securityprotect.service.Level3ProtectConfigService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityLoginService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityPasswordService;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@ -82,9 +79,6 @@ public class LoginService implements StpInterface {
@Resource
private EmployeeService employeeService;
@Resource
private DepartmentService departmentService;
@Resource
private CaptchaService captchaService;
@ -106,9 +100,6 @@ public class LoginService implements StpInterface {
@Resource
private SecurityPasswordService protectPasswordService;
@Resource
private IFileStorageService fileStorageService;
@Resource
private ApiEncryptService apiEncryptService;
@ -132,7 +123,7 @@ public class LoginService implements StpInterface {
}
/**
* 员工登
* 员工登
*
* @return 返回用户登录信息
*/
@ -196,7 +187,7 @@ public class LoginService implements StpInterface {
}
// 密码错误
if (!SecurityPasswordService.matchesPwd(requestPassword, employeeEntity.getLoginPwd())) {
if (!SecurityPasswordService.matchesPwd(employeeService.generateSaltPassword(requestPassword, employeeEntity.getEmployeeUid()), employeeEntity.getLoginPwd())) {
// 记录登录失败
saveLoginLog(employeeEntity, ip, userAgent, "密码错误", LoginLogResultEnum.LOGIN_FAIL, loginDeviceEnum);
// 记录等级保护次数
@ -273,7 +264,7 @@ public class LoginService implements StpInterface {
/**
* 根据登token 获取员请求工信息
* 根据登token 获取员请求工信息
*/
public RequestEmployee getLoginEmployee(String loginId, HttpServletRequest request) {
if (loginId == null) {

View File

@ -2,9 +2,6 @@ package net.lab1024.sa.admin.module.system.role.domain.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotNull;
/**
* 角色

View File

@ -3,7 +3,6 @@ package net.lab1024.sa.admin.module.system.role.manager;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import net.lab1024.sa.admin.module.system.role.dao.RoleMenuDao;
import net.lab1024.sa.admin.module.system.role.domain.entity.RoleMenuEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

View File

@ -1,14 +1,13 @@
package net.lab1024.sa.admin.module.system.role.service;
import com.google.common.collect.Lists;
import net.lab1024.sa.admin.module.system.role.domain.entity.RoleDataScopeEntity;
import net.lab1024.sa.admin.module.system.role.domain.form.RoleDataScopeUpdateForm;
import net.lab1024.sa.admin.module.system.role.domain.vo.RoleDataScopeVO;
import net.lab1024.sa.admin.module.system.role.manager.RoleDataScopeManager;
import net.lab1024.sa.base.common.code.UserErrorCode;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartBeanUtil;
import net.lab1024.sa.admin.module.system.role.domain.entity.RoleDataScopeEntity;
import net.lab1024.sa.admin.module.system.role.manager.RoleDataScopeManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

View File

@ -131,6 +131,17 @@
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
</dependency>
<dependency>
@ -179,8 +190,8 @@
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-java-sdk-s3</artifactId>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
</dependency>
@ -250,17 +261,12 @@
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId>
<artifactId>poi-ooxml</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml-full</artifactId>
</dependency>
<dependency>

View File

@ -52,8 +52,7 @@ public class SmartPageUtil {
log.error("《存在SQL注入》 : {}", sortItem.getColumn());
throw new BusinessException("存在SQL注入风险请联系技术工作人员");
}
orderItemList.add(new OrderItem(sortItem.getColumn(), sortItem.getIsAsc()));
orderItemList.add(sortItem.getIsAsc() ? OrderItem.asc(sortItem.getColumn()) : OrderItem.desc(sortItem.getColumn()));
}
page.setOrders(orderItemList);
return page;
@ -94,7 +93,7 @@ public class SmartPageUtil {
return newPageResult;
}
public static <T> PageResult subListPage(Integer pageNum, Integer pageSize, List<T> list) {
public static <T> PageResult<T> subListPage(Integer pageNum, Integer pageSize, List<T> list) {
PageResult<T> pageRet = new PageResult<T>();
//总条数
int count = list.size();

View File

@ -18,6 +18,9 @@ public class SmartRequestUtil {
private static final ThreadLocal<RequestUser> REQUEST_THREAD_LOCAL = new ThreadLocal<>();
public static void setRequestUser(RequestUser requestUser) {
if(requestUser == null){
return;
}
REQUEST_THREAD_LOCAL.set(requestUser);
}

View File

@ -11,7 +11,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import static cn.hutool.core.util.CharsetUtil.UTF_8;
@ -55,9 +54,8 @@ public class SmartResponseUtil {
if (SmartStringUtil.isNotEmpty(fileName)) {
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaTypeFactory.getMediaType(fileName).orElse(MediaType.APPLICATION_OCTET_STREAM) + ";charset=utf-8");
try {
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, UTF_8).replaceAll("\\+", "%20"));
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment;filename=" + URLEncoder.encode(fileName, "utf-8").replaceAll("\\+", "%20"));
} catch (UnsupportedEncodingException e) {
log.error(e.getMessage(), e);
throw new RuntimeException(e);
}
response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, HttpHeaders.CONTENT_DISPOSITION);

View File

@ -1,12 +1,5 @@
package net.lab1024.sa.base.config;
import com.amazonaws.ClientConfiguration;
import com.amazonaws.Protocol;
import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import lombok.Data;
import net.lab1024.sa.base.module.support.file.service.FileStorageCloudServiceImpl;
import net.lab1024.sa.base.module.support.file.service.FileStorageLocalServiceImpl;
@ -17,6 +10,13 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
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 java.net.URI;
/**
* 文件上传 配置
@ -31,6 +31,10 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@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";
@ -69,15 +73,17 @@ public class FileConfig implements WebMvcConfigurer {
*/
@Bean
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
public AmazonS3 initAmazonS3() {
ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setProtocol(Protocol.HTTPS);
return AmazonS3ClientBuilder.standard()
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey)))
.withClientConfiguration(clientConfig)
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region))
.withPathStyleAccessEnabled(false)
.withChunkedEncodingDisabled(true)
public S3Client initAmazonS3() {
return S3Client.builder()
.region(Region.AWS_GLOBAL)
.endpointOverride(URI.create((urlPrefix.startsWith(HTTPS) ? HTTPS : HTTP) + endpoint))
.credentialsProvider(
StaticCredentialsProvider.create(
AwsBasicCredentials.create(accessKey, secretKey)))
.serviceConfiguration(S3Configuration.builder()
.pathStyleAccessEnabled(false)
.chunkedEncodingEnabled(false)
.build())
.build();
}

View File

@ -3,13 +3,13 @@ package net.lab1024.sa.base.config;
import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.module.support.repeatsubmit.RepeatSubmitAspect;
import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitCaffeineTicket;
import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitRedisTicket;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/**
* 重复提交配置
@ -24,22 +24,22 @@ import javax.annotation.Resource;
public class RepeatSubmitConfig {
@Resource
private ValueOperations<String, String> valueOperations;
private RedisTemplate<String, Object> redisTemplate;
@Bean
public RepeatSubmitAspect repeatSubmitAspect() {
RepeatSubmitRedisTicket caffeineTicket = new RepeatSubmitRedisTicket(valueOperations, this::ticket);
return new RepeatSubmitAspect(caffeineTicket);
RepeatSubmitRedisTicket ticket = new RepeatSubmitRedisTicket(redisTemplate, this::ticket);
return new RepeatSubmitAspect(ticket);
}
/**
* 获取指明某个用户的凭证
*/
private String ticket(String servletPath) {
private String ticket(HttpServletRequest request) {
Long userId = SmartRequestUtil.getRequestUserId();
if (null == userId) {
return StringConst.EMPTY;
}
return servletPath + "_" + userId;
return request.getServletPath() + "_" + userId;
}
}

View File

@ -6,15 +6,10 @@ import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.ErrorCodeRegister;
import net.lab1024.sa.base.common.enumeration.SystemEnvironmentEnum;
import net.lab1024.sa.base.common.util.SmartEnumUtil;
import net.lab1024.sa.base.module.support.reload.ReloadCommand;
import net.lab1024.sa.base.module.support.reload.core.SmartReloadManager;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.web.context.WebServerApplicationContext;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;

View File

@ -3,9 +3,9 @@ package net.lab1024.sa.base.module.support.captcha;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.domain.SystemEnvironment;
import net.lab1024.sa.base.constant.RedisKeyConst;
@ -18,7 +18,6 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.awt.*;
import java.util.Objects;
import java.util.UUID;
/**
* 图形验证码 服务
@ -70,7 +69,7 @@ public class CaptchaService {
* 图片 base64格式
*/
// uuid 唯一标识
String uuid = UUID.randomUUID().toString().replace("-", StringConst.EMPTY);
String uuid = IdUtil.fastSimpleUUID();
CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaUuid(uuid);

View File

@ -40,7 +40,7 @@ import java.util.Optional;
@Service
public class CodeGeneratorService {
private static final String COLUMN_NULLABLE_IDENTIFY = "NO";
private static final String COLUMN_NO_NULLABLE_IDENTIFY = "NO";
private static final String COLUMN_PRIMARY_KEY = "PRI";
@ -65,7 +65,7 @@ public class CodeGeneratorService {
public List<TableColumnVO> getTableColumns(String tableName) {
List<TableColumnVO> tableColumns = codeGeneratorDao.selectTableColumn(tableName);
for (TableColumnVO tableColumn : tableColumns) {
tableColumn.setNullableFlag(!COLUMN_NULLABLE_IDENTIFY.equalsIgnoreCase(tableColumn.getIsNullable()));
tableColumn.setNullableFlag(!COLUMN_NO_NULLABLE_IDENTIFY.equalsIgnoreCase(tableColumn.getIsNullable()));
tableColumn.setPrimaryKeyFlag(COLUMN_PRIMARY_KEY.equalsIgnoreCase(tableColumn.getColumnKey()));
tableColumn.setAutoIncreaseFlag(SmartStringUtil.isNotEmpty(tableColumn.getExtra()) && COLUMN_AUTO_INCREASE.equalsIgnoreCase(tableColumn.getExtra()));
}

View File

@ -4,6 +4,7 @@ import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ZipUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
@ -82,7 +83,7 @@ public class CodeGeneratorTemplateService {
}
public void zipGeneratedFiles(OutputStream outputStream, String tableName, CodeGeneratorConfigEntity codeGeneratorConfigEntity) {
String uuid = UUID.randomUUID().toString();
String uuid = IdUtil.fastSimpleUUID();
File dir = new File(uuid);
// 1生产文件

View File

@ -8,7 +8,6 @@ import net.lab1024.sa.base.module.support.codegenerator.domain.form.CodeGenerato
import net.lab1024.sa.base.module.support.codegenerator.domain.model.CodeField;
import net.lab1024.sa.base.module.support.codegenerator.domain.model.CodeQueryField;
import net.lab1024.sa.base.module.support.codegenerator.service.variable.CodeGenerateBaseVariableService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*;

View File

@ -18,7 +18,6 @@ import net.lab1024.sa.base.module.support.file.domain.form.FileQueryForm;
import net.lab1024.sa.base.module.support.file.domain.vo.FileDownloadVO;
import net.lab1024.sa.base.module.support.file.domain.vo.FileUploadVO;
import net.lab1024.sa.base.module.support.file.domain.vo.FileVO;
import net.lab1024.sa.base.module.support.redis.RedisService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityFileService;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
@ -55,9 +54,6 @@ public class FileService {
@Resource
private FileDao fileDao;
@Resource
private RedisService redisService;
@Resource
private SecurityFileService securityFileService;

View File

@ -2,10 +2,7 @@ package net.lab1024.sa.base.module.support.file.service;
import cn.hutool.core.date.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import cn.hutool.core.util.IdUtil;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.SystemErrorCode;
import net.lab1024.sa.base.common.domain.ResponseDTO;
@ -21,22 +18,29 @@ import net.lab1024.sa.base.module.support.file.domain.vo.FileVO;
import net.lab1024.sa.base.module.support.redis.RedisService;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.FileCopyUtils;
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;
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 云计算 实现
@ -45,7 +49,7 @@ import java.util.UUID;
* @Date 2019年10月11日 15:34:47
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
@Slf4j
public class FileStorageCloudServiceImpl implements IFileStorageService {
@ -66,7 +70,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
private static final String USER_METADATA_FILE_SIZE = "file-size";
@Resource
private AmazonS3 amazonS3;
private S3Client s3Client;
@Resource
private FileConfig cloudConfig;
@ -86,44 +90,44 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
}
String fileType = FilenameUtils.getExtension(originalFileName);
String uuid = UUID.randomUUID().toString().replaceAll("-", "");
String uuid = IdUtil.fastSimpleUUID();
String time = LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_FORMATTER);
String fileKey = path + uuid + "_" + time+ "." + fileType;
String fileKey = path + uuid + "_" + time + "." + fileType;
// 文件名称 URL 编码
String urlEncoderFilename;
try {
urlEncoderFilename = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name());
urlEncoderFilename = URLEncoder.encode(originalFileName, "utf-8");
} catch (UnsupportedEncodingException e) {
log.error("文件上传服务URL ENCODE-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败");
throw new RuntimeException(e);
}
ObjectMetadata meta = new ObjectMetadata();
meta.setContentEncoding(StandardCharsets.UTF_8.name());
meta.setContentDisposition("attachment;filename=" + urlEncoderFilename);
Map<String, String> userMetadata = new HashMap<>(10);
userMetadata.put(USER_METADATA_FILE_NAME, urlEncoderFilename);
userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
meta.setUserMetadata(userMetadata);
meta.setContentLength(file.getSize());
meta.setContentType(this.getContentType(fileType));
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();
InputStream inputStream = null;
try {
amazonS3.putObject(cloudConfig.getBucketName(), fileKey, file.getInputStream(), meta);
inputStream = file.getInputStream();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
} catch (IOException e) {
log.error("文件上传-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
IOUtils.closeQuietly(inputStream);
}
// 根据文件路径获取并设置访问权限
CannedAccessControlList acl = this.getACL(path);
amazonS3.setObjectAcl(cloudConfig.getBucketName(), fileKey, acl);
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;
if (CannedAccessControlList.Private.equals(acl)) {
if (ObjectCannedACL.PRIVATE.equals(acl)) {
// 获取临时访问的URL
url = this.getFileUrl(fileKey).getData();
}
@ -136,8 +140,8 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
/**
* 获取文件url
*
* @param fileKey
* @return
* @param fileKey 文件key
* @return url
*/
@Override
public ResponseDTO<String> getFileUrl(String fileKey) {
@ -159,10 +163,14 @@ 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();
Date expiration = new Date(System.currentTimeMillis() + cloudConfig.getPrivateUrlExpireSeconds() * 1000L);
URL url = amazonS3.generatePresignedUrl(cloudConfig.getBucketName(), fileKey, expiration);
fileVO.setFileUrl(url.toString());
S3Presigner presigner = S3Presigner.builder().region(Region.of(cloudConfig.getRegion())).build();
PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest);
String url = presignedGetObjectRequest.url().toString();
fileVO.setFileUrl(url);
redisService.set(fileRedisKey, fileVO, cloudConfig.getPrivateUrlExpireSeconds() - 5);
}
@ -175,11 +183,11 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
*/
@Override
public ResponseDTO<FileDownloadVO> download(String key) {
//获取oss对象
S3Object s3Object = amazonS3.getObject(cloudConfig.getBucketName(), key);
// 获取文件 meta
ObjectMetadata metadata = s3Object.getObjectMetadata();
Map<String, String> userMetadata = metadata.getUserMetadata();
HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getBucketName()).key(key).build();
HeadObjectResponse headObjectResponse = s3Client.headObject(objectRequest);
Map<String, String> userMetadata = headObjectResponse.metadata();
FileMetadataVO metadataDTO = null;
if (MapUtils.isNotEmpty(userMetadata)) {
metadataDTO = new FileMetadataVO();
@ -190,43 +198,31 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
metadataDTO.setFileSize(fileSize);
}
// 获得输入流
InputStream objectContent = s3Object.getObjectContent();
try {
// 输入流转换为字节流
byte[] buffer = FileCopyUtils.copyToByteArray(objectContent);
//获取oss对象
GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(key).build();
ResponseBytes<GetObjectResponse> s3ClientObject = s3Client.getObject(getObjectRequest, ResponseTransformer.toBytes());
FileDownloadVO fileDownloadVO = new FileDownloadVO();
fileDownloadVO.setData(buffer);
fileDownloadVO.setMetadata(metadataDTO);
return ResponseDTO.ok(fileDownloadVO);
} catch (IOException e) {
log.error("文件下载-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "下载失败");
} finally {
try {
// 关闭输入流
objectContent.close();
s3Object.close();
} catch (IOException e) {
log.error("文件下载-发生异常:", e);
}
}
// 输入流转换为字节流
byte[] buffer = s3ClientObject.asByteArray();
FileDownloadVO fileDownloadVO = new FileDownloadVO();
fileDownloadVO.setData(buffer);
fileDownloadVO.setMetadata(metadataDTO);
return ResponseDTO.ok(fileDownloadVO);
}
/**
* 根据文件夹路径 返回对应的访问权限
*
* @param fileKey
* @return
* @param fileKey 文件key
* @return 权限
*/
private CannedAccessControlList getACL(String fileKey) {
private ObjectCannedACL getACL(String fileKey) {
// 公用读
if (fileKey.contains(FileFolderTypeEnum.FOLDER_PUBLIC)) {
return CannedAccessControlList.PublicRead;
return ObjectCannedACL.PUBLIC_READ;
}
// 其他默认私有读写
return CannedAccessControlList.Private;
return ObjectCannedACL.PRIVATE;
}
/**
@ -235,11 +231,11 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
* ps不能删除fileKey不为空的文件夹
*
* @param fileKey 文件or文件夹
* @return
*/
@Override
public ResponseDTO<String> delete(String fileKey) {
amazonS3.deleteObject(cloudConfig.getBucketName(), fileKey);
DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).build();
s3Client.deleteObject(deleteObjectRequest);
return ResponseDTO.ok();
}

View File

@ -19,7 +19,6 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;

View File

@ -13,7 +13,6 @@ import net.lab1024.sa.base.module.support.helpdoc.domain.form.HelpDocUpdateForm;
import net.lab1024.sa.base.module.support.helpdoc.domain.vo.HelpDocDetailVO;
import net.lab1024.sa.base.module.support.helpdoc.domain.vo.HelpDocVO;
import net.lab1024.sa.base.module.support.helpdoc.manager.HelpDocManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

View File

@ -1,7 +1,7 @@
package net.lab1024.sa.base.module.support.mail;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.IdUtil;
import freemarker.cache.StringTemplateLoader;
import freemarker.template.Configuration;
import freemarker.template.Template;
@ -16,7 +16,6 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringSubstitutor;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
@ -163,7 +162,7 @@ public class MailService {
private String freemarkerResolverContent(String htmlTemplate, Map<String, Object> templateParamsMap) {
Configuration configuration = new Configuration(Configuration.VERSION_2_3_23);
StringTemplateLoader stringLoader = new StringTemplateLoader();
String templateName = UUID.fastUUID().toString(true);
String templateName = IdUtil.fastSimpleUUID();
stringLoader.putTemplate(templateName, htmlTemplate);
configuration.setTemplateLoader(stringLoader);
try {

View File

@ -13,13 +13,23 @@ import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
/**
* 重复提交 aop切口
* 重复提交 aop切口 <br>
* -------------------------<br>
* 着重说明<br>
* 注解属性 intervalMilliSecond 是指 一段时间内只允许有一次请求<br>
* intervalMilliSecond = 0: 表示只有上个请求执行完以后才可以执行<br>
* intervalMilliSecond > 0: 表示指定时间内只才能执行特别提醒<br>
* ------------------------<br>
* 特殊说明 intervalMilliSecond > 0 <br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 50 同一时间内可能会有2个请求同时在执行<br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 200 同一时间内只能有1请求执行且执行完后100ms才会执行下一次请求<br>
*
* @Author 1024创新实验室: 胡克
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -43,36 +53,46 @@ public class RepeatSubmitAspect {
@Around("@annotation(net.lab1024.sa.base.module.support.repeatsubmit.annoation.RepeatSubmit)")
public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ticketToken = attributes.getRequest().getServletPath();
String ticket = this.repeatSubmitTicket.getTicket(ticketToken);
if (attributes == null) {
return point.proceed();
}
/**
* 第一步生成防重复提交的 ticket凭证
* ticket 是根据 Request对象 自定义 生成的可以加入请求user相关属性作为生成要素
*/
HttpServletRequest request = attributes.getRequest();
String ticket = this.repeatSubmitTicket.generateTicket(request);
if (StringUtils.isEmpty(ticket)) {
return point.proceed();
}
Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
int limit = annotation.value();
Long intervalMilliSecond = (long) annotation.intervalMilliSecond();
// 获取上一次请求时间
Long lastRequestTime = this.repeatSubmitTicket.getTicketTimestamp(ticket);
// 校验是否限制时间内重复提交
if (lastRequestTime != null && System.currentTimeMillis() < lastRequestTime + limit) {
/**
* 第二步根据 ticket 凭证进行 加锁
* 能加锁则可以执行
* 若不能加锁则证明还是时间间隔interval中
*/
boolean lockSuccessFlag = this.repeatSubmitTicket.tryLock(ticket, System.currentTimeMillis(), intervalMilliSecond);
if (!lockSuccessFlag) {
return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT);
}
// 执行
Object obj = null;
try {
// ticket 设置在执行中
this.repeatSubmitTicket.putTicket(ticket);
// 执行
obj = point.proceed();
return point.proceed();
} catch (Throwable throwable) {
log.error("", throwable);
log.error(throwable.getMessage(), throwable);
throw throwable;
} finally {
this.repeatSubmitTicket.unLock(ticket, intervalMilliSecond);
}
return obj;
}
}

View File

@ -7,10 +7,19 @@ import java.lang.annotation.Target;
/**
* 标记 需要防止重复提交 的注解<br>
* 单位毫秒
* 单位毫秒<br>
* -------------------------<br>
* 着重说明<br>
* 注解属性 intervalMilliSecond 是指 一段时间内只允许有一次请求<br>
* intervalMilliSecond = 0: 表示只有上个请求执行完以后才可以执行<br>
* intervalMilliSecond > 0: 表示指定时间内只才能执行特别提醒<br>
* ------------------------<br>
* 特殊说明 intervalMilliSecond > 0 <br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 50 同一时间内可能会有2个请求同时在执行<br>
* 若假设 方法执行时间为 100ms intervalMilliSecond = 200 同一时间内只能有1请求执行且执行完后100ms才会执行下一次请求<br>
*
* @Author 1024创新实验室: 胡克
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -20,8 +29,8 @@ import java.lang.annotation.Target;
public @interface RepeatSubmit {
/**
* 重复提交间隔时间/毫秒
* 间隔时间/毫秒
*/
int value() default 300;
int intervalMilliSecond() default 0;
}

View File

@ -1,42 +1,42 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import javax.servlet.http.HttpServletRequest;
import java.util.function.Function;
/**
* 凭证用于校验重复提交的东西
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public abstract class AbstractRepeatSubmitTicket {
private final Function<String, String> ticketFunction;
private final Function<HttpServletRequest, String> generateTicketFunction;
public AbstractRepeatSubmitTicket(Function<String, String> ticketFunction) {
this.ticketFunction = ticketFunction;
public AbstractRepeatSubmitTicket(Function<HttpServletRequest, String> generateTicketFunction) {
this.generateTicketFunction = generateTicketFunction;
}
/**
* 获取凭证
* 生成 加锁的 凭证
*/
public String getTicket(String ticketToken) {
return this.ticketFunction.apply(ticketToken);
public String generateTicket(HttpServletRequest request) {
return this.generateTicketFunction.apply(request);
}
/**
* 获取凭证 时间戳
* 加锁
*/
public abstract Long getTicketTimestamp(String ticket);
public abstract boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond);
/**
* 设置本次请求时间
* 移除锁
*/
public abstract void putTicket(String ticket);
public abstract void unLock(String ticket, Long intervalMilliSecond);
}

View File

@ -1,44 +0,0 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 凭证内存实现
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class RepeatSubmitCaffeineTicket extends AbstractRepeatSubmitTicket {
/**
* 限制缓存最大数量 超过后先放入的会自动移除
* 默认缓存时间
* 初始大小为100万
*/
private static final Cache<String, Long> cache = Caffeine.newBuilder()
.maximumSize(100 * 10000)
.expireAfterWrite(300 * 1000L, TimeUnit.MILLISECONDS).build();
public RepeatSubmitCaffeineTicket(Function<String, String> ticketFunction) {
super(ticketFunction);
}
@Override
public Long getTicketTimestamp(String ticket) {
return cache.getIfPresent(ticket);
}
@Override
public void putTicket(String ticket) {
cache.put(ticket, System.currentTimeMillis());
}
}

View File

@ -0,0 +1,60 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import com.google.common.collect.Interner;
import com.google.common.collect.Interners;
import com.google.common.collect.Maps;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
/**
* 凭证内存实现
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class RepeatSubmitMemoryTicket extends AbstractRepeatSubmitTicket {
private Interner<String> pool = Interners.newStrongInterner();
private ConcurrentMap<String, Long> ticketMap = Maps.newConcurrentMap();
public RepeatSubmitMemoryTicket(Function<HttpServletRequest, String> ticketFunction) {
super(ticketFunction);
}
@Override
public boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond) {
synchronized (pool.intern(ticket)) {
Long lastTime = ticketMap.putIfAbsent(ticket, currentTimestamp);
if (lastTime == null) {
return true;
}
if (intervalMilliSecond <= 0) {
return false;
}
if (currentTimestamp - lastTime < intervalMilliSecond) {
return false;
}
ticketMap.put(ticket, currentTimestamp);
return true;
}
}
@Override
public void unLock(String ticket, Long intervalMilliSecond) {
if (intervalMilliSecond > 0) {
return;
}
ticketMap.remove(ticket);
}
}

View File

@ -1,38 +1,47 @@
package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.core.RedisTemplate;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
/**
* 凭证redis实现
*
* @Author 1024创新实验室: 罗伊
* @Date 2020-11-25 20:56:58
* @Author 1024创新实验室-主任: 卓大
* @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket {
private final ValueOperations<String, String> redisValueOperations;
private final RedisTemplate<String, Object> redisTemplate;
public RepeatSubmitRedisTicket(ValueOperations<String, String> redisValueOperations,
Function<String, String> ticketFunction) {
public RepeatSubmitRedisTicket(RedisTemplate<String, Object> redisTemplate,
Function<HttpServletRequest, String> ticketFunction) {
super(ticketFunction);
this.redisValueOperations = redisValueOperations;
this.redisTemplate = redisTemplate;
}
@Override
public Long getTicketTimestamp(String ticket) {
String ticketLastTime = redisValueOperations.get(ticket);
return ticketLastTime == null ? null : Long.valueOf(ticketLastTime);
public boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond) {
if (intervalMilliSecond > 0) {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(ticket, String.valueOf(currentTimestamp), intervalMilliSecond, TimeUnit.MILLISECONDS));
} else {
return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(ticket, String.valueOf(currentTimestamp)));
}
}
@Override
public void putTicket(String ticket) {
redisValueOperations.getOperations().delete(ticket);
this.getTicketTimestamp(ticket);
public void unLock(String ticket, Long intervalMilliSecond) {
if (intervalMilliSecond > 0) {
return;
}
redisTemplate.delete(ticket);
}
}

View File

@ -2,6 +2,7 @@ package net.lab1024.sa.base.module.support.securityprotect.service;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.apache.commons.io.IOUtils;
import org.apache.tika.config.TikaConfig;
import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream;
@ -14,6 +15,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.List;
@ -35,27 +37,9 @@ public class SecurityFileService {
private Level3ProtectConfigService level3ProtectConfigService;
// 定义白名单MIME类型
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList(
"application/json",
"application/zip",
"application/x-7z-compressed",
"application/pdf",
"application/vnd.ms-excel",
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-works",
"text/csv",
"audio/*",
"video/*",
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList("application/json", "application/zip", "application/x-7z-compressed", "application/pdf", "application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", "application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation", "application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.ms-works", "text/csv", "audio/*", "video/*",
// 图片类型 svg有安全隐患所以不使用"image/*"
"image/jpeg",
"image/png",
"image/gif",
"image/bmp"
);
"image/jpeg", "image/png", "image/gif", "image/bmp");
/**
* 检测文件安全类型
@ -73,8 +57,7 @@ public class SecurityFileService {
// 文件类型安全检测
if (level3ProtectConfigService.isFileDetectFlag()) {
String fileType = getFileMimeType(file);
if (ALLOWED_MIME_TYPES.stream()
.noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
if (ALLOWED_MIME_TYPES.stream().noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
return ResponseDTO.userErrorParam("禁止上传此文件类型");
}
}
@ -89,16 +72,20 @@ public class SecurityFileService {
* @return 文件的 MIME 类型
*/
public static String getFileMimeType(MultipartFile file) {
InputStream inputStream = null;
try {
inputStream = file.getInputStream();
TikaConfig tika = new TikaConfig();
Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename());
TikaInputStream stream = TikaInputStream.get(file.getInputStream());
TikaInputStream stream = TikaInputStream.get(inputStream);
MediaType mimetype = tika.getDetector().detect(stream, metadata);
return mimetype.toString();
} catch (IOException | TikaException e) {
log.error(e.getMessage(), e);
return MimeTypes.OCTET_STREAM;
} finally {
IOUtils.closeQuietly(inputStream);
}
}

View File

@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
* @Date 2022-03-25 21:46:07
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/
public abstract class SerialNumberBaseService implements SerialNumberService {
@ -37,7 +37,7 @@ public abstract class SerialNumberBaseService implements SerialNumberService {
@Resource
protected SerialNumberDao serialNumberDao;
private ConcurrentHashMap<Integer, SerialNumberInfoBO> serialNumberMap = new ConcurrentHashMap<>();
protected ConcurrentHashMap<Integer, SerialNumberInfoBO> serialNumberMap = new ConcurrentHashMap<>();
public abstract List<String> generateSerialNumberList(SerialNumberInfoBO serialNumber, int count);

View File

@ -1,23 +1,34 @@
package net.lab1024.sa.base.module.support.serialnumber.service.impl;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.exception.BusinessException;
import net.lab1024.sa.base.common.util.SmartDateFormatterEnum;
import net.lab1024.sa.base.common.util.SmartEnumUtil;
import net.lab1024.sa.base.common.util.SmartLocalDateUtil;
import net.lab1024.sa.base.common.util.SmartStringUtil;
import net.lab1024.sa.base.constant.RedisKeyConst;
import net.lab1024.sa.base.module.support.redis.RedisService;
import net.lab1024.sa.base.module.support.serialnumber.constant.SerialNumberRuleTypeEnum;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberEntity;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberGenerateResultBO;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberInfoBO;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberLastGenerateBO;
import net.lab1024.sa.base.module.support.serialnumber.service.SerialNumberBaseService;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* 单据序列号 基于redis锁实现
* 单据序列号 基于redis key-value increase 实现
*
* @Author 1024创新实验室-主任: 卓大
* @Date 2022-03-25 21:46:07
* @Date 2025-08-03 22:46:07
* @Wechat zhuoda1024
* @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -25,86 +36,125 @@ import java.util.List;
@Slf4j
public class SerialNumberRedisService extends SerialNumberBaseService {
private static final int MAX_GET_LOCK_COUNT = 5;
private static final long SLEEP_MILLISECONDS = 200L;
@Resource
private RedisService redisService;
@Resource
private RedisTemplate redisTemplate;
@Override
public void initLastGenerateData(List<SerialNumberEntity> serialNumberEntityList) {
if (serialNumberEntityList == null) {
return;
}
//删除之前的
redisService.delete(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO);
// 设置redis的上次值
for (SerialNumberEntity serialNumberEntity : serialNumberEntityList) {
SerialNumberLastGenerateBO lastGenerateBO = SerialNumberLastGenerateBO
.builder()
.serialNumberId(serialNumberEntity.getSerialNumberId())
.lastNumber(serialNumberEntity.getLastNumber())
.lastTime(serialNumberEntity.getLastTime())
.build();
if (serialNumberEntity.getLastTime() == null) {
continue;
}
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberEntity.getSerialNumberId()),
lastGenerateBO
);
String redisKey = generateRedisKeyByDate(serialNumberEntity.getSerialNumberId(),
SmartEnumUtil.getEnumByName(serialNumberEntity.getRuleType().toUpperCase(), SerialNumberRuleTypeEnum.class),
serialNumberEntity.getLastTime().toLocalDate());
Object o = redisTemplate.opsForValue().get(redisKey);
if (o == null) {
redisTemplate.opsForValue().set(redisKey, serialNumberEntity.getLastNumber());
}
}
}
/**
* 每天凌晨一点进行检测
* 检测单位数量为3; 3天前3月前3年前
*/
@Scheduled(cron = "0 0 1 * * ?")
public void tryDeleteUnusedRedisKey() {
for (SerialNumberInfoBO serialNumberInfoBO : serialNumberMap.values()) {
SerialNumberRuleTypeEnum typeEnum = serialNumberInfoBO.getSerialNumberRuleTypeEnum();
String dateStr = "";
switch (typeEnum) {
case DAY:
dateStr = SmartLocalDateUtil.format(LocalDate.now().minusDays(3), SmartDateFormatterEnum.YMD);
case MONTH:
dateStr = SmartLocalDateUtil.format(LocalDate.now().minusMonths(3), SmartDateFormatterEnum.YM);
case YEAR:
dateStr = String.valueOf(LocalDate.now().minusYears(3));
}
if (SmartStringUtil.isNotEmpty(dateStr)) {
String redisKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfoBO.getSerialNumberId() + ":" + dateStr;
redisService.delete(redisKey);
}
}
}
@Override
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
SerialNumberGenerateResultBO serialNumberGenerateResult = null;
String lockKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfo.getSerialNumberId();
boolean lock = false;
for (int i = 0; i < MAX_GET_LOCK_COUNT; i++) {
try {
lock = redisService.getLock(lockKey, 60 * 1000L);
if (lock) {
break;
}
Thread.sleep(SLEEP_MILLISECONDS);
} catch (Throwable e) {
log.error(e.getMessage(), e);
// 根据步长计算 redis 增加值
ArrayList<Integer> list = new ArrayList<>(count);
int redisIncrease = 0;
for (int i = 0; i < count; i++) {
int stepIncrease = 1;
Integer stepRandomRange = serialNumberInfo.getStepRandomRange();
if (stepRandomRange > 1) {
stepIncrease = RandomUtil.getSecureRandom().nextInt(serialNumberInfo.getStepRandomRange()) + 1;
}
redisIncrease += stepIncrease;
list.add(stepIncrease);
}
if (!lock) {
throw new BusinessException("SerialNumber 尝试5次未能生成单号");
}
try {
// 获取上次的生成结果
SerialNumberLastGenerateBO lastGenerateBO = (SerialNumberLastGenerateBO) redisService.mget(
RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()));
String redisKey = generateRedisKeyByDate(serialNumberInfo.getSerialNumberId(), serialNumberInfo.getSerialNumberRuleTypeEnum(), LocalDate.now());
Long increaseResult = redisTemplate.opsForValue().increment(redisKey, redisIncrease);
ArrayList<Long> numberList = new ArrayList<>(count);
Long number = increaseResult;
for (Integer i : list) {
number = number - i;
numberList.add((number + 1));
}
Collections.reverse(numberList);
SerialNumberGenerateResultBO serialNumberGenerateResult = SerialNumberGenerateResultBO
.builder()
.serialNumberId(serialNumberInfo.getSerialNumberId())
.lastNumber(increaseResult)
.lastTime(LocalDateTime.now())
.numberList(numberList)
.isReset(false)
.build();
// 生成
serialNumberGenerateResult = super.loopNumberList(lastGenerateBO, serialNumberInfo, count);
// 将生成信息保存的内存和数据库
lastGenerateBO.setLastNumber(serialNumberGenerateResult.getLastNumber());
lastGenerateBO.setLastTime(serialNumberGenerateResult.getLastTime());
serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime());
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()), lastGenerateBO);
// 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult);
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
} catch (Throwable e) {
log.error(e.getMessage(), e);
throw e;
} finally {
redisService.unLock(lockKey);
}
}
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
private String generateRedisKeyByDate(Integer serialNumberId, SerialNumberRuleTypeEnum serialNumberRuleTypeEnum, LocalDate localDate) {
switch (serialNumberRuleTypeEnum) {
case DAY:
String dayStr = SmartLocalDateUtil.format(localDate, SmartDateFormatterEnum.YMD);
return RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId + ":" + dayStr;
case MONTH:
String monthStr = SmartLocalDateUtil.format(localDate, SmartDateFormatterEnum.YM);
return RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId + ":" + monthStr;
case YEAR:
String yearStr = String.valueOf(localDate.getYear());
return RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId + ":" + yearStr;
case NONE:
return RedisKeyConst.Support.SERIAL_NUMBER + serialNumberId;
default:
throw new BusinessException("serialNumberRuleTypeEnum not exist error");
}
}
}

View File

@ -64,10 +64,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -109,7 +125,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 20

View File

@ -64,10 +64,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -109,7 +125,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 20
@ -133,7 +149,7 @@ reload:
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 前缀 例如:Bear
# token 前缀 例如:Bearer
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天2592000秒-1 代表永久有效
timeout: 2592000
@ -167,4 +183,4 @@ smart:
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60
db-refresh-interval: 60

View File

@ -7,14 +7,14 @@ spring:
driver-class-name: com.mysql.cj.jdbc.Driver
initial-size: 10
min-idle: 10
max-active: 100
max-active: 200
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
filters: stat
druid:
username: druid
password: 1024lab
password: 1024
login:
enabled: false
method:
@ -63,10 +63,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -93,14 +109,13 @@ file:
url-prefix: https://${file.storage.cloud.bucket-name}.${file.storage.cloud.endpoint}/
private-url-expire-seconds: 3600
# open api配置
springdoc:
swagger-ui:
enabled: true # 开关
doc-expansion: none #关闭展开
tags-sorter: alpha
server-base-url: https://preview.smartadmin.vip/smart-admin-api
server-base-url:
api-docs:
enabled: true # 开关
knife4j:
@ -110,7 +125,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 100
@ -131,7 +146,7 @@ reload:
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 前缀 例如:Bear
# token 前缀 例如:Bearer
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天2592000秒-1 代表永久有效
timeout: 2592000

View File

@ -64,10 +64,26 @@ spring:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
# 上传文件和请求大小
servlet:
multipart:
max-file-size: 20MB # 单个文件的最大大小
max-request-size: 10MB # 整个请求的最大大小
# 缓存实现类型
cache:
type: redis
# 健康检查
management:
endpoints:
web:
exposure:
include: health,info
health:
mail:
enabled: false
# tomcat 配置,主要用于 配置 访问日志(便于将来排查错误)
server:
tomcat:
@ -109,7 +125,7 @@ knife4j:
username: api # Basic认证用户名
password: 1024 # Basic认证密码
# RestTemplate 请求配置
# RestTemplate 请求配置 毫秒
http:
pool:
max-total: 20
@ -133,7 +149,7 @@ reload:
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: Authorization
# token 前缀 例如:Bear
# token 前缀 例如:Bearer
token-prefix: Bearer
# token 有效期(单位:秒) 默认30天2592000秒-1 代表永久有效
timeout: 2592000
@ -167,4 +183,4 @@ smart:
# 数据库配置检测-开关 默认开启 可选(作用是固定间隔读取数据库配置更新任务,关闭后只能重启服务或通过接口修改定时任务,建议开启)
db-refresh-enabled: true
# 数据库配置检测-执行间隔 默认120秒 可选
db-refresh-interval: 60
db-refresh-interval: 60

View File

@ -12,7 +12,7 @@
<a-config-provider
:locale="antdLocale"
:theme="{
algorithm: compactFlag ? theme.compactAlgorithm : theme.defaultAlgorithm,
algorithm: themeAlgorithm,
token: {
colorPrimary: themeColors[colorIndex].primaryColor,
colorLink: themeColors[colorIndex].primaryColor,
@ -48,12 +48,10 @@
import { messages } from '/@/i18n';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useSpinStore } from '/@/store/modules/system/spin';
import { theme } from 'ant-design-vue';
import { Popover, theme } from 'ant-design-vue';
import { themeColors } from '/@/theme/color.js';
import { Popover } from 'ant-design-vue';
import SmartCopyIcon from '/@/components/framework/smart-copy-icon/index.vue';
import _ from 'lodash';
const slots = useSlots();
const antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale);
const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale);
dayjs.locale(dayjsLocale);
@ -61,16 +59,24 @@
// loading
let spinStore = useSpinStore();
const spinning = computed(() => spinStore.loading);
//
const compactFlag = computed(() => useAppConfigStore().compactFlag);
//
const colorIndex = computed(() => {
return useAppConfigStore().colorIndex;
});
//
const themeAlgorithm = computed(() => {
let themeArray = [];
themeArray.push(useAppConfigStore().darkModeFlag ? theme.darkAlgorithm : theme.defaultAlgorithm);
if (useAppConfigStore().compactFlag) {
themeArray.push(theme.compactAlgorithm);
}
return themeArray;
});
//
const borderRadius = computed(() => {
return useAppConfigStore().borderRadius;
});
function transformCellText({ text, column, record, index }) {
if (column && column.textEllipsisFlag === true) {
return h(
@ -78,7 +84,14 @@
{ placement: 'bottom' },
{
default: () =>
h('div', { style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }, id: `${column.dataIndex}${index}` }, text),
h(
'div',
{
style: { whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' },
id: `${column.dataIndex}${index}`,
},
text
),
content: () =>
h('div', { style: { display: 'flex' } }, [
h('div', text),
@ -91,9 +104,24 @@
}
}
const { useToken } = theme;
const { token } = useToken();
</script>
<style scoped lang="less">
<style lang="less">
@color-bg-container: v-bind('token.colorBgContainer');
:deep(.ant-table-column-sorters) {
align-items: flex-start !important;
}
.smart-query-form {
background-color: @color-bg-container;
padding: 5px 10px;
margin-bottom: 10px;
}
.smart-detail-header {
background-color: @color-bg-container;
padding: 10px;
}
</style>

View File

@ -74,13 +74,13 @@
watch(
() => props.modelValue,
(value) => {
newColumn.forEach(item=>{
value.forEach(itemNewColumns=>{
if(item.dataIndex==itemNewColumns.dataIndex){
Object.assign(item,itemNewColumns)
newColumn.forEach((item) => {
value.forEach((itemNewColumns) => {
if (item.dataIndex === itemNewColumns.dataIndex) {
Object.assign(item, itemNewColumns);
}
})
})
});
});
},
{
deep: true,
@ -90,6 +90,7 @@
//
async function buildUserTableColumns() {
if (!props.tableId) {
return;
}
@ -127,6 +128,7 @@
// 退
function handleExitFullScreen() {
document.querySelector('#smartAdminHeader').style.display = 'block';
fullScreenFlag.value = false;
useAppConfigStore().exitFullScreen();
document.removeEventListener('fullscreenchange', handleFullscreenChange);
@ -137,6 +139,7 @@
// -
function launchElementFullScreen(element) {
document.querySelector('#smartAdminHeader').style.display = 'none';
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
@ -179,28 +182,29 @@
// ----------------- -------------------
const smartTableColumnModal = ref();
function showModal() {
smartTableColumnModal.value.show(newColumn, props.tableId);
}
//
function updateColumn(changeColumnArray) {
let obj={}
let obj = {};
// 使
//
if(_.isEmpty(changeColumnArray)){
if (_.isEmpty(changeColumnArray)) {
obj = mergeColumn(_.cloneDeep(originalColumn), changeColumnArray);
}else{
} else {
obj = mergeColumn(_.cloneDeep(newColumn), changeColumnArray);
}
const newColumns = obj.newColumns;
newColumn.forEach(item=>{
obj.newColumns.forEach(itemNewColumns=>{
if(item.dataIndex==itemNewColumns.dataIndex){
Object.assign(item,itemNewColumns)
newColumn.forEach((item) => {
obj.newColumns.forEach((itemNewColumns) => {
if (item.dataIndex === itemNewColumns.dataIndex) {
Object.assign(item, itemNewColumns);
}
})
})
});
});
emit(
'update:modelValue',
newColumns.filter((e) => e.showFlag)
@ -217,6 +221,6 @@
buildUserTableColumns();
}
},
{ immediate: true }
{ immediate: false }
);
</script>

View File

@ -16,6 +16,8 @@ export const appDefaultConfig = {
sideMenuWidth: 200,
//标签页位置
pageTagLocation: 'center',
// 夜间模式
darkModeFlag: false,
// 菜单主题
sideMenuTheme: 'dark',
// 主题颜色索引

View File

@ -17,6 +17,7 @@ export default {
'setting.table.yHeight': 'Table Height',
'setting.pagetag.location': 'TagPage Position',
'setting.color': 'Theme Color',
'setting.darkmode': 'Dark Mode',
'setting.menu.layout': 'Menu Layout',
'setting.menu.width': 'Menu Width',
'setting.menu.theme': 'Menu Theme',

View File

@ -17,6 +17,7 @@ export default {
'setting.table.yHeight': '表格高度',
'setting.pagetag.location': '标签页位置',
'setting.color': '主题颜色',
'setting.darkmode': '夜间模式',
'setting.menu.layout': '菜单布局',
'setting.menu.width': '菜单宽度',
'setting.menu.theme': '菜单主题',

View File

@ -252,7 +252,6 @@
}
.dropdown-tabs {
background-color: @base-bg-color;
border-radius: 4px;
}

View File

@ -13,7 +13,7 @@
<a-form layout="horizontal" :label-col="{ span: 8 }">
<a-form-item label="语言/Language">
<a-select v-model:value="formState.language" @change="changeLanguage" style="width: 120px">
<a-select-option v-for="item in i18nList" :key="item.value" :value="item.value">{{ item.text }}</a-select-option>
<a-select-option v-for="item in i18nList" :key="item.value" :value="item.value">{{ item.text }} </a-select-option>
</a-select>
</a-form-item>
<a-form-item :label="$t('setting.color')">
@ -51,12 +51,6 @@
<a-input @change="changePageWidth" v-model:value="formState.pageWidth" />
像素px或者 百分比
</a-form-item>
<a-form-item :label="$t('setting.compact')">
<a-radio-group v-model:value="formState.compactFlag" button-style="solid" @change="changeCompactFlag">
<a-radio-button :value="false">默认</a-radio-button>
<a-radio-button :value="true">紧凑</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.menu.layout')">
<a-radio-group @change="changeLayout" button-style="solid" v-model:value="formState.layout">
<a-radio-button v-for="item in $smartEnumPlugin.getValueDescList('LAYOUT_ENUM')" :key="item.value" :value="item.value">
@ -70,6 +64,12 @@
<a-radio-button value="light">Light</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.compact')">
<a-radio-group v-model:value="formState.compactFlag" button-style="solid" @change="changeCompactFlag">
<a-radio-button :value="false">默认</a-radio-button>
<a-radio-button :value="true">紧凑</a-radio-button>
</a-radio-group>
</a-form-item>
<a-form-item :label="$t('setting.pagetag.location')">
<a-radio-group v-model:value="formState.pageTagLocation" button-style="solid" @change="changePageTagLocation">
<a-radio-button value="top">顶部</a-radio-button>
@ -92,7 +92,6 @@
<a-form-item :label="$t('setting.bread')">
<a-switch
@change="changeBreadCrumbFlag"
:disabled="formState.pageTagLocation === 'top'"
v-model:checked="formState.breadCrumbFlag"
checked-children="显示"
un-checked-children="隐藏"
@ -115,23 +114,26 @@
un-checked-children="默认不展开"
/>
</a-form-item>
<a-form-item :label="$t('setting.darkmode')">
<a-switch @change="changeDarkMode" v-model:checked="formState.darkModeFlag" checked-children="开启" un-checked-children="关闭" />
</a-form-item>
<br />
<br />
</a-form>
<div class="footer">
<a-button style="margin-right: 8px" type="primary" @click="copy">复制配置信息</a-button>
<a-button type="block" danger @click="reset">恢复默认配置 </a-button>
<a-button type="primary" danger @click="reset">恢复默认配置</a-button>
</div>
</a-drawer>
</template>
<script setup>
import { ref, reactive, h, watch } from 'vue';
import { h, reactive, ref, watch } from 'vue';
import { i18nList } from '/@/i18n/index';
import { useI18n } from 'vue-i18n';
import localStorageKeyConst from '/@/constants/local-storage-key-const';
import { LAYOUT_ENUM } from '/@/constants/layout-const';
import { localRead, localSave } from '/@/utils/local-util';
import { localSave } from '/@/utils/local-util';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { Modal } from 'ant-design-vue';
import { appDefaultConfig } from '/@/config/app-config';
@ -203,6 +205,8 @@
sideMenuWidth: appConfigStore.sideMenuWidth,
//
sideMenuTheme: appConfigStore.sideMenuTheme,
//
darkModeFlag: appConfigStore.darkModeFlag,
//
compactFlag: appConfigStore.compactFlag,
//
@ -229,21 +233,8 @@
let formState = reactive({ ...formValue });
watch(
() => formState.pageTagLocation,
() => {
if (formState.pageTagLocation === 'top') {
formState.breadCrumbFlag = false;
} else {
formState.breadCrumbFlag = true;
}
},
{
immediate: true,
}
);
const { locale } = useI18n();
function changeLanguage(languageValue) {
locale.value = languageValue;
appConfigStore.$patch({
@ -293,6 +284,7 @@
compactFlag: e.target.value,
});
}
function changeBorderRadius(e) {
appConfigStore.$patch({
borderRadius: e,
@ -310,6 +302,7 @@
pageTagFlag: e,
});
}
function changeFlatPattern(e) {
appConfigStore.$patch({
flatPattern: e,
@ -345,6 +338,12 @@
watermarkFlag: e,
});
}
function changeDarkMode(e) {
appConfigStore.$patch({
darkModeFlag: e,
});
}
</script>
<style lang="less" scoped>
.footer {
@ -354,9 +353,9 @@
width: 100%;
border-top: 1px solid #e9e9e9;
padding: 10px 16px;
background: #fff;
text-align: left;
z-index: 1;
z-index: 99999;
background-color: white;
}
.color-container {

View File

@ -9,7 +9,7 @@
-->
<template>
<!-- 标签页共两部分1标签 2标签操作区 -->
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-row style="position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag">
<a-tabs style="width: 100%" type="card" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
@ -138,18 +138,17 @@
<style scoped lang="less">
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
@color-primary-bg: v-bind('token.colorPrimaryBg');
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
background-color: #ffffff;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
color: #606266;
.smart-page-tag-operate-icon {
width: 20px;
@ -167,6 +166,7 @@
.smart-page-tag-operate:hover {
color: @color-primary;
background-color: @color-primary-bg;
}
.smart-page-tag {
@ -180,7 +180,6 @@
padding-right: 20px;
padding-left: 20px;
user-select: none;
background: #fff;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-close {
@ -193,11 +192,11 @@
:deep(.ant-tabs-nav) {
margin: 0;
&::before{
border-bottom: none !important;
}
}
:deep(.ant-tabs-nav::before) {
border-bottom: 1px solid #ffffff;
}
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
padding: 5px 8px 3px 15px;
@ -209,12 +208,16 @@
}
:deep(.ant-tabs-tab-active) {
background-color: @color-primary-bg;
.smart-page-tag-close {
color: @color-primary;
}
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
background-color: white;
background-color: @color-primary-bg;
.smart-page-tag-close {
color: @color-primary;
}

View File

@ -9,7 +9,7 @@
-->
<template>
<!-- 标签页共两部分1标签 2标签操作区 -->
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-row style="position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag">
<a-tabs style="width: 100%" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
@ -51,259 +51,215 @@
</template>
<script setup>
import { AppstoreOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
import { AppstoreOutlined, CloseOutlined } from '@ant-design/icons-vue';
import { computed, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
//
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
//
const pageTagFlag = computed(() => useAppConfigStore().$state.pageTagFlag);
const router = useRouter();
const route = useRoute();
const mode = ref('top');
const tagNav = computed(() => useUserStore().getTagNav || []);
const selectedKey = ref(route.name);
const router = useRouter();
const route = useRoute();
const mode = ref('top');
const tagNav = computed(() => useUserStore().getTagNav || []);
const selectedKey = ref(route.name);
watch(
() => route.name,
(newValue, oldValue) => {
selectedKey.value = newValue;
},
{ immediate: true }
);
watch(
() => route.name,
(newValue, oldValue) => {
selectedKey.value = newValue;
},
{ immediate: true }
);
//
function selectTab(name) {
if (selectedKey.value === name) {
return;
}
// tag
let tag = tagNav.value.find((e) => e.menuName === name);
if (!tag) {
router.push({ name: HOME_PAGE_NAME });
return;
}
// router.push({ name, query: Object.assign({ _keepAlive: 1 }, tag.menuQuery) });
router.push({ name, query: tag.menuQuery });
}
//
function closeByMenu(closeAll) {
let find = tagNav.value.find((e) => e.menuName === selectedKey.value);
if (!find || closeAll) {
closeTag(null, true);
} else {
closeTag(find, true);
}
}
//
function closeTag(item, closeAll) {
// tag
if (item && !closeAll) {
let goName = HOME_PAGE_NAME;
let goQuery = undefined;
if (item.fromMenuName && item.fromMenuName !== item.menuName && tagNav.value.some((e) => e.menuName === item.fromMenuName)) {
goName = item.fromMenuName;
goQuery = item.fromMenuQuery;
} else {
// tag
let index = tagNav.value.findIndex((e) => e.menuName === item.menuName);
if (index > 0) {
// tag
let leftTagNav = tagNav.value[index - 1];
goName = leftTagNav.menuName;
goQuery = leftTagNav.menuQuery;
}
//
function selectTab(name) {
if (selectedKey.value === name) {
return;
}
// router.push({ name: goName, query: Object.assign({ _keepAlive: 1 }, goQuery) });
router.push({ name: goName, query: goQuery });
} else if (!item && closeAll) {
// tag
router.push({ name: HOME_PAGE_NAME });
// tag
let tag = tagNav.value.find((e) => e.menuName === name);
if (!tag) {
router.push({ name: HOME_PAGE_NAME });
return;
}
// router.push({ name, query: Object.assign({ _keepAlive: 1 }, tag.menuQuery) });
router.push({ name, query: tag.menuQuery });
}
// tag closeTagNav
useUserStore().closeTagNav(item ? item.menuName : null, closeAll);
}
const { useToken } = theme;
const { token } = useToken();
const borderRadius = 8 + 'px';
//
function closeByMenu(closeAll) {
let find = tagNav.value.find((e) => e.menuName === selectedKey.value);
if (!find || closeAll) {
closeTag(null, true);
} else {
closeTag(find, true);
}
}
//
function closeTag(item, closeAll) {
// tag
if (item && !closeAll) {
let goName = HOME_PAGE_NAME;
let goQuery = undefined;
if (item.fromMenuName && item.fromMenuName !== item.menuName && tagNav.value.some((e) => e.menuName === item.fromMenuName)) {
goName = item.fromMenuName;
goQuery = item.fromMenuQuery;
} else {
// tag
let index = tagNav.value.findIndex((e) => e.menuName === item.menuName);
if (index > 0) {
// tag
let leftTagNav = tagNav.value[index - 1];
goName = leftTagNav.menuName;
goQuery = leftTagNav.menuQuery;
}
}
// router.push({ name: goName, query: Object.assign({ _keepAlive: 1 }, goQuery) });
router.push({ name: goName, query: goQuery });
} else if (!item && closeAll) {
// tag
router.push({ name: HOME_PAGE_NAME });
}
// tag closeTagNav
useUserStore().closeTagNav(item ? item.menuName : null, closeAll);
}
const { useToken } = theme;
const { token } = useToken();
const borderRadius = 8 + 'px';
</script>
<style scoped lang="less">
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
@color-primary-bg: v-bind('token.colorPrimaryBg');
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
background-color: #ffffff;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
color: #606266;
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
.smart-page-tag-operate-icon {
width: 20px;
height: 20px;
transition: all 1s;
transform-origin: 10px 20px;
}
.smart-page-tag-operate-icon:hover {
width: 20px;
height: 20px;
transform: rotate(360deg);
}
}
.smart-page-tag-operate:hover {
color: @color-primary;
}
.smart-page-tag {
position: relative;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: flex-end;
justify-content: space-between;
min-height: @page-tag-height;
padding-right: 20px;
padding-left: 20px;
user-select: none;
background: #fff;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-close {
margin-left: 5px;
font-size: 12px;
color: #666666;
}
/** 覆盖 ant design vue的 tabs 样式,变小一点 **/
:deep(.ant-tabs-nav) {
margin: 0;
// padding: 0 0 2px 0;
min-height: 35px;
min-width: 120px;
box-sizing: border-box;
}
:deep(.ant-tabs-nav::before) {
border-bottom: 1px solid #ffffff;
}
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
padding: 5px 18px 3px 24px;
border-radius: v-bind(borderRadius) v-bind(borderRadius) 0 0;
margin: 0 -10px;
&:nth-child(1) {
margin-left: 0 !important;
.smart-page-tag-operate-icon {
width: 20px;
height: 20px;
transition: all 1s;
transform-origin: 10px 20px;
}
&:nth-last-child(2) {
margin-right: 0 !important;
.smart-page-tag-operate-icon:hover {
width: 20px;
height: 20px;
transform: rotate(360deg);
}
}
.smart-page-tag-content {
display: inline-block;
min-width: 100px;
&::after {
content: '';
position: absolute;
width: 1px;
height: 16px;
position: absolute;
right: 9px;
z-index: -2;
top: 10px;
background: #eeeeee;
}
.smart-page-tag-icon{
margin-right:5px;
}
.smart-page-tag-operate:hover {
color: @color-primary;
}
:deep(.ant-tabs-tab-active) {
.smart-page-tag {
position: relative;
background-size: 60% 100%;
& + .ant-tabs-tab {
margin-left: -50px;
box-sizing: border-box;
display: flex;
align-content: center;
align-items: flex-end;
justify-content: space-between;
min-height: @page-tag-height;
padding-right: 20px;
padding-left: 20px;
user-select: none;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-close {
margin-left: 5px;
font-size: 12px;
color: #666666;
}
&::before {
content: '';
background: url(/@/assets/images/nav/active_bg2.svg) no-repeat left;
width: 50%;
height: 35px;
// background-size: 130%;
z-index: -1;
position: absolute;
left: -4px;
bottom: 0;
/** 覆盖 ant design vue的 tabs 样式,变小一点 **/
:deep(.ant-tabs-nav) {
margin: 0;
// padding: 0 0 2px 0;
min-height: 35px;
min-width: 120px;
box-sizing: border-box;
&::before{
border-bottom: none !important;
}
}
&::after {
content: '';
background: url(/@/assets/images/nav/active_bg2.svg) no-repeat left;
width: 50%;
height: 35px;
transform: scaleX(-1);
// background-size: 130%;
z-index: -1;
position: absolute;
right: -4px;
bottom: 0;
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
padding: 5px 18px 3px 24px;
border-radius: v-bind(borderRadius) v-bind(borderRadius) 0 0;
margin: 0 -10px;
&:nth-child(1) {
margin-left: 0 !important;
}
&:nth-last-child(2) {
margin-right: 0 !important;
}
}
.smart-page-tag-content {
&::before {
display: inline-block;
min-width: 100px;
&::after {
content: '';
position: absolute;
height: 35px;
background: #e9efff;
width: 60%;
left: 0;
right: 0;
// top: 0;
bottom: 0;
margin: auto;
z-index: -1;
width: 1px;
height: 16px;
position: absolute;
right: 9px;
z-index: -2;
top: 10px;
background: #eeeeee;
}
&::after {
display: none;
.smart-page-tag-icon {
margin-right: 5px;
}
}
.smart-page-tag-close {
color: @color-primary;
}
}
:deep(.ant-tabs-ink-bar) {
display: none;
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
&:not(.ant-tabs-tab-active) {
:deep(.ant-tabs-tab-active) {
position: relative;
background-size: 60% 100%;
& + .ant-tabs-tab {
margin-left: -50px;
}
&::before {
content: '';
background: url(/@/assets/images/nav/active_bg2_default.svg) no-repeat left;
mask: url(/@/assets/images/nav/active_bg2.svg) no-repeat left;
background-color: @color-primary-bg;
width: 50%;
height: 35px;
// background-size: 130%;
z-index: -2;
z-index: -1;
position: absolute;
left: -4px;
bottom: 0;
}
&::after {
content: '';
background: url(/@/assets/images/nav/active_bg2_default.svg) no-repeat left;
mask: url(/@/assets/images/nav/active_bg2.svg) no-repeat left;
background-color: @color-primary-bg;
width: 50%;
height: 35px;
transform: scaleX(-1);
@ -313,13 +269,13 @@ const borderRadius = 8 + 'px';
right: -4px;
bottom: 0;
}
.smart-page-tag-content {
color:rgba(0,0,0,.88);
&::before {
content: '';
position: absolute;
height: 35px;
background: #dee1e6;
background: @color-primary-bg;
width: 60%;
left: 0;
right: 0;
@ -328,15 +284,76 @@ const borderRadius = 8 + 'px';
margin: auto;
z-index: -1;
}
&::after {
display: none;
}
}
.smart-page-tag-close {
color: @color-primary;
}
}
.smart-page-tag-close {
color: @color-primary;
:deep(.ant-tabs-ink-bar) {
display: none;
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
&:not(.ant-tabs-tab-active) {
&::before {
content: '';
mask: url(/@/assets/images/nav/active_bg2_default.svg) no-repeat left;
background-color: @color-primary-bg;
width: 50%;
height: 35px;
// background-size: 130%;
z-index: -2;
position: absolute;
left: -4px;
bottom: 0;
}
&::after {
content: '';
mask: url(/@/assets/images/nav/active_bg2_default.svg) no-repeat left;
background-color: @color-primary-bg;
width: 50%;
height: 35px;
transform: scaleX(-1);
// background-size: 130%;
z-index: -1;
position: absolute;
right: -4px;
bottom: 0;
}
.smart-page-tag-content {
color: @color-primary;
&::before {
content: '';
position: absolute;
height: 35px;
background: @color-primary-bg;
width: 60%;
left: 0;
right: 0;
// top: 0;
bottom: 0;
margin: auto;
z-index: -1;
}
&::after {
display: none;
}
}
}
.smart-page-tag-close {
color: @color-primary;
}
}
}
}
</style>

View File

@ -9,16 +9,17 @@
-->
<template>
<!-- 标签页共两部分1标签 2标签操作区 -->
<a-row style="border-bottom: 1px solid #eeeeee; position: relative" v-show="pageTagFlag">
<a-row style=" position: relative" v-show="pageTagFlag">
<a-dropdown :trigger="['contextmenu']">
<div class="smart-page-tag">
<a-tabs style="width: 100%" :tab-position="mode" v-model:activeKey="selectedKey" size="small" @tabClick="selectTab">
<a-tab-pane v-for="item in tagNav" :key="item.menuName">
<template #tab>
<span>
<span class="smart-page-tag-span">
<home-outlined v-if="item.menuName === HOME_PAGE_NAME" class="smart-page-tag-close smart-page-tag-icon" />
<component class="smart-page-tag-icon" v-else :is="$antIcons[item.menuIcon]" />
{{ item.menuTitle }}
<close-outlined @click.stop="closeTag(item, false)" v-if="item.menuName !== HOME_PAGE_NAME" class="smart-page-tag-close" />
<home-outlined style="font-size: 12px" v-if="item.menuName === HOME_PAGE_NAME" class="smart-page-tag-close" />
</span>
</template>
</a-tab-pane>
@ -137,18 +138,17 @@
<style scoped lang="less">
@smart-page-tag-operate-width: 40px;
@color-primary: v-bind('token.colorPrimary');
@color-primary-bg: v-bind('token.colorPrimaryBg');
.smart-page-tag-operate {
width: @smart-page-tag-operate-width;
height: @smart-page-tag-operate-width;
background-color: #ffffff;
font-size: 17px;
text-align: center;
vertical-align: middle;
line-height: @smart-page-tag-operate-width;
padding-right: 10px;
cursor: pointer;
color: #606266;
.smart-page-tag-operate-icon {
width: 20px;
@ -168,6 +168,14 @@
color: @color-primary;
}
.smart-page-tag-span:first-child {
padding-right: 15px;
.smart-page-tag-icon {
font-size: 12px;
}
}
.smart-page-tag {
position: relative;
box-sizing: border-box;
@ -179,9 +187,12 @@
padding-right: 20px;
padding-left: 20px;
user-select: none;
background: #fff;
width: calc(100% - @smart-page-tag-operate-width);
.smart-page-tag-icon {
margin-right: 4px;
}
.smart-page-tag-close {
margin-left: 5px;
font-size: 10px;
@ -194,10 +205,9 @@
margin: 0;
padding: 0 0 2px 0;
max-height: 32px;
}
:deep(.ant-tabs-nav::before) {
border-bottom: 1px solid #ffffff;
&::before{
border-bottom: none !important;
}
}
:deep(.ant-tabs-small > .ant-tabs-nav .ant-tabs-tab) {
@ -207,13 +217,16 @@
}
:deep(.ant-tabs-tab-active) {
background-color: #eeeeee;
background-color: @color-primary-bg;
.smart-page-tag-close {
color: @color-primary;
}
}
:deep(.ant-tabs-nav .ant-tabs-tab:hover) {
background-color: #eeeeee;
background-color: @color-primary-bg;
.smart-page-tag-close {
color: @color-primary;
}

View File

@ -8,7 +8,7 @@
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div id="smartAdminPageTag">
<div id="smartAdminPageTag" class="page-tag-div">
<DefaultTab v-if="pageTagStyle === PAGE_TAG_ENUM.DEFAULT.value" />
<AntdTab v-if="pageTagStyle === PAGE_TAG_ENUM.ANTD.value" />
<ChromeTab v-if="pageTagStyle === PAGE_TAG_ENUM.CHROME.value" />
@ -22,6 +22,15 @@
import AntdTab from './components/antd-tab.vue';
import ChromeTab from './components/chrome-tab.vue';
import { PAGE_TAG_ENUM } from '/@/constants/layout-const.js';
import { theme } from 'ant-design-vue';
const pageTagStyle = computed(() => useAppConfigStore().$state.pageTagStyle);
const { useToken } = theme;
const { token } = useToken();
</script>
<style lang="less" scoped>
@color-border-secondary: v-bind('token.colorBorderSecondary');
.page-tag-div{
border-bottom: 1px solid @color-border-secondary;
}
</style>

View File

@ -43,6 +43,7 @@
import _ from 'lodash';
import menuEmitter from './side-expand-menu-mitt';
import { useUserStore } from '/@/store/modules/system/user';
import { theme } from 'ant-design-vue';
//
let topMenu = ref({});
@ -83,12 +84,20 @@
}
defineExpose({ updateSelectKeyAndOpenKey });
const { useToken } = theme;
const { token } = useToken();
</script>
<style scoped lang="less">
@color-bg-container: v-bind('token.colorBgContainer');
@color-border-secondary: v-bind('token.colorBorderSecondary');
.recursion-container {
height: 100vh;
background: #ffffff;
background-color: @color-bg-container;
}
.recursion-container ::-webkit-scrollbar { width: 0 !important }
.bottom-menu{
overflow: auto;
display: flex;
@ -102,8 +111,7 @@
justify-content: center;
height: @header-user-height;
font-size: 16px;
color: #515a6e;
border-bottom: 1px solid #f3f3f3;
border-right: 1px solid #f3f3f3;
border-bottom: 1px solid @color-border-secondary;
border-right: 1px solid @color-border-secondary;
}
</style>

View File

@ -12,14 +12,13 @@
<!-- 顶部logo区域 -->
<div class="logo" @click="onGoHome" :style="sideMenuWidth" v-if="!collapsed">
<img class="logo-img" :src="logoImg" />
<div class="title smart-logo title-light" v-if="isLight">{{ websiteName }}</div>
<div class="title smart-logo title-dark" v-if="!isLight">{{ websiteName }}</div>
<div class="title" >{{ websiteName }}</div>
</div>
<div class="min-logo" @click="onGoHome" v-if="collapsed">
<img class="logo-img" :src="logoImg" />
</div>
<!-- 次级菜单展示 -->
<a-menu :selectedKeys="selectedKeys" :theme="theme" :openKeys="openKeys" mode="inline">
<a-menu :selectedKeys="selectedKeys" theme="light" :openKeys="openKeys" mode="inline">
<template v-for="item in topMenu.children" :key="item.menuId">
<template v-if="item.visibleFlag">
<template v-if="$lodash.isEmpty(item.children)">
@ -123,25 +122,25 @@
defineExpose({ updateSelectKeyAndOpenKey });
const isLight = computed(() => useAppConfigStore().$state.sideMenuTheme === 'light');
const color = computed(() => {
let isLight = useAppConfigStore().$state.sideMenuTheme === 'light';
return {
background: isLight ? '#FFFFFF' : '#001529',
};
const darkModeFlag = computed(() => useAppConfigStore().$state.darkModeFlag);
const logoHeight = computed(() => {
if(useAppConfigStore().$state.compactFlag){
return '40px';
}else{
return '46px';
}
});
</script>
<style scoped lang="less">
.recursion-container {
height: 100%;
background-color: v-bind('color.background');
}
.min-logo {
height: @header-user-height;
line-height: @header-user-height;
height: v-bind(logoHeight);
line-height: v-bind(logoHeight);
padding: 0px 15px 0px 15px;
// background-color: v-bind('color.background');
width: 80px;
z-index: 21;
@ -158,15 +157,15 @@
display: flex;
align-items: center;
justify-content: center;
height: @header-user-height;
height: v-bind(logoHeight);
font-size: 16px;
color: #515a6e;
border-bottom: 1px solid #f3f3f3;
border-right: 1px solid #f3f3f3;
}
.logo {
height: @header-user-height;
line-height: @header-user-height;
height: v-bind(logoHeight);
line-height: v-bind(logoHeight);
padding: 0px 15px 0px 15px;
width: 100%;
z-index: 100;
@ -174,6 +173,7 @@
justify-content: space-between;
align-items: center;
cursor: pointer;
background-color: #001529;
.logo-img {
width: 30px;
@ -186,7 +186,7 @@
overflow: hidden;
word-wrap: break-word;
white-space: nowrap;
color: v-bind('theme === "light" ? "#001529": "#ffffff"');
color: #ffffff;
}
}
</style>

View File

@ -10,7 +10,7 @@
<template>
<div class="top-menu-container">
<!-- 一级菜单展示 -->
<a-menu :selectedKeys="selectedKeys" mode="horizontal" :theme="theme">
<a-menu :selectedKeys="selectedKeys" mode="horizontal" theme="dark">
<template v-for="item in menuTree" :key="item.menuId">
<template v-if="item.visibleFlag">
<a-menu-item :key="item.menuId.toString()" @click="onSelectMenu(item)">
@ -29,13 +29,10 @@
import { computed, ref } from 'vue';
import { MENU_TYPE_ENUM } from '/@/constants/system/menu-const';
import { router } from '/@/router';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useUserStore } from '/@/store/modules/system/user';
import menuEmitter from './top-expand-menu-mitt';
const websiteName = computed(() => useAppConfigStore().websiteName);
const theme = computed(() => useAppConfigStore().$state.sideMenuTheme);
const menuTree = computed(() => useUserStore().getMenuTree || []);
const props = defineProps({
@ -108,15 +105,15 @@
width: v-bind('menuInfo.width');
flex-shrink: 0;
}
.ant-menu-dark {
background: #1677ff;
color: #fff;
}
.ant-menu-light {
background: #1677ff;
color: #fff;
}
:deep(.ant-menu-item-selected){
//.ant-menu-dark {
// background: #1677ff;
// color: #fff;
//}
//.ant-menu-light {
// background: #1677ff;
// color: #fff;
//}
:deep(.ant-menu-item-selected) {
background: #0958d9 !important;
color: #fff !important;
}
@ -145,7 +142,7 @@
overflow: hidden;
word-wrap: break-word;
white-space: nowrap;
color: v-bind('theme === "light" ? "#001529": "#ffffff"');
color: #ffffff;
}
}
</style>

View File

@ -48,6 +48,7 @@
import HeaderAvatar from '../header-user-space/header-avatar.vue';
import HeaderSetting from '../header-user-space/header-setting.vue';
import HeaderMessage from '../header-user-space/header-message.vue';
import { theme } from 'ant-design-vue';
//
const headerSetting = ref();
@ -96,9 +97,14 @@
function onGoHome() {
router.push({ name: HOME_PAGE_NAME });
}
const { useToken } = theme;
const { token } = useToken();
</script>
<style lang="less" scoped>
@color-border-secondary: v-bind('token.colorBorderSecondary');
.header-main {
display: flex;
flex-direction: row;
@ -107,7 +113,7 @@
padding-left: 16px;
height: 48px;
z-index: 21;
border-bottom: 1px solid rgb(238, 238, 238);
border-bottom: 1px solid @color-border-secondary;
.logo {
min-width: 192px;

View File

@ -105,6 +105,7 @@
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
import { theme as antDesignTheme } from 'ant-design-vue';
const appConfigStore = useAppConfigStore();
const windowHeight = ref(window.innerHeight);
@ -139,7 +140,7 @@
watch(
pageTagLocation,
(newVal) => {
if (newVal == 'top') {
if (newVal === 'top') {
nextTick(() => {
sizeComputed();
});
@ -200,8 +201,14 @@
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
const { useToken } = antDesignTheme;
const { token } = useToken();
</script>
<style scoped lang="less">
@color-bg-container: v-bind('token.colorBgContainer');
@color-border-secondary: v-bind('token.colorBorderSecondary');
:deep(.ant-layout-header) {
height: auto;
}
@ -210,14 +217,14 @@
}
.smart-layout-header {
background: #fff;
background: @color-bg-container;
padding: 0;
z-index: 21;
}
.smart-layout-header-user {
height: @header-user-height;
border-bottom: 1px solid #f6f6f6;
border-bottom: 1px solid @color-border-secondary;
}
.smart-layout-header-left {
@ -237,7 +244,7 @@
}
.home-button:hover {
background-color: #efefef;
background-color: @color-bg-container;
}
.location-breadcrumb {
@ -301,7 +308,7 @@
background-color: inherit;
min-height: auto;
position: relative;
padding: 10px 10px 0px 10px;
padding: 5px 10px 0px 10px;
height: calc(100% - v-bind(dueHeight) px);
overflow-x: hidden;
}

View File

@ -62,7 +62,11 @@
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)" style="height: 100%;" class="admin-content">
<div
v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)"
style="height: 100%"
class="admin-content"
>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
@ -94,7 +98,7 @@
</template>
<script setup>
import { computed, onMounted, ref, watch } from 'vue';
import { computed, onMounted, ref, watch, nextTick } from 'vue';
import { useAppConfigStore } from '../store/modules/system/app-config';
import HeaderUserSpace from './components/header-user-space/index.vue';
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
@ -109,7 +113,8 @@
import { useRouter } from 'vue-router';
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
import { nextTick } from 'vue';
import { theme as antDesignTheme } from 'ant-design-vue';
const appConfigStore = useAppConfigStore();
const windowHeight = ref(window.innerHeight);
@ -133,7 +138,7 @@
const pageTagLocation = computed(() => useAppConfigStore().$state.pageTagLocation);
//
const dueHeight = computed(() => {
let due = 40;
let due = 45;
if (useAppConfigStore().$state.pageTagFlag) {
due = due + 40;
}
@ -146,7 +151,7 @@
watch(
pageTagLocation,
(newVal) => {
if (newVal == 'top') {
if (newVal === 'top') {
nextTick(() => {
sizeComputed();
});
@ -158,6 +163,7 @@
);
const rightWidth = ref(0);
function sizeComputed() {
const tagParentElement = document.querySelector('.location-breadcrumb');
const tagsElement = tagParentElement.querySelector('.ant-tabs-nav-list');
@ -201,6 +207,7 @@
};
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
@ -211,9 +218,14 @@
// ----------------------- keep-alive -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
const { useToken } = antDesignTheme;
const { token } = useToken();
</script>
<style lang="less" scoped>
@color-border-secondary: v-bind('token.colorBorderSecondary');
@color-bg-container: v-bind('token.colorBgContainer');
:deep(.ant-layout-header) {
height: auto;
}
@ -223,14 +235,14 @@
}
.layout-header {
background: #fff;
background: @color-bg-container;
padding: 0;
z-index: 21;
}
.layout-header-user {
height: @header-user-height;
border-bottom: 1px solid #f6f6f6;
border-bottom: 1px solid @color-border-secondary;
}
.layout-header-left {
@ -242,6 +254,7 @@
overflow: hidden;
display: flex;
}
.collapsed-button {
margin-left: 10px;
line-height: @header-user-height;
@ -255,7 +268,7 @@
}
.home-button:hover {
background-color: #efefef;
background-color: @color-bg-container;
}
.location-breadcrumb {
@ -280,6 +293,7 @@
height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
&.fixed-side {
position: fixed;
height: 100vh;
@ -291,10 +305,12 @@
.side-menu::-webkit-scrollbar {
width: 4px;
}
.side-menu::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
}
.side-menu::-webkit-scrollbar-track {
border-radius: 0;
background: rgba(0, 0, 0, 0.1);
@ -338,7 +354,7 @@
min-height: auto;
position: relative;
overflow-x: hidden;
padding: 10px 10px 0px 10px;
padding: 5px 10px 0px 10px;
height: calc(100% - v-bind(dueHeight) px);
}
}

View File

@ -10,7 +10,7 @@
<template>
<a-layout class="admin-layout" style="min-height: 100%">
<!-- 侧边菜单 side-menu -->
<a-layout-sider :theme="theme" class="side-menu" :collapsed="collapsed" :trigger="null">
<a-layout-sider theme="light" class="side-menu" :collapsed="collapsed" :trigger="null">
<!-- 左侧菜单 -->
<TopExpandMenu :collapsed="collapsed" />
</a-layout-sider>
@ -62,7 +62,11 @@
:url="item.meta.frameUrl"
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)" style="height: 100%;" class="admin-content">
<div
v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name != e.name)"
style="height: 100%"
class="admin-content"
>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
@ -71,7 +75,9 @@
</div>
</a-layout-content>
<!-- footer 版权公司信息 -->
<a-layout-footer class="smart-layout-footer" v-show="footerFlag"> <SmartFooter /></a-layout-footer>
<a-layout-footer class="smart-layout-footer" v-show="footerFlag">
<SmartFooter />
</a-layout-footer>
<!---- 回到顶部 --->
<a-back-top :target="backTopTarget" :visibilityHeight="80" />
</a-layout>
@ -96,6 +102,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 { theme as antDesignTheme } from 'ant-design-vue';
const windowHeight = ref(window.innerHeight);
@ -125,7 +132,7 @@
watch(
pageTagLocation,
(newVal) => {
if (newVal == 'top') {
if (newVal === 'top') {
nextTick(() => {
sizeComputed();
});
@ -136,6 +143,7 @@
}
);
const rightWidth = ref(0);
function sizeComputed() {
const tagParentElement = document.querySelector('.location-breadcrumb');
const tagsElement = tagParentElement.querySelector('.ant-tabs-nav-list');
@ -149,6 +157,7 @@
ro.observe(tagsElement);
ro.observe(parentElement);
}
//
const collapsed = ref(false);
@ -183,27 +192,36 @@
// ----------------------- keep-alive -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
const { useToken } = antDesignTheme;
const { token } = useToken();
console.log(33,token.value)
</script>
<style scoped lang="less">
@color-border-secondary: v-bind('token.colorBorderSecondary');
@color-bg-container: v-bind('token.colorBgContainer');
:deep(.ant-layout-header) {
height: auto;
}
:deep(.layout-header) {
height: auto;
}
.smart-layout-header {
background: #fff;
background: @color-bg-container;
padding: 0;
z-index: 21;
}
.smart-layout-header-user {
height: @header-user-height;
border-bottom: 1px solid #f6f6f6;
border-bottom: 1px solid @color-border-secondary;
}
.smart-layout-header-left {
@ -223,7 +241,7 @@
}
.home-button:hover {
background-color: #efefef;
background-color: @color-bg-container;
}
.location-breadcrumb {
@ -252,13 +270,16 @@
top: 0;
}
}
.side-menu::-webkit-scrollbar {
width: 4px;
}
.side-menu::-webkit-scrollbar-thumb {
border-radius: 10px;
background: rgba(0, 0, 0, 0.2);
}
.side-menu::-webkit-scrollbar-track {
border-radius: 0;
background: rgba(0, 0, 0, 0.1);
@ -270,6 +291,7 @@
height: 100vh;
max-width: 100px;
width: auto !important;
&.fixed-side {
position: fixed;
height: 100vh;
@ -300,7 +322,7 @@
background-color: inherit;
min-height: auto;
position: relative;
padding: 10px 10px 0px 10px;
padding: 5px 10px 0px 10px;
height: calc(100% - v-bind(dueHeight) px);
overflow-x: hidden;
}

View File

@ -9,9 +9,11 @@
<a-layout-content :id="LAYOUT_ELEMENT_IDS.content" class="admin-layout-content">
<!---标签页-->
<div class="page-tag-div" v-show="(pageTagFlag && !fullScreenFlag) || breadCrumbFlag" :id="LAYOUT_ELEMENT_IDS.header">
<MenuLocationBreadcrumb v-if="pageTagLocation !== 'top'" />
<PageTag />
</div>
<div class="bread-crumb-div" v-if="breadCrumbFlag">
<MenuLocationBreadcrumb />
</div>
<!--不keepAlive的iframe使用单个iframe组件-->
<IframeIndex v-if="iframeNotKeepAlivePageFlag" :key="route.name" :name="route.name" :url="route.meta.frameUrl" />
@ -26,7 +28,11 @@
/>
<!--非iframe使用router-view-->
<div v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name !== e.name)" :style="{height: contentBoxHeight+'px'}" class="admin-content">
<div
v-show="!iframeNotKeepAlivePageFlag && keepAliveIframePages.every((e) => route.name !== e.name)"
:style="{ height: contentBoxHeight + 'px' }"
class="admin-content"
>
<router-view v-slot="{ Component }">
<keep-alive :include="keepAliveIncludes">
<component :is="Component" :key="route.name" />
@ -58,6 +64,7 @@
import { HOME_PAGE_NAME } from '/@/constants/system/home-const';
import { LAYOUT_ELEMENT_IDS } from '/@/layout/layout-const.js';
import MenuLocationBreadcrumb from './components/menu-location-breadcrumb/index.vue';
import { theme as antDesignTheme } from 'ant-design-vue';
const windowHeight = ref(window.innerHeight);
//
@ -78,42 +85,48 @@
const footerFlag = computed(() => useAppConfigStore().$state.footerFlag);
//
const watermarkFlag = computed(() => useAppConfigStore().$state.watermarkFlag);
//
const pageTagLocation = computed(() => useAppConfigStore().$state.pageTagLocation);
//
const breadCrumbFlag = computed(() => useAppConfigStore().$state.breadCrumbFlag);
//
const pageWidth = computed(() => useAppConfigStore().$state.pageWidth);
let contentBoxHeight=ref()
//
const dueHeight = computed(() => {
if (fullScreenFlag.value) {
return '0';
let contentBoxHeight = ref();
//
const contentTop = computed(() => {
let due = 45;
let existComponentCount = 0;
if (useAppConfigStore().$state.pageTagFlag) {
existComponentCount++;
}
let due = '45px';
if (useAppConfigStore().$state.pageTagFlag || useAppConfigStore().$state.breadCrumbFlag) {
due = '85px';
if (useAppConfigStore().$state.breadCrumbFlag) {
existComponentCount++;
due = 40;
}
if (
useAppConfigStore().$state.pageTagFlag &&
useAppConfigStore().$state.pageTagLocation === 'center' &&
useAppConfigStore().$state.breadCrumbFlag
) {
due = '125px';
}
return due;
return due + existComponentCount * 40 + 'px';
});
watch(() => dueHeight.value, () => {
let dom=document.querySelector('.admin-layout-content')
contentBoxHeight.value=dom.offsetHeight - 20 - dueHeight.value.split('px')[0]
//
const breadCrumbTop = computed(() => {
if (useAppConfigStore().$state.pageTagFlag) {
return '88px';
} else {
return '45px';
}
});
watch(
() => contentTop.value,
() => {
let dom = document.querySelector('.admin-layout-content');
contentBoxHeight.value = dom.offsetHeight - 20 - contentTop.value.split('px')[0];
}
);
onMounted(() => {
let dom=document.querySelector('.admin-layout-content')
contentBoxHeight.value=dom.offsetHeight - 20 - dueHeight.value.split('px')[0]
let dom = document.querySelector('.admin-layout-content');
contentBoxHeight.value = dom.offsetHeight - 20 - contentTop.value.split('px')[0];
});
//
onMounted(() => {
if (watermarkFlag.value) {
@ -140,6 +153,7 @@
};
const router = useRouter();
function goHome() {
router.push({ name: HOME_PAGE_NAME });
}
@ -150,9 +164,14 @@
// ----------------------- keep-alive -----------------------
let { route, keepAliveIncludes, iframeNotKeepAlivePageFlag, keepAliveIframePages } = smartKeepAlive();
const { useToken } = antDesignTheme;
const { token } = useToken();
</script>
<style lang="less" scoped>
@color-border-secondary: v-bind('token.colorBorderSecondary');
@color-bg-container: v-bind('token.colorBgContainer');
.admin-layout {
min-height: 100%;
@ -174,11 +193,12 @@
overflow-x: hidden;
padding: 10px 0;
width: v-bind(pageWidth);
margin-top: v-bind(dueHeight);
margin-top: v-bind(contentTop);
margin-left: auto;
margin-right: auto;
.page-tag-div {
background: @color-bg-container;
position: fixed;
top: 48px;
width: v-bind(pageWidth);
@ -186,6 +206,15 @@
line-height: 40px;
z-index: 3;
}
.bread-crumb-div {
position: fixed;
top: v-bind(breadCrumbTop);
width: v-bind(pageWidth);
height: 40px;
line-height: 40px;
z-index: 3;
}
}
}

View File

@ -30,8 +30,6 @@ 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 { Table } from 'ant-design-vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import '/@/utils/ployfill';
import { useDictStore } from '/@/store/modules/system/dict.js';
import { dictApi } from '/@/api/support/dict-api.js';

View File

@ -39,7 +39,7 @@ export const useDictStore = defineStore({
// 是数字的话,需要特殊处理
if (_.isNumber(dataValue)) {
let target = _.find(dict, { dataValue: dataValue });
let target = _.find(dict, { dataValue: String(dataValue) });
return target ? target.dataLabel : '';
}

View File

@ -204,6 +204,7 @@ export const useUserStore = defineStore({
// @ts-ignore
findTag.fromMenuName = from.name;
findTag.fromMenuQuery = from.query;
findTag.menuQuery = route.query;
} else {
// @ts-ignore
this.tagNav.push({
@ -212,7 +213,7 @@ export const useUserStore = defineStore({
// @ts-ignore
menuTitle: route.meta.title,
menuQuery: route.query,
menuIcon:route.meta?.icon,
menuIcon:route.meta.icon,
// @ts-ignore
fromMenuName: from.name,
fromMenuQuery: from.query,

View File

@ -65,11 +65,6 @@
margin-bottom: 10px;
}
/******************************** 查询表格样式 ********************************/
.smart-query-form {
background-color: #ffffff;
padding: 5px 10px;
margin-bottom: 10px;
}
.smart-table-operate {
.ant-btn {

View File

@ -8,7 +8,7 @@
* @Copyright 1024创新实验室 https://1024lab.net Since 2012
-->
<template>
<div class="detail-header">
<div class="smart-detail-header">
<a-page-header :title="detail.enterpriseName" :avatar="{ src: logo }">
<template #extra>
<a-button @click="showUpdate" type="primary">编辑</a-button>
@ -126,9 +126,3 @@
});
</script>
<style lang="less" scoped>
.detail-header {
background-color: #fff;
padding: 10px;
}
</style>

View File

@ -136,7 +136,7 @@
</template>
<script setup>
import { message } from 'ant-design-vue';
import { message, theme } from 'ant-design-vue';
import dayjs from 'dayjs';
import _ from 'lodash';
import { computed, inject, reactive, ref } from 'vue';
@ -279,9 +279,13 @@
'常量类:' + formData.moduleName + 'Const.java', //
];
});
const { useToken } = theme;
const { token } = useToken();
</script>
<style lang="less" scoped>
@color-fill-tertiary: v-bind('token.colorFillTertiary');
.preview-title {
font-weight: 600;
margin: 5px 0px;
@ -289,7 +293,7 @@
.preview-block {
font-size: 14px;
background-color: #f9f9f9;
padding: 10px 5px;
background-color: @color-fill-tertiary;
}
</style>

View File

@ -122,7 +122,6 @@
.preview-block {
font-size: 14px;
background-color: #f9f9f9;
padding: 10px 5px;
}
</style>

View File

@ -182,7 +182,6 @@
.preview-block {
font-size: 12px;
background-color: #f9f9f9;
padding: 10px 5px;
}

View File

@ -21,7 +21,7 @@
<a-input-number style="width: 100%" v-model:value="form.sortOrder" :min="0" :max="1000" />
</a-form-item>
<a-form-item label="备注" name="remark">
<a-textarea v-model="form.remark" style="width: 100%; height: 100px; outline: none" />
<a-textarea v-model:value="form.remark" style="width: 100%; height: 100px; outline: none" />
</a-form-item>
</a-form>
</a-modal>

Some files were not shown because too many files have changed in this diff Show More