Compare commits

...

7 Commits

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

View File

@ -4,6 +4,8 @@
**<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</font>** 功能要求,支持登录限制、接口国产加解密、数据脱敏等一系列安全要求。 **<font color="#DC143C">国内首个满足《网络安全-三级等保》、《数据安全》</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

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

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(false)
.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,38 @@ 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()); PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).metadata(userMetadata).contentLength(file.getSize()).contentType(this.getContentType(fileType)).contentEncoding(StandardCharsets.UTF_8.name()).contentDisposition("attachment;filename=" + urlEncoderFilename).build();
meta.setContentType(this.getContentType(fileType)); 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); ObjectCannedACL acl = this.getACL(path);
amazonS3.setObjectAcl(cloudConfig.getBucketName(), fileKey, acl); PutObjectAclRequest aclRequest = PutObjectAclRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).acl(this.getACL(path)).build();
s3Client.putObjectAcl(aclRequest);
// 返回上传结果 // 返回上传结果
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 +135,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 +158,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 +178,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 +193,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 +226,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

@ -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

@ -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

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

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(false)
.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,44 @@ 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()); PutObjectRequest putObjectRequest = PutObjectRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).metadata(userMetadata).contentLength(file.getSize()).contentType(this.getContentType(fileType)).contentEncoding(StandardCharsets.UTF_8.name()).contentDisposition("attachment;filename=" + urlEncoderFilename).build();
meta.setContentType(this.getContentType(fileType)); 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); ObjectCannedACL acl = this.getACL(path);
amazonS3.setObjectAcl(cloudConfig.getBucketName(), fileKey, acl); PutObjectAclRequest aclRequest = PutObjectAclRequest.builder().bucket(cloudConfig.getBucketName()).key(fileKey).acl(this.getACL(path)).build();
s3Client.putObjectAcl(aclRequest);
// 返回上传结果 // 返回上传结果
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 +140,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 +163,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 +183,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 +198,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 +231,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

@ -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

@ -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

@ -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

@ -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

@ -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

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

View File

@ -17,6 +17,7 @@ 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',

View File

@ -17,6 +17,7 @@ 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': '菜单主题',

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>
@ -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,6 +205,8 @@
sideMenuWidth: appConfigStore.sideMenuWidth, sideMenuWidth: appConfigStore.sideMenuWidth,
// //
sideMenuTheme: appConfigStore.sideMenuTheme, sideMenuTheme: appConfigStore.sideMenuTheme,
//
darkModeFlag: appConfigStore.darkModeFlag,
// //
compactFlag: appConfigStore.compactFlag, compactFlag: appConfigStore.compactFlag,
// //
@ -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,6 +302,7 @@
pageTagFlag: e, pageTagFlag: e,
}); });
} }
function changeFlatPattern(e) { function changeFlatPattern(e) {
appConfigStore.$patch({ appConfigStore.$patch({
flatPattern: e, flatPattern: 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>

View File

@ -9,16 +9,17 @@
--> -->
<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">
<a-tab-pane v-for="item in tagNav" :key="item.menuName"> <a-tab-pane v-for="item in tagNav" :key="item.menuName">
<template #tab> <template #tab>
<span> <span class="smart-page-tag-span">
<home-outlined v-if="item.menuName === HOME_PAGE_NAME" class="smart-page-tag-close smart-page-tag-icon" />
<component class="smart-page-tag-icon" v-else :is="$antIcons[item.menuIcon]" />
{{ item.menuTitle }} {{ item.menuTitle }}
<close-outlined @click.stop="closeTag(item, false)" v-if="item.menuName !== HOME_PAGE_NAME" class="smart-page-tag-close" /> <close-outlined @click.stop="closeTag(item, false)" v-if="item.menuName !== HOME_PAGE_NAME" class="smart-page-tag-close" />
<home-outlined style="font-size: 12px" v-if="item.menuName === HOME_PAGE_NAME" class="smart-page-tag-close" />
</span> </span>
</template> </template>
</a-tab-pane> </a-tab-pane>
@ -137,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;
@ -168,6 +168,14 @@
color: @color-primary; color: @color-primary;
} }
.smart-page-tag-span:first-child {
padding-right: 15px;
.smart-page-tag-icon {
font-size: 12px;
}
}
.smart-page-tag { .smart-page-tag {
position: relative; position: relative;
box-sizing: border-box; box-sizing: border-box;
@ -179,9 +187,12 @@
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-icon {
margin-right: 4px;
}
.smart-page-tag-close { .smart-page-tag-close {
margin-left: 5px; margin-left: 5px;
font-size: 10px; font-size: 10px;
@ -194,10 +205,9 @@
margin: 0; margin: 0;
padding: 0 0 2px 0; padding: 0 0 2px 0;
max-height: 32px; max-height: 32px;
} &::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) {
@ -207,13 +217,16 @@
} }
:deep(.ant-tabs-tab-active) { :deep(.ant-tabs-tab-active) {
background-color: #eeeeee; 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: #eeeeee; background-color: @color-primary-bg;
.smart-page-tag-close { .smart-page-tag-close {
color: @color-primary; color: @color-primary;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,8 +30,6 @@ import 'ant-design-vue/dist/reset.css';
import '/@/theme/index.less'; import '/@/theme/index.less';
import { localRead } from '/@/utils/local-util.js'; import { localRead } from '/@/utils/local-util.js';
import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js'; import LocalStorageKeyConst from '/@/constants/local-storage-key-const.js';
import { Table } from 'ant-design-vue';
import { useAppConfigStore } from '/@/store/modules/system/app-config';
import '/@/utils/ployfill'; import '/@/utils/ployfill';
import { useDictStore } from '/@/store/modules/system/dict.js'; import { useDictStore } from '/@/store/modules/system/dict.js';
import { dictApi } from '/@/api/support/dict-api.js'; import { dictApi } from '/@/api/support/dict-api.js';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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