Compare commits

...

8 Commits

Author SHA1 Message Date
CoderKK
1e1fe131da
Pre Merge pull request !86 from CoderKK/master 2025-08-10 05:02:09 +00:00
zhuoda
d2c55e35ff v3.26.0 【优化】分页请求2次;【优化】菜单展开单个代码优化;【优化】操作记录返回结果;【优化】json viewer升级;【优化】S3协议优化;【优化】代码生成字典优化; 2025-08-10 13:02:01 +08: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
236 changed files with 2707 additions and 2021 deletions

View File

@ -4,6 +4,8 @@
**<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</font>** 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。 **<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">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>** 同时 **<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> </modules>
<properties> <properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<springboot.version>3.3.1</springboot.version> <java.version>17</java.version>
<spring-mock.version>2.0.8</spring-mock.version> <springboot.version>3.5.4</springboot.version>
<spring-security-crypto.version>6.4.3</spring-security-crypto.version> <spring-security-crypto.version>6.5.1</spring-security-crypto.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version> <mybatis-plus.version>3.5.12</mybatis-plus.version>
<p6spy.version>3.9.1</p6spy.version> <p6spy.version>3.9.1</p6spy.version>
<knife4j.version>4.4.0</knife4j.version> <springdoc-openapi.version>2.8.9</springdoc-openapi.version>
<fastjson.version>2.0.52</fastjson.version> <knife4j.version>4.6.0</knife4j.version>
<druid.version>1.2.23</druid.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-linkedhashmap.version>1.4.2</google-linkedhashmap.version>
<google-guava.version>20.0</google-guava.version> <google-guava.version>20.0</google-guava.version>
<reflections.version>0.9.11</reflections.version> <reflections.version>0.10.2</reflections.version>
<commons-io.version>2.15.0</commons-io.version> <commons-io.version>2.19.0</commons-io.version>
<commons-lang3.version>3.12.0</commons-lang3.version> <commons-lang3.version>3.18.0</commons-lang3.version>
<commons-collections4.version>4.4</commons-collections4.version> <commons-collections4.version>4.5.0</commons-collections4.version>
<commons-compress.version>1.26.0</commons-compress.version> <commons-compress.version>1.27.1</commons-compress.version>
<commons-codec.version>1.13</commons-codec.version> <commons-codec.version>1.18.0</commons-codec.version>
<commons-text.version>1.9</commons-text.version> <commons-text.version>1.13.1</commons-text.version>
<xerces.version>2.12.0</xerces.version> <fast-excel.version>1.2.0</fast-excel.version>
<fast-excel.version>1.0.0</fast-excel.version> <poi.version>5.4.1</poi.version>
<poi.version>5.2.4</poi.version> <awssdk-s3.version>2.31.78</awssdk-s3.version>
<ooxml-schemas.version>1.4</ooxml-schemas.version> <mysql-connector-j.version>9.3.0</mysql-connector-j.version>
<aws-java-sdk.version>1.11.842</aws-java-sdk.version> <hutool.version>5.8.39</hutool.version>
<log4j-spring-boot.version>2.23.1</log4j-spring-boot.version> <velocity-engine-core.version>2.4.1</velocity-engine-core.version>
<hutool.version>5.8.29</hutool.version>
<velocity-engine-core.version>2.3</velocity-engine-core.version>
<velocity-tools.version>3.1</velocity-tools.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> <ip2region.version>2.7.0</ip2region.version>
<bcprov.version>1.80</bcprov.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> <smartdb.version>1.2.0</smartdb.version>
<redisson.version>3.25.0</redisson.version> <redisson.version>3.50.0</redisson.version>
<snakeyaml.version>2.2</snakeyaml.version> <snakeyaml.version>2.4</snakeyaml.version>
<freemarker.version>2.3.33</freemarker.version> <freemarker.version>2.3.34</freemarker.version>
<jsoup.version>1.18.1</jsoup.version> <jsoup.version>1.21.1</jsoup.version>
<tika.version>3.1.0</tika.version> <tika.version>3.1.0</tika.version>
<httpcomponents.version>5.5</httpcomponents.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!--BOM begin--> <!--SpringBoot BOM begin-->
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId> <artifactId>spring-boot-dependencies</artifactId>
@ -69,19 +66,16 @@
<type>pom</type> <type>pom</type>
<scope>import</scope> <scope>import</scope>
</dependency> </dependency>
<!--BOM end--> <!--SpringBoot BOM end-->
<!--mybatis-plus BOM begin-->
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId> <artifactId>mybatis-plus-bom</artifactId>
<version>${mybatis-plus.version}</version> <version>${mybatis-plus.version}</version>
<exclusions> <type>pom</type>
<exclusion> <scope>import</scope>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency> </dependency>
<!--mybatis-plus BOM end-->
<dependency> <dependency>
<groupId>org.springframework.security</groupId> <groupId>org.springframework.security</groupId>
@ -89,6 +83,12 @@
<version>${spring-security-crypto.version}</version> <version>${spring-security-crypto.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>${mysql-connector-j.version}</version>
</dependency>
<dependency> <dependency>
<groupId>p6spy</groupId> <groupId>p6spy</groupId>
<artifactId>p6spy</artifactId> <artifactId>p6spy</artifactId>
@ -96,7 +96,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <groupId>com.github.xingfudeshi</groupId>
<artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId> <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
<version>${knife4j.version}</version> <version>${knife4j.version}</version>
</dependency> </dependency>
@ -107,7 +107,6 @@
<version>${fastjson.version}</version> <version>${fastjson.version}</version>
</dependency> </dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId> <artifactId>druid-spring-boot-3-starter</artifactId>
@ -169,9 +168,9 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>aws-java-sdk-s3</artifactId> <artifactId>s3</artifactId>
<version>${aws-java-sdk.version}</version> <version>${awssdk-s3.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<artifactId>commons-logging</artifactId> <artifactId>commons-logging</artifactId>
@ -217,6 +216,7 @@
<artifactId>sa-token-redis-jackson</artifactId> <artifactId>sa-token-redis-jackson</artifactId>
<version>${sa-token.version}</version> <version>${sa-token.version}</version>
</dependency> </dependency>
<!-- sa-token end --> <!-- sa-token end -->
<!--ip 地址--> <!--ip 地址-->
@ -254,12 +254,6 @@
<version>${poi.version}</version> <version>${poi.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>${poi.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>
<artifactId>poi-scratchpad</artifactId> <artifactId>poi-scratchpad</artifactId>
@ -268,20 +262,8 @@
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId> <artifactId>poi-ooxml-full</artifactId>
<version>${ooxml-schemas.version}</version> <version>${poi.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>
</dependency> </dependency>
<dependency> <dependency>
@ -295,6 +277,25 @@
<groupId>org.redisson</groupId> <groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId> <artifactId>redisson-spring-boot-starter</artifactId>
<version>${redisson.version}</version> <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>
<dependency> <dependency>
@ -321,6 +322,12 @@
<version>${tika.version}</version> <version>${tika.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpcomponents.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>

View File

@ -67,7 +67,8 @@ public class AdminInterceptor implements HandlerInterceptor {
Method method = ((HandlerMethod) handler).getMethod(); Method method = ((HandlerMethod) handler).getMethod();
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class); NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) { if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
SmartRequestUtil.setRequestUser(requestEmployee);
return true; return true;
} }
@ -76,8 +77,8 @@ public class AdminInterceptor implements HandlerInterceptor {
return false; return false;
} }
// 检测token 活跃频率 // 更新活跃
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
// --------------- 第三步 校验 权限 --------------- // --------------- 第三步 校验 权限 ---------------
@ -122,15 +123,12 @@ public class AdminInterceptor implements HandlerInterceptor {
/** /**
* 检测token 最低活跃频率单位如果 token 超过此时间没有访问系统就会被冻结 * 更新活跃时间
*/ */
private void checkActiveTimeout(RequestEmployee requestEmployee) { private void updateActiveTimeout(RequestEmployee requestEmployee) {
// 用户不在线也不用检测
if (requestEmployee == null) { if (requestEmployee == null) {
return; return;
} }
StpUtil.checkActiveTimeout();
StpUtil.updateLastActiveToNow(); StpUtil.updateLastActiveToNow();
} }

View File

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

View File

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

View File

@ -138,16 +138,20 @@ public class EmployeeService {
} }
EmployeeEntity entity = SmartBeanUtil.copy(employeeAddForm, EmployeeEntity.class); 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(); String randomPassword = securityPasswordService.randomPassword();
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(password)); String generateSaltPassword = this.generateSaltPassword(randomPassword, employeeUid);
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(generateSaltPassword));
// 保存数据 // 保存数据
entity.setDeletedFlag(Boolean.FALSE); entity.setDeletedFlag(Boolean.FALSE);
employeeManager.saveEmployee(entity, employeeAddForm.getRoleIdList()); 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) { public ResponseDTO<String> updateAvatar(EmployeeUpdateAvatarForm employeeUpdateAvatarForm) {
Long employeeId = employeeUpdateAvatarForm.getEmployeeId(); 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("原密码有误,请重新输入"); return ResponseDTO.userErrorParam("原密码有误,请重新输入");
} }
// 新旧密码相同 // 新旧密码相同
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword()) ){ if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword())) {
return ResponseDTO.userErrorParam("新密码与原始密码相同,请重新输入"); 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()) { if (!passwordRepeatTimes.getOk()) {
return ResponseDTO.error(passwordRepeatTimes); return ResponseDTO.error(passwordRepeatTimes);
} }
// 更新密码 // 更新密码
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getNewPassword()); String newEncryptPassword = SecurityPasswordService.getEncryptPwd(this.generateSaltPassword(updatePasswordForm.getNewPassword(), employeeEntity.getEmployeeUid()));
EmployeeEntity updateEntity = new EmployeeEntity(); EmployeeEntity updateEntity = new EmployeeEntity();
updateEntity.setEmployeeId(employeeId); updateEntity.setEmployeeId(employeeId);
updateEntity.setLoginPwd(newEncryptPassword); updateEntity.setLoginPwd(newEncryptPassword);
@ -405,8 +407,14 @@ public class EmployeeService {
* 重置密码 * 重置密码
*/ */
public ResponseDTO<String> resetPassword(Long employeeId) { 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(); 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); return ResponseDTO.ok(password);
} }
@ -426,4 +434,14 @@ public class EmployeeService {
return employeeDao.getByLoginName(loginName, false); 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.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; 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.domain.entity.EmployeeEntity;
import net.lab1024.sa.admin.module.system.employee.service.EmployeeService; import net.lab1024.sa.admin.module.system.employee.service.EmployeeService;
import net.lab1024.sa.admin.module.system.login.domain.LoginForm; 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.captcha.domain.CaptchaVO;
import net.lab1024.sa.base.module.support.config.ConfigKeyEnum; 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.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.LoginLogResultEnum;
import net.lab1024.sa.base.module.support.loginlog.LoginLogService; import net.lab1024.sa.base.module.support.loginlog.LoginLogService;
import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogEntity; import net.lab1024.sa.base.module.support.loginlog.domain.LoginLogEntity;
@ -79,9 +77,6 @@ public class LoginService implements StpInterface {
@Resource @Resource
private EmployeeService employeeService; private EmployeeService employeeService;
@Resource
private DepartmentService departmentService;
@Resource @Resource
private CaptchaService captchaService; private CaptchaService captchaService;
@ -103,9 +98,6 @@ public class LoginService implements StpInterface {
@Resource @Resource
private SecurityPasswordService protectPasswordService; private SecurityPasswordService protectPasswordService;
@Resource
private IFileStorageService fileStorageService;
@Resource @Resource
private ApiEncryptService apiEncryptService; 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); saveLoginLog(employeeEntity, ip, userAgent, "密码错误", LoginLogResultEnum.LOGIN_FAIL, loginDeviceEnum);
// 记录等级保护次数 // 记录等级保护次数

View File

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

View File

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

View File

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

View File

@ -1,12 +1,5 @@
package net.lab1024.sa.base.config; 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 lombok.Data;
import net.lab1024.sa.base.module.support.file.service.FileStorageCloudServiceImpl; import net.lab1024.sa.base.module.support.file.service.FileStorageCloudServiceImpl;
import net.lab1024.sa.base.module.support.file.service.FileStorageLocalServiceImpl; 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.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 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 @Configuration
public class FileConfig implements WebMvcConfigurer { 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_CLOUD = "cloud";
private static final String MODE_LOCAL = "local"; private static final String MODE_LOCAL = "local";
@ -69,15 +73,17 @@ public class FileConfig implements WebMvcConfigurer {
*/ */
@Bean @Bean
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD) @ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
public AmazonS3 initAmazonS3() { public S3Client initAmazonS3() {
ClientConfiguration clientConfig = new ClientConfiguration(); return S3Client.builder()
clientConfig.setProtocol(Protocol.HTTPS); .region(Region.AWS_GLOBAL)
return AmazonS3ClientBuilder.standard() .endpointOverride(URI.create((urlPrefix.startsWith(HTTPS) ? HTTPS : HTTP) + endpoint))
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey))) .credentialsProvider(
.withClientConfiguration(clientConfig) StaticCredentialsProvider.create(
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region)) AwsBasicCredentials.create(accessKey, secretKey)))
.withPathStyleAccessEnabled(false) .serviceConfiguration(S3Configuration.builder()
.withChunkedEncodingDisabled(true) .pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false)
.build())
.build(); .build();
} }

View File

@ -1,13 +1,14 @@
package net.lab1024.sa.base.config; package net.lab1024.sa.base.config;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import net.lab1024.sa.base.common.constant.StringConst; import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.util.SmartRequestUtil; 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.RepeatSubmitAspect;
import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitRedisTicket; import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitRedisTicket;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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 { public class RepeatSubmitConfig {
@Resource @Resource
private ValueOperations<String, String> valueOperations; private RedisTemplate<String, Object> redisTemplate;
@Bean @Bean
public RepeatSubmitAspect repeatSubmitAspect() { public RepeatSubmitAspect repeatSubmitAspect() {
RepeatSubmitRedisTicket caffeineTicket = new RepeatSubmitRedisTicket(valueOperations, this::ticket); RepeatSubmitRedisTicket ticket = new RepeatSubmitRedisTicket(redisTemplate, this::ticket);
return new RepeatSubmitAspect(caffeineTicket); return new RepeatSubmitAspect(ticket);
} }
/** /**
* 获取指明某个用户的凭证 * 获取指明某个用户的凭证
*/ */
private String ticket(String servletPath) { private String ticket(HttpServletRequest request) {
Long userId = SmartRequestUtil.getRequestUserId(); Long userId = SmartRequestUtil.getRequestUserId();
if (null == userId) { if (null == userId) {
return StringConst.EMPTY; 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.common.swagger.SmartOperationCustomizer;
import net.lab1024.sa.base.constant.SwaggerTagConst; import net.lab1024.sa.base.constant.SwaggerTagConst;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
import org.springdoc.core.customizers.OpenApiBuilderCustomizer; import org.springdoc.core.customizers.OpenApiBuilderCustomizer;
import org.springdoc.core.customizers.ServerBaseUrlCustomizer; import org.springdoc.core.customizers.ServerBaseUrlCustomizer;
import org.springdoc.core.models.GroupedOpenApi; 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.Bean;
import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import java.util.List; import java.util.List;
import java.util.Optional; 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)); .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 @Bean
public GroupedOpenApi businessApi() { public GroupedOpenApi businessApi() {
return GroupedOpenApi.builder() return GroupedOpenApi.builder()
@ -122,11 +140,11 @@ public class SwaggerConfig {
Optional<JavadocProvider> javadocProvider) { Optional<JavadocProvider> javadocProvider) {
List<ServerBaseUrlCustomizer> list = Lists.newArrayList(new ServerBaseUrlCustomizer() { List<ServerBaseUrlCustomizer> list = Lists.newArrayList(new ServerBaseUrlCustomizer() {
@Override @Override
public String customize(String baseUrl) { public String customize(String serverBaseUrl, HttpRequest request) {
if (StringUtils.isNotBlank(serverBaseUrl)) { if (StringUtils.isNotBlank(serverBaseUrl)) {
return serverBaseUrl; return serverBaseUrl;
} }
return baseUrl; return serverBaseUrl;
} }
}); });
return new OpenAPIService(openAPI, securityParser, springDocConfigProperties, return new OpenAPIService(openAPI, securityParser, springDocConfigProperties,

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.StringConst; import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.domain.RequestUser; import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartIpUtil; import net.lab1024.sa.base.common.util.SmartIpUtil;
import net.lab1024.sa.base.common.util.SmartRequestUtil; import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.module.support.operatelog.OperateLogDao; import net.lab1024.sa.base.module.support.operatelog.OperateLogDao;
@ -46,7 +47,7 @@ import java.util.concurrent.ThreadPoolExecutor;
* @Date 2021-12-08 20:48:52 * @Date 2021-12-08 20:48:52
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Slf4j @Slf4j
@Aspect @Aspect
@ -71,14 +72,14 @@ public abstract class OperateLogAspect {
public void logPointCut() { public void logPointCut() {
} }
@AfterReturning(pointcut = "logPointCut()") @AfterReturning(pointcut = "logPointCut()", returning = "responseDTO")
public void doAfterReturning(JoinPoint joinPoint) { public void doAfterReturning(JoinPoint joinPoint, Object responseDTO) {
handleLog(joinPoint, null); handleLog(joinPoint, null, responseDTO);
} }
@AfterThrowing(value = "logPointCut()", throwing = "e") @AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) { public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e); handleLog(joinPoint, e, null);
} }
/** /**
@ -109,16 +110,15 @@ public abstract class OperateLogAspect {
taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
} }
protected void handleLog(final JoinPoint joinPoint, final Exception e) { protected void handleLog(final JoinPoint joinPoint, final Exception e, Object responseDTO) {
try { try {
OperateLog operateLog = this.getAnnotationLog(joinPoint); OperateLog operateLog = this.getAnnotationLog(joinPoint);
if (operateLog == null) { if (operateLog == null) {
return; return;
} }
this.submitLog(joinPoint, e); this.submitLog(joinPoint, e, responseDTO);
} catch (Exception exp) { } catch (Exception exp) {
log.error("保存操作日志异常:{}", exp.getMessage()); log.error("保存操作日志异常:{}", exp.getMessage());
exp.printStackTrace();
} }
} }
@ -173,11 +173,8 @@ public abstract class OperateLogAspect {
/** /**
* 提交存储操作日志 * 提交存储操作日志
* *
* @param joinPoint
* @param e
* @throws Exception
*/ */
private void submitLog(final JoinPoint joinPoint, final Throwable e) throws Exception { private void submitLog(final JoinPoint joinPoint, final Throwable e, Object responseDTO) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//设置用户信息 //设置用户信息
RequestUser user = SmartRequestUtil.getRequestUser(); RequestUser user = SmartRequestUtil.getRequestUser();
@ -191,7 +188,7 @@ public abstract class OperateLogAspect {
String methodName = joinPoint.getSignature().getName(); String methodName = joinPoint.getSignature().getName();
String operateMethod = className + "." + methodName; String operateMethod = className + "." + methodName;
String failReason = null; String failReason = null;
Boolean successFlag = true; boolean successFlag = true;
if (e != null) { if (e != null) {
successFlag = false; successFlag = false;
failReason = getExceptionString(e); failReason = getExceptionString(e);
@ -210,15 +207,32 @@ public abstract class OperateLogAspect {
.userAgent(user.getUserAgent()) .userAgent(user.getUserAgent())
.failReason(failReason) .failReason(failReason)
.successFlag(successFlag).build(); .successFlag(successFlag).build();
Operation apiOperation = this.getApiOperation(joinPoint); Operation apiOperation = this.getApiOperation(joinPoint);
if (apiOperation != null) { if (apiOperation != null) {
operateLogEntity.setContent(apiOperation.summary()); operateLogEntity.setContent(apiOperation.summary());
} }
Tag api = this.getApi(joinPoint); Tag api = this.getApi(joinPoint);
if (api != null) { if (api != null) {
String name = api.name(); String name = api.name();
operateLogEntity.setModule(StrUtil.join(",", name)); operateLogEntity.setModule(StrUtil.join(",", name));
} }
// 处理返回值 ResponseDTO
if(responseDTO instanceof ResponseDTO) {
ResponseDTO response = (ResponseDTO) responseDTO;
ResponseDTO logResponseDTO = new ResponseDTO(
response.getCode(),
response.getLevel(),
response.getOk(),
response.getMsg(),
null
);
logResponseDTO.setDataType(response.getDataType());
operateLogEntity.setResponse(JSON.toJSONString(logResponseDTO));
}
taskExecutor.execute(() -> { taskExecutor.execute(() -> {
this.saveLog(operateLogEntity); this.saveLog(operateLogEntity);
}); });

View File

@ -71,6 +71,11 @@ public class OperateLogEntity {
*/ */
private String param; private String param;
/**
* 返回值
*/
private String response;
/** /**
* 客户ip * 客户ip
*/ */

View File

@ -47,6 +47,9 @@ public class OperateLogVO {
@Schema(description = "请求参数") @Schema(description = "请求参数")
private String param; private String param;
@Schema(description = "返回值")
private String response;
@Schema(description = "客户ip") @Schema(description = "客户ip")
private String ip; private String ip;

View File

@ -1,5 +1,6 @@
package net.lab1024.sa.base.module.support.repeatsubmit; package net.lab1024.sa.base.module.support.repeatsubmit;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.UserErrorCode; import net.lab1024.sa.base.common.code.UserErrorCode;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
@ -16,10 +17,19 @@ import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method; 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创新实验室: 胡克 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @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)") @Around("@annotation(net.lab1024.sa.base.module.support.repeatsubmit.annoation.RepeatSubmit)")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ticketToken = attributes.getRequest().getServletPath(); if (attributes == null) {
String ticket = this.repeatSubmitTicket.getTicket(ticketToken); return point.proceed();
}
/**
* 第一步生成防重复提交的 ticket凭证
* ticket 是根据 Request对象 自定义 生成的可以加入请求user相关属性作为生成要素
*/
HttpServletRequest request = attributes.getRequest();
String ticket = this.repeatSubmitTicket.generateTicket(request);
if (StringUtils.isEmpty(ticket)) { if (StringUtils.isEmpty(ticket)) {
return point.proceed(); return point.proceed();
} }
Method method = ((MethodSignature) point.getSignature()).getMethod(); Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
int limit = annotation.value(); Long intervalMilliSecond = (long) annotation.intervalMilliSecond();
// 获取上一次请求时间 /**
Long lastRequestTime = this.repeatSubmitTicket.getTicketTimestamp(ticket); * 第二步根据 ticket 凭证进行 加锁
// 校验是否限制时间内重复提交 * 能加锁则可以执行
if (lastRequestTime != null && System.currentTimeMillis() < lastRequestTime + limit) { * 若不能加锁则证明还是时间间隔interval中
*/
boolean lockSuccessFlag = this.repeatSubmitTicket.tryLock(ticket, System.currentTimeMillis(), intervalMilliSecond);
if (!lockSuccessFlag) {
return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT); return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT);
} }
// 执行
Object obj = null;
try { try {
// ticket 设置在执行中 return point.proceed();
this.repeatSubmitTicket.putTicket(ticket);
// 执行
obj = point.proceed();
} catch (Throwable throwable) { } catch (Throwable throwable) {
log.error("", throwable); log.error(throwable.getMessage(), throwable);
throw 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>
* 着重说明<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创新实验室: 胡克 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 20:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -20,8 +29,8 @@ import java.lang.annotation.Target;
public @interface RepeatSubmit { 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; package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import jakarta.servlet.http.HttpServletRequest;
import java.util.function.Function; import java.util.function.Function;
/** /**
* 凭证用于校验重复提交的东西 * 凭证用于校验重复提交的东西
* *
* @Author 1024创新实验室: 罗伊 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
public abstract class AbstractRepeatSubmitTicket { public abstract class AbstractRepeatSubmitTicket {
private final Function<String, String> ticketFunction; private final Function<HttpServletRequest, String> generateTicketFunction;
public AbstractRepeatSubmitTicket(Function<String, String> ticketFunction) { public AbstractRepeatSubmitTicket(Function<HttpServletRequest, String> generateTicketFunction) {
this.ticketFunction = ticketFunction; this.generateTicketFunction = generateTicketFunction;
} }
/** /**
* 获取凭证 * 生成 加锁的 凭证
*/ */
public String getTicket(String ticketToken) { public String generateTicket(HttpServletRequest request) {
return this.ticketFunction.apply(ticketToken); 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; 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; import java.util.function.Function;
/** /**
* 凭证redis实现 * 凭证redis实现
* *
* @Author 1024创新实验室: 罗伊 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket { public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket {
private final ValueOperations<String, String> redisValueOperations; private final RedisTemplate<String, Object> redisTemplate;
public RepeatSubmitRedisTicket(ValueOperations<String, String> redisValueOperations, public RepeatSubmitRedisTicket(RedisTemplate<String, Object> redisTemplate,
Function<String, String> ticketFunction) { Function<HttpServletRequest, String> ticketFunction) {
super(ticketFunction); super(ticketFunction);
this.redisValueOperations = redisValueOperations; this.redisTemplate = redisTemplate;
} }
@Override @Override
public Long getTicketTimestamp(String ticket) { public boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond) {
String ticketLastTime = redisValueOperations.get(ticket); if (intervalMilliSecond > 0) {
return ticketLastTime == null ? null : Long.valueOf(ticketLastTime); 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 @Override
public void putTicket(String ticket) { public void unLock(String ticket, Long intervalMilliSecond) {
redisValueOperations.getOperations().delete(ticket); if (intervalMilliSecond > 0) {
this.getTicketTimestamp(ticket); 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 lombok.extern.slf4j.Slf4j;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.apache.commons.io.IOUtils;
import org.apache.tika.config.TikaConfig; import org.apache.tika.config.TikaConfig;
import org.apache.tika.exception.TikaException; import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream; import org.apache.tika.io.TikaInputStream;
@ -14,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -35,27 +37,9 @@ public class SecurityFileService {
private Level3ProtectConfigService level3ProtectConfigService; private Level3ProtectConfigService level3ProtectConfigService;
// 定义白名单MIME类型 // 定义白名单MIME类型
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList( 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/*",
"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/*" // 图片类型 svg有安全隐患所以不使用"image/*"
"image/jpeg", "image/jpeg", "image/png", "image/gif", "image/bmp");
"image/png",
"image/gif",
"image/bmp"
);
/** /**
* 检测文件安全类型 * 检测文件安全类型
@ -73,8 +57,7 @@ public class SecurityFileService {
// 文件类型安全检测 // 文件类型安全检测
if (level3ProtectConfigService.isFileDetectFlag()) { if (level3ProtectConfigService.isFileDetectFlag()) {
String fileType = getFileMimeType(file); String fileType = getFileMimeType(file);
if (ALLOWED_MIME_TYPES.stream() if (ALLOWED_MIME_TYPES.stream().noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
.noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
return ResponseDTO.userErrorParam("禁止上传此文件类型"); return ResponseDTO.userErrorParam("禁止上传此文件类型");
} }
} }
@ -89,16 +72,20 @@ public class SecurityFileService {
* @return 文件的 MIME 类型 * @return 文件的 MIME 类型
*/ */
public static String getFileMimeType(MultipartFile file) { public static String getFileMimeType(MultipartFile file) {
InputStream inputStream = null;
try { try {
inputStream = file.getInputStream();
TikaConfig tika = new TikaConfig(); TikaConfig tika = new TikaConfig();
Metadata metadata = new Metadata(); Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename()); 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); MediaType mimetype = tika.getDetector().detect(stream, metadata);
return mimetype.toString(); return mimetype.toString();
} catch (IOException | TikaException e) { } catch (IOException | TikaException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return MimeTypes.OCTET_STREAM; return MimeTypes.OCTET_STREAM;
} finally {
IOUtils.closeQuietly(inputStream);
} }
} }

View File

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

View File

@ -27,7 +27,7 @@ import java.util.concurrent.ConcurrentHashMap;
* @Date 2022-03-25 21:46:07 * @Date 2022-03-25 21:46:07
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @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 { public abstract class SerialNumberBaseService implements SerialNumberService {
@ -37,7 +37,7 @@ public abstract class SerialNumberBaseService implements SerialNumberService {
@Resource @Resource
protected SerialNumberDao serialNumberDao; 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); 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; package net.lab1024.sa.base.module.support.serialnumber.service.impl;
import cn.hutool.core.util.RandomUtil;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; 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.constant.RedisKeyConst;
import net.lab1024.sa.base.module.support.redis.RedisService; 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.SerialNumberEntity;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberGenerateResultBO; 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.SerialNumberInfoBO;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberLastGenerateBO;
import net.lab1024.sa.base.module.support.serialnumber.service.SerialNumberBaseService; 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; import java.util.List;
/** /**
* 单据序列号 基于redis锁实现 * 单据序列号 基于redis key-value increase 实现
* *
* @Author 1024创新实验室-主任: 卓大 * @Author 1024创新实验室-主任: 卓大
* @Date 2022-03-25 21:46:07 * @Date 2025-08-03 22:46:07
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -25,86 +36,128 @@ import java.util.List;
@Slf4j @Slf4j
public class SerialNumberRedisService extends SerialNumberBaseService { public class SerialNumberRedisService extends SerialNumberBaseService {
private static final int MAX_GET_LOCK_COUNT = 5;
private static final long SLEEP_MILLISECONDS = 200L;
@Resource @Resource
private RedisService redisService; private RedisService redisService;
@Resource
private RedisTemplate redisTemplate;
@Override @Override
public void initLastGenerateData(List<SerialNumberEntity> serialNumberEntityList) { public void initLastGenerateData(List<SerialNumberEntity> serialNumberEntityList) {
if (serialNumberEntityList == null) { if (serialNumberEntityList == null) {
return; return;
} }
//删除之前的 // 设置redis的上次值
redisService.delete(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO);
for (SerialNumberEntity serialNumberEntity : serialNumberEntityList) { for (SerialNumberEntity serialNumberEntity : serialNumberEntityList) {
SerialNumberLastGenerateBO lastGenerateBO = SerialNumberLastGenerateBO if (serialNumberEntity.getLastTime() == null) {
.builder() continue;
.serialNumberId(serialNumberEntity.getSerialNumberId()) }
.lastNumber(serialNumberEntity.getLastNumber())
.lastTime(serialNumberEntity.getLastTime())
.build();
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO, String redisKey = generateRedisKeyByDate(serialNumberEntity.getSerialNumberId(),
String.valueOf(serialNumberEntity.getSerialNumberId()), SmartEnumUtil.getEnumByName(serialNumberEntity.getRuleType().toUpperCase(), SerialNumberRuleTypeEnum.class),
lastGenerateBO 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 @Override
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) { public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
SerialNumberGenerateResultBO serialNumberGenerateResult = null; // 根据步长计算 redis 增加值
String lockKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfo.getSerialNumberId(); ArrayList<Integer> list = new ArrayList<>(count);
int redisIncrease = 0;
boolean lock = false; for (int i = 0; i < count; i++) {
for (int i = 0; i < MAX_GET_LOCK_COUNT; i++) { int stepIncrease = 1;
try { Integer stepRandomRange = serialNumberInfo.getStepRandomRange();
lock = redisService.getLock(lockKey, 60 * 1000L); if (stepRandomRange > 1) {
if (lock) { stepIncrease = RandomUtil.getSecureRandom().nextInt(1, serialNumberInfo.getStepRandomRange() + 1);
break;
}
Thread.sleep(SLEEP_MILLISECONDS);
} catch (Throwable e) {
log.error(e.getMessage(), e);
} }
redisIncrease += stepIncrease;
list.add(stepIncrease);
} }
if (!lock) {
throw new BusinessException("SerialNumber 尝试5次未能生成单号");
}
try { try {
// 获取上次的生成结果 String redisKey = generateRedisKeyByDate(serialNumberInfo.getSerialNumberId(), serialNumberInfo.getSerialNumberRuleTypeEnum(), LocalDate.now());
SerialNumberLastGenerateBO lastGenerateBO = (SerialNumberLastGenerateBO) redisService.mget( Long increaseResult = redisTemplate.opsForValue().increment(redisKey, redisIncrease);
RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId())); 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(), serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(), serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime()); serialNumberGenerateResult.getLastTime());
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()), lastGenerateBO);
// 把生成过程保存到数据库里 // 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult); super.saveRecord(serialNumberGenerateResult);
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
} catch (Throwable e) { } catch (Throwable e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
throw 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

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

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

View File

@ -26,7 +26,7 @@
AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords})) AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords}))
</if> </if>
<if test="query.requestKeywords != null and query.requestKeywords != ''"> <if test="query.requestKeywords != null and query.requestKeywords != ''">
AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords})) AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}) OR INSTR(response,#{query.requestKeywords}))
</if> </if>
<if test="query.successFlag != null"> <if test="query.successFlag != null">
AND success_flag = #{query.successFlag} AND success_flag = #{query.successFlag}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import cn.dev33.satoken.annotation.SaIgnore;
import cn.dev33.satoken.exception.SaTokenException; import cn.dev33.satoken.exception.SaTokenException;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.strategy.SaAnnotationStrategy; import cn.dev33.satoken.strategy.SaAnnotationStrategy;
import cn.dev33.satoken.strategy.SaStrategy;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.admin.module.system.login.domain.RequestEmployee; import net.lab1024.sa.admin.module.system.login.domain.RequestEmployee;
import net.lab1024.sa.admin.module.system.login.service.LoginService; import net.lab1024.sa.admin.module.system.login.service.LoginService;
@ -68,7 +67,8 @@ public class AdminInterceptor implements HandlerInterceptor {
Method method = ((HandlerMethod) handler).getMethod(); Method method = ((HandlerMethod) handler).getMethod();
NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class); NoNeedLogin noNeedLogin = ((HandlerMethod) handler).getMethodAnnotation(NoNeedLogin.class);
if (noNeedLogin != null) { if (noNeedLogin != null) {
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
SmartRequestUtil.setRequestUser(requestEmployee);
return true; return true;
} }
@ -77,8 +77,8 @@ public class AdminInterceptor implements HandlerInterceptor {
return false; return false;
} }
// 检测token 活跃频率 // 更新活跃
checkActiveTimeout(requestEmployee); updateActiveTimeout(requestEmployee);
// --------------- 第三步 校验 权限 --------------- // --------------- 第三步 校验 权限 ---------------
@ -123,15 +123,12 @@ public class AdminInterceptor implements HandlerInterceptor {
/** /**
* 检测token 最低活跃频率单位如果 token 超过此时间没有访问系统就会被冻结 * 更新活跃时间
*/ */
private void checkActiveTimeout(RequestEmployee requestEmployee) { private void updateActiveTimeout(RequestEmployee requestEmployee) {
// 用户不在线也不用检测
if (requestEmployee == null) { if (requestEmployee == null) {
return; return;
} }
StpUtil.checkActiveTimeout();
StpUtil.updateLastActiveToNow(); StpUtil.updateLastActiveToNow();
} }

View File

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

View File

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

View File

@ -1,6 +1,7 @@
package net.lab1024.sa.admin.module.system.employee.service; package net.lab1024.sa.admin.module.system.employee.service;
import cn.dev33.satoken.stp.StpUtil; import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.lang.UUID;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import net.lab1024.sa.admin.module.system.department.dao.DepartmentDao; import net.lab1024.sa.admin.module.system.department.dao.DepartmentDao;
@ -138,16 +139,20 @@ public class EmployeeService {
} }
EmployeeEntity entity = SmartBeanUtil.copy(employeeAddForm, EmployeeEntity.class); EmployeeEntity entity = SmartBeanUtil.copy(employeeAddForm, EmployeeEntity.class);
// 员工uid
String employeeUid = UUID.randomUUID(true).toString(true);
entity.setEmployeeUid(employeeUid);
// 设置密码 默认密码 // 设置密码 随机密码
String password = securityPasswordService.randomPassword(); String randomPassword = securityPasswordService.randomPassword();
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(password)); String generateSaltPassword = this.generateSaltPassword(randomPassword, employeeUid);
entity.setLoginPwd(SecurityPasswordService.getEncryptPwd(generateSaltPassword));
// 保存数据 // 保存数据
entity.setDeletedFlag(Boolean.FALSE); entity.setDeletedFlag(Boolean.FALSE);
employeeManager.saveEmployee(entity, employeeAddForm.getRoleIdList()); 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) { public ResponseDTO<String> updateAvatar(EmployeeUpdateAvatarForm employeeUpdateAvatarForm) {
Long employeeId = employeeUpdateAvatarForm.getEmployeeId(); 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("原密码有误,请重新输入"); return ResponseDTO.userErrorParam("原密码有误,请重新输入");
} }
// 新旧密码相同 // 新旧密码相同
if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword()) ){ if (Objects.equals(updatePasswordForm.getOldPassword(), updatePasswordForm.getNewPassword())) {
return ResponseDTO.userErrorParam("新密码与原始密码相同,请重新输入"); 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()) { if (!passwordRepeatTimes.getOk()) {
return ResponseDTO.error(passwordRepeatTimes); return ResponseDTO.error(passwordRepeatTimes);
} }
// 更新密码 // 更新密码
String newEncryptPassword = SecurityPasswordService.getEncryptPwd(updatePasswordForm.getNewPassword()); String newEncryptPassword = SecurityPasswordService.getEncryptPwd(this.generateSaltPassword(updatePasswordForm.getNewPassword(), employeeEntity.getEmployeeUid()));
EmployeeEntity updateEntity = new EmployeeEntity(); EmployeeEntity updateEntity = new EmployeeEntity();
updateEntity.setEmployeeId(employeeId); updateEntity.setEmployeeId(employeeId);
updateEntity.setLoginPwd(newEncryptPassword); updateEntity.setLoginPwd(newEncryptPassword);
@ -405,8 +408,14 @@ public class EmployeeService {
* 重置密码 * 重置密码
*/ */
public ResponseDTO<String> resetPassword(Long employeeId) { 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(); 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); return ResponseDTO.ok(password);
} }
@ -426,4 +435,14 @@ public class EmployeeService {
return employeeDao.getByLoginName(loginName, false); 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); return ResponseDTO.ok(loginResult);
} }
@Operation(summary = "退出登 @author 卓大") @Operation(summary = "退出登 @author 卓大")
@GetMapping("/login/logout") @GetMapping("/login/logout")
public ResponseDTO<String> logout() { public ResponseDTO<String> logout() {
return loginService.logout(SmartRequestUtil.getRequestUser()); 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.core.util.RandomUtil;
import cn.hutool.extra.servlet.ServletUtil; import cn.hutool.extra.servlet.ServletUtil;
import lombok.extern.slf4j.Slf4j; 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.department.service.DepartmentService;
import net.lab1024.sa.admin.module.system.employee.domain.entity.EmployeeEntity; 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.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.Level3ProtectConfigService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityLoginService; import net.lab1024.sa.base.module.support.securityprotect.service.SecurityLoginService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityPasswordService; 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 org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -82,9 +79,6 @@ public class LoginService implements StpInterface {
@Resource @Resource
private EmployeeService employeeService; private EmployeeService employeeService;
@Resource
private DepartmentService departmentService;
@Resource @Resource
private CaptchaService captchaService; private CaptchaService captchaService;
@ -106,9 +100,6 @@ public class LoginService implements StpInterface {
@Resource @Resource
private SecurityPasswordService protectPasswordService; private SecurityPasswordService protectPasswordService;
@Resource
private IFileStorageService fileStorageService;
@Resource @Resource
private ApiEncryptService apiEncryptService; private ApiEncryptService apiEncryptService;
@ -132,7 +123,7 @@ public class LoginService implements StpInterface {
} }
/** /**
* 员工登 * 员工登
* *
* @return 返回用户登录信息 * @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); 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) { public RequestEmployee getLoginEmployee(String loginId, HttpServletRequest request) {
if (loginId == null) { 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 io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; 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 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.dao.RoleMenuDao;
import net.lab1024.sa.admin.module.system.role.domain.entity.RoleMenuEntity; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;

View File

@ -1,14 +1,13 @@
package net.lab1024.sa.admin.module.system.role.service; package net.lab1024.sa.admin.module.system.role.service;
import com.google.common.collect.Lists; 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.form.RoleDataScopeUpdateForm;
import net.lab1024.sa.admin.module.system.role.domain.vo.RoleDataScopeVO; 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.code.UserErrorCode;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartBeanUtil; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;

View File

@ -131,6 +131,17 @@
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId> <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>
<dependency> <dependency>
@ -179,8 +190,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.amazonaws</groupId> <groupId>software.amazon.awssdk</groupId>
<artifactId>aws-java-sdk-s3</artifactId> <artifactId>s3</artifactId>
</dependency> </dependency>
@ -250,17 +261,12 @@
<dependency> <dependency>
<groupId>org.apache.poi</groupId> <groupId>org.apache.poi</groupId>
<artifactId>ooxml-schemas</artifactId> <artifactId>poi-ooxml</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.fasterxml.jackson.datatype</groupId> <groupId>org.apache.poi</groupId>
<artifactId>jackson-datatype-jsr310</artifactId> <artifactId>poi-ooxml-full</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency> </dependency>
<dependency> <dependency>

View File

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

View File

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

View File

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

View File

@ -1,12 +1,5 @@
package net.lab1024.sa.base.config; 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 lombok.Data;
import net.lab1024.sa.base.module.support.file.service.FileStorageCloudServiceImpl; import net.lab1024.sa.base.module.support.file.service.FileStorageCloudServiceImpl;
import net.lab1024.sa.base.module.support.file.service.FileStorageLocalServiceImpl; 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.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 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 @Configuration
public class FileConfig implements WebMvcConfigurer { 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_CLOUD = "cloud";
private static final String MODE_LOCAL = "local"; private static final String MODE_LOCAL = "local";
@ -69,15 +73,17 @@ public class FileConfig implements WebMvcConfigurer {
*/ */
@Bean @Bean
@ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD) @ConditionalOnProperty(prefix = "file.storage", name = {"mode"}, havingValue = MODE_CLOUD)
public AmazonS3 initAmazonS3() { public S3Client initAmazonS3() {
ClientConfiguration clientConfig = new ClientConfiguration(); return S3Client.builder()
clientConfig.setProtocol(Protocol.HTTPS); .region(Region.AWS_GLOBAL)
return AmazonS3ClientBuilder.standard() .endpointOverride(URI.create((urlPrefix.startsWith(HTTPS) ? HTTPS : HTTP) + endpoint))
.withCredentials(new AWSStaticCredentialsProvider(new BasicAWSCredentials(accessKey, secretKey))) .credentialsProvider(
.withClientConfiguration(clientConfig) StaticCredentialsProvider.create(
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, region)) AwsBasicCredentials.create(accessKey, secretKey)))
.withPathStyleAccessEnabled(false) .serviceConfiguration(S3Configuration.builder()
.withChunkedEncodingDisabled(true) .pathStyleAccessEnabled(true)
.chunkedEncodingEnabled(false)
.build())
.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.constant.StringConst;
import net.lab1024.sa.base.common.util.SmartRequestUtil; 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.RepeatSubmitAspect;
import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitCaffeineTicket;
import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitRedisTicket; import net.lab1024.sa.base.module.support.repeatsubmit.ticket.RepeatSubmitRedisTicket;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; 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.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
/** /**
* 重复提交配置 * 重复提交配置
@ -24,22 +24,22 @@ import javax.annotation.Resource;
public class RepeatSubmitConfig { public class RepeatSubmitConfig {
@Resource @Resource
private ValueOperations<String, String> valueOperations; private RedisTemplate<String, Object> redisTemplate;
@Bean @Bean
public RepeatSubmitAspect repeatSubmitAspect() { public RepeatSubmitAspect repeatSubmitAspect() {
RepeatSubmitRedisTicket caffeineTicket = new RepeatSubmitRedisTicket(valueOperations, this::ticket); RepeatSubmitRedisTicket ticket = new RepeatSubmitRedisTicket(redisTemplate, this::ticket);
return new RepeatSubmitAspect(caffeineTicket); return new RepeatSubmitAspect(ticket);
} }
/** /**
* 获取指明某个用户的凭证 * 获取指明某个用户的凭证
*/ */
private String ticket(String servletPath) { private String ticket(HttpServletRequest request) {
Long userId = SmartRequestUtil.getRequestUserId(); Long userId = SmartRequestUtil.getRequestUserId();
if (null == userId) { if (null == userId) {
return StringConst.EMPTY; 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.code.ErrorCodeRegister;
import net.lab1024.sa.base.common.enumeration.SystemEnvironmentEnum; import net.lab1024.sa.base.common.enumeration.SystemEnvironmentEnum;
import net.lab1024.sa.base.common.util.SmartEnumUtil; 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.annotation.Value;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.boot.web.context.WebServerApplicationContext; import org.springframework.boot.web.context.WebServerApplicationContext;
import org.springframework.boot.web.context.WebServerInitializedEvent; import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.boot.web.server.WebServer;
import org.springframework.context.ApplicationListener; import org.springframework.context.ApplicationListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order; import org.springframework.core.annotation.Order;
import org.springframework.core.env.Environment; import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component; 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.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha; import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.img.ImgUtil; import cn.hutool.core.img.ImgUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil; import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j; 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.ResponseDTO;
import net.lab1024.sa.base.common.domain.SystemEnvironment; import net.lab1024.sa.base.common.domain.SystemEnvironment;
import net.lab1024.sa.base.constant.RedisKeyConst; import net.lab1024.sa.base.constant.RedisKeyConst;
@ -18,7 +18,6 @@ import org.springframework.stereotype.Service;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.awt.*; import java.awt.*;
import java.util.Objects; import java.util.Objects;
import java.util.UUID;
/** /**
* 图形验证码 服务 * 图形验证码 服务
@ -70,7 +69,7 @@ public class CaptchaService {
* 图片 base64格式 * 图片 base64格式
*/ */
// uuid 唯一标识 // uuid 唯一标识
String uuid = UUID.randomUUID().toString().replace("-", StringConst.EMPTY); String uuid = IdUtil.fastSimpleUUID();
CaptchaVO captchaVO = new CaptchaVO(); CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaUuid(uuid); captchaVO.setCaptchaUuid(uuid);

View File

@ -40,7 +40,7 @@ import java.util.Optional;
@Service @Service
public class CodeGeneratorService { 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"; private static final String COLUMN_PRIMARY_KEY = "PRI";
@ -65,7 +65,7 @@ public class CodeGeneratorService {
public List<TableColumnVO> getTableColumns(String tableName) { public List<TableColumnVO> getTableColumns(String tableName) {
List<TableColumnVO> tableColumns = codeGeneratorDao.selectTableColumn(tableName); List<TableColumnVO> tableColumns = codeGeneratorDao.selectTableColumn(tableName);
for (TableColumnVO tableColumn : tableColumns) { 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.setPrimaryKeyFlag(COLUMN_PRIMARY_KEY.equalsIgnoreCase(tableColumn.getColumnKey()));
tableColumn.setAutoIncreaseFlag(SmartStringUtil.isNotEmpty(tableColumn.getExtra()) && COLUMN_AUTO_INCREASE.equalsIgnoreCase(tableColumn.getExtra())); 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.date.DateUtil;
import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ZipUtil; import cn.hutool.core.util.ZipUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
@ -82,7 +83,7 @@ public class CodeGeneratorTemplateService {
} }
public void zipGeneratedFiles(OutputStream outputStream, String tableName, CodeGeneratorConfigEntity codeGeneratorConfigEntity) { public void zipGeneratedFiles(OutputStream outputStream, String tableName, CodeGeneratorConfigEntity codeGeneratorConfigEntity) {
String uuid = UUID.randomUUID().toString(); String uuid = IdUtil.fastSimpleUUID();
File dir = new File(uuid); File dir = new File(uuid);
// 1生产文件 // 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.CodeField;
import net.lab1024.sa.base.module.support.codegenerator.domain.model.CodeQueryField; import net.lab1024.sa.base.module.support.codegenerator.domain.model.CodeQueryField;
import net.lab1024.sa.base.module.support.codegenerator.service.variable.CodeGenerateBaseVariableService; 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 org.apache.commons.lang3.tuple.ImmutablePair;
import java.util.*; 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.FileDownloadVO;
import net.lab1024.sa.base.module.support.file.domain.vo.FileUploadVO; 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.file.domain.vo.FileVO;
import net.lab1024.sa.base.module.support.redis.RedisService;
import net.lab1024.sa.base.module.support.securityprotect.service.SecurityFileService; import net.lab1024.sa.base.module.support.securityprotect.service.SecurityFileService;
import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -55,9 +54,6 @@ public class FileService {
@Resource @Resource
private FileDao fileDao; private FileDao fileDao;
@Resource
private RedisService redisService;
@Resource @Resource
private SecurityFileService securityFileService; 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.DatePattern;
import cn.hutool.core.date.LocalDateTimeUtil; import cn.hutool.core.date.LocalDateTimeUtil;
import com.amazonaws.services.s3.AmazonS3; import cn.hutool.core.util.IdUtil;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.S3Object;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.code.SystemErrorCode; import net.lab1024.sa.base.common.code.SystemErrorCode;
import net.lab1024.sa.base.common.domain.ResponseDTO; 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 net.lab1024.sa.base.module.support.redis.RedisService;
import org.apache.commons.collections4.MapUtils; import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FilenameUtils; import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile; 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 javax.annotation.Resource;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.UnsupportedEncodingException; import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID;
/** /**
* 云计算 实现 * 云计算 实现
@ -45,7 +49,7 @@ import java.util.UUID;
* @Date 2019年10月11日 15:34:47 * @Date 2019年10月11日 15:34:47
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Slf4j @Slf4j
public class FileStorageCloudServiceImpl implements IFileStorageService { public class FileStorageCloudServiceImpl implements IFileStorageService {
@ -66,7 +70,7 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
private static final String USER_METADATA_FILE_SIZE = "file-size"; private static final String USER_METADATA_FILE_SIZE = "file-size";
@Resource @Resource
private AmazonS3 amazonS3; private S3Client s3Client;
@Resource @Resource
private FileConfig cloudConfig; private FileConfig cloudConfig;
@ -86,44 +90,51 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
} }
String fileType = FilenameUtils.getExtension(originalFileName); 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 time = LocalDateTimeUtil.format(LocalDateTime.now(), DatePattern.PURE_DATETIME_FORMATTER);
String fileKey = path + uuid + "_" + time+ "." + fileType; String fileKey = path + uuid + "_" + time + "." + fileType;
// 文件名称 URL 编码 // 文件名称 URL 编码
String urlEncoderFilename; String urlEncoderFilename;
try { try {
urlEncoderFilename = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name()); urlEncoderFilename = URLEncoder.encode(originalFileName, "utf-8");
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
log.error("文件上传服务URL ENCODE-发生异常:", e); throw new RuntimeException(e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败");
} }
ObjectMetadata meta = new ObjectMetadata();
meta.setContentEncoding(StandardCharsets.UTF_8.name());
meta.setContentDisposition("attachment;filename=" + urlEncoderFilename);
Map<String, String> userMetadata = new HashMap<>(10); Map<String, String> userMetadata = new HashMap<>(10);
userMetadata.put(USER_METADATA_FILE_NAME, urlEncoderFilename); userMetadata.put(USER_METADATA_FILE_NAME, urlEncoderFilename);
userMetadata.put(USER_METADATA_FILE_FORMAT, fileType); userMetadata.put(USER_METADATA_FILE_FORMAT, fileType);
userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize())); userMetadata.put(USER_METADATA_FILE_SIZE, String.valueOf(file.getSize()));
meta.setUserMetadata(userMetadata);
meta.setContentLength(file.getSize()); // 根据文件路径获取并设置访问权限
meta.setContentType(this.getContentType(fileType)); ObjectCannedACL acl = this.getACL(path);
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(cloudConfig.getBucketName())
.key(fileKey)
.metadata(userMetadata)
.contentLength(file.getSize())
.contentType(this.getContentType(fileType))
.contentEncoding(StandardCharsets.UTF_8.name())
.contentDisposition("attachment;filename=" + urlEncoderFilename)
.acl(acl)
.build();
InputStream inputStream = null;
try { try {
amazonS3.putObject(cloudConfig.getBucketName(), fileKey, file.getInputStream(), meta); inputStream = file.getInputStream();
s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(inputStream, file.getSize()));
} catch (IOException e) { } catch (IOException e) {
log.error("文件上传-发生异常:", e); log.error("文件上传-发生异常:", e);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败"); return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "上传失败");
} finally {
IOUtils.closeQuietly(inputStream);
} }
// 根据文件路径获取并设置访问权限
CannedAccessControlList acl = this.getACL(path);
amazonS3.setObjectAcl(cloudConfig.getBucketName(), fileKey, acl);
// 返回上传结果 // 返回上传结果
FileUploadVO uploadVO = new FileUploadVO(); FileUploadVO uploadVO = new FileUploadVO();
uploadVO.setFileName(originalFileName); uploadVO.setFileName(originalFileName);
uploadVO.setFileType(fileType); uploadVO.setFileType(fileType);
// 根据 访问权限 返回不同的 URL // 根据 访问权限 返回不同的 URL
String url = cloudConfig.getUrlPrefix() + fileKey; String url = cloudConfig.getUrlPrefix() + fileKey;
if (CannedAccessControlList.Private.equals(acl)) { if (ObjectCannedACL.PRIVATE.equals(acl)) {
// 获取临时访问的URL // 获取临时访问的URL
url = this.getFileUrl(fileKey).getData(); url = this.getFileUrl(fileKey).getData();
} }
@ -136,8 +147,8 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
/** /**
* 获取文件url * 获取文件url
* *
* @param fileKey * @param fileKey 文件key
* @return * @return url
*/ */
@Override @Override
public ResponseDTO<String> getFileUrl(String fileKey) { public ResponseDTO<String> getFileUrl(String fileKey) {
@ -159,10 +170,14 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
if (fileVO == null) { if (fileVO == null) {
return ResponseDTO.userErrorParam("文件不存在"); 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); S3Presigner presigner = S3Presigner.builder().region(Region.of(cloudConfig.getRegion())).build();
URL url = amazonS3.generatePresignedUrl(cloudConfig.getBucketName(), fileKey, expiration);
fileVO.setFileUrl(url.toString()); PresignedGetObjectRequest presignedGetObjectRequest = presigner.presignGetObject(getObjectPresignRequest);
String url = presignedGetObjectRequest.url().toString();
fileVO.setFileUrl(url);
redisService.set(fileRedisKey, fileVO, cloudConfig.getPrivateUrlExpireSeconds() - 5); redisService.set(fileRedisKey, fileVO, cloudConfig.getPrivateUrlExpireSeconds() - 5);
} }
@ -175,11 +190,11 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
*/ */
@Override @Override
public ResponseDTO<FileDownloadVO> download(String key) { public ResponseDTO<FileDownloadVO> download(String key) {
//获取oss对象
S3Object s3Object = amazonS3.getObject(cloudConfig.getBucketName(), key);
// 获取文件 meta // 获取文件 meta
ObjectMetadata metadata = s3Object.getObjectMetadata(); HeadObjectRequest objectRequest = HeadObjectRequest.builder().bucket(this.cloudConfig.getBucketName()).key(key).build();
Map<String, String> userMetadata = metadata.getUserMetadata(); HeadObjectResponse headObjectResponse = s3Client.headObject(objectRequest);
Map<String, String> userMetadata = headObjectResponse.metadata();
FileMetadataVO metadataDTO = null; FileMetadataVO metadataDTO = null;
if (MapUtils.isNotEmpty(userMetadata)) { if (MapUtils.isNotEmpty(userMetadata)) {
metadataDTO = new FileMetadataVO(); metadataDTO = new FileMetadataVO();
@ -190,43 +205,31 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
metadataDTO.setFileSize(fileSize); metadataDTO.setFileSize(fileSize);
} }
// 获得输入流 //获取oss对象
InputStream objectContent = s3Object.getObjectContent(); GetObjectRequest getObjectRequest = GetObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(key).build();
try { ResponseBytes<GetObjectResponse> s3ClientObject = s3Client.getObject(getObjectRequest, ResponseTransformer.toBytes());
// 输入流转换为字节流
byte[] buffer = FileCopyUtils.copyToByteArray(objectContent);
FileDownloadVO fileDownloadVO = new FileDownloadVO(); // 输入流转换为字节流
fileDownloadVO.setData(buffer); byte[] buffer = s3ClientObject.asByteArray();
fileDownloadVO.setMetadata(metadataDTO); FileDownloadVO fileDownloadVO = new FileDownloadVO();
return ResponseDTO.ok(fileDownloadVO); fileDownloadVO.setData(buffer);
} catch (IOException e) { fileDownloadVO.setMetadata(metadataDTO);
log.error("文件下载-发生异常:", e); return ResponseDTO.ok(fileDownloadVO);
return ResponseDTO.error(SystemErrorCode.SYSTEM_ERROR, "下载失败");
} finally {
try {
// 关闭输入流
objectContent.close();
s3Object.close();
} catch (IOException e) {
log.error("文件下载-发生异常:", e);
}
}
} }
/** /**
* 根据文件夹路径 返回对应的访问权限 * 根据文件夹路径 返回对应的访问权限
* *
* @param fileKey * @param fileKey 文件key
* @return * @return 权限
*/ */
private CannedAccessControlList getACL(String fileKey) { private ObjectCannedACL getACL(String fileKey) {
// 公用读 // 公用读
if (fileKey.contains(FileFolderTypeEnum.FOLDER_PUBLIC)) { if (fileKey.contains(FileFolderTypeEnum.FOLDER_PUBLIC)) {
return CannedAccessControlList.PublicRead; return ObjectCannedACL.PUBLIC_READ;
} }
// 其他默认私有读写 // 其他默认私有读写
return CannedAccessControlList.Private; return ObjectCannedACL.PRIVATE;
} }
/** /**
@ -235,11 +238,11 @@ public class FileStorageCloudServiceImpl implements IFileStorageService {
* ps不能删除fileKey不为空的文件夹 * ps不能删除fileKey不为空的文件夹
* *
* @param fileKey 文件or文件夹 * @param fileKey 文件or文件夹
* @return
*/ */
@Override @Override
public ResponseDTO<String> delete(String fileKey) { 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(); return ResponseDTO.ok();
} }

View File

@ -19,7 +19,6 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.PostConstruct; import javax.annotation.PostConstruct;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.file.Files; 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.HelpDocDetailVO;
import net.lab1024.sa.base.module.support.helpdoc.domain.vo.HelpDocVO; import net.lab1024.sa.base.module.support.helpdoc.domain.vo.HelpDocVO;
import net.lab1024.sa.base.module.support.helpdoc.manager.HelpDocManager; 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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;

View File

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

View File

@ -3,11 +3,12 @@ package net.lab1024.sa.base.module.support.operatelog.core;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.constant.StringConst; import net.lab1024.sa.base.common.constant.StringConst;
import net.lab1024.sa.base.common.domain.RequestUser; import net.lab1024.sa.base.common.domain.RequestUser;
import net.lab1024.sa.base.common.domain.ResponseDTO;
import net.lab1024.sa.base.common.util.SmartIpUtil; import net.lab1024.sa.base.common.util.SmartIpUtil;
import net.lab1024.sa.base.common.util.SmartRequestUtil; import net.lab1024.sa.base.common.util.SmartRequestUtil;
import net.lab1024.sa.base.module.support.operatelog.OperateLogDao; import net.lab1024.sa.base.module.support.operatelog.OperateLogDao;
@ -20,7 +21,6 @@ import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.bind.BindResult; import org.springframework.boot.context.properties.bind.BindResult;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
@ -36,7 +36,6 @@ import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter; import java.io.PrintWriter;
import java.io.StringWriter; import java.io.StringWriter;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.net.BindException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
@ -48,7 +47,7 @@ import java.util.concurrent.ThreadPoolExecutor;
* @Date 2021-12-08 20:48:52 * @Date 2021-12-08 20:48:52
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
@Slf4j @Slf4j
@Aspect @Aspect
@ -73,14 +72,14 @@ public abstract class OperateLogAspect {
public void logPointCut() { public void logPointCut() {
} }
@AfterReturning(pointcut = "logPointCut()") @AfterReturning(pointcut = "logPointCut()", returning = "responseDTO")
public void doAfterReturning(JoinPoint joinPoint) { public void doAfterReturning(JoinPoint joinPoint, Object responseDTO) {
handleLog(joinPoint, null); handleLog(joinPoint, null, responseDTO);
} }
@AfterThrowing(value = "logPointCut()", throwing = "e") @AfterThrowing(value = "logPointCut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) { public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
handleLog(joinPoint, e); handleLog(joinPoint, e, null);
} }
/** /**
@ -111,16 +110,15 @@ public abstract class OperateLogAspect {
taskExecutor.setWaitForTasksToCompleteOnShutdown(true); taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
} }
protected void handleLog(final JoinPoint joinPoint, final Exception e) { protected void handleLog(final JoinPoint joinPoint, final Exception e, Object responseDTO) {
try { try {
OperateLog operateLog = this.getAnnotationLog(joinPoint); OperateLog operateLog = this.getAnnotationLog(joinPoint);
if (operateLog == null) { if (operateLog == null) {
return; return;
} }
this.submitLog(joinPoint, e); this.submitLog(joinPoint, e, responseDTO);
} catch (Exception exp) { } catch (Exception exp) {
log.error("保存操作日志异常:{}", exp.getMessage()); log.error("保存操作日志异常:{}", exp.getMessage());
exp.printStackTrace();
} }
} }
@ -175,11 +173,8 @@ public abstract class OperateLogAspect {
/** /**
* 提交存储操作日志 * 提交存储操作日志
* *
* @param joinPoint
* @param e
* @throws Exception
*/ */
private void submitLog(final JoinPoint joinPoint, final Throwable e) throws Exception { private void submitLog(final JoinPoint joinPoint, final Throwable e, Object responseDTO) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
//设置用户信息 //设置用户信息
RequestUser user = SmartRequestUtil.getRequestUser(); RequestUser user = SmartRequestUtil.getRequestUser();
@ -193,7 +188,7 @@ public abstract class OperateLogAspect {
String methodName = joinPoint.getSignature().getName(); String methodName = joinPoint.getSignature().getName();
String operateMethod = className + "." + methodName; String operateMethod = className + "." + methodName;
String failReason = null; String failReason = null;
Boolean successFlag = true; boolean successFlag = true;
if (e != null) { if (e != null) {
successFlag = false; successFlag = false;
failReason = getExceptionString(e); failReason = getExceptionString(e);
@ -212,15 +207,32 @@ public abstract class OperateLogAspect {
.userAgent(user.getUserAgent()) .userAgent(user.getUserAgent())
.failReason(failReason) .failReason(failReason)
.successFlag(successFlag).build(); .successFlag(successFlag).build();
Operation apiOperation = this.getApiOperation(joinPoint); Operation apiOperation = this.getApiOperation(joinPoint);
if (apiOperation != null) { if (apiOperation != null) {
operateLogEntity.setContent(apiOperation.summary()); operateLogEntity.setContent(apiOperation.summary());
} }
Tag api = this.getApi(joinPoint); Tag api = this.getApi(joinPoint);
if (api != null) { if (api != null) {
String name = api.name(); String name = api.name();
operateLogEntity.setModule(StrUtil.join(",", name)); operateLogEntity.setModule(StrUtil.join(",", name));
} }
// 处理返回值 ResponseDTO
if(responseDTO instanceof ResponseDTO) {
ResponseDTO response = (ResponseDTO) responseDTO;
ResponseDTO logResponseDTO = new ResponseDTO(
response.getCode(),
response.getLevel(),
response.getOk(),
response.getMsg(),
null
);
logResponseDTO.setDataType(response.getDataType());
operateLogEntity.setResponse(JSON.toJSONString(logResponseDTO));
}
taskExecutor.execute(() -> { taskExecutor.execute(() -> {
this.saveLog(operateLogEntity); this.saveLog(operateLogEntity);
}); });

View File

@ -71,6 +71,11 @@ public class OperateLogEntity {
*/ */
private String param; private String param;
/**
* 返回值
*/
private String response;
/** /**
* 客户ip * 客户ip
*/ */

View File

@ -47,6 +47,9 @@ public class OperateLogVO {
@Schema(description = "请求参数") @Schema(description = "请求参数")
private String param; private String param;
@Schema(description = "返回值")
private String response;
@Schema(description = "客户ip") @Schema(description = "客户ip")
private String ip; private String ip;

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.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; 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创新实验室: 胡克 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @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)") @Around("@annotation(net.lab1024.sa.base.module.support.repeatsubmit.annoation.RepeatSubmit)")
public Object around(ProceedingJoinPoint point) throws Throwable { public Object around(ProceedingJoinPoint point) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
String ticketToken = attributes.getRequest().getServletPath(); if (attributes == null) {
String ticket = this.repeatSubmitTicket.getTicket(ticketToken); return point.proceed();
}
/**
* 第一步生成防重复提交的 ticket凭证
* ticket 是根据 Request对象 自定义 生成的可以加入请求user相关属性作为生成要素
*/
HttpServletRequest request = attributes.getRequest();
String ticket = this.repeatSubmitTicket.generateTicket(request);
if (StringUtils.isEmpty(ticket)) { if (StringUtils.isEmpty(ticket)) {
return point.proceed(); return point.proceed();
} }
Method method = ((MethodSignature) point.getSignature()).getMethod(); Method method = ((MethodSignature) point.getSignature()).getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class); RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
int limit = annotation.value(); Long intervalMilliSecond = (long) annotation.intervalMilliSecond();
// 获取上一次请求时间 /**
Long lastRequestTime = this.repeatSubmitTicket.getTicketTimestamp(ticket); * 第二步根据 ticket 凭证进行 加锁
// 校验是否限制时间内重复提交 * 能加锁则可以执行
if (lastRequestTime != null && System.currentTimeMillis() < lastRequestTime + limit) { * 若不能加锁则证明还是时间间隔interval中
*/
boolean lockSuccessFlag = this.repeatSubmitTicket.tryLock(ticket, System.currentTimeMillis(), intervalMilliSecond);
if (!lockSuccessFlag) {
return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT); return ResponseDTO.error(UserErrorCode.REPEAT_SUBMIT);
} }
// 执行
Object obj = null;
try { try {
// ticket 设置在执行中 return point.proceed();
this.repeatSubmitTicket.putTicket(ticket);
// 执行
obj = point.proceed();
} catch (Throwable throwable) { } catch (Throwable throwable) {
log.error("", throwable); log.error(throwable.getMessage(), throwable);
throw 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>
* 着重说明<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创新实验室: 胡克 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 20:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -20,8 +29,8 @@ import java.lang.annotation.Target;
public @interface RepeatSubmit { 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; package net.lab1024.sa.base.module.support.repeatsubmit.ticket;
import javax.servlet.http.HttpServletRequest;
import java.util.function.Function; import java.util.function.Function;
/** /**
* 凭证用于校验重复提交的东西 * 凭证用于校验重复提交的东西
* *
* @Author 1024创新实验室: 罗伊 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
public abstract class AbstractRepeatSubmitTicket { public abstract class AbstractRepeatSubmitTicket {
private final Function<String, String> ticketFunction; private final Function<HttpServletRequest, String> generateTicketFunction;
public AbstractRepeatSubmitTicket(Function<String, String> ticketFunction) { public AbstractRepeatSubmitTicket(Function<HttpServletRequest, String> generateTicketFunction) {
this.ticketFunction = ticketFunction; this.generateTicketFunction = generateTicketFunction;
} }
/** /**
* 获取凭证 * 生成 加锁的 凭证
*/ */
public String getTicket(String ticketToken) { public String generateTicket(HttpServletRequest request) {
return this.ticketFunction.apply(ticketToken); 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; 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; import java.util.function.Function;
/** /**
* 凭证redis实现 * 凭证redis实现
* *
* @Author 1024创新实验室: 罗伊 * @Author 1024创新实验室-主任: 卓大
* @Date 2020-11-25 20:56:58 * @Date 2025-07-26 23:56:58
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
*/ */
public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket { public class RepeatSubmitRedisTicket extends AbstractRepeatSubmitTicket {
private final ValueOperations<String, String> redisValueOperations; private final RedisTemplate<String, Object> redisTemplate;
public RepeatSubmitRedisTicket(ValueOperations<String, String> redisValueOperations, public RepeatSubmitRedisTicket(RedisTemplate<String, Object> redisTemplate,
Function<String, String> ticketFunction) { Function<HttpServletRequest, String> ticketFunction) {
super(ticketFunction); super(ticketFunction);
this.redisValueOperations = redisValueOperations; this.redisTemplate = redisTemplate;
} }
@Override @Override
public Long getTicketTimestamp(String ticket) { public boolean tryLock(String ticket, Long currentTimestamp, Long intervalMilliSecond) {
String ticketLastTime = redisValueOperations.get(ticket); if (intervalMilliSecond > 0) {
return ticketLastTime == null ? null : Long.valueOf(ticketLastTime); 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 @Override
public void putTicket(String ticket) { public void unLock(String ticket, Long intervalMilliSecond) {
redisValueOperations.getOperations().delete(ticket); if (intervalMilliSecond > 0) {
this.getTicketTimestamp(ticket); 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 lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.domain.ResponseDTO; import net.lab1024.sa.base.common.domain.ResponseDTO;
import org.apache.commons.io.IOUtils;
import org.apache.tika.config.TikaConfig; import org.apache.tika.config.TikaConfig;
import org.apache.tika.exception.TikaException; import org.apache.tika.exception.TikaException;
import org.apache.tika.io.TikaInputStream; import org.apache.tika.io.TikaInputStream;
@ -14,6 +15,7 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@ -35,27 +37,9 @@ public class SecurityFileService {
private Level3ProtectConfigService level3ProtectConfigService; private Level3ProtectConfigService level3ProtectConfigService;
// 定义白名单MIME类型 // 定义白名单MIME类型
private static final List<String> ALLOWED_MIME_TYPES = Arrays.asList( 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/*",
"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/*" // 图片类型 svg有安全隐患所以不使用"image/*"
"image/jpeg", "image/jpeg", "image/png", "image/gif", "image/bmp");
"image/png",
"image/gif",
"image/bmp"
);
/** /**
* 检测文件安全类型 * 检测文件安全类型
@ -73,8 +57,7 @@ public class SecurityFileService {
// 文件类型安全检测 // 文件类型安全检测
if (level3ProtectConfigService.isFileDetectFlag()) { if (level3ProtectConfigService.isFileDetectFlag()) {
String fileType = getFileMimeType(file); String fileType = getFileMimeType(file);
if (ALLOWED_MIME_TYPES.stream() if (ALLOWED_MIME_TYPES.stream().noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
.noneMatch(allowedType -> matchesMimeType(fileType, allowedType))) {
return ResponseDTO.userErrorParam("禁止上传此文件类型"); return ResponseDTO.userErrorParam("禁止上传此文件类型");
} }
} }
@ -89,16 +72,20 @@ public class SecurityFileService {
* @return 文件的 MIME 类型 * @return 文件的 MIME 类型
*/ */
public static String getFileMimeType(MultipartFile file) { public static String getFileMimeType(MultipartFile file) {
InputStream inputStream = null;
try { try {
inputStream = file.getInputStream();
TikaConfig tika = new TikaConfig(); TikaConfig tika = new TikaConfig();
Metadata metadata = new Metadata(); Metadata metadata = new Metadata();
metadata.set(TikaCoreProperties.RESOURCE_NAME_KEY, file.getOriginalFilename()); 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); MediaType mimetype = tika.getDetector().detect(stream, metadata);
return mimetype.toString(); return mimetype.toString();
} catch (IOException | TikaException e) { } catch (IOException | TikaException e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
return MimeTypes.OCTET_STREAM; 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 * @Date 2022-03-25 21:46:07
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @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 { public abstract class SerialNumberBaseService implements SerialNumberService {
@ -37,7 +37,7 @@ public abstract class SerialNumberBaseService implements SerialNumberService {
@Resource @Resource
protected SerialNumberDao serialNumberDao; 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); 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; package net.lab1024.sa.base.module.support.serialnumber.service.impl;
import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.lab1024.sa.base.common.exception.BusinessException; 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.constant.RedisKeyConst;
import net.lab1024.sa.base.module.support.redis.RedisService; 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.SerialNumberEntity;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberGenerateResultBO; 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.SerialNumberInfoBO;
import net.lab1024.sa.base.module.support.serialnumber.domain.SerialNumberLastGenerateBO;
import net.lab1024.sa.base.module.support.serialnumber.service.SerialNumberBaseService; 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 javax.annotation.Resource;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
/** /**
* 单据序列号 基于redis锁实现 * 单据序列号 基于redis key-value increase 实现
* *
* @Author 1024创新实验室-主任: 卓大 * @Author 1024创新实验室-主任: 卓大
* @Date 2022-03-25 21:46:07 * @Date 2025-08-03 22:46:07
* @Wechat zhuoda1024 * @Wechat zhuoda1024
* @Email lab1024@163.com * @Email lab1024@163.com
* @Copyright <a href="https://1024lab.net">1024创新实验室</a> * @Copyright <a href="https://1024lab.net">1024创新实验室</a>
@ -25,86 +36,125 @@ import java.util.List;
@Slf4j @Slf4j
public class SerialNumberRedisService extends SerialNumberBaseService { public class SerialNumberRedisService extends SerialNumberBaseService {
private static final int MAX_GET_LOCK_COUNT = 5;
private static final long SLEEP_MILLISECONDS = 200L;
@Resource @Resource
private RedisService redisService; private RedisService redisService;
@Resource
private RedisTemplate redisTemplate;
@Override @Override
public void initLastGenerateData(List<SerialNumberEntity> serialNumberEntityList) { public void initLastGenerateData(List<SerialNumberEntity> serialNumberEntityList) {
if (serialNumberEntityList == null) { if (serialNumberEntityList == null) {
return; return;
} }
//删除之前的 // 设置redis的上次值
redisService.delete(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO);
for (SerialNumberEntity serialNumberEntity : serialNumberEntityList) { for (SerialNumberEntity serialNumberEntity : serialNumberEntityList) {
SerialNumberLastGenerateBO lastGenerateBO = SerialNumberLastGenerateBO if (serialNumberEntity.getLastTime() == null) {
.builder() continue;
.serialNumberId(serialNumberEntity.getSerialNumberId()) }
.lastNumber(serialNumberEntity.getLastNumber())
.lastTime(serialNumberEntity.getLastTime())
.build();
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO, String redisKey = generateRedisKeyByDate(serialNumberEntity.getSerialNumberId(),
String.valueOf(serialNumberEntity.getSerialNumberId()), SmartEnumUtil.getEnumByName(serialNumberEntity.getRuleType().toUpperCase(), SerialNumberRuleTypeEnum.class),
lastGenerateBO 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 @Override
public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) { public List<String> generateSerialNumberList(SerialNumberInfoBO serialNumberInfo, int count) {
SerialNumberGenerateResultBO serialNumberGenerateResult = null; // 根据步长计算 redis 增加值
String lockKey = RedisKeyConst.Support.SERIAL_NUMBER + serialNumberInfo.getSerialNumberId(); ArrayList<Integer> list = new ArrayList<>(count);
int redisIncrease = 0;
boolean lock = false; for (int i = 0; i < count; i++) {
for (int i = 0; i < MAX_GET_LOCK_COUNT; i++) { int stepIncrease = 1;
try { Integer stepRandomRange = serialNumberInfo.getStepRandomRange();
lock = redisService.getLock(lockKey, 60 * 1000L); if (stepRandomRange > 1) {
if (lock) { stepIncrease = RandomUtil.getSecureRandom().nextInt(serialNumberInfo.getStepRandomRange()) + 1;
break;
}
Thread.sleep(SLEEP_MILLISECONDS);
} catch (Throwable e) {
log.error(e.getMessage(), e);
} }
redisIncrease += stepIncrease;
list.add(stepIncrease);
} }
if (!lock) {
throw new BusinessException("SerialNumber 尝试5次未能生成单号");
}
try { try {
// 获取上次的生成结果 String redisKey = generateRedisKeyByDate(serialNumberInfo.getSerialNumberId(), serialNumberInfo.getSerialNumberRuleTypeEnum(), LocalDate.now());
SerialNumberLastGenerateBO lastGenerateBO = (SerialNumberLastGenerateBO) redisService.mget( Long increaseResult = redisTemplate.opsForValue().increment(redisKey, redisIncrease);
RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId())); 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(), serialNumberDao.updateLastNumberAndTime(serialNumberInfo.getSerialNumberId(),
serialNumberGenerateResult.getLastNumber(), serialNumberGenerateResult.getLastNumber(),
serialNumberGenerateResult.getLastTime()); serialNumberGenerateResult.getLastTime());
redisService.mset(RedisKeyConst.Support.SERIAL_NUMBER_LAST_INFO,
String.valueOf(serialNumberInfo.getSerialNumberId()), lastGenerateBO);
// 把生成过程保存到数据库里 // 把生成过程保存到数据库里
super.saveRecord(serialNumberGenerateResult); super.saveRecord(serialNumberGenerateResult);
return formatNumberList(serialNumberGenerateResult, serialNumberInfo);
} catch (Throwable e) { } catch (Throwable e) {
log.error(e.getMessage(), e); log.error(e.getMessage(), e);
throw 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

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

@ -48,7 +48,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")
@ -106,7 +106,7 @@
#end #end
#if($field.frontComponent == "DictSelect") #if($field.frontComponent == "DictSelect")
<a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}"> <a-form-item label="$codeGeneratorTool.removeEnumDesc($!{field.label})" name="${field.fieldName}">
<DictSelect width="100%" v-model:value="form.${field.fieldName}" dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/> <DictSelect width="100%" v-model:value="form.${field.fieldName}" :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="$!{field.label}"/>
</a-form-item> </a-form-item>
#end #end
#if($field.frontComponent == "Date") #if($field.frontComponent == "Date")

View File

@ -22,7 +22,7 @@
#end #end
#if($field.queryTypeEnum == "Dict") #if($field.queryTypeEnum == "Dict")
<a-form-item label="${field.label}" class="smart-query-form-item"> <a-form-item label="${field.label}" class="smart-query-form-item">
<DictSelect dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" /> <DictSelect :dict-code="DICT_CODE_ENUM.$!{field.dict} || '$!{field.dict}'" placeholder="${field.label}" v-model:value="queryForm.${field.fieldName}" width="${field.width}" />
</a-form-item> </a-form-item>
#end #end
#if($field.queryTypeEnum == "Enum") #if($field.queryTypeEnum == "Enum")

View File

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

View File

@ -26,7 +26,7 @@
AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords})) AND (INSTR(module,#{query.keywords}) OR INSTR(content,#{query.keywords}))
</if> </if>
<if test="query.requestKeywords != null and query.requestKeywords != ''"> <if test="query.requestKeywords != null and query.requestKeywords != ''">
AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords})) AND (INSTR(url,#{query.requestKeywords}) OR INSTR(method,#{query.requestKeywords}) OR INSTR(param,#{query.requestKeywords}) OR INSTR(response,#{query.requestKeywords}))
</if> </if>
<if test="query.successFlag != null"> <if test="query.successFlag != null">
AND success_flag = #{query.successFlag} AND success_flag = #{query.successFlag}

View File

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

View File

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

View File

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

View File

@ -43,7 +43,7 @@
"vue": "3.4.27", "vue": "3.4.27",
"vue-i18n": "9.13.1", "vue-i18n": "9.13.1",
"vue-router": "4.3.2", "vue-router": "4.3.2",
"vue3-json-viewer": "2.2.2" "vue3-json-viewer": "2.3.1"
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "5.0.4", "@vitejs/plugin-vue": "5.0.4",

View File

@ -12,7 +12,7 @@
<a-config-provider <a-config-provider
:locale="antdLocale" :locale="antdLocale"
:theme="{ :theme="{
algorithm: compactFlag ? theme.compactAlgorithm : theme.defaultAlgorithm, algorithm: themeAlgorithm,
token: { token: {
colorPrimary: themeColors[colorIndex].primaryColor, colorPrimary: themeColors[colorIndex].primaryColor,
colorLink: themeColors[colorIndex].primaryColor, colorLink: themeColors[colorIndex].primaryColor,
@ -48,12 +48,10 @@
import { messages } from '/@/i18n'; import { messages } from '/@/i18n';
import { useAppConfigStore } from '/@/store/modules/system/app-config'; import { useAppConfigStore } from '/@/store/modules/system/app-config';
import { useSpinStore } from '/@/store/modules/system/spin'; 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 { themeColors } from '/@/theme/color.js';
import { Popover } from 'ant-design-vue';
import SmartCopyIcon from '/@/components/framework/smart-copy-icon/index.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 antdLocale = computed(() => messages[useAppConfigStore().language].antdLocale);
const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale); const dayjsLocale = computed(() => messages[useAppConfigStore().language].dayjsLocale);
dayjs.locale(dayjsLocale); dayjs.locale(dayjsLocale);
@ -61,16 +59,24 @@
// loading // loading
let spinStore = useSpinStore(); let spinStore = useSpinStore();
const spinning = computed(() => spinStore.loading); const spinning = computed(() => spinStore.loading);
//
const compactFlag = computed(() => useAppConfigStore().compactFlag);
// //
const colorIndex = computed(() => { const colorIndex = computed(() => {
return useAppConfigStore().colorIndex; 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(() => { const borderRadius = computed(() => {
return useAppConfigStore().borderRadius; return useAppConfigStore().borderRadius;
}); });
function transformCellText({ text, column, record, index }) { function transformCellText({ text, column, record, index }) {
if (column && column.textEllipsisFlag === true) { if (column && column.textEllipsisFlag === true) {
return h( return h(
@ -78,7 +84,14 @@
{ placement: 'bottom' }, { placement: 'bottom' },
{ {
default: () => 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: () => content: () =>
h('div', { style: { display: 'flex' } }, [ h('div', { style: { display: 'flex' } }, [
h('div', text), h('div', text),
@ -91,9 +104,24 @@
} }
} }
const { useToken } = theme;
const { token } = useToken();
</script> </script>
<style scoped lang="less"> <style lang="less">
@color-bg-container: v-bind('token.colorBgContainer');
:deep(.ant-table-column-sorters) { :deep(.ant-table-column-sorters) {
align-items: flex-start !important; 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> </style>

View File

@ -51,7 +51,6 @@
v-model:pageSize="queryForm.pageSize" v-model:pageSize="queryForm.pageSize"
:total="total" :total="total"
@change="ajaxQuery" @change="ajaxQuery"
@showSizeChange="ajaxQuery"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
<a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null"> <a-modal v-model:open="visibleDiff" width="90%" title="数据比对" :footer="null">

View File

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

View File

@ -71,7 +71,6 @@
v-model:pageSize="params.pageSize" v-model:pageSize="params.pageSize"
:total="total" :total="total"
@change="queryEmployee" @change="queryEmployee"
@showSizeChange="queryEmployee"
:show-total="(total) => `共${total}条`" :show-total="(total) => `共${total}条`"
/> />
</div> </div>

View File

@ -16,6 +16,8 @@ export const appDefaultConfig = {
sideMenuWidth: 200, sideMenuWidth: 200,
//标签页位置 //标签页位置
pageTagLocation: 'center', pageTagLocation: 'center',
// 夜间模式
darkModeFlag: false,
// 菜单主题 // 菜单主题
sideMenuTheme: 'dark', sideMenuTheme: 'dark',
// 主题颜色索引 // 主题颜色索引
@ -25,7 +27,7 @@ export const appDefaultConfig = {
// 圆角 // 圆角
borderRadius: 6, borderRadius: 6,
// 菜单展开模式 // 菜单展开模式
flatPattern: false, menuSingleExpandFlag: true,
// 标签页 // 标签页
pageTagFlag: true, pageTagFlag: true,
// 标签页样式: default、 antd、chrome // 标签页样式: default、 antd、chrome

View File

@ -17,14 +17,15 @@ export default {
'setting.table.yHeight': 'Table Height', 'setting.table.yHeight': 'Table Height',
'setting.pagetag.location': 'TagPage Position', 'setting.pagetag.location': 'TagPage Position',
'setting.color': 'Theme Color', 'setting.color': 'Theme Color',
'setting.darkmode': 'Dark Mode',
'setting.menu.layout': 'Menu Layout', 'setting.menu.layout': 'Menu Layout',
'setting.menu.width': 'Menu Width', 'setting.menu.width': 'Menu Width',
'setting.menu.theme': 'Menu Theme', 'setting.menu.theme': 'Menu Theme',
'setting.menu.expand': 'Menu Expand',
'setting.page.width': 'Page Width', 'setting.page.width': 'Page Width',
'setting.border.radius': 'Border Radius', 'setting.border.radius': 'Border Radius',
'setting.compact': 'Page Compact', 'setting.compact': 'Page Compact',
'setting.bread': 'Show Bread', 'setting.bread': 'Show Bread',
'setting.flatPattern': 'Flat Pattern',
'setting.pagetag': 'Show PageTag', 'setting.pagetag': 'Show PageTag',
'setting.pagetag.style': 'PageTag Style', 'setting.pagetag.style': 'PageTag Style',
'setting.footer': 'Show Footer', 'setting.footer': 'Show Footer',

View File

@ -17,14 +17,15 @@ export default {
'setting.table.yHeight': '表格高度', 'setting.table.yHeight': '表格高度',
'setting.pagetag.location': '标签页位置', 'setting.pagetag.location': '标签页位置',
'setting.color': '主题颜色', 'setting.color': '主题颜色',
'setting.darkmode': '夜间模式',
'setting.menu.layout': '菜单布局', 'setting.menu.layout': '菜单布局',
'setting.menu.width': '菜单宽度', 'setting.menu.width': '菜单宽度',
'setting.menu.theme': '菜单主题', 'setting.menu.theme': '菜单主题',
'setting.menu.expand': '菜单展开',
'setting.compact': '页面紧凑', 'setting.compact': '页面紧凑',
'setting.border.radius': '页面圆角', 'setting.border.radius': '页面圆角',
'setting.page.width': '页面宽度', 'setting.page.width': '页面宽度',
'setting.bread': '面包屑', 'setting.bread': '面包屑',
'setting.flatPattern': '菜单展开模式',
'setting.pagetag': '标签页', 'setting.pagetag': '标签页',
'setting.pagetag.style': '标签页样式', 'setting.pagetag.style': '标签页样式',
'setting.footer': '页脚', 'setting.footer': '页脚',

View File

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

View File

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

View File

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

View File

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

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