mirror of
https://gitee.com/dromara/RuoYi-Vue-Plus.git
synced 2026-03-13 17:40:53 +08:00
Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91ba3869e7 | ||
|
|
1bb597b855 | ||
|
|
348d7fc534 | ||
|
|
76218091ad | ||
|
|
95c9e37797 | ||
|
|
aa277b373b | ||
|
|
79d9f47053 | ||
|
|
f984f08a14 | ||
|
|
6f94095bb0 | ||
|
|
2d4685ac5f | ||
|
|
7f9e4e14f0 | ||
|
|
c5777c01c1 | ||
|
|
459e9caf14 | ||
|
|
0940ba6762 | ||
|
|
d8ed23f227 | ||
|
|
948eba6566 | ||
|
|
1a14bdf256 | ||
|
|
bbc684b335 | ||
|
|
2b8f4e1d2c | ||
|
|
d634c2a292 | ||
|
|
8b97e7bc53 | ||
|
|
88f871002c | ||
|
|
874ad7c9b7 | ||
|
|
89d6f6f247 | ||
|
|
1324a1cb16 | ||
|
|
b6eacfa5a8 | ||
|
|
961bca462e | ||
|
|
496df8494e | ||
|
|
da4dffcfce | ||
|
|
2f1f9689e0 | ||
|
|
8110413fdf | ||
|
|
c1f64d3450 | ||
|
|
cb00f4c9c1 | ||
|
|
79512c69b2 | ||
|
|
a5fb128f11 | ||
|
|
8a04e3c88f | ||
|
|
dac447b76f | ||
|
|
35a9e4c8e8 | ||
|
|
0d87c12d3c | ||
|
|
f20a0c4342 | ||
|
|
6c8d637bd2 | ||
|
|
20e9957db2 | ||
|
|
9baded9326 | ||
|
|
b5902debb6 | ||
|
|
bcd5bb0f86 | ||
|
|
1a461f7d3d | ||
|
|
e23d99d85b | ||
|
|
f07c20afab | ||
|
|
420553eaa6 | ||
|
|
1d8d93eaa3 | ||
|
|
5f0d09fd45 | ||
|
|
1c2b7d7017 | ||
|
|
5fb2890167 | ||
|
|
1165c8dc06 | ||
|
|
ee09377997 | ||
|
|
1921b22a57 | ||
|
|
8718989c52 | ||
|
|
36069cd0e4 | ||
|
|
39b19ac361 | ||
|
|
279488e7ed | ||
|
|
e28e15d943 | ||
|
|
b44b5551e3 | ||
|
|
0cb4b35f53 | ||
|
|
9571e71707 | ||
|
|
dfa7d88255 | ||
|
|
8d29091afa | ||
|
|
116fa0053d | ||
|
|
0c08455b32 | ||
|
|
581203ba15 | ||
|
|
50fa220471 | ||
|
|
287effdc6d | ||
|
|
1d4fcf737a | ||
|
|
e672a3bc6c | ||
|
|
ec703ceeb8 | ||
|
|
6aa4e83413 | ||
|
|
65d677ac90 | ||
|
|
aca2b6d498 | ||
|
|
dd5f72cc99 | ||
|
|
b1d3d87360 | ||
|
|
e67fc5ebd4 | ||
|
|
6a2c74537e | ||
|
|
041e226059 | ||
|
|
0418b6c6ff | ||
|
|
c9272acce2 | ||
|
|
8d51adee10 | ||
|
|
6d4cc28dcd | ||
|
|
fc35a1469f | ||
|
|
f70a37c050 | ||
|
|
181f461984 | ||
|
|
75618347fa | ||
|
|
5a57e6b835 | ||
|
|
d1d47d2599 | ||
|
|
f35938a068 | ||
|
|
888c14615d | ||
|
|
fa6c9696f0 | ||
|
|
37038449ab | ||
|
|
9bff358afd | ||
|
|
d2a45156a2 | ||
|
|
9df0a8de1c | ||
|
|
b58085fde1 | ||
|
|
5ea8d8c950 | ||
|
|
3318109044 | ||
|
|
aa1f89e253 | ||
|
|
35c77403d6 | ||
|
|
603fb7b92d | ||
|
|
6cf0c79433 | ||
|
|
3934e119d6 | ||
|
|
33a6a21fdf | ||
|
|
7800b1259f | ||
|
|
3623fc33d9 | ||
|
|
f8612eb52e | ||
|
|
8d32b0311a | ||
|
|
60bcd2d6e9 | ||
|
|
5ccb511064 | ||
|
|
78baf6497a | ||
|
|
0719e53f01 | ||
|
|
5f2c4205a5 | ||
|
|
2fe4c96706 | ||
|
|
5c634940c2 | ||
|
|
6036f8750b | ||
|
|
dbcd8f58eb | ||
|
|
8905e232e5 | ||
|
|
4f15158486 | ||
|
|
d2413abd5c | ||
|
|
f7ffadeaff | ||
|
|
f9eec856e7 | ||
|
|
62562650fe | ||
|
|
df171097c3 | ||
|
|
1977aabc9a | ||
|
|
483c4e6d0a |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -38,6 +38,7 @@ nbdist/
|
||||
######################################################################
|
||||
# Others
|
||||
*.log
|
||||
*.log.gz
|
||||
*.xml.versionsBackup
|
||||
*.swp
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-monitor-admin" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.3" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-monitor-admin/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-server:5.5.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-server:5.5.3" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-admin/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<configuration default="false" name="ruoyi-snailjob-server" type="docker-deploy" factoryName="dockerfile" server-name="Docker">
|
||||
<deployment type="dockerfile">
|
||||
<settings>
|
||||
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.0" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.3" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -8,10 +8,9 @@
|
||||
[](https://github.com/dromara/RuoYi-Vue-Plus)
|
||||
[](https://gitcode.com/dromara/RuoYi-Vue-Plus)
|
||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus/blob/5.X/LICENSE)
|
||||
[](https://www.jetbrains.com/?from=RuoYi-Vue-Plus)
|
||||
<br>
|
||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
|
||||
[]()
|
||||
[](https://gitee.com/dromara/RuoYi-Vue-Plus)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
@@ -33,12 +32,12 @@
|
||||
|
||||
MaxKey 业界领先单点登录产品 - https://gitee.com/dromara/MaxKey <br>
|
||||
CCFlow 驰聘低代码-流程-表单 - https://gitee.com/opencc/RuoYi-JFlow <br>
|
||||
数舵科技 软件定制开发APP小程序等 - http://www.shuduokeji.com/ <br>
|
||||
数舵科技 软件定制开发APP小程序等 - https://www.shuduokeji.com/ <br>
|
||||
引迈信息 软件开发平台 - https://www.jnpfsoft.com/index.html?from=plus-doc <br>
|
||||
<font color="red">**启山商城系统 多租户商城源码可免费商用可二次开发 - https://www.73app.cn/** </font><br>
|
||||
Mall4J 高质量Java商城系统 - https://www.mall4j.com/cn/?statId=11 <br>
|
||||
aizuda flowlong 工作流 - https://gitee.com/aizuda/flowlong <br>
|
||||
Ruoyi-Plus-Uniapp - https://ruoyi.plus <br>
|
||||
Topiam IAM/IDaaS身份管理平台 - https://www.topiam.cn/ <br>
|
||||
|
||||
[如何成为赞助商 加群联系作者详谈 每日PV2500-3000 IP1700-2500](https://plus-doc.dromara.org/#/common/add_group)
|
||||
|
||||
|
||||
30
pom.xml
30
pom.xml
@@ -13,32 +13,32 @@
|
||||
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
|
||||
|
||||
<properties>
|
||||
<revision>5.5.0</revision>
|
||||
<spring-boot.version>3.5.6</spring-boot.version>
|
||||
<revision>5.5.3</revision>
|
||||
<spring-boot.version>3.5.10</spring-boot.version>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<java.version>17</java.version>
|
||||
<mybatis.version>3.5.16</mybatis.version>
|
||||
<springdoc.version>2.8.13</springdoc.version>
|
||||
<mybatis.version>3.5.19</mybatis.version>
|
||||
<springdoc.version>2.8.15</springdoc.version>
|
||||
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
<velocity.version>2.3</velocity.version>
|
||||
<satoken.version>1.44.0</satoken.version>
|
||||
<mybatis-plus.version>3.5.14</mybatis-plus.version>
|
||||
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<hutool.version>5.8.40</hutool.version>
|
||||
<spring-boot-admin.version>3.5.3</spring-boot-admin.version>
|
||||
<redisson.version>3.51.0</redisson.version>
|
||||
<hutool.version>5.8.43</hutool.version>
|
||||
<spring-boot-admin.version>3.5.6</spring-boot-admin.version>
|
||||
<redisson.version>3.52.0</redisson.version>
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<dynamic-ds.version>4.3.1</dynamic-ds.version>
|
||||
<snailjob.version>1.8.0</snailjob.version>
|
||||
<snailjob.version>1.9.0</snailjob.version>
|
||||
<mapstruct-plus.version>1.5.0</mapstruct-plus.version>
|
||||
<mapstruct-plus.lombok.version>0.2.0</mapstruct-plus.lombok.version>
|
||||
<lombok.version>1.18.40</lombok.version>
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<bouncycastle.version>1.80</bouncycastle.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<!-- 离线IP地址定位库 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<ip2region.version>3.3.2</ip2region.version>
|
||||
<!-- OSS 配置 -->
|
||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
@@ -46,9 +46,9 @@
|
||||
<!-- 限制框架中的fastjson版本 -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20250603</anyline.version>
|
||||
<anyline.version>8.7.3-20251210</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
<warm-flow.version>1.8.1</warm-flow.version>
|
||||
<warm-flow.version>1.8.4</warm-flow.version>
|
||||
|
||||
<!-- 插件版本 -->
|
||||
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
|
||||
@@ -226,13 +226,13 @@
|
||||
<artifactId>s3</artifactId>
|
||||
<version>${aws.sdk.version}</version>
|
||||
</dependency>
|
||||
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
|
||||
<!-- 客户端的性能增强传输管理器 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3-transfer-manager</artifactId>
|
||||
<version>${aws.sdk.version}</version>
|
||||
</dependency>
|
||||
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
|
||||
<!-- 适用于 Netty 的客户端 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>netty-nio-client</artifactId>
|
||||
|
||||
@@ -48,6 +48,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -106,7 +107,7 @@ public class AuthController {
|
||||
Long userId = LoginHelper.getUserId();
|
||||
scheduledExecutorService.schedule(() -> {
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setMessage("欢迎登录RuoYi-Vue-Plus后台管理系统");
|
||||
dto.setMessage(DateUtils.getTodayHour(new Date()) + "好,欢迎登录 RuoYi-Vue-Plus 后台管理系统");
|
||||
dto.setUserIds(List.of(userId));
|
||||
SseMessageUtils.publishMessage(dto);
|
||||
}, 5, TimeUnit.SECONDS);
|
||||
@@ -147,8 +148,8 @@ public class AuthController {
|
||||
StpUtil.checkLogin();
|
||||
// 获取第三方登录信息
|
||||
AuthResponse<AuthUser> response = SocialUtils.loginAuth(
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
loginBody.getSource(), loginBody.getSocialCode(),
|
||||
loginBody.getSocialState(), socialProperties);
|
||||
AuthUser authUserData = response.getData();
|
||||
// 判断授权响应是否成功
|
||||
if (!response.ok()) {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package org.dromara.web.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
@@ -14,14 +15,13 @@ import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.reflect.ReflectUtils;
|
||||
import org.dromara.common.mail.config.properties.MailProperties;
|
||||
import org.dromara.common.mail.utils.MailUtils;
|
||||
import org.dromara.common.ratelimiter.annotation.RateLimiter;
|
||||
import org.dromara.common.ratelimiter.enums.LimitType;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.web.core.WaveAndCircleCaptcha;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import org.dromara.sms4j.api.SmsBlend;
|
||||
import org.dromara.sms4j.api.entity.SmsResponse;
|
||||
import org.dromara.sms4j.core.factory.SmsFactory;
|
||||
@@ -33,6 +33,7 @@ import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.awt.*;
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
@@ -130,19 +131,21 @@ public class CaptchaController {
|
||||
String uuid = IdUtil.simpleUUID();
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
// 生成验证码
|
||||
CaptchaType captchaType = captchaProperties.getType();
|
||||
String captchaType = captchaProperties.getType();
|
||||
CodeGenerator codeGenerator;
|
||||
if (CaptchaType.MATH == captchaType) {
|
||||
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false);
|
||||
if ("math".equals(captchaType)) {
|
||||
codeGenerator = new MathGenerator(captchaProperties.getNumberLength(), false);
|
||||
} else {
|
||||
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength());
|
||||
codeGenerator = new RandomGenerator(captchaProperties.getCharLength());
|
||||
}
|
||||
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
|
||||
WaveAndCircleCaptcha captcha = new WaveAndCircleCaptcha(160, 60);
|
||||
// captcha.setBackground(Color.WHITE); // 不设置就是透明底
|
||||
captcha.setFont(new Font("Arial", Font.BOLD, 45));
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.createCode();
|
||||
// 如果是数学验证码,使用SpEL表达式处理验证码结果
|
||||
String code = captcha.getCode();
|
||||
if (CaptchaType.MATH == captchaType) {
|
||||
if ("math".equals(captchaType)) {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
|
||||
code = exp.getValue(String.class);
|
||||
|
||||
@@ -24,9 +24,7 @@ captcha:
|
||||
# 是否启用验证码校验
|
||||
enable: true
|
||||
# 验证码类型 math 数组计算 char 字符验证
|
||||
type: MATH
|
||||
# line 线段干扰 circle 圆圈干扰 shear 扭曲干扰
|
||||
category: CIRCLE
|
||||
type: math
|
||||
# 数字验证码位数
|
||||
numberLength: 1
|
||||
# 字符验证码长度
|
||||
|
||||
@@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效
|
||||
user.not.exists=对不起, 您的账号:{0} 不存在.
|
||||
user.password.not.match=用户不存在/密码错误
|
||||
user.password.retry.limit.count=密码输入错误{0}次
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,账户锁定{1}分钟
|
||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||
role.blocked=角色已封禁,请联系管理员
|
||||
@@ -47,10 +47,10 @@ repeat.submit.message=不允许重复提交,请稍候再试
|
||||
rate.limiter.message=访问过于频繁,请稍候再试
|
||||
sms.code.not.blank=短信验证码不能为空
|
||||
sms.code.retry.limit.count=短信验证码输入错误{0}次
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,账户锁定{1}分钟
|
||||
email.code.not.blank=邮箱验证码不能为空
|
||||
email.code.retry.limit.count=邮箱验证码输入错误{0}次
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,账户锁定{1}分钟
|
||||
xcx.code.not.blank=小程序[code]不能为空
|
||||
social.source.not.blank=第三方登录平台[source]不能为空
|
||||
social.code.not.blank=第三方登录平台[code]不能为空
|
||||
|
||||
@@ -5,7 +5,7 @@ user.jcaptcha.expire=验证码已失效
|
||||
user.not.exists=对不起, 您的账号:{0} 不存在.
|
||||
user.password.not.match=用户不存在/密码错误
|
||||
user.password.retry.limit.count=密码输入错误{0}次
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定{1}分钟
|
||||
user.password.retry.limit.exceed=密码输入错误{0}次,账户锁定{1}分钟
|
||||
user.password.delete=对不起,您的账号:{0} 已被删除
|
||||
user.blocked=对不起,您的账号:{0} 已禁用,请联系管理员
|
||||
role.blocked=角色已封禁,请联系管理员
|
||||
@@ -47,10 +47,10 @@ repeat.submit.message=不允许重复提交,请稍候再试
|
||||
rate.limiter.message=访问过于频繁,请稍候再试
|
||||
sms.code.not.blank=短信验证码不能为空
|
||||
sms.code.retry.limit.count=短信验证码输入错误{0}次
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
sms.code.retry.limit.exceed=短信验证码输入错误{0}次,账户锁定{1}分钟
|
||||
email.code.not.blank=邮箱验证码不能为空
|
||||
email.code.retry.limit.count=邮箱验证码输入错误{0}次
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,帐户锁定{1}分钟
|
||||
email.code.retry.limit.exceed=邮箱验证码输入错误{0}次,账户锁定{1}分钟
|
||||
xcx.code.not.blank=小程序[code]不能为空
|
||||
social.source.not.blank=第三方登录平台[source]不能为空
|
||||
social.code.not.blank=第三方登录平台[code]不能为空
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>5.5.0</revision>
|
||||
<revision>5.5.3</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -3,17 +3,12 @@ package org.dromara.common.core.config;
|
||||
import jakarta.annotation.PreDestroy;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.concurrent.BasicThreadFactory;
|
||||
import org.dromara.common.core.config.properties.ThreadPoolProperties;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.Threads;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.core.task.VirtualThreadTaskExecutor;
|
||||
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 线程池配置
|
||||
@@ -22,7 +17,6 @@ import java.util.concurrent.ThreadPoolExecutor;
|
||||
**/
|
||||
@Slf4j
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(ThreadPoolProperties.class)
|
||||
public class ThreadPoolConfig {
|
||||
|
||||
/**
|
||||
@@ -50,7 +44,7 @@ public class ThreadPoolConfig {
|
||||
@Override
|
||||
protected void afterExecute(Runnable r, Throwable t) {
|
||||
super.afterExecute(r, t);
|
||||
Threads.printException(r, t);
|
||||
printException(r, t);
|
||||
}
|
||||
};
|
||||
this.scheduledExecutorService = scheduledThreadPoolExecutor;
|
||||
@@ -59,15 +53,57 @@ public class ThreadPoolConfig {
|
||||
|
||||
/**
|
||||
* 销毁事件
|
||||
* 停止线程池
|
||||
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
|
||||
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
|
||||
* 如果仍然超時,則強制退出.
|
||||
* 另对在shutdown时线程本身被调用中断做了处理.
|
||||
*/
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
try {
|
||||
log.info("====关闭后台任务任务线程池====");
|
||||
Threads.shutdownAndAwaitTermination(scheduledExecutorService);
|
||||
ScheduledExecutorService pool = scheduledExecutorService;
|
||||
if (pool != null && !pool.isShutdown()) {
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
|
||||
pool.shutdownNow();
|
||||
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
|
||||
log.info("Pool did not terminate");
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
pool.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error(e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印线程异常信息
|
||||
*/
|
||||
public static void printException(Runnable r, Throwable t) {
|
||||
if (t == null && r instanceof Future<?>) {
|
||||
try {
|
||||
Future<?> future = (Future<?>) r;
|
||||
if (future.isDone()) {
|
||||
future.get();
|
||||
}
|
||||
} catch (CancellationException ce) {
|
||||
t = ce;
|
||||
} catch (ExecutionException ee) {
|
||||
t = ee.getCause();
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (t != null) {
|
||||
log.error(t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package org.dromara.common.core.config.properties;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* 线程池 配置属性
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Data
|
||||
@ConfigurationProperties(prefix = "thread-pool")
|
||||
public class ThreadPoolProperties {
|
||||
|
||||
/**
|
||||
* 是否开启线程池
|
||||
*/
|
||||
private boolean enabled;
|
||||
|
||||
/**
|
||||
* 队列最大长度
|
||||
*/
|
||||
private int queueCapacity;
|
||||
|
||||
/**
|
||||
* 线程池维护线程所允许的空闲时间
|
||||
*/
|
||||
private int keepAliveSeconds;
|
||||
|
||||
}
|
||||
@@ -82,4 +82,10 @@ public interface SystemConstants {
|
||||
*/
|
||||
Long DEFAULT_DEPT_ID = 100L;
|
||||
|
||||
/**
|
||||
* 排除敏感属性字段
|
||||
*/
|
||||
String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -67,7 +67,8 @@ public class CompleteTaskDTO implements Serializable {
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
if (variables == null) {
|
||||
return new HashMap<>(16);
|
||||
variables = new HashMap<>(16);
|
||||
return variables;
|
||||
}
|
||||
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
|
||||
return variables;
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.common.core.domain.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 流程实例业务扩展对象
|
||||
*
|
||||
* @author may
|
||||
* @date 2025-08-05
|
||||
*/
|
||||
@Data
|
||||
public class FlowInstanceBizExtDTO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 流程实例ID
|
||||
*/
|
||||
private Long instanceId;
|
||||
|
||||
/**
|
||||
* 业务ID
|
||||
*/
|
||||
private String businessId;
|
||||
|
||||
/**
|
||||
* 业务编码
|
||||
*/
|
||||
private String businessCode;
|
||||
|
||||
/**
|
||||
* 业务标题
|
||||
*/
|
||||
private String businessTitle;
|
||||
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.dromara.common.core.domain.dto;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
@@ -40,6 +41,11 @@ public class StartProcessDTO implements Serializable {
|
||||
*/
|
||||
private Map<String, Object> variables;
|
||||
|
||||
/**
|
||||
* 流程业务扩展信息
|
||||
*/
|
||||
private FlowInstanceBizExtDTO bizExt;
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
if (variables == null) {
|
||||
return new HashMap<>(16);
|
||||
@@ -47,4 +53,11 @@ public class StartProcessDTO implements Serializable {
|
||||
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
|
||||
return variables;
|
||||
}
|
||||
|
||||
public FlowInstanceBizExtDTO getBizExt() {
|
||||
if (ObjectUtil.isNull(bizExt)) {
|
||||
bizExt = new FlowInstanceBizExtDTO();
|
||||
}
|
||||
return bizExt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ public class UserDTO implements Serializable {
|
||||
private String sex;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
* 账号状态(0正常 1停用)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
package org.dromara.common.core.service;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 通用 参数配置服务
|
||||
*
|
||||
@@ -15,4 +21,80 @@ public interface ConfigService {
|
||||
*/
|
||||
String getConfigValue(String configKey);
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取布尔值
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Boolean 值
|
||||
*/
|
||||
default Boolean getConfigBool(String configKey) {
|
||||
return Convert.toBool(getConfigValue(configKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取整数值
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Integer 值
|
||||
*/
|
||||
default Integer getConfigInt(String configKey) {
|
||||
return Convert.toInt(getConfigValue(configKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取长整型值
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Long 值
|
||||
*/
|
||||
default Long getConfigLong(String configKey) {
|
||||
return Convert.toLong(getConfigValue(configKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取 BigDecimal 值
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return BigDecimal 值
|
||||
*/
|
||||
default BigDecimal getConfigDecimal(String configKey) {
|
||||
return Convert.toBigDecimal(getConfigValue(configKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取 Map 类型的配置
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Dict 对象,如果配置为空或无法解析,返回空 Dict
|
||||
*/
|
||||
Dict getConfigMap(String configKey);
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取 Map 类型的配置列表
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Dict 列表,如果配置为空或无法解析,返回空列表
|
||||
*/
|
||||
List<Dict> getConfigArrayMap(String configKey);
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取指定类型的配置对象
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @param clazz 目标对象类型
|
||||
* @param <T> 目标对象泛型
|
||||
* @return 对象实例,如果配置为空或无法解析,返回 null
|
||||
*/
|
||||
<T> T getConfigObject(String configKey, Class<T> clazz);
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取指定类型的配置列表
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @param clazz 目标元素类型
|
||||
* @param <T> 元素类型泛型
|
||||
* @return 指定类型列表,如果配置为空或无法解析,返回空列表
|
||||
*/
|
||||
<T> List<T> getConfigArray(String configKey, Class<T> clazz);
|
||||
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public interface WorkflowService {
|
||||
* @param businessIds 业务id
|
||||
* @return 结果
|
||||
*/
|
||||
boolean deleteInstance(List<Long> businessIds);
|
||||
boolean deleteInstance(List<String> businessIds);
|
||||
|
||||
/**
|
||||
* 获取当前流程状态
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package org.dromara.common.core.utils;
|
||||
|
||||
import cn.hutool.core.date.DateUnit;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import org.apache.commons.lang3.time.DateFormatUtils;
|
||||
import org.dromara.common.core.enums.FormatsType;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
@@ -297,4 +299,80 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据指定日期时间获取时间段(凌晨 / 上午 / 中午 / 下午 / 晚上)
|
||||
*
|
||||
* @param date 日期时间
|
||||
* @return 时间段描述
|
||||
*/
|
||||
public static String getTodayHour(Date date) {
|
||||
int hour = DateUtil.hour(date, true);
|
||||
if (hour <= 6) {
|
||||
return "凌晨";
|
||||
} else if (hour < 12) {
|
||||
return "上午";
|
||||
} else if (hour == 12) {
|
||||
return "中午";
|
||||
} else if (hour <= 18) {
|
||||
return "下午";
|
||||
} else {
|
||||
return "晚上";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将日期格式化为仿微信的友好时间
|
||||
* <p>
|
||||
* 规则说明:
|
||||
* 1. 未来时间:yyyy-MM-dd HH:mm
|
||||
* 2. 今天:
|
||||
* - 1 分钟内:刚刚
|
||||
* - 1 小时内:X 分钟前
|
||||
* - 超过 1 小时:凌晨/上午/中午/下午/晚上 HH:mm
|
||||
* 3. 昨天:昨天 HH:mm
|
||||
* 4. 本周:周X HH:mm
|
||||
* 5. 今年内:MM-dd HH:mm
|
||||
* 6. 非今年:yyyy-MM-dd HH:mm
|
||||
*
|
||||
* @param date 日期时间
|
||||
* @return 格式化后的时间描述
|
||||
*/
|
||||
public static String formatFriendlyTime(Date date) {
|
||||
if (date == null) {
|
||||
return "";
|
||||
}
|
||||
Date now = DateUtil.date();
|
||||
|
||||
// 未来时间或非今年
|
||||
if (date.after(now) || DateUtil.year(date) != DateUtil.year(now)) {
|
||||
return parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM, date);
|
||||
}
|
||||
|
||||
// 今天
|
||||
if (DateUtil.isSameDay(date, now)) {
|
||||
long minutes = DateUtil.between(date, now, DateUnit.MINUTE);
|
||||
if (minutes < 1) {
|
||||
return "刚刚";
|
||||
}
|
||||
if (minutes < 60) {
|
||||
return minutes + "分钟前";
|
||||
}
|
||||
return getTodayHour(date) + " " + DateUtil.format(date, "HH:mm");
|
||||
}
|
||||
|
||||
// 昨天
|
||||
if (DateUtil.isSameDay(date, DateUtil.yesterday())) {
|
||||
return "昨天 " + DateUtil.format(date, "HH:mm");
|
||||
}
|
||||
|
||||
// 本周
|
||||
if (DateUtil.isSameWeek(date, now, true)) {
|
||||
return DateUtil.dayOfWeekEnum(date).toChinese("周")
|
||||
+ " " + DateUtil.format(date, "HH:mm");
|
||||
}
|
||||
|
||||
// 今年内其它时间
|
||||
return DateUtil.format(date, "MM-dd HH:mm");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.dromara.common.core.utils;
|
||||
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 脱敏工具类
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class DesensitizedUtils extends DesensitizedUtil {
|
||||
|
||||
/**
|
||||
* 灵活脱敏方法
|
||||
*
|
||||
* @param value 原始字符串
|
||||
* @param prefixVisible 前面可见长度
|
||||
* @param suffixVisible 后面可见长度
|
||||
* @param maskLength 中间掩码长度(固定显示多少 *,如果总长度不足则自动缩减)
|
||||
* @return 脱敏后字符串
|
||||
*/
|
||||
public static String mask(String value, int prefixVisible, int suffixVisible, int maskLength) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
int len = value.length();
|
||||
int prefixMaskLimit = prefixVisible + maskLength;
|
||||
int fullLimit = prefixMaskLimit + suffixVisible;
|
||||
|
||||
// 规则 1:长度 <= 中间掩码长度 → 全掩码
|
||||
if (len <= maskLength) {
|
||||
return StrUtil.repeat('*', len);
|
||||
}
|
||||
String mask = StrUtil.repeat('*', maskLength);
|
||||
|
||||
// 规则 2:长度 <= 前缀 + 中间掩码
|
||||
if (len <= prefixMaskLimit) {
|
||||
return value.substring(0, len - maskLength) + mask;
|
||||
}
|
||||
|
||||
String prefix = value.substring(0, prefixVisible);
|
||||
|
||||
// 规则 3:长度 <= 前缀 + 中间掩码 + 后缀
|
||||
if (len <= fullLimit) {
|
||||
int suffixLen = len - prefixMaskLimit;
|
||||
return prefix + mask + value.substring(len - suffixLen);
|
||||
}
|
||||
|
||||
// 规则 4:标准形态
|
||||
return prefix + mask + value.substring(len - suffixVisible);
|
||||
}
|
||||
|
||||
/**
|
||||
* 高安全级别脱敏方法(Token / 私钥)
|
||||
*
|
||||
* @param value 原始字符串
|
||||
* @param prefixVisible 前面可见长度(推荐0~4)
|
||||
* @param suffixVisible 后面可见长度(推荐0~4)
|
||||
* @return 脱敏后字符串
|
||||
*/
|
||||
public static String maskHighSecurity(String value, int prefixVisible, int suffixVisible) {
|
||||
if (StrUtil.isBlank(value)) {
|
||||
return value;
|
||||
}
|
||||
int len = value.length();
|
||||
|
||||
// 规则1:长度 <= 前缀可见长度 → 全部掩码
|
||||
if (len <= prefixVisible) {
|
||||
return StrUtil.repeat('*', len);
|
||||
}
|
||||
|
||||
// 规则2:长度 <= 前缀 + 后缀可见长度 → 优先掩码后面
|
||||
if (len <= prefixVisible + suffixVisible) {
|
||||
return value.substring(0, len - prefixVisible) + StrUtil.repeat('*', prefixVisible);
|
||||
}
|
||||
|
||||
// 规则3:标准形态 → 前后可见,中间全部掩码
|
||||
return value.substring(0, prefixVisible)
|
||||
+ StrUtil.repeat('*', len - prefixVisible - suffixVisible)
|
||||
+ value.substring(len - suffixVisible);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
package org.dromara.common.core.utils;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.concurrent.*;
|
||||
|
||||
/**
|
||||
* 线程相关工具类.
|
||||
*
|
||||
* @author ruoyi
|
||||
*/
|
||||
@Slf4j
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Threads {
|
||||
/**
|
||||
* 停止线程池
|
||||
* 先使用shutdown, 停止接收新任务并尝试完成所有已存在任务.
|
||||
* 如果超时, 则调用shutdownNow, 取消在workQueue中Pending的任务,并中断所有阻塞函数.
|
||||
* 如果仍然超時,則強制退出.
|
||||
* 另对在shutdown时线程本身被调用中断做了处理.
|
||||
*/
|
||||
public static void shutdownAndAwaitTermination(ExecutorService pool) {
|
||||
if (pool != null && !pool.isShutdown()) {
|
||||
pool.shutdown();
|
||||
try {
|
||||
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
|
||||
pool.shutdownNow();
|
||||
if (!pool.awaitTermination(120, TimeUnit.SECONDS)) {
|
||||
log.info("Pool did not terminate");
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException ie) {
|
||||
pool.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印线程异常信息
|
||||
*/
|
||||
public static void printException(Runnable r, Throwable t) {
|
||||
if (t == null && r instanceof Future<?>) {
|
||||
try {
|
||||
Future<?> future = (Future<?>) r;
|
||||
if (future.isDone()) {
|
||||
future.get();
|
||||
}
|
||||
} catch (CancellationException ce) {
|
||||
t = ce;
|
||||
} catch (ExecutionException ee) {
|
||||
t = ee.getCause();
|
||||
} catch (InterruptedException ie) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
if (t != null) {
|
||||
log.error(t.getMessage(), t);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,51 +20,24 @@ public class AddressUtils {
|
||||
public static final String UNKNOWN_IP = "XX XX";
|
||||
// 内网地址
|
||||
public static final String LOCAL_ADDRESS = "内网IP";
|
||||
// 未知地址
|
||||
public static final String UNKNOWN_ADDRESS = "未知";
|
||||
|
||||
public static String getRealAddressByIP(String ip) {
|
||||
// 处理空串并过滤HTML标签
|
||||
ip = HtmlUtil.cleanHtmlTag(StringUtils.blankToDefault(ip,""));
|
||||
// 判断是否为IPv4
|
||||
if (NetUtils.isIPv4(ip)) {
|
||||
return resolverIPv4Region(ip);
|
||||
}
|
||||
boolean isIPv4 = NetUtils.isIPv4(ip);
|
||||
// 判断是否为IPv6
|
||||
if (NetUtils.isIPv6(ip)) {
|
||||
return resolverIPv6Region(ip);
|
||||
}
|
||||
boolean isIPv6 = NetUtils.isIPv6(ip);
|
||||
// 如果不是IPv4或IPv6,则返回未知IP
|
||||
return UNKNOWN_IP;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IPv4地址查询IP归属行政区域
|
||||
* @param ip ipv4地址
|
||||
* @return 归属行政区域
|
||||
*/
|
||||
private static String resolverIPv4Region(String ip){
|
||||
if (!isIPv4 && !isIPv6) {
|
||||
return UNKNOWN_IP;
|
||||
}
|
||||
// 内网不查询
|
||||
if (NetUtils.isInnerIP(ip)) {
|
||||
if ((isIPv4 && NetUtils.isInnerIP(ip)) || (isIPv6 && NetUtils.isInnerIPv6(ip))) {
|
||||
return LOCAL_ADDRESS;
|
||||
}
|
||||
return RegionUtils.getCityInfo(ip);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IPv6地址查询IP归属行政区域
|
||||
* @param ip ipv6地址
|
||||
* @return 归属行政区域
|
||||
*/
|
||||
private static String resolverIPv6Region(String ip){
|
||||
// 内网不查询
|
||||
if (NetUtils.isInnerIPv6(ip)) {
|
||||
return LOCAL_ADDRESS;
|
||||
}
|
||||
log.warn("ip2region不支持IPV6地址解析:{}", ip);
|
||||
// 不支持IPv6,不再进行没有必要的IP地址信息的解析,直接返回
|
||||
// 如有需要,可自行实现IPv6地址信息解析逻辑,并在这里返回
|
||||
return UNKNOWN_ADDRESS;
|
||||
// Tips:Ip2Region 提供了精简的IPv6地址库,精简的IPv6地址库并不能完全支持IPv6地址的查询,且准确度上可能会存在问题,如需要准确的IPv6地址查询,建议自行实现
|
||||
return RegionUtils.getRegion(ip);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,50 +1,154 @@
|
||||
package org.dromara.common.core.utils.ip;
|
||||
|
||||
import cn.hutool.core.io.resource.NoResourceException;
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
import org.lionsoul.ip2region.service.Config;
|
||||
import org.lionsoul.ip2region.service.Ip2Region;
|
||||
import org.lionsoul.ip2region.xdb.Util;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 根据ip地址定位工具类,离线方式
|
||||
* 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">集成 ip2region 实现离线IP地址定位库</a>
|
||||
* IP地址行政区域工具类
|
||||
* 参考地址:<a href="https://gitee.com/lionsoul/ip2region/tree/master/binding/java">ip2region xdb java 查询客户端实现</a>
|
||||
* xdb数据库文件下载:<a href="https://gitee.com/lionsoul/ip2region/tree/master/data">ip2region data</a>
|
||||
*
|
||||
* @author lishuyan
|
||||
* @author 秋辞未寒
|
||||
*/
|
||||
@Slf4j
|
||||
public class RegionUtils {
|
||||
|
||||
// IP地址库文件名称
|
||||
public static final String IP_XDB_FILENAME = "ip2region.xdb";
|
||||
// 默认IPv4地址库文件路径
|
||||
// 下载地址:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v4.xdb
|
||||
public static final String DEFAULT_IPV4_XDB_PATH = "ip2region_v4.xdb";
|
||||
|
||||
private static final Searcher SEARCHER;
|
||||
// 默认IPv6地址库文件路径
|
||||
// 下载地址:https://gitee.com/lionsoul/ip2region/blob/master/data/ip2region_v6.xdb
|
||||
public static final String DEFAULT_IPV6_XDB_PATH = "ip2region_v6.xdb";
|
||||
|
||||
// 默认缓存切片大小为15MB(仅针对BufferCache全量读取有效,如果你的xdb数据库很大,合理设置该值可以有效提升BufferCache模式下的查询效率,具体可以查看Ip2Region的README)
|
||||
// 注意:设置过大的值可能会申请内存时,因内存不足而导致OOM,请合理设置该值。
|
||||
// README:https://gitee.com/lionsoul/ip2region/tree/master/binding/java
|
||||
public static final int DEFAULT_CACHE_SLICE_BYTES = 1024 * 1024 * 15;
|
||||
|
||||
// 未知地址
|
||||
public static final String UNKNOWN_ADDRESS = "未知";
|
||||
|
||||
// Ip2Region服务实例
|
||||
private static Ip2Region ip2Region;
|
||||
|
||||
// 初始化Ip2Region服务实例
|
||||
static {
|
||||
try {
|
||||
// 1、将 ip2region 数据库文件 xdb 从 ClassPath 加载到内存。
|
||||
// 2、基于加载到内存的 xdb 数据创建一个 Searcher 查询对象。
|
||||
SEARCHER = Searcher.newWithBuffer(ResourceUtil.readBytes(IP_XDB_FILENAME));
|
||||
log.info("RegionUtils初始化成功,加载IP地址库数据成功!");
|
||||
} catch (NoResourceException e) {
|
||||
throw new ServiceException("RegionUtils初始化失败,原因:IP地址库数据不存在!");
|
||||
// 注意:Ip2Region 的xdb文件加载策略 CachePolicy 有三种,分别是:BufferCache(全量读取xdb到内存中)、VIndexCache(默认策略,按需读取并缓存)、NoCache(实时读取)
|
||||
// 本项目工具使用的 CachePolicy 为 BufferCache,BufferCache会加载整个xdb文件到内存中,setXdbInputStream 仅支持 BufferCache 策略。
|
||||
// 因为加载整个xdb文件会耗费非常大的内存,如果你不希望加载整个xdb到内存中,更推荐使用 VIndexCache 或 NoCache(即实时读取文件)策略和 setXdbPath/setXdbFile 加载方法(需要注意的一点,setXdbPath 和 setXdbFile 不支持读取ClassPath(即源码和resource目录)中的文件)。
|
||||
// 一般而言,更建议把xdb数据库放到一个指定的文件目录中(即不打包进jar包中),然后使用 VIndexCache + 配合SearcherPool的并发池读取数据,更方便随时更新xdb数据库
|
||||
|
||||
InputStream v4InputStream = ResourceUtil.getStream(DEFAULT_IPV4_XDB_PATH);
|
||||
|
||||
// IPv4配置
|
||||
Config v4Config = Config.custom()
|
||||
.setCachePolicy(Config.BufferCache)
|
||||
//.setXdbFile(v4TempXdb)
|
||||
.setXdbInputStream(v4InputStream)
|
||||
//
|
||||
.setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES)
|
||||
.asV4();
|
||||
|
||||
// IPv6配置
|
||||
Config v6Config = null;
|
||||
InputStream v6XdbInputStream = ResourceUtil.getStreamSafe(DEFAULT_IPV6_XDB_PATH);
|
||||
if (v6XdbInputStream == null) {
|
||||
log.warn("未加载 IPv6 地址库:未在类路径下找到文件 {}。当前仅启用 IPv4 查询。如需启用 IPv6,请将 ip2region_v6.xdb 放置到 resources 目录", DEFAULT_IPV6_XDB_PATH);
|
||||
} else {
|
||||
v6Config = Config.custom()
|
||||
.setCachePolicy(Config.BufferCache)
|
||||
//.setXdbFile(v6TempXdb)
|
||||
.setXdbInputStream(v6XdbInputStream)
|
||||
.setCacheSliceBytes(DEFAULT_CACHE_SLICE_BYTES)
|
||||
.asV6();
|
||||
}
|
||||
|
||||
// 初始化Ip2Region实例
|
||||
RegionUtils.ip2Region = Ip2Region.create(v4Config, v6Config);
|
||||
log.debug("IP工具初始化成功,加载IP地址库数据成功!");
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException("RegionUtils初始化失败,原因:" + e.getMessage());
|
||||
throw new ServiceException("RegionUtils初始化失败,原因:{}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IP地址离线获取城市
|
||||
*
|
||||
* @param ipString ip地址字符串
|
||||
*/
|
||||
public static String getCityInfo(String ip) {
|
||||
public static String getRegion(String ipString) {
|
||||
try {
|
||||
// 3、执行查询
|
||||
String region = SEARCHER.search(StringUtils.trim(ip));
|
||||
return region.replace("0|", "").replace("|0", "");
|
||||
String region = ip2Region.search(ipString);
|
||||
if (StringUtils.isBlank(region)) {
|
||||
region = UNKNOWN_ADDRESS;
|
||||
}
|
||||
return region;
|
||||
} catch (Exception e) {
|
||||
log.error("IP地址离线获取城市异常 {}", ip);
|
||||
return "未知";
|
||||
log.error("IP地址离线获取城市异常 {}", ipString);
|
||||
return UNKNOWN_ADDRESS;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据IP地址离线获取城市
|
||||
*
|
||||
* @param ipBytes ip地址字节数组
|
||||
*/
|
||||
public static String getRegion(byte[] ipBytes) {
|
||||
try {
|
||||
String region = ip2Region.search(ipBytes);
|
||||
if (StringUtils.isBlank(region)) {
|
||||
region = UNKNOWN_ADDRESS;
|
||||
}
|
||||
return region;
|
||||
} catch (Exception e) {
|
||||
log.error("IP地址离线获取城市异常 {}", Util.ipToString(ipBytes));
|
||||
return UNKNOWN_ADDRESS;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭Ip2Region服务
|
||||
*/
|
||||
public static void close() {
|
||||
if (ip2Region == null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ip2Region.close(10000);
|
||||
} catch (Exception e) {
|
||||
log.error("Ip2Region服务关闭异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭Ip2Region服务
|
||||
*
|
||||
* @param timeout 关闭超时时间
|
||||
*/
|
||||
public static void close(final Duration timeout) {
|
||||
if (ip2Region == null) {
|
||||
return;
|
||||
}
|
||||
if (timeout == null) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ip2Region.close(timeout.toMillis());
|
||||
} catch (Exception e) {
|
||||
log.error("Ip2Region服务关闭异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.dromara.common.excel.annotation;
|
||||
|
||||
import org.dromara.common.excel.core.ExcelOptionsProvider;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* Excel动态下拉选项注解
|
||||
*
|
||||
* @author Angus
|
||||
*/
|
||||
@Target({ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Inherited
|
||||
public @interface ExcelDynamicOptions {
|
||||
|
||||
/**
|
||||
* 提供者类全限定名
|
||||
* <p>
|
||||
* {@link org.dromara.common.excel.core.ExcelOptionsProvider} 接口实现类 class
|
||||
*/
|
||||
Class<? extends ExcelOptionsProvider> providerClass();
|
||||
}
|
||||
@@ -28,7 +28,7 @@ public class ExcelBigNumberConvert implements Converter<Long> {
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -29,7 +29,10 @@ public class CellMergeHandler {
|
||||
// 行合并开始下标
|
||||
this.rowIndex = hasTitle ? 1 : 0;
|
||||
}
|
||||
|
||||
private CellMergeHandler(final boolean hasTitle, final int rowIndex) {
|
||||
this.hasTitle = hasTitle;
|
||||
this.rowIndex = hasTitle ? rowIndex : 0;
|
||||
}
|
||||
@SneakyThrows
|
||||
public List<CellRangeAddress> handle(List<?> rows) {
|
||||
// 如果入参为空集合则返回空集
|
||||
@@ -103,6 +106,10 @@ public class CellMergeHandler {
|
||||
}
|
||||
|
||||
if (isAddResult && i > current) {
|
||||
//如果是同一行,则跳过合并
|
||||
if (current + rowIndex == lastRow) {
|
||||
continue;
|
||||
}
|
||||
result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
|
||||
}
|
||||
}
|
||||
@@ -147,12 +154,12 @@ public class CellMergeHandler {
|
||||
private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
|
||||
final String[] mergeBy = cellMerge.mergeBy();
|
||||
if (StrUtil.isAllNotBlank(mergeBy)) {
|
||||
//比对当前行和上一行的各个属性值一一比对 如果全为真 则为真
|
||||
// 比对当前行和上一行的各个属性值一一比对 如果全为真 则为真
|
||||
for (String fieldName : mergeBy) {
|
||||
final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName);
|
||||
final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName);
|
||||
if (!Objects.equals(valPre, valCurrent)) {
|
||||
//依赖字段如有任一不等值,则标记为不可合并
|
||||
// 依赖字段如有任一不等值,则标记为不可合并
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -177,6 +184,16 @@ public class CellMergeHandler {
|
||||
return new FieldColumnIndex(colIndex, cellMerge);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建一个单元格合并处理器实例
|
||||
*
|
||||
* @param hasTitle 是否合并标题
|
||||
* @param rowIndex 行索引
|
||||
* @return 单元格合并处理器
|
||||
*/
|
||||
public static CellMergeHandler of(final boolean hasTitle, final int rowIndex) {
|
||||
return new CellMergeHandler(hasTitle, rowIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个单元格合并处理器实例
|
||||
|
||||
@@ -2,15 +2,16 @@ package org.dromara.common.excel.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.idev.excel.metadata.Head;
|
||||
import cn.idev.excel.write.handler.WorkbookWriteHandler;
|
||||
import cn.idev.excel.write.handler.context.WorkbookWriteHandlerContext;
|
||||
import cn.idev.excel.write.handler.SheetWriteHandler;
|
||||
import cn.idev.excel.write.merge.AbstractMergeStrategy;
|
||||
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
|
||||
import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Sheet;
|
||||
import org.apache.poi.ss.util.CellRangeAddress;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 列值重复合并策略
|
||||
@@ -18,7 +19,7 @@ import java.util.*;
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy implements WorkbookWriteHandler {
|
||||
public class CellMergeStrategy extends AbstractMergeStrategy implements SheetWriteHandler {
|
||||
|
||||
private final List<CellRangeAddress> cellList;
|
||||
|
||||
@@ -30,29 +31,34 @@ public class CellMergeStrategy extends AbstractMergeStrategy implements Workbook
|
||||
this.cellList = CellMergeHandler.of(hasTitle).handle(list);
|
||||
}
|
||||
|
||||
public CellMergeStrategy(List<?> list, boolean hasTitle, int rowIndex) {
|
||||
this.cellList = CellMergeHandler.of(hasTitle, rowIndex).handle(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void merge(Sheet sheet, Cell cell, Head head, Integer relativeRowIndex) {
|
||||
if (CollUtil.isEmpty(cellList)){
|
||||
if (CollUtil.isEmpty(cellList)) {
|
||||
return;
|
||||
}
|
||||
//单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
|
||||
// 单元格写入了,遍历合并区域,如果该Cell在区域内,但非首行,则清空
|
||||
final int rowIndex = cell.getRowIndex();
|
||||
for (CellRangeAddress cellAddresses : cellList) {
|
||||
final int firstRow = cellAddresses.getFirstRow();
|
||||
if (cellAddresses.isInRange(cell) && rowIndex != firstRow){
|
||||
if (cellAddresses.isInRange(cell) && rowIndex != firstRow) {
|
||||
cell.setBlank();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterWorkbookDispose(final WorkbookWriteHandlerContext context) {
|
||||
if (CollUtil.isEmpty(cellList)){
|
||||
public void afterSheetCreate(final WriteWorkbookHolder writeWorkbookHolder, final WriteSheetHolder writeSheetHolder) {
|
||||
if (CollUtil.isEmpty(cellList)) {
|
||||
return;
|
||||
}
|
||||
//当前表格写完后,统一写入
|
||||
// 在 Sheet 创建时提前写入合并区域;后续写入只会影响首格,不会移除合并
|
||||
final Sheet sheet = writeSheetHolder.getSheet();
|
||||
for (CellRangeAddress item : cellList) {
|
||||
context.getWriteContext().writeSheetHolder().getSheet().addMergedRegion(item);
|
||||
sheet.addMergedRegion(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||
import org.dromara.common.excel.annotation.ExcelDynamicOptions;
|
||||
import org.dromara.common.excel.annotation.ExcelEnumFormat;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
@@ -117,6 +118,15 @@ public class ExcelDownHandler implements SheetWriteHandler {
|
||||
ExcelEnumFormat format = field.getDeclaredAnnotation(ExcelEnumFormat.class);
|
||||
List<Object> values = EnumUtil.getFieldValues(format.enumClass(), format.textField());
|
||||
options = StreamUtils.toList(values, Convert::toStr);
|
||||
} else if (field.isAnnotationPresent(ExcelDynamicOptions.class)) {
|
||||
// 处理动态下拉选项
|
||||
ExcelDynamicOptions dynamicOptions = field.getDeclaredAnnotation(ExcelDynamicOptions.class);
|
||||
// 获取提供者实例
|
||||
ExcelOptionsProvider provider = SpringUtils.getBean(dynamicOptions.providerClass());
|
||||
Set<String> providerOptions = provider.getOptions();
|
||||
if (CollUtil.isNotEmpty(providerOptions)) {
|
||||
options = new ArrayList<>(providerOptions);
|
||||
}
|
||||
}
|
||||
if (ObjectUtil.isNotEmpty(options)) {
|
||||
// 仅当下拉可选项不为空时执行
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package org.dromara.common.excel.core;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Excel下拉选项数据提供接口
|
||||
*
|
||||
* @author Angus
|
||||
*/
|
||||
public interface ExcelOptionsProvider {
|
||||
|
||||
/**
|
||||
* 获取下拉选项数据
|
||||
*
|
||||
* @return 下拉选项列表
|
||||
*/
|
||||
Set<String> getOptions();
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.exc.MismatchedInputException;
|
||||
import lombok.AccessLevel;
|
||||
@@ -167,4 +168,58 @@ public class JsonUtils {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为合法 JSON(对象或数组)
|
||||
*
|
||||
* @param str 待校验字符串
|
||||
* @return true = 合法 JSON,false = 非法或空
|
||||
*/
|
||||
public static boolean isJson(String str) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
OBJECT_MAPPER.readTree(str);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为 JSON 对象({})
|
||||
*
|
||||
* @param str 待校验字符串
|
||||
* @return true = JSON 对象
|
||||
*/
|
||||
public static boolean isJsonObject(String str) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JsonNode node = OBJECT_MAPPER.readTree(str);
|
||||
return node.isObject();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断字符串是否为 JSON 数组([])
|
||||
*
|
||||
* @param str 待校验字符串
|
||||
* @return true = JSON 数组
|
||||
*/
|
||||
public static boolean isJsonArray(String str) {
|
||||
if (StringUtils.isBlank(str)) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
JsonNode node = OBJECT_MAPPER.readTree(str);
|
||||
return node.isArray();
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package org.dromara.common.json.validate;
|
||||
|
||||
import jakarta.validation.Constraint;
|
||||
import jakarta.validation.Payload;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
/**
|
||||
* JSON 格式校验注解
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Documented
|
||||
@Target({ElementType.METHOD, ElementType.FIELD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Constraint(validatedBy = JsonPatternValidator.class)
|
||||
public @interface JsonPattern {
|
||||
|
||||
/**
|
||||
* 限制 JSON 类型,默认为 {@link JsonType#ANY},即对象或数组都允许
|
||||
*/
|
||||
JsonType type() default JsonType.ANY;
|
||||
|
||||
/**
|
||||
* 校验失败时的提示消息
|
||||
*/
|
||||
String message() default "不是有效的 JSON 格式";
|
||||
|
||||
Class<?>[] groups() default {};
|
||||
|
||||
Class<? extends Payload>[] payload() default {};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package org.dromara.common.json.validate;
|
||||
|
||||
import jakarta.validation.ConstraintValidator;
|
||||
import jakarta.validation.ConstraintValidatorContext;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
|
||||
/**
|
||||
* JSON 格式校验器
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
public class JsonPatternValidator implements ConstraintValidator<JsonPattern, String> {
|
||||
|
||||
/**
|
||||
* 注解中指定的 JSON 类型枚举
|
||||
*/
|
||||
private JsonType jsonType;
|
||||
|
||||
/**
|
||||
* 初始化校验器,从注解中提取 JSON 类型
|
||||
*
|
||||
* @param annotation 注解实例
|
||||
*/
|
||||
@Override
|
||||
public void initialize(JsonPattern annotation) {
|
||||
this.jsonType = annotation.type();
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验字符串是否为合法 JSON
|
||||
*
|
||||
* @param value 待校验字符串
|
||||
* @param context 校验上下文,可用于自定义错误信息
|
||||
* @return true = 合法 JSON 或为空,false = 非法 JSON
|
||||
*/
|
||||
@Override
|
||||
public boolean isValid(String value, ConstraintValidatorContext context) {
|
||||
if (StringUtils.isBlank(value)) {
|
||||
// 交给 @NotBlank 或 @NotNull 控制是否允许为空
|
||||
return true;
|
||||
}
|
||||
// 根据 JSON 类型进行不同的校验
|
||||
return switch (jsonType) {
|
||||
case ANY -> JsonUtils.isJson(value);
|
||||
case OBJECT -> JsonUtils.isJsonObject(value);
|
||||
case ARRAY -> JsonUtils.isJsonArray(value);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.dromara.common.json.validate;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* JSON 类型枚举
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum JsonType {
|
||||
|
||||
/**
|
||||
* JSON 对象,例如 {"a":1}
|
||||
*/
|
||||
OBJECT,
|
||||
|
||||
/**
|
||||
* JSON 数组,例如 [1,2,3]
|
||||
*/
|
||||
ARRAY,
|
||||
|
||||
/**
|
||||
* 任意 JSON 类型,对象或数组都可以
|
||||
*/
|
||||
ANY
|
||||
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import org.aspectj.lang.annotation.AfterReturning;
|
||||
import org.aspectj.lang.annotation.AfterThrowing;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Before;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.domain.model.LoginUser;
|
||||
import org.dromara.common.core.utils.ServletUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
@@ -39,12 +40,6 @@ import java.util.*;
|
||||
@AutoConfiguration
|
||||
public class LogAspect {
|
||||
|
||||
/**
|
||||
* 排除敏感属性字段
|
||||
*/
|
||||
public static final String[] EXCLUDE_PROPERTIES = { "password", "oldPassword", "newPassword", "confirmPassword" };
|
||||
|
||||
|
||||
/**
|
||||
* 计时 key
|
||||
*/
|
||||
@@ -160,7 +155,7 @@ public class LogAspect {
|
||||
String params = argsArrayToString(joinPoint.getArgs(), excludeParamNames);
|
||||
operLog.setOperParam(StringUtils.substring(params, 0, 3800));
|
||||
} else {
|
||||
MapUtil.removeAny(paramsMap, EXCLUDE_PROPERTIES);
|
||||
MapUtil.removeAny(paramsMap, SystemConstants.EXCLUDE_PROPERTIES);
|
||||
MapUtil.removeAny(paramsMap, excludeParamNames);
|
||||
operLog.setOperParam(StringUtils.substring(JsonUtils.toJsonString(paramsMap), 0, 3800));
|
||||
}
|
||||
@@ -174,7 +169,7 @@ public class LogAspect {
|
||||
if (ArrayUtil.isEmpty(paramsArray)) {
|
||||
return params.toString();
|
||||
}
|
||||
String[] exclude = ArrayUtil.addAll(excludeParamNames, EXCLUDE_PROPERTIES);
|
||||
String[] exclude = ArrayUtil.addAll(excludeParamNames, SystemConstants.EXCLUDE_PROPERTIES);
|
||||
for (Object o : paramsArray) {
|
||||
if (ObjectUtil.isNotNull(o) && !isFilterObject(o)) {
|
||||
String str = "";
|
||||
|
||||
@@ -192,7 +192,7 @@ public class MailUtils {
|
||||
/**
|
||||
* 发送邮件给多人
|
||||
*
|
||||
* @param mailAccount 邮件帐户信息
|
||||
* @param mailAccount 邮件账户信息
|
||||
* @param tos 收件人列表
|
||||
* @param subject 标题
|
||||
* @param content 正文
|
||||
@@ -207,7 +207,7 @@ public class MailUtils {
|
||||
/**
|
||||
* 发送邮件给多人
|
||||
*
|
||||
* @param mailAccount 邮件帐户信息
|
||||
* @param mailAccount 邮件账户信息
|
||||
* @param tos 收件人列表
|
||||
* @param ccs 抄送人列表,可以为null或空
|
||||
* @param bccs 密送人列表,可以为null或空
|
||||
@@ -343,7 +343,7 @@ public class MailUtils {
|
||||
/**
|
||||
* 发送邮件给多人
|
||||
*
|
||||
* @param mailAccount 邮件帐户信息
|
||||
* @param mailAccount 邮件账户信息
|
||||
* @param tos 收件人列表
|
||||
* @param subject 标题
|
||||
* @param content 正文
|
||||
@@ -360,7 +360,7 @@ public class MailUtils {
|
||||
/**
|
||||
* 发送邮件给多人
|
||||
*
|
||||
* @param mailAccount 邮件帐户信息
|
||||
* @param mailAccount 邮件账户信息
|
||||
* @param tos 收件人列表
|
||||
* @param ccs 抄送人列表,可以为null或空
|
||||
* @param bccs 密送人列表,可以为null或空
|
||||
@@ -400,7 +400,7 @@ public class MailUtils {
|
||||
/**
|
||||
* 发送邮件给多人
|
||||
*
|
||||
* @param mailAccount 邮件帐户信息
|
||||
* @param mailAccount 邮件账户信息
|
||||
* @param useGlobalSession 是否全局共享Session
|
||||
* @param tos 收件人列表
|
||||
* @param ccs 抄送人列表,可以为null或空
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
package org.dromara.common.mybatis.handler;
|
||||
|
||||
import cn.dev33.satoken.exception.NotLoginException;
|
||||
import cn.hutool.http.HttpStatus;
|
||||
import com.baomidou.dynamic.datasource.exception.CannotFindDataSourceException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.mybatis.spring.MyBatisSystemException;
|
||||
import org.springframework.dao.DuplicateKeyException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -35,13 +36,54 @@ public class MybatisExceptionHandler {
|
||||
@ExceptionHandler(MyBatisSystemException.class)
|
||||
public R<Void> handleCannotFindDataSourceException(MyBatisSystemException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
String message = e.getMessage();
|
||||
if (StringUtils.contains(message, "CannotFindDataSourceException")) {
|
||||
Throwable root = getRootCause(e);
|
||||
if (root instanceof NotLoginException) {
|
||||
log.error("请求地址'{}',认证失败'{}',无法访问系统资源", requestURI, root.getMessage());
|
||||
return R.fail(HttpStatus.HTTP_UNAUTHORIZED, "认证失败,无法访问系统资源");
|
||||
}
|
||||
if (root instanceof CannotFindDataSourceException) {
|
||||
log.error("请求地址'{}', 未找到数据源", requestURI);
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "未找到数据源,请联系管理员确认");
|
||||
}
|
||||
log.error("请求地址'{}', Mybatis系统异常", requestURI, e);
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, message);
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, e.getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取异常的根因(递归查找)
|
||||
*
|
||||
* @param e 当前异常
|
||||
* @return 根因异常(最底层的 cause)
|
||||
* <p>
|
||||
* 逻辑说明:
|
||||
* 1. 如果 e 没有 cause,说明 e 本身就是根因,直接返回
|
||||
* 2. 如果 e 的 cause 和自身相同(防止循环引用),也返回 e
|
||||
* 3. 否则递归调用,继续向下寻找最底层的 cause
|
||||
*/
|
||||
public static Throwable getRootCause(Throwable e) {
|
||||
Throwable cause = e.getCause();
|
||||
if (cause == null || cause == e) {
|
||||
return e;
|
||||
}
|
||||
return getRootCause(cause);
|
||||
}
|
||||
|
||||
/**
|
||||
* 在异常链中查找指定类型的异常
|
||||
*
|
||||
* @param e 当前异常
|
||||
* @param clazz 目标异常类
|
||||
* @return 找到的指定类型异常,如果没有找到返回 null
|
||||
*/
|
||||
public static Throwable findCause(Throwable e, Class<? extends Throwable> clazz) {
|
||||
Throwable t = e;
|
||||
while (t != null && t != t.getCause()) {
|
||||
if (clazz.isInstance(t)) {
|
||||
return t;
|
||||
}
|
||||
t = t.getCause();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ public class DataBaseHelper {
|
||||
String databaseProductName = metaData.getDatabaseProductName();
|
||||
return DataBaseType.find(databaseProductName);
|
||||
} catch (SQLException e) {
|
||||
throw new ServiceException(e.getMessage());
|
||||
throw new RuntimeException("获取数据库类型失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@ public class DataPermissionHelper {
|
||||
/**
|
||||
* 开启忽略数据权限(开启后需手动调用 {@link #disableIgnore()} 关闭)
|
||||
*/
|
||||
public static void enableIgnore() {
|
||||
private static void enableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNull(ignoreStrategy)) {
|
||||
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().dataPermission(true).build());
|
||||
@@ -126,7 +126,7 @@ public class DataPermissionHelper {
|
||||
/**
|
||||
* 关闭忽略数据权限
|
||||
*/
|
||||
public static void disableIgnore() {
|
||||
private static void disableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNotNull(ignoreStrategy)) {
|
||||
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package org.dromara.common.mybatis.utils;
|
||||
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
|
||||
/**
|
||||
* ID 生成工具类
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public final class IdGeneratorUtil {
|
||||
|
||||
private static final IdentifierGenerator GENERATOR = SpringUtils.getBean(IdentifierGenerator.class);
|
||||
|
||||
/**
|
||||
* 生成字符串类型主键 ID
|
||||
* <p>
|
||||
* 调用 {@link IdentifierGenerator#nextId(Object)},返回 String 格式 ID。
|
||||
* </p>
|
||||
*
|
||||
* @return 字符串格式主键 ID
|
||||
*/
|
||||
public static String nextId() {
|
||||
return GENERATOR.nextId(null).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Long 类型主键 ID
|
||||
* <p>
|
||||
* 自动将生成的数字型主键转换为 Long 类型
|
||||
* </p>
|
||||
*
|
||||
* @return Long 类型主键 ID
|
||||
*/
|
||||
public static Long nextLongId() {
|
||||
return GENERATOR.nextId(null).longValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 Number 类型主键 ID
|
||||
* <p>
|
||||
* 推荐在需要保留原始 Number 类型时使用
|
||||
* </p>
|
||||
*
|
||||
* @return Number 类型主键 ID
|
||||
*/
|
||||
public static Number nextNumberId() {
|
||||
return GENERATOR.nextId(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体生成数字型主键 ID
|
||||
* <p>
|
||||
* 若自定义的 {@link IdentifierGenerator} 根据实体内容生成 ID,则可以使用本方法
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @return Number 类型主键 ID
|
||||
*/
|
||||
public static Number nextId(Object entity) {
|
||||
return GENERATOR.nextId(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体生成字符串主键 ID
|
||||
* <p>
|
||||
* 与 {@link #nextId(Object)} 类似,但返回 String 类型
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @return 字符串格式主键 ID
|
||||
*/
|
||||
public static String nextStringId(Object entity) {
|
||||
return GENERATOR.nextId(entity).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 32 位 UUID
|
||||
* <p>
|
||||
* 底层使用 {@link IdWorker#get32UUID()}
|
||||
* </p>
|
||||
*
|
||||
* @return 32 位 UUID 字符串
|
||||
*/
|
||||
public static String nextUUID() {
|
||||
return IdWorker.get32UUID();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据实体生成 32 位 UUID
|
||||
* <p>
|
||||
* 默认 {@link IdentifierGenerator#nextUUID(Object)} 实现忽略实体,但保留该方法便于扩展。
|
||||
* </p>
|
||||
*
|
||||
* @param entity 实体对象
|
||||
* @return 32 位 UUID 字符串
|
||||
*/
|
||||
public static String nextUUID(Object entity) {
|
||||
return GENERATOR.nextUUID(entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带指定前缀的字符串主键 ID
|
||||
* <p>
|
||||
* 示例:prefix = "ORD",生成结果形如:{@code ORD20251211000123}
|
||||
* </p>
|
||||
*
|
||||
* @param prefix 自定义前缀
|
||||
* @return 带前缀的字符串主键 ID
|
||||
*/
|
||||
public static String nextIdWithPrefix(String prefix) {
|
||||
return prefix + nextId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成带前缀的 UUID
|
||||
*
|
||||
* @param prefix 前缀
|
||||
* @return prefix + UUID
|
||||
*/
|
||||
public static String nextUUIDWithPrefix(String prefix) {
|
||||
return prefix + nextUUID();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,7 @@
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<exclusions>
|
||||
<!-- 将基于 CRT 的 HTTP 客户端从类路径中移除 -->
|
||||
<!-- 东西 30M 特别大的 jar 包 性能跟 Netty 差不多 有需要可以自行替换使用 -->
|
||||
<exclusion>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>aws-crt-client</artifactId>
|
||||
@@ -49,13 +49,13 @@
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 将基于 Netty 的 HTTP 客户端从类路径中移除 -->
|
||||
<!-- 适用于 Netty 的客户端 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>netty-nio-client</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
|
||||
<!-- 客户端的性能增强传输管理器 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3-transfer-manager</artifactId>
|
||||
|
||||
@@ -14,7 +14,9 @@ import org.dromara.common.oss.exception.OssException;
|
||||
import org.dromara.common.oss.properties.OssProperties;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.core.async.*;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
|
||||
import software.amazon.awssdk.core.async.ResponsePublisher;
|
||||
import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
@@ -33,6 +35,7 @@ import java.nio.channels.WritableByteChannel;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
@@ -94,7 +97,11 @@ public class OssClient {
|
||||
.region(of())
|
||||
.forcePathStyle(isStyle)
|
||||
.httpClient(NettyNioAsyncHttpClient.builder()
|
||||
.connectionTimeout(Duration.ofSeconds(60)).build())
|
||||
.connectionTimeout(Duration.ofSeconds(60))
|
||||
.connectionAcquisitionTimeout(Duration.ofSeconds(30))
|
||||
.maxConcurrency(100)
|
||||
.maxPendingConnectionAcquires(1000)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
//AWS基于 CRT 的 S3 AsyncClient 实例用作 S3 传输管理器的底层客户端
|
||||
@@ -134,7 +141,8 @@ public class OssClient {
|
||||
try {
|
||||
// 构建上传请求对象
|
||||
FileUpload fileUpload = transferManager.uploadFile(
|
||||
x -> x.putObjectRequest(
|
||||
x -> {
|
||||
x.source(filePath).putObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(key)
|
||||
.contentMD5(StringUtils.isNotEmpty(md5Digest) ? md5Digest : null)
|
||||
@@ -142,10 +150,13 @@ public class OssClient {
|
||||
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
||||
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
||||
//.acl(getAccessPolicy().getObjectCannedACL())
|
||||
.build())
|
||||
.addTransferListener(LoggingTransferListener.create())
|
||||
.source(filePath).build());
|
||||
|
||||
.build()
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
x.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
}
|
||||
);
|
||||
// 等待上传完成并获取上传结果
|
||||
CompletedFileUpload uploadResult = fileUpload.completionFuture().join();
|
||||
String eTag = uploadResult.response().eTag();
|
||||
@@ -185,16 +196,21 @@ public class OssClient {
|
||||
|
||||
// 使用 transferManager 进行上传
|
||||
Upload upload = transferManager.upload(
|
||||
x -> x.requestBody(body).addTransferListener(LoggingTransferListener.create())
|
||||
.putObjectRequest(
|
||||
x -> {
|
||||
x.requestBody(body).putObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(key)
|
||||
.contentType(contentType)
|
||||
// 用于设置对象的访问控制列表(ACL)。不同云厂商对ACL的支持和实现方式有所不同,
|
||||
// 因此根据具体的云服务提供商,你可能需要进行不同的配置(自行开启,阿里云有acl权限配置,腾讯云没有acl权限配置)
|
||||
//.acl(getAccessPolicy().getObjectCannedACL())
|
||||
.build())
|
||||
.build());
|
||||
.build()
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
x.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// 将输入流写入请求体
|
||||
body.writeInputStream(inputStream);
|
||||
@@ -222,13 +238,17 @@ public class OssClient {
|
||||
Path tempFilePath = FileUtils.createTempFile().toPath();
|
||||
// 使用 S3TransferManager 下载文件
|
||||
FileDownload downloadFile = transferManager.downloadFile(
|
||||
x -> x.getObjectRequest(
|
||||
x -> {
|
||||
x.destination(tempFilePath).getObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(removeBaseUrl(path))
|
||||
.build())
|
||||
.addTransferListener(LoggingTransferListener.create())
|
||||
.destination(tempFilePath)
|
||||
.build());
|
||||
.build()
|
||||
);
|
||||
if (log.isDebugEnabled()) {
|
||||
x.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
}
|
||||
);
|
||||
// 等待文件下载操作完成
|
||||
downloadFile.completionFuture().join();
|
||||
return tempFilePath;
|
||||
@@ -237,8 +257,8 @@ public class OssClient {
|
||||
/**
|
||||
* 下载文件从 Amazon S3 到 输出流
|
||||
*
|
||||
* @param key 文件在 Amazon S3 中的对象键
|
||||
* @param out 输出流
|
||||
* @param key 文件在 Amazon S3 中的对象键
|
||||
* @param out 输出流
|
||||
* @param consumer 自定义处理逻辑
|
||||
* @throws OssException 如果下载失败,抛出自定义异常
|
||||
*/
|
||||
@@ -253,26 +273,24 @@ public class OssClient {
|
||||
/**
|
||||
* 下载文件从 Amazon S3 到 输出流
|
||||
*
|
||||
* @param key 文件在 Amazon S3 中的对象键
|
||||
* @param key 文件在 Amazon S3 中的对象键
|
||||
* @param contentLengthConsumer 文件大小消费者函数
|
||||
* @return 写出订阅器
|
||||
* @throws OssException 如果下载失败,抛出自定义异常
|
||||
*/
|
||||
public WriteOutSubscriber<OutputStream> download(String key, Consumer<Long> contentLengthConsumer) {
|
||||
try {
|
||||
// 构建下载请求
|
||||
DownloadRequest<ResponsePublisher<GetObjectResponse>> publisherDownloadRequest = DownloadRequest.builder()
|
||||
// 文件对象
|
||||
.getObjectRequest(y -> y.bucket(properties.getBucketName())
|
||||
.key(key)
|
||||
.build())
|
||||
.addTransferListener(LoggingTransferListener.create())
|
||||
DownloadRequest.TypedBuilder<ResponsePublisher<GetObjectResponse>> typedBuilder = DownloadRequest.builder()
|
||||
// 使用发布订阅转换器
|
||||
.responseTransformer(AsyncResponseTransformer.toPublisher())
|
||||
.build();
|
||||
// 文件对象
|
||||
.getObjectRequest(y -> y.bucket(properties.getBucketName()).key(key).build());
|
||||
if (log.isDebugEnabled()) {
|
||||
typedBuilder.addTransferListener(LoggingTransferListener.create());
|
||||
}
|
||||
|
||||
// 使用 S3TransferManager 下载文件
|
||||
Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(publisherDownloadRequest);
|
||||
Download<ResponsePublisher<GetObjectResponse>> publisherDownload = transferManager.download(typedBuilder.build());
|
||||
// 获取下载发布订阅转换器
|
||||
ResponsePublisher<GetObjectResponse> publisher = publisherDownload.completionFuture().join().result();
|
||||
// 执行文件大小消费者函数
|
||||
@@ -282,7 +300,7 @@ public class OssClient {
|
||||
// 构建写出订阅器对象
|
||||
return out -> {
|
||||
// 创建可写入的字节通道
|
||||
try(WritableByteChannel channel = Channels.newChannel(out)){
|
||||
try (WritableByteChannel channel = Channels.newChannel(out)) {
|
||||
// 订阅数据
|
||||
publisher.subscribe(byteBuffer -> {
|
||||
while (byteBuffer.hasRemaining()) {
|
||||
@@ -317,13 +335,13 @@ public class OssClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取私有URL链接
|
||||
* 创建下载请求的预签名URL
|
||||
*
|
||||
* @param objectKey 对象KEY
|
||||
* @param expiredTime 链接授权到期时间
|
||||
*/
|
||||
public String getPrivateUrl(String objectKey, Duration expiredTime) {
|
||||
// 使用 AWS S3 预签名 URL 的生成器 获取对象的预签名 URL
|
||||
public String createPresignedGetUrl(String objectKey, Duration expiredTime) {
|
||||
// 使用 AWS S3 预签名 URL 的生成器 获取下载对象的预签名 URL
|
||||
URL url = presigner.presignGetObject(
|
||||
x -> x.signatureDuration(expiredTime)
|
||||
.getObjectRequest(
|
||||
@@ -332,7 +350,28 @@ public class OssClient {
|
||||
.build())
|
||||
.build())
|
||||
.url();
|
||||
return url.toString();
|
||||
return url.toExternalForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建上传请求的预签名URL
|
||||
*
|
||||
* @param objectKey 对象KEY
|
||||
* @param expiredTime 链接授权到期时间
|
||||
* @param metadata 元数据
|
||||
*/
|
||||
public String createPresignedPutUrl(String objectKey, Duration expiredTime, Map<String, String> metadata) {
|
||||
// 使用 AWS S3 预签名 URL 的生成器 获取上传文件对象的预签名 URL
|
||||
URL url = presigner.presignPutObject(
|
||||
x -> x.signatureDuration(expiredTime)
|
||||
.putObjectRequest(
|
||||
y -> y.bucket(properties.getBucketName())
|
||||
.key(objectKey)
|
||||
.metadata(metadata)
|
||||
.build())
|
||||
.build())
|
||||
.url();
|
||||
return url.toExternalForm();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -43,16 +43,12 @@
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- <!– redis序列化替代方案 比json快无数的跨语言二进制序列化 –>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.apache.fury</groupId>-->
|
||||
<!-- <artifactId>fury-core</artifactId>-->
|
||||
<!-- <version>0.9.0</version>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- <dependency>-->
|
||||
<!-- <groupId>org.slf4j</groupId>-->
|
||||
<!-- <artifactId>slf4j-api</artifactId>-->
|
||||
<!-- </dependency>-->
|
||||
<!-- redis序列化替代方案 比json快无数的跨语言二进制序列化 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.fory</groupId>
|
||||
<artifactId>fory-core</artifactId>
|
||||
<version>0.13.1</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
|
||||
@@ -53,9 +53,10 @@ public class RedisConfig {
|
||||
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
|
||||
// 指定序列化输入的类型,类必须是非final修饰的。序列化时将对象全类名一起保存下来
|
||||
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL);
|
||||
// LoggerFactory.useSlf4jLogging(true);
|
||||
// FuryCodec furyCodec = new FuryCodec();
|
||||
// CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, furyCodec, furyCodec);
|
||||
// org.apache.fory.logging.LoggerFactory 包别引入错了
|
||||
// LoggerFactory.useSlf4jLogging(true);
|
||||
// ForyCodec foryCodec = new ForyCodec();
|
||||
// CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, foryCodec, foryCodec);
|
||||
TypedJsonJacksonCodec jsonCodec = new TypedJsonJacksonCodec(Object.class, om);
|
||||
// 组合序列化 key 使用 String 内容使用通用 json 格式
|
||||
CompositeCodec codec = new CompositeCodec(StringCodec.INSTANCE, jsonCodec, jsonCodec);
|
||||
|
||||
@@ -7,7 +7,9 @@ import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.router.SaRouter;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import cn.dev33.satoken.util.SaResult;
|
||||
import cn.dev33.satoken.util.SaTokenConsts;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.HttpStatus;
|
||||
@@ -55,6 +57,8 @@ public class SecurityConfig implements WebMvcConfigurer {
|
||||
// 对未排除的路径进行检查
|
||||
.check(() -> {
|
||||
HttpServletRequest request = ServletUtils.getRequest();
|
||||
HttpServletResponse response = ServletUtils.getResponse();
|
||||
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
|
||||
// 检查是否登录 是否有token
|
||||
StpUtil.checkLogin();
|
||||
|
||||
@@ -94,7 +98,11 @@ public class SecurityConfig implements WebMvcConfigurer {
|
||||
.setAuth(obj -> {
|
||||
SaHttpBasicUtil.check(username + ":" + password);
|
||||
})
|
||||
.setError(e -> SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED));
|
||||
.setError(e -> {
|
||||
HttpServletResponse response = ServletUtils.getResponse();
|
||||
response.setContentType(SaTokenConsts.CONTENT_TYPE_APPLICATION_JSON);
|
||||
return SaResult.error(e.getMessage()).setCode(HttpStatus.UNAUTHORIZED);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package org.dromara.common.sensitive.core;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.DesensitizedUtil;
|
||||
import lombok.AllArgsConstructor;
|
||||
import org.dromara.common.core.utils.DesensitizedUtils;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -80,6 +81,18 @@ public enum SensitiveStrategy {
|
||||
*/
|
||||
FIRST_MASK(DesensitizedUtil::firstMask),
|
||||
|
||||
/**
|
||||
* 通用字符串脱敏
|
||||
* 可配置前后可见长度和中间掩码长度
|
||||
* 默认示例:前4位可见,后4位可见,中间固定4个*
|
||||
*/
|
||||
STRING_MASK(s -> DesensitizedUtils.mask(s, 4, 4, 4)),
|
||||
|
||||
/**
|
||||
* 高安全级别脱敏(Token / 私钥):前2位可见,后2位可见,中间全部掩码
|
||||
*/
|
||||
MASK_HIGH_SECURITY(s -> DesensitizedUtils.maskHighSecurity(s, 2, 2)),
|
||||
|
||||
/**
|
||||
* 清空为""
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package me.zhyd.oauth.request;
|
||||
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import com.xkcoding.http.support.HttpHeader;
|
||||
import me.zhyd.oauth.cache.AuthStateCache;
|
||||
import me.zhyd.oauth.config.AuthConfig;
|
||||
import me.zhyd.oauth.config.AuthDefaultSource;
|
||||
import me.zhyd.oauth.enums.scope.AuthDingTalkScope;
|
||||
import me.zhyd.oauth.exception.AuthException;
|
||||
import me.zhyd.oauth.model.AuthCallback;
|
||||
import me.zhyd.oauth.model.AuthToken;
|
||||
import me.zhyd.oauth.model.AuthUser;
|
||||
import me.zhyd.oauth.utils.AuthScopeUtils;
|
||||
import me.zhyd.oauth.utils.GlobalAuthUtils;
|
||||
import me.zhyd.oauth.utils.HttpUtils;
|
||||
import me.zhyd.oauth.utils.UrlBuilder;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 新版钉钉二维码登录
|
||||
*
|
||||
* @author yadong.zhang (yadong.zhang0415(a)gmail.com)
|
||||
* @since 1.16.7
|
||||
*/
|
||||
public class AuthDingTalkV2Request extends AuthDefaultRequest {
|
||||
|
||||
public AuthDingTalkV2Request(AuthConfig config) {
|
||||
super(config, AuthDefaultSource.DINGTALK_V2);
|
||||
}
|
||||
|
||||
public AuthDingTalkV2Request(AuthConfig config, AuthStateCache authStateCache) {
|
||||
super(config, AuthDefaultSource.DINGTALK_V2, authStateCache);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String authorize(String state) {
|
||||
return UrlBuilder.fromBaseUrl(source.authorize())
|
||||
.queryParam("response_type", "code")
|
||||
.queryParam("client_id", config.getClientId())
|
||||
.queryParam("scope", this.getScopes(",", true, AuthScopeUtils.getDefaultScopes(AuthDingTalkScope.values())))
|
||||
.queryParam("redirect_uri", GlobalAuthUtils.urlEncode(config.getRedirectUri()))
|
||||
.queryParam("prompt", "consent")
|
||||
.queryParam("org_type", config.getDingTalkOrgType())
|
||||
.queryParam("corpId", config.getDingTalkCorpId())
|
||||
.queryParam("exclusiveLogin", config.isDingTalkExclusiveLogin())
|
||||
.queryParam("exclusiveCorpId", config.getDingTalkExclusiveCorpId())
|
||||
.queryParam("state", getRealState(state))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthToken getAccessToken(AuthCallback authCallback) {
|
||||
Map<String, String> params = new HashMap<>();
|
||||
params.put("grantType", "authorization_code");
|
||||
params.put("clientId", config.getClientId());
|
||||
params.put("clientSecret", config.getClientSecret());
|
||||
params.put("code", authCallback.getCode());
|
||||
String response = new HttpUtils(config.getHttpConfig()).post(this.source.accessToken(), JSONObject.toJSONString(params)).getBody();
|
||||
JSONObject accessTokenObject = JSONObject.parseObject(response);
|
||||
if (!accessTokenObject.containsKey("accessToken")) {
|
||||
throw new AuthException(JSONObject.toJSONString(response), source);
|
||||
}
|
||||
return AuthToken.builder()
|
||||
.accessToken(accessTokenObject.getString("accessToken"))
|
||||
.refreshToken(accessTokenObject.getString("refreshToken"))
|
||||
.expireIn(accessTokenObject.getIntValue("expireIn"))
|
||||
.corpId(accessTokenObject.getString("corpId"))
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthUser getUserInfo(AuthToken authToken) {
|
||||
HttpHeader header = new HttpHeader();
|
||||
header.add("x-acs-dingtalk-access-token", authToken.getAccessToken());
|
||||
|
||||
String response = new HttpUtils(config.getHttpConfig()).get(this.source.userInfo(), null, header, false).getBody();
|
||||
JSONObject object = JSONObject.parseObject(response);
|
||||
|
||||
authToken.setOpenId(object.getString("openId"));
|
||||
authToken.setUnionId(object.getString("unionId"));
|
||||
return AuthUser.builder()
|
||||
.rawUserInfo(object)
|
||||
.uuid(object.getString("unionId"))
|
||||
.username(object.getString("nick"))
|
||||
.nickname(object.getString("nick"))
|
||||
.avatar(object.getString("avatarUrl"))
|
||||
.snapshotUser(object.getBooleanValue("visitor"))
|
||||
.token(authToken)
|
||||
.source(source.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回获取accessToken的url
|
||||
*
|
||||
* @param code 授权码
|
||||
* @return 返回获取accessToken的url
|
||||
*/
|
||||
protected String accessTokenUrl(String code) {
|
||||
return UrlBuilder.fromBaseUrl(source.accessToken())
|
||||
.queryParam("code", code)
|
||||
.queryParam("clientId", config.getClientId())
|
||||
.queryParam("clientSecret", config.getClientSecret())
|
||||
.queryParam("grantType", "authorization_code")
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -28,9 +28,14 @@ public class SocialLoginConfigProperties {
|
||||
private String redirectUri;
|
||||
|
||||
/**
|
||||
* 是否获取unionId
|
||||
* 是否需要申请unionid,目前只针对qq登录
|
||||
*/
|
||||
private boolean unionId;
|
||||
private Boolean unionId;
|
||||
|
||||
/**
|
||||
* Microsoft Entra ID(原微软 AAD)中的租户 ID
|
||||
*/
|
||||
private String tenantId;
|
||||
|
||||
/**
|
||||
* Coding 企业名称
|
||||
|
||||
@@ -14,9 +14,6 @@ import org.dromara.common.social.gitea.AuthGiteaRequest;
|
||||
import org.dromara.common.social.maxkey.AuthMaxKeyRequest;
|
||||
import org.dromara.common.social.topiam.AuthTopIamRequest;
|
||||
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* 认证授权工具类
|
||||
*
|
||||
@@ -43,7 +40,7 @@ public class SocialUtils {
|
||||
AuthConfig.AuthConfigBuilder builder = AuthConfig.builder()
|
||||
.clientId(obj.getClientId())
|
||||
.clientSecret(obj.getClientSecret())
|
||||
.redirectUri(URLEncoder.encode(obj.getRedirectUri(), StandardCharsets.UTF_8))
|
||||
.redirectUri(obj.getRedirectUri())
|
||||
.scopes(obj.getScopes());
|
||||
return switch (source.toLowerCase()) {
|
||||
case "dingtalk" -> new AuthDingTalkV2Request(builder.build(), STATE_CACHE);
|
||||
@@ -60,7 +57,7 @@ public class SocialUtils {
|
||||
case "taobao" -> new AuthTaobaoRequest(builder.build(), STATE_CACHE);
|
||||
case "douyin" -> new AuthDouyinRequest(builder.build(), STATE_CACHE);
|
||||
case "linkedin" -> new AuthLinkedinRequest(builder.build(), STATE_CACHE);
|
||||
case "microsoft" -> new AuthMicrosoftRequest(builder.build(), STATE_CACHE);
|
||||
case "microsoft" -> new AuthMicrosoftRequest(builder.tenantId(obj.getTenantId()).build(), STATE_CACHE);
|
||||
case "renren" -> new AuthRenrenRequest(builder.build(), STATE_CACHE);
|
||||
case "stack_overflow" -> new AuthStackOverflowRequest(builder.stackOverflowKey(obj.getStackOverflowKey()).build(), STATE_CACHE);
|
||||
case "huawei" -> new AuthHuaweiV3Request(builder.build(), STATE_CACHE);
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
package org.dromara.common.sse.core;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.redis.utils.RedisUtils;
|
||||
import org.dromara.common.sse.dto.SseMessageDto;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
@@ -26,6 +33,12 @@ public class SseEmitterManager {
|
||||
|
||||
private final static Map<Long, Map<String, SseEmitter>> USER_TOKEN_EMITTERS = new ConcurrentHashMap<>();
|
||||
|
||||
public SseEmitterManager() {
|
||||
// 定时执行 SSE 心跳检测
|
||||
SpringUtils.getBean(ScheduledExecutorService.class)
|
||||
.scheduleWithFixedDelay(this::sseMonitor, 60L, 60L, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立与指定用户的 SSE 连接
|
||||
*
|
||||
@@ -38,6 +51,12 @@ public class SseEmitterManager {
|
||||
// 每个用户可以有多个 SSE 连接,通过 token 进行区分
|
||||
Map<String, SseEmitter> emitters = USER_TOKEN_EMITTERS.computeIfAbsent(userId, k -> new ConcurrentHashMap<>());
|
||||
|
||||
// 关闭已存在的SseEmitter,防止超过最大连接数
|
||||
SseEmitter oldEmitter = emitters.remove(token);
|
||||
if (oldEmitter != null) {
|
||||
oldEmitter.complete();
|
||||
}
|
||||
|
||||
// 创建一个新的 SseEmitter 实例,超时时间设置为一天 避免连接之后直接关闭浏览器导致连接停滞
|
||||
SseEmitter emitter = new SseEmitter(86400000L);
|
||||
|
||||
@@ -97,6 +116,44 @@ public class SseEmitterManager {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* SSE 心跳检测,关闭无效连接
|
||||
*/
|
||||
public void sseMonitor() {
|
||||
final SseEmitter.SseEventBuilder heartbeat = SseEmitter.event().comment("heartbeat");
|
||||
// 记录需要移除的用户ID
|
||||
List<Long> toRemoveUsers = new ArrayList<>();
|
||||
|
||||
USER_TOKEN_EMITTERS.forEach((userId, emitterMap) -> {
|
||||
if (CollUtil.isEmpty(emitterMap)) {
|
||||
toRemoveUsers.add(userId);
|
||||
return;
|
||||
}
|
||||
|
||||
emitterMap.entrySet().removeIf(entry -> {
|
||||
try {
|
||||
entry.getValue().send(heartbeat);
|
||||
return false;
|
||||
} catch (Exception ex) {
|
||||
try {
|
||||
entry.getValue().complete();
|
||||
} catch (Exception ignore) {
|
||||
// 忽略重复关闭异常
|
||||
}
|
||||
return true; // 发送失败 → 移除该连接
|
||||
}
|
||||
});
|
||||
|
||||
// 移除空连接用户
|
||||
if (emitterMap.isEmpty()) {
|
||||
toRemoveUsers.add(userId);
|
||||
}
|
||||
});
|
||||
|
||||
// 循环结束后统一清理空用户,避免并发修改异常
|
||||
toRemoveUsers.forEach(USER_TOKEN_EMITTERS::remove);
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅SSE消息主题,并提供一个消费者函数来处理接收到的消息
|
||||
*
|
||||
|
||||
@@ -55,7 +55,7 @@ public class TenantHelper {
|
||||
/**
|
||||
* 开启忽略租户(开启后需手动调用 {@link #disableIgnore()} 关闭)
|
||||
*/
|
||||
public static void enableIgnore() {
|
||||
private static void enableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNull(ignoreStrategy)) {
|
||||
InterceptorIgnoreHelper.handle(IgnoreStrategy.builder().tenantLine(true).build());
|
||||
@@ -69,7 +69,7 @@ public class TenantHelper {
|
||||
/**
|
||||
* 关闭忽略租户
|
||||
*/
|
||||
public static void disableIgnore() {
|
||||
private static void disableIgnore() {
|
||||
IgnoreStrategy ignoreStrategy = getIgnoreStrategy();
|
||||
if (ObjectUtil.isNotNull(ignoreStrategy)) {
|
||||
boolean noOtherIgnoreStrategy = !Boolean.TRUE.equals(ignoreStrategy.getDynamicTableName())
|
||||
|
||||
@@ -46,8 +46,14 @@ public class TranslationHandler extends JsonSerializer<Object> implements Contex
|
||||
gen.writeNull();
|
||||
return;
|
||||
}
|
||||
Object result = trans.translation(value, translation.other());
|
||||
gen.writeObject(result);
|
||||
try {
|
||||
Object result = trans.translation(value, translation.other());
|
||||
gen.writeObject(result);
|
||||
} catch (Exception e) {
|
||||
log.error("翻译处理异常,type: {}, value: {}", translation.type(), value, e);
|
||||
// 出现异常时输出原始值而不是中断序列化
|
||||
gen.writeObject(value);
|
||||
}
|
||||
} else {
|
||||
gen.writeObject(value);
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ public class NicknameTranslationImpl implements TranslationInterface<String> {
|
||||
@Override
|
||||
public String translation(Object key, String other) {
|
||||
if (key instanceof Long id) {
|
||||
return userService.selectNicknameByIds(id.toString());
|
||||
return userService.selectNicknameById(id);
|
||||
} else if (key instanceof String ids) {
|
||||
return userService.selectNicknameByIds(ids);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.dromara.common.translation.core.impl;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import org.dromara.common.core.service.UserService;
|
||||
import org.dromara.common.translation.annotation.TranslationType;
|
||||
import org.dromara.common.translation.constant.TransConstant;
|
||||
@@ -19,9 +20,6 @@ public class UserNameTranslationImpl implements TranslationInterface<String> {
|
||||
|
||||
@Override
|
||||
public String translation(Object key, String other) {
|
||||
if (key instanceof Long id) {
|
||||
return userService.selectUserNameById(id);
|
||||
}
|
||||
return null;
|
||||
return userService.selectUserNameById(Convert.toLong(key));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,8 @@
|
||||
package org.dromara.common.web.config;
|
||||
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.CircleCaptcha;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
import cn.hutool.captcha.ShearCaptcha;
|
||||
import org.dromara.common.web.config.properties.CaptchaProperties;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Lazy;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 验证码配置
|
||||
@@ -21,45 +13,4 @@ import java.awt.*;
|
||||
@EnableConfigurationProperties(CaptchaProperties.class)
|
||||
public class CaptchaConfig {
|
||||
|
||||
private static final int WIDTH = 160;
|
||||
private static final int HEIGHT = 60;
|
||||
private static final Color BACKGROUND = Color.LIGHT_GRAY;
|
||||
private static final Font FONT = new Font("Arial", Font.BOLD, 48);
|
||||
|
||||
/**
|
||||
* 圆圈干扰验证码
|
||||
*/
|
||||
@Lazy
|
||||
@Bean
|
||||
public CircleCaptcha circleCaptcha() {
|
||||
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(WIDTH, HEIGHT);
|
||||
captcha.setBackground(BACKGROUND);
|
||||
captcha.setFont(FONT);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 线段干扰的验证码
|
||||
*/
|
||||
@Lazy
|
||||
@Bean
|
||||
public LineCaptcha lineCaptcha() {
|
||||
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(WIDTH, HEIGHT);
|
||||
captcha.setBackground(BACKGROUND);
|
||||
captcha.setFont(FONT);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扭曲干扰验证码
|
||||
*/
|
||||
@Lazy
|
||||
@Bean
|
||||
public ShearCaptcha shearCaptcha() {
|
||||
ShearCaptcha captcha = CaptchaUtil.createShearCaptcha(WIDTH, HEIGHT);
|
||||
captcha.setBackground(BACKGROUND);
|
||||
captcha.setFont(FONT);
|
||||
return captcha;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.dromara.common.web.config.properties;
|
||||
|
||||
import org.dromara.common.web.enums.CaptchaCategory;
|
||||
import org.dromara.common.web.enums.CaptchaType;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@@ -19,12 +17,7 @@ public class CaptchaProperties {
|
||||
/**
|
||||
* 验证码类型
|
||||
*/
|
||||
private CaptchaType type;
|
||||
|
||||
/**
|
||||
* 验证码类别
|
||||
*/
|
||||
private CaptchaCategory category;
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 数字验证码位数
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
package org.dromara.common.web.core;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import cn.hutool.core.img.GraphicsUtil;
|
||||
import cn.hutool.core.img.ImgUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.Serial;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
/**
|
||||
* 带干扰线、波浪、圆的验证码
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class WaveAndCircleCaptcha extends AbstractCaptcha {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
// 构造方法(略,与之前一致)
|
||||
public WaveAndCircleCaptcha(int width, int height) {
|
||||
this(width, height, 4);
|
||||
}
|
||||
|
||||
public WaveAndCircleCaptcha(int width, int height, int codeCount) {
|
||||
this(width, height, codeCount, 6);
|
||||
}
|
||||
|
||||
public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount) {
|
||||
this(width, height, new RandomGenerator(codeCount), interfereCount);
|
||||
}
|
||||
|
||||
public WaveAndCircleCaptcha(int width, int height, CodeGenerator generator, int interfereCount) {
|
||||
super(width, height, generator, interfereCount);
|
||||
}
|
||||
|
||||
public WaveAndCircleCaptcha(int width, int height, int codeCount, int interfereCount, float size) {
|
||||
super(width, height, new RandomGenerator(codeCount), interfereCount, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Image createImage(String code) {
|
||||
final BufferedImage image = new BufferedImage(
|
||||
width,
|
||||
height,
|
||||
(null == this.background) ? BufferedImage.TYPE_4BYTE_ABGR : BufferedImage.TYPE_INT_RGB
|
||||
);
|
||||
final Graphics2D g = ImgUtil.createGraphics(image, this.background);
|
||||
|
||||
try {
|
||||
drawString(g, code);
|
||||
// 扭曲
|
||||
shear(g, this.width, this.height, ObjectUtil.defaultIfNull(this.background, Color.WHITE));
|
||||
drawInterfere(g);
|
||||
} finally {
|
||||
g.dispose();
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
private void drawString(Graphics2D g, String code) {
|
||||
// 设置抗锯齿(让字体渲染更清晰)
|
||||
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
|
||||
|
||||
if (this.textAlpha != null) {
|
||||
g.setComposite(this.textAlpha);
|
||||
}
|
||||
|
||||
GraphicsUtil.drawStringColourful(g, code, this.font, this.width, this.height);
|
||||
}
|
||||
|
||||
protected void drawInterfere(Graphics2D g) {
|
||||
ThreadLocalRandom random = RandomUtil.getRandom();
|
||||
int circleCount = Math.max(0, this.interfereCount - 1);
|
||||
|
||||
// 圈圈
|
||||
for (int i = 0; i < circleCount; i++) {
|
||||
g.setColor(ImgUtil.randomColor(random));
|
||||
int x = random.nextInt(width);
|
||||
int y = random.nextInt(height);
|
||||
int w = random.nextInt(height >> 1);
|
||||
int h = random.nextInt(height >> 1);
|
||||
g.drawOval(x, y, w, h);
|
||||
}
|
||||
|
||||
// 仅 1 条平滑波浪线
|
||||
if (this.interfereCount >= 1) {
|
||||
g.setColor(getRandomColor(120, 230, random));
|
||||
drawSmoothWave(g, random);
|
||||
}
|
||||
}
|
||||
|
||||
private void drawSmoothWave(Graphics2D g, ThreadLocalRandom random) {
|
||||
int amplitude = random.nextInt(8) + 5; // 波动幅度
|
||||
int wavelength = random.nextInt(40) + 30; // 波长
|
||||
double phase = random.nextDouble() * Math.PI * 2;
|
||||
|
||||
// ✅ 关键:限制 baseY 在中间区域
|
||||
int centerY = height / 2;
|
||||
int verticalJitter = Math.max(5, height / 6); // 至少偏移5像素
|
||||
int baseY = centerY - verticalJitter + random.nextInt(verticalJitter * 2);
|
||||
|
||||
g.setStroke(new BasicStroke(2.5f)); // 线宽
|
||||
|
||||
int[] xPoints = new int[width];
|
||||
int[] yPoints = new int[width];
|
||||
for (int x = 0; x < width; x++) {
|
||||
int y = baseY + (int) (amplitude * Math.sin((double) x / wavelength * 2 * Math.PI + phase));
|
||||
// 限制 y 不要超出图像边界(可选)
|
||||
y = Math.max(amplitude, Math.min(y, height - amplitude));
|
||||
xPoints[x] = x;
|
||||
yPoints[x] = y;
|
||||
}
|
||||
g.drawPolyline(xPoints, yPoints, width);
|
||||
}
|
||||
|
||||
private Color getRandomColor(int min, int max, ThreadLocalRandom random) {
|
||||
int range = max - min;
|
||||
return new Color(
|
||||
min + random.nextInt(range),
|
||||
min + random.nextInt(range),
|
||||
min + random.nextInt(range)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 扭曲
|
||||
*
|
||||
* @param g {@link Graphics}
|
||||
* @param w1 w1
|
||||
* @param h1 h1
|
||||
* @param color 颜色
|
||||
*/
|
||||
private void shear(Graphics g, int w1, int h1, Color color) {
|
||||
shearX(g, w1, h1, color);
|
||||
shearY(g, w1, h1, color);
|
||||
}
|
||||
|
||||
/**
|
||||
* X坐标扭曲
|
||||
*
|
||||
* @param g {@link Graphics}
|
||||
* @param w1 宽
|
||||
* @param h1 高
|
||||
* @param color 颜色
|
||||
*/
|
||||
private void shearX(Graphics g, int w1, int h1, Color color) {
|
||||
|
||||
int period = RandomUtil.randomInt(this.width);
|
||||
|
||||
int frames = 1;
|
||||
int phase = RandomUtil.randomInt(2);
|
||||
|
||||
for (int i = 0; i < h1; i++) {
|
||||
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
|
||||
g.copyArea(0, i, w1, 1, (int) d, 0);
|
||||
g.setColor(color);
|
||||
g.drawLine((int) d, i, 0, i);
|
||||
g.drawLine((int) d + w1, i, w1, i);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Y坐标扭曲
|
||||
*
|
||||
* @param g {@link Graphics}
|
||||
* @param w1 宽
|
||||
* @param h1 高
|
||||
* @param color 颜色
|
||||
*/
|
||||
private void shearY(Graphics g, int w1, int h1, Color color) {
|
||||
|
||||
int period = RandomUtil.randomInt(this.height >> 1);
|
||||
|
||||
int frames = 20;
|
||||
int phase = 7;
|
||||
for (int i = 0; i < w1; i++) {
|
||||
double d = (double) (period >> 1) * Math.sin((double) i / (double) period + (6.2831853071795862D * (double) phase) / (double) frames);
|
||||
g.copyArea(i, 0, 1, h1, 0, (int) d);
|
||||
g.setColor(color);
|
||||
// 擦除原位置的痕迹
|
||||
g.drawLine(i, (int) d, i, 0);
|
||||
g.drawLine(i, (int) d + h1, i, h1);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package org.dromara.common.web.enums;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CircleCaptcha;
|
||||
import cn.hutool.captcha.LineCaptcha;
|
||||
import cn.hutool.captcha.ShearCaptcha;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码类别
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CaptchaCategory {
|
||||
|
||||
/**
|
||||
* 线段干扰
|
||||
*/
|
||||
LINE(LineCaptcha.class),
|
||||
|
||||
/**
|
||||
* 圆圈干扰
|
||||
*/
|
||||
CIRCLE(CircleCaptcha.class),
|
||||
|
||||
/**
|
||||
* 扭曲干扰
|
||||
*/
|
||||
SHEAR(ShearCaptcha.class);
|
||||
|
||||
private final Class<? extends AbstractCaptcha> clazz;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package org.dromara.common.web.enums;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 验证码类型
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CaptchaType {
|
||||
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
MATH(MathGenerator.class),
|
||||
|
||||
/**
|
||||
* 字符
|
||||
*/
|
||||
CHAR(RandomGenerator.class);
|
||||
|
||||
private final Class<? extends CodeGenerator> clazz;
|
||||
}
|
||||
@@ -14,7 +14,9 @@ import org.dromara.common.core.exception.SseException;
|
||||
import org.dromara.common.core.exception.base.BaseException;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.springframework.context.MessageSourceResolvable;
|
||||
import org.springframework.context.support.DefaultMessageSourceResolvable;
|
||||
import org.springframework.expression.ExpressionException;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.validation.BindException;
|
||||
import org.springframework.web.HttpRequestMethodNotSupportedException;
|
||||
@@ -23,6 +25,8 @@ import org.springframework.web.bind.MissingPathVariableException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.context.request.async.AsyncRequestTimeoutException;
|
||||
import org.springframework.web.method.annotation.HandlerMethodValidationException;
|
||||
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
@@ -42,7 +46,7 @@ public class GlobalExceptionHandler {
|
||||
*/
|
||||
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
|
||||
public R<Void> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException e,
|
||||
HttpServletRequest request) {
|
||||
HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
log.error("请求地址'{}',不支持'{}'请求", requestURI, e.getMethod());
|
||||
return R.fail(HttpStatus.HTTP_BAD_METHOD, e.getMessage());
|
||||
@@ -123,7 +127,7 @@ public class GlobalExceptionHandler {
|
||||
*/
|
||||
@ResponseStatus(org.springframework.http.HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(IOException.class)
|
||||
public void handleRuntimeException(IOException e, HttpServletRequest request) {
|
||||
public void handleIoException(IOException e, HttpServletRequest request) {
|
||||
String requestURI = request.getRequestURI();
|
||||
if (requestURI.contains("sse")) {
|
||||
// sse 经常性连接中断 例如关闭浏览器 直接屏蔽
|
||||
@@ -132,6 +136,13 @@ public class GlobalExceptionHandler {
|
||||
log.error("请求地址'{}',连接中断", requestURI, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* sse 连接超时异常 不需要处理
|
||||
*/
|
||||
@ExceptionHandler(AsyncRequestTimeoutException.class)
|
||||
public void handleRuntimeException(AsyncRequestTimeoutException e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
@@ -182,6 +193,16 @@ public class GlobalExceptionHandler {
|
||||
return R.fail(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 方法参数校验异常 用于处理 @Validated 注解
|
||||
*/
|
||||
@ExceptionHandler(HandlerMethodValidationException.class)
|
||||
public R<Void> handlerMethodValidationException(HandlerMethodValidationException e) {
|
||||
log.error(e.getMessage());
|
||||
String message = StreamUtils.join(e.getAllErrors(), MessageSourceResolvable::getDefaultMessage, ", ");
|
||||
return R.fail(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON 解析异常(Jackson 在处理 JSON 格式出错时抛出)
|
||||
* 可能是请求体格式非法,也可能是服务端反序列化失败
|
||||
@@ -202,4 +223,13 @@ public class GlobalExceptionHandler {
|
||||
return R.fail(HttpStatus.HTTP_BAD_REQUEST, "请求参数格式错误:" + e.getMostSpecificCause().getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* SpEL 表达式相关异常
|
||||
*/
|
||||
@ExceptionHandler(ExpressionException.class)
|
||||
public R<Void> handleSpelException(ExpressionException e, HttpServletRequest request) {
|
||||
log.error("请求地址'{}',SpEL解析异常: {}", request.getRequestURI(), e.getMessage());
|
||||
return R.fail(HttpStatus.HTTP_INTERNAL_ERROR, "SpEL解析失败:" + e.getMessage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -2,11 +2,17 @@ package org.dromara.common.web.interceptor;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ArrayUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.node.ArrayNode;
|
||||
import com.fasterxml.jackson.databind.node.ObjectNode;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.time.StopWatch;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.web.filter.RepeatedlyRequestWrapper;
|
||||
@@ -14,8 +20,10 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.web.servlet.ModelAndView;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* web的调用时间统计拦截器
|
||||
@@ -31,19 +39,25 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String url = request.getMethod() + " " + request.getRequestURI();
|
||||
|
||||
// 打印请求参数
|
||||
if (isJsonRequest(request)) {
|
||||
String jsonParam = "";
|
||||
if (request instanceof RepeatedlyRequestWrapper) {
|
||||
BufferedReader reader = request.getReader();
|
||||
jsonParam = IoUtil.read(reader);
|
||||
jsonParam = IoUtil.read(request.getReader());
|
||||
if (StringUtils.isNotBlank(jsonParam)) {
|
||||
ObjectMapper objectMapper = JsonUtils.getObjectMapper();
|
||||
JsonNode rootNode = objectMapper.readTree(jsonParam);
|
||||
removeSensitiveFields(rootNode, SystemConstants.EXCLUDE_PROPERTIES);
|
||||
jsonParam = rootNode.toString();
|
||||
}
|
||||
}
|
||||
log.info("[PLUS]开始请求 => URL[{}],参数类型[json],参数:[{}]", url, jsonParam);
|
||||
} else {
|
||||
Map<String, String[]> parameterMap = request.getParameterMap();
|
||||
if (MapUtil.isNotEmpty(parameterMap)) {
|
||||
String parameters = JsonUtils.toJsonString(parameterMap);
|
||||
Map<String, String[]> map = new LinkedHashMap<>(parameterMap);
|
||||
MapUtil.removeAny(map, SystemConstants.EXCLUDE_PROPERTIES);
|
||||
String parameters = JsonUtils.toJsonString(map);
|
||||
log.info("[PLUS]开始请求 => URL[{}],参数类型[param],参数:[{}]", url, parameters);
|
||||
} else {
|
||||
log.info("[PLUS]开始请求 => URL[{}],无参数", url);
|
||||
@@ -57,6 +71,30 @@ public class PlusWebInvokeTimeInterceptor implements HandlerInterceptor {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void removeSensitiveFields(JsonNode node, String[] excludeProperties) {
|
||||
if (node == null) {
|
||||
return;
|
||||
}
|
||||
if (node.isObject()) {
|
||||
ObjectNode objectNode = (ObjectNode) node;
|
||||
// 收集要删除的字段名(避免 ConcurrentModification)
|
||||
Set<String> fieldsToRemove = new HashSet<>();
|
||||
objectNode.fieldNames().forEachRemaining(fieldName -> {
|
||||
if (ArrayUtil.contains(excludeProperties, fieldName)) {
|
||||
fieldsToRemove.add(fieldName);
|
||||
}
|
||||
});
|
||||
fieldsToRemove.forEach(objectNode::remove);
|
||||
// 递归处理子节点
|
||||
objectNode.elements().forEachRemaining(child -> removeSensitiveFields(child, excludeProperties));
|
||||
} else if (node.isArray()) {
|
||||
ArrayNode arrayNode = (ArrayNode) node;
|
||||
for (JsonNode child : arrayNode) {
|
||||
removeSensitiveFields(child, excludeProperties);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.dromara.common.websocket.holder.WebSocketSessionHolder;
|
||||
import org.dromara.common.websocket.utils.WebSocketUtils;
|
||||
import org.springframework.web.socket.*;
|
||||
import org.springframework.web.socket.handler.AbstractWebSocketHandler;
|
||||
import org.springframework.web.socket.handler.ConcurrentWebSocketSessionDecorator;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
@@ -33,7 +34,7 @@ public class PlusWebSocketHandler extends AbstractWebSocketHandler {
|
||||
log.info("[connect] invalid token received. sessionId: {}", session.getId());
|
||||
return;
|
||||
}
|
||||
WebSocketSessionHolder.addSession(loginUser.getUserId(), session);
|
||||
WebSocketSessionHolder.addSession(loginUser.getUserId(), new ConcurrentWebSocketSessionDecorator(session, 10 * 1000, 64000));
|
||||
log.info("[connect] sessionId: {},userId:{},userType:{}", session.getId(), loginUser.getUserId(), loginUser.getUserType());
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ public class WebSocketUtils {
|
||||
* @param session WebSocket会话
|
||||
* @param message 要发送的WebSocket消息对象
|
||||
*/
|
||||
private synchronized static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
private static void sendMessage(WebSocketSession session, WebSocketMessage<?> message) {
|
||||
if (session == null || !session.isOpen()) {
|
||||
log.warn("[send] session会话已经关闭");
|
||||
} else {
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
package com.aizuda.snailjob.server.common.register;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.aizuda.snailjob.common.core.enums.NodeTypeEnum;
|
||||
import com.aizuda.snailjob.common.core.util.JsonUtil;
|
||||
import com.aizuda.snailjob.common.core.util.NetUtil;
|
||||
import com.aizuda.snailjob.common.core.util.SnailJobVersion;
|
||||
import com.aizuda.snailjob.common.core.util.StreamUtils;
|
||||
import com.aizuda.snailjob.common.log.SnailJobLog;
|
||||
import com.aizuda.snailjob.server.common.cache.CacheConsumerGroup;
|
||||
import com.aizuda.snailjob.server.common.config.SystemProperties;
|
||||
import com.aizuda.snailjob.server.common.convert.RegisterNodeInfoConverter;
|
||||
import com.aizuda.snailjob.server.common.dto.ServerNodeExtAttrs;
|
||||
import com.aizuda.snailjob.server.common.handler.InstanceManager;
|
||||
import com.aizuda.snailjob.template.datasource.persistence.po.ServerNode;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.web.ServerProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 服务端注册
|
||||
*
|
||||
* @author opensnail
|
||||
* @date 2023-06-07
|
||||
* @since 1.6.0
|
||||
*/
|
||||
@Component(ServerRegister.BEAN_NAME)
|
||||
@RequiredArgsConstructor
|
||||
public class ServerRegister extends AbstractRegister {
|
||||
public static final String BEAN_NAME = "serverRegister";
|
||||
private final ScheduledExecutorService serverRegisterNode = Executors.newSingleThreadScheduledExecutor(r -> new Thread(r, "server-register-node"));
|
||||
public static final int DELAY_TIME = 30;
|
||||
public static final String CURRENT_CID;
|
||||
public static final String GROUP_NAME = "DEFAULT_SERVER";
|
||||
public static final String NAMESPACE_ID = "DEFAULT_SERVER_NAMESPACE_ID";
|
||||
private final InstanceManager instanceManager;
|
||||
private final SystemProperties systemProperties;
|
||||
private final ServerProperties serverProperties;
|
||||
|
||||
static {
|
||||
CURRENT_CID = IdUtil.getSnowflakeNextIdStr();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(int type) {
|
||||
return getNodeType().equals(type);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void beforeProcessor(RegisterContext context) {
|
||||
// 新增扩展参数
|
||||
ServerNodeExtAttrs serverNodeExtAttrs = new ServerNodeExtAttrs();
|
||||
serverNodeExtAttrs.setWebPort(serverProperties.getPort());
|
||||
serverNodeExtAttrs.setSystemVersion(SnailJobVersion.getVersion());
|
||||
|
||||
context.setGroupName(GROUP_NAME);
|
||||
context.setHostId(CURRENT_CID);
|
||||
String serverHost = systemProperties.getServerHost();
|
||||
if (StrUtil.isEmptyIfStr(serverHost)) {
|
||||
serverHost = NetUtil.getLocalIpStr();
|
||||
}
|
||||
context.setHostIp(serverHost);
|
||||
context.setHostPort(systemProperties.getServerPort());
|
||||
context.setContextPath(Optional.ofNullable(serverProperties.getServlet().getContextPath()).orElse(StrUtil.EMPTY));
|
||||
context.setNamespaceId(NAMESPACE_ID);
|
||||
context.setExtAttrs(JsonUtil.toJsonString(serverNodeExtAttrs));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LocalDateTime getExpireAt() {
|
||||
return LocalDateTime.now().plusSeconds(DELAY_TIME);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doRegister(RegisterContext context, ServerNode serverNode) {
|
||||
refreshExpireAt(Lists.newArrayList(serverNode));
|
||||
return Boolean.TRUE;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected void afterProcessor(final ServerNode serverNode) {
|
||||
try {
|
||||
// 同步当前POD消费的组的节点信息
|
||||
// netty的client只会注册到一个服务端,若组分配的和client连接的不是一个POD则会导致当前POD没有其他客户端的注册信息
|
||||
ConcurrentMap<String /*groupName*/, Set<String>/*namespaceId*/> allConsumerGroupName = CacheConsumerGroup.getAllConsumerGroupName();
|
||||
if (CollUtil.isNotEmpty(allConsumerGroupName)) {
|
||||
Set<String> namespaceIdSets = StreamUtils.toSetByFlatMap(allConsumerGroupName.values(), Set::stream);
|
||||
if (CollUtil.isEmpty(namespaceIdSets)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<ServerNode> serverNodes = serverNodeMapper.selectList(
|
||||
new LambdaQueryWrapper<ServerNode>()
|
||||
.eq(ServerNode::getNodeType, NodeTypeEnum.CLIENT.getType())
|
||||
.in(ServerNode::getNamespaceId, namespaceIdSets)
|
||||
.in(ServerNode::getGroupName, allConsumerGroupName.keySet()));
|
||||
for (final ServerNode node : serverNodes) {
|
||||
// 刷新全量本地缓存
|
||||
instanceManager.registerOrUpdate(RegisterNodeInfoConverter.INSTANCE.toRegisterNodeInfo(node));
|
||||
// 刷新过期时间
|
||||
CacheConsumerGroup.addOrUpdate(node.getGroupName(), node.getNamespaceId());
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
SnailJobLog.LOCAL.error("Client refresh failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Integer getNodeType() {
|
||||
return NodeTypeEnum.SERVER.getType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
SnailJobLog.LOCAL.info("ServerRegister start");
|
||||
|
||||
serverRegisterNode.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
this.register(new RegisterContext());
|
||||
} catch (Exception e) {
|
||||
SnailJobLog.LOCAL.error("Server-side registration failed", e);
|
||||
}
|
||||
}, 0, DELAY_TIME * 2 / 3, TimeUnit.SECONDS);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
SnailJobLog.LOCAL.info("ServerRegister close");
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,8 @@ public class TestDemoServiceImpl implements ITestDemoService {
|
||||
private LambdaQueryWrapper<TestDemo> buildQueryWrapper(TestDemoBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<TestDemo> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(bo.getDeptId() != null, TestDemo::getDeptId, bo.getDeptId());
|
||||
lqw.eq(bo.getUserId() != null, TestDemo::getUserId, bo.getUserId());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getTestKey()), TestDemo::getTestKey, bo.getTestKey());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getValue()), TestDemo::getValue, bo.getValue());
|
||||
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.dromara.demo.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.demo.domain.TestTree;
|
||||
@@ -9,7 +10,6 @@ import org.dromara.demo.domain.bo.TestTreeBo;
|
||||
import org.dromara.demo.domain.vo.TestTreeVo;
|
||||
import org.dromara.demo.mapper.TestTreeMapper;
|
||||
import org.dromara.demo.service.ITestTreeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
@@ -44,6 +44,8 @@ public class TestTreeServiceImpl implements ITestTreeService {
|
||||
private LambdaQueryWrapper<TestTree> buildQueryWrapper(TestTreeBo bo) {
|
||||
Map<String, Object> params = bo.getParams();
|
||||
LambdaQueryWrapper<TestTree> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(bo.getDeptId() != null, TestTree::getDeptId, bo.getDeptId());
|
||||
lqw.eq(bo.getUserId() != null, TestTree::getUserId, bo.getUserId());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getTreeName()), TestTree::getTreeName, bo.getTreeName());
|
||||
lqw.between(params.get("beginCreateTime") != null && params.get("endCreateTime") != null,
|
||||
TestTree::getCreateTime, params.get("beginCreateTime"), params.get("endCreateTime"));
|
||||
|
||||
@@ -90,10 +90,12 @@ public class GenController extends BaseController {
|
||||
/**
|
||||
* 导入表结构(保存)
|
||||
*
|
||||
* @param tables 表名串
|
||||
* @param tables 表名串
|
||||
* @param dataName 数据源名称
|
||||
*/
|
||||
@SaCheckPermission("tool:gen:import")
|
||||
@Log(title = "代码生成", businessType = BusinessType.IMPORT)
|
||||
@Lock4j(keys = {"#dataName"}, acquireTimeout = 10000)
|
||||
@RepeatSubmit()
|
||||
@PostMapping("/importTable")
|
||||
public R<Void> importTableSave(String tables, String dataName) {
|
||||
@@ -175,7 +177,7 @@ public class GenController extends BaseController {
|
||||
*/
|
||||
@SaCheckPermission("tool:gen:edit")
|
||||
@Log(title = "代码生成", businessType = BusinessType.UPDATE)
|
||||
@Lock4j
|
||||
@Lock4j(keys = {"#tableId"}, acquireTimeout = 5000)
|
||||
@GetMapping("/synchDb/{tableId}")
|
||||
public R<Void> synchDb(@PathVariable("tableId") Long tableId) {
|
||||
genTableService.synchDb(tableId);
|
||||
@@ -214,7 +216,7 @@ public class GenController extends BaseController {
|
||||
*/
|
||||
@SaCheckPermission("tool:gen:list")
|
||||
@GetMapping(value = "/getDataNames")
|
||||
public R<Object> getCurrentDataSourceNameList(){
|
||||
public R<Object> getCurrentDataSourceNameList() {
|
||||
return R.ok(DataBaseHelper.getDataSourceNameList());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,13 +4,12 @@ import com.baomidou.mybatisplus.annotation.FieldStrategy;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.apache.ibatis.type.JdbcType;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 代码生成业务字段表 gen_table_column
|
||||
@@ -115,6 +114,7 @@ public class GenTableColumn extends BaseEntity {
|
||||
/**
|
||||
* 字典类型
|
||||
*/
|
||||
@TableField(updateStrategy = FieldStrategy.ALWAYS, jdbcType = JdbcType.VARCHAR)
|
||||
private String dictType;
|
||||
|
||||
/**
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.baomidou.dynamic.datasource.annotation.DS;
|
||||
import com.baomidou.dynamic.datasource.annotation.DSTransactional;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -28,6 +27,7 @@ import org.dromara.common.core.utils.file.FileUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.mybatis.utils.IdGeneratorUtil;
|
||||
import org.dromara.generator.constant.GenConstants;
|
||||
import org.dromara.generator.domain.GenTable;
|
||||
import org.dromara.generator.domain.GenTableColumn;
|
||||
@@ -60,7 +60,6 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
|
||||
private final GenTableMapper baseMapper;
|
||||
private final GenTableColumnMapper genTableColumnMapper;
|
||||
private final IdentifierGenerator identifierGenerator;
|
||||
|
||||
private static final String[] TABLE_IGNORE = new String[]{"sj_", "flow_", "gen_"};
|
||||
|
||||
@@ -322,7 +321,7 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
GenTable table = baseMapper.selectGenTableById(tableId);
|
||||
List<Long> menuIds = new ArrayList<>();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
menuIds.add(identifierGenerator.nextId(null).longValue());
|
||||
menuIds.add(IdGeneratorUtil.nextLongId());
|
||||
}
|
||||
table.setMenuIds(menuIds);
|
||||
// 设置主键列信息
|
||||
@@ -468,7 +467,7 @@ public class GenTableServiceImpl implements IGenTableService {
|
||||
GenTable table = baseMapper.selectGenTableById(tableId);
|
||||
List<Long> menuIds = new ArrayList<>();
|
||||
for (int i = 0; i < 6; i++) {
|
||||
menuIds.add(identifierGenerator.nextId(null).longValue());
|
||||
menuIds.add(IdGeneratorUtil.nextLongId());
|
||||
}
|
||||
table.setMenuIds(menuIds);
|
||||
// 设置主键列信息
|
||||
|
||||
@@ -1,26 +1,27 @@
|
||||
package org.dromara.system.controller.system;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.*;
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.bo.SysClientBo;
|
||||
import org.dromara.system.service.ISysClientService;
|
||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.system.domain.bo.SysClientBo;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.service.ISysClientService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 客户端管理
|
||||
@@ -76,6 +77,9 @@ public class SysClientController extends BaseController {
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody SysClientBo bo) {
|
||||
if (!sysClientService.checkClickKeyUnique(bo)) {
|
||||
return R.fail("新增客户端'" + bo.getClientKey() + "'失败,客户端key已存在");
|
||||
}
|
||||
return toAjax(sysClientService.insertByBo(bo));
|
||||
}
|
||||
|
||||
@@ -87,6 +91,9 @@ public class SysClientController extends BaseController {
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody SysClientBo bo) {
|
||||
if (!sysClientService.checkClickKeyUnique(bo)) {
|
||||
return R.fail("修改客户端'" + bo.getClientKey() + "'失败,客户端key已存在");
|
||||
}
|
||||
return toAjax(sysClientService.updateByBo(bo));
|
||||
}
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@ public class SysMenuController extends BaseController {
|
||||
return R.fail("新增菜单'" + menu.getMenuName() + "'失败,菜单名称已存在");
|
||||
} else if (SystemConstants.YES_FRAME.equals(menu.getIsFrame()) && !StringUtils.ishttp(menu.getPath())) {
|
||||
return R.fail("新增菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
|
||||
} else if (!menuService.checkRouteConfigUnique(menu)) {
|
||||
return R.fail("新增菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
|
||||
}
|
||||
return toAjax(menuService.insertMenu(menu));
|
||||
}
|
||||
@@ -156,6 +158,8 @@ public class SysMenuController extends BaseController {
|
||||
return R.fail("修改菜单'" + menu.getMenuName() + "'失败,地址必须以http(s)://开头");
|
||||
} else if (menu.getMenuId().equals(menu.getParentId())) {
|
||||
return R.fail("修改菜单'" + menu.getMenuName() + "'失败,上级菜单不能选择自己");
|
||||
} else if (!menuService.checkRouteConfigUnique(menu)) {
|
||||
return R.fail("修改菜单'" + menu.getMenuName() + "'失败,路由名称或地址已存在");
|
||||
}
|
||||
return toAjax(menuService.updateMenu(menu));
|
||||
}
|
||||
|
||||
@@ -2,21 +2,20 @@ package org.dromara.system.controller.system;
|
||||
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.validate.QueryGroup;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.dromara.system.domain.bo.SysOssBo;
|
||||
import org.dromara.system.domain.vo.SysOssUploadVo;
|
||||
import org.dromara.system.domain.vo.SysOssVo;
|
||||
import org.dromara.system.service.ISysOssService;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -70,9 +69,6 @@ public class SysOssController extends BaseController {
|
||||
@Log(title = "OSS对象存储", businessType = BusinessType.INSERT)
|
||||
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<SysOssUploadVo> upload(@RequestPart("file") MultipartFile file) {
|
||||
if (ObjectUtil.isNull(file)) {
|
||||
return R.fail("上传文件不能为空");
|
||||
}
|
||||
SysOssVo oss = ossService.upload(file);
|
||||
SysOssUploadVo uploadVo = new SysOssUploadVo();
|
||||
uploadVo.setUrl(oss.getUrl());
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.dromara.system.controller.system;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.digest.BCrypt;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
@@ -114,7 +115,7 @@ public class SysProfileController extends BaseController {
|
||||
@Log(title = "用户头像", businessType = BusinessType.UPDATE)
|
||||
@PostMapping(value = "/avatar", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
public R<AvatarVo> avatar(@RequestPart("avatarfile") MultipartFile avatarfile) {
|
||||
if (!avatarfile.isEmpty()) {
|
||||
if (ObjectUtil.isNotNull(avatarfile) && !avatarfile.isEmpty()) {
|
||||
String extension = FileUtil.extName(avatarfile.getOriginalFilename());
|
||||
if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) {
|
||||
return R.fail("文件格式不正确,请上传" + Arrays.toString(MimeTypeUtils.IMAGE_EXTENSION) + "格式");
|
||||
|
||||
@@ -193,4 +193,19 @@ public class SysTenantController extends BaseController {
|
||||
return R.ok("同步租户字典成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步租户参数配置
|
||||
*/
|
||||
@SaCheckRole(TenantConstants.SUPER_ADMIN_ROLE_KEY)
|
||||
@Log(title = "租户管理", businessType = BusinessType.INSERT)
|
||||
@Lock4j
|
||||
@GetMapping("/syncTenantConfig")
|
||||
public R<Void> syncTenantConfig() {
|
||||
if (!TenantHelper.isEnable()) {
|
||||
return R.fail("当前未开启租户模式");
|
||||
}
|
||||
tenantService.syncTenantConfig();
|
||||
return R.ok("同步租户参数配置成功");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -130,11 +130,11 @@ public class SysMenu extends BaseEntity {
|
||||
public String getRouterPath() {
|
||||
String routerPath = this.path;
|
||||
// 内链打开外网方式
|
||||
if (getParentId() != 0L && isInnerLink()) {
|
||||
if (!Constants.TOP_PARENT_ID.equals(getParentId()) && isInnerLink()) {
|
||||
routerPath = innerLinkReplaceEach(routerPath);
|
||||
}
|
||||
// 非外链并且是一级目录(类型为目录)
|
||||
if (0L == getParentId() && SystemConstants.TYPE_DIR.equals(getMenuType())
|
||||
if (Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_DIR.equals(getMenuType())
|
||||
&& SystemConstants.NO_FRAME.equals(getIsFrame())) {
|
||||
routerPath = "/" + this.path;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ public class SysMenu extends BaseEntity {
|
||||
String component = SystemConstants.LAYOUT;
|
||||
if (StringUtils.isNotEmpty(this.component) && !isMenuFrame()) {
|
||||
component = this.component;
|
||||
} else if (StringUtils.isEmpty(this.component) && getParentId() != 0L && isInnerLink()) {
|
||||
} else if (StringUtils.isEmpty(this.component) && !Constants.TOP_PARENT_ID.equals(getParentId()) && isInnerLink()) {
|
||||
component = SystemConstants.INNER_LINK;
|
||||
} else if (StringUtils.isEmpty(this.component) && isParentView()) {
|
||||
component = SystemConstants.PARENT_VIEW;
|
||||
@@ -164,7 +164,7 @@ public class SysMenu extends BaseEntity {
|
||||
* 是否为菜单内部跳转
|
||||
*/
|
||||
public boolean isMenuFrame() {
|
||||
return getParentId() == 0L && SystemConstants.TYPE_MENU.equals(menuType) && isFrame.equals(SystemConstants.NO_FRAME);
|
||||
return Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_MENU.equals(menuType) && isFrame.equals(SystemConstants.NO_FRAME);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +178,7 @@ public class SysMenu extends BaseEntity {
|
||||
* 是否为parent_view组件
|
||||
*/
|
||||
public boolean isParentView() {
|
||||
return getParentId() != 0L && SystemConstants.TYPE_DIR.equals(menuType);
|
||||
return !Constants.TOP_PARENT_ID.equals(getParentId()) && SystemConstants.TYPE_DIR.equals(menuType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SysUser extends TenantEntity {
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
* 账号状态(0正常 1停用)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import jakarta.validation.constraints.Size;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.common.core.constant.RegexConstants;
|
||||
import org.dromara.common.json.validate.JsonPattern;
|
||||
import org.dromara.common.json.validate.JsonType;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
import org.dromara.system.domain.SysMenu;
|
||||
|
||||
@@ -61,6 +63,7 @@ public class SysMenuBo extends BaseEntity {
|
||||
/**
|
||||
* 路由参数
|
||||
*/
|
||||
@JsonPattern(type = JsonType.OBJECT, message = "路由参数必须符合JSON格式")
|
||||
private String queryParam;
|
||||
|
||||
/**
|
||||
|
||||
@@ -78,7 +78,7 @@ public class SysUserBo extends BaseEntity {
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
* 账号状态(0正常 1停用)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -63,9 +63,9 @@ public class SysUserExportVo implements Serializable {
|
||||
private String sex;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
* 账号状态(0正常 1停用)
|
||||
*/
|
||||
@ExcelProperty(value = "帐号状态", converter = ExcelDictConvert.class)
|
||||
@ExcelProperty(value = "账号状态", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_normal_disable")
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -67,9 +67,9 @@ public class SysUserImportVo implements Serializable {
|
||||
private String sex;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
* 账号状态(0正常 1停用)
|
||||
*/
|
||||
@ExcelProperty(value = "帐号状态", converter = ExcelDictConvert.class)
|
||||
@ExcelProperty(value = "账号状态", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(dictType = "sys_normal_disable")
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -89,7 +89,7 @@ public class SysUserVo implements Serializable {
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 帐号状态(0正常 1停用)
|
||||
* 账号状态(0正常 1停用)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
package org.dromara.system.service;
|
||||
|
||||
import org.dromara.system.domain.SysClient;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
import org.dromara.system.domain.bo.SysClientBo;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.system.domain.bo.SysClientBo;
|
||||
import org.dromara.system.domain.vo.SysClientVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -57,4 +56,11 @@ public interface ISysClientService {
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 校验客户端key是否唯一
|
||||
*
|
||||
* @param client 客户端信息
|
||||
* @return 结果
|
||||
*/
|
||||
boolean checkClickKeyUnique(SysClientBo client);
|
||||
}
|
||||
|
||||
@@ -160,4 +160,13 @@ public interface ISysMenuService {
|
||||
* @return 结果
|
||||
*/
|
||||
boolean checkMenuNameUnique(SysMenuBo menu);
|
||||
|
||||
/**
|
||||
* 校验路由组合是否唯一
|
||||
*
|
||||
* @param menu 菜单信息
|
||||
* @return 结果
|
||||
*/
|
||||
boolean checkRouteConfigUnique(SysMenuBo menu);
|
||||
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package org.dromara.system.service;
|
||||
|
||||
import org.dromara.system.domain.vo.SysTenantVo;
|
||||
import org.dromara.system.domain.bo.SysTenantBo;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.system.domain.bo.SysTenantBo;
|
||||
import org.dromara.system.domain.vo.SysTenantVo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
@@ -84,4 +84,9 @@ public interface ISysTenantService {
|
||||
* 同步租户字典
|
||||
*/
|
||||
void syncTenantDict();
|
||||
|
||||
/**
|
||||
* 同步租户参数配置
|
||||
*/
|
||||
void syncTenantConfig();
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ public interface ISysUserService {
|
||||
* 修改用户状态
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param status 帐号状态
|
||||
* @param status 账号状态
|
||||
* @return 结果
|
||||
*/
|
||||
int updateUserStatus(Long userId, String status);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.dromara.system.service.impl;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.crypto.SecureUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
||||
@@ -136,4 +137,19 @@ public class SysClientServiceImpl implements ISysClientService {
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
return baseMapper.deleteByIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验客户端key是否唯一
|
||||
*
|
||||
* @param client 客户端信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public boolean checkClickKeyUnique(SysClientBo client) {
|
||||
boolean exist = baseMapper.exists(new LambdaQueryWrapper<SysClient>()
|
||||
.eq(SysClient::getClientKey, client.getClientKey())
|
||||
.ne(ObjectUtil.isNotNull(client.getId()), SysClient::getId, client.getId()));
|
||||
return !exist;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.dromara.system.service.impl;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
@@ -14,6 +15,7 @@ import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.ObjectUtils;
|
||||
import org.dromara.common.core.utils.SpringUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.redis.utils.CacheUtils;
|
||||
@@ -82,6 +84,7 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
|
||||
|
||||
/**
|
||||
* 获取注册开关
|
||||
*
|
||||
* @param tenantId 租户id
|
||||
* @return true开启,false关闭
|
||||
*/
|
||||
@@ -212,4 +215,54 @@ public class SysConfigServiceImpl implements ISysConfigService, ConfigService {
|
||||
return SpringUtils.getAopProxy(this).selectConfigByKey(configKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取 Map 类型的配置
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Dict 对象,如果配置为空或无法解析,返回空 Dict
|
||||
*/
|
||||
@Override
|
||||
public Dict getConfigMap(String configKey) {
|
||||
String configValue = getConfigValue(configKey);
|
||||
return JsonUtils.parseMap(configValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取 Map 类型的配置列表
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @return Dict 列表,如果配置为空或无法解析,返回空列表
|
||||
*/
|
||||
@Override
|
||||
public List<Dict> getConfigArrayMap(String configKey) {
|
||||
String configValue = getConfigValue(configKey);
|
||||
return JsonUtils.parseArrayMap(configValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取指定类型的配置对象
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @param clazz 目标对象类型
|
||||
* @return 对象实例,如果配置为空或无法解析,返回 null
|
||||
*/
|
||||
@Override
|
||||
public <T> T getConfigObject(String configKey, Class<T> clazz) {
|
||||
String configValue = getConfigValue(configKey);
|
||||
return JsonUtils.parseObject(configValue, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据参数 key 获取指定类型的配置列表=
|
||||
*
|
||||
* @param configKey 参数 key
|
||||
* @param clazz 目标元素类型
|
||||
* @return 指定类型列表,如果配置为空或无法解析,返回空列表
|
||||
*/
|
||||
@Override
|
||||
public <T> List<T> getConfigArray(String configKey, Class<T> clazz) {
|
||||
String configValue = getConfigValue(configKey);
|
||||
return JsonUtils.parseArray(configValue, clazz);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
@@ -29,13 +30,17 @@ import org.dromara.system.service.ISysMenuService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 菜单 业务层处理
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class SysMenuServiceImpl implements ISysMenuService {
|
||||
@@ -241,6 +246,8 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
.setWeight(menu.getOrderNum());
|
||||
menuTree.put("menuType", menu.getMenuType());
|
||||
menuTree.put("icon", menu.getIcon());
|
||||
menuTree.put("visible", menu.getVisible());
|
||||
menuTree.put("status", menu.getStatus());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,6 +358,51 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
return !exist;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验路由名称是否唯一
|
||||
*
|
||||
* @param menuBo 菜单信息
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
public boolean checkRouteConfigUnique(SysMenuBo menuBo) {
|
||||
SysMenu menu = MapstructUtils.convert(menuBo, SysMenu.class);
|
||||
if (SystemConstants.TYPE_BUTTON.equals(menu.getMenuType())) {
|
||||
return true;
|
||||
}
|
||||
long menuId = ObjectUtil.isNull(menu.getMenuId()) ? -1L : menu.getMenuId();
|
||||
Long parentId = menu.getParentId();
|
||||
String path = menu.getPath();
|
||||
String routeName = StringUtils.isEmpty(menu.getRouteName()) ? path : menu.getRouteName();
|
||||
List<SysMenu> sysMenuList = baseMapper.selectList(
|
||||
new LambdaQueryWrapper<SysMenu>()
|
||||
.in(SysMenu::getMenuType, SystemConstants.TYPE_DIR, SystemConstants.TYPE_MENU)
|
||||
.and(w ->
|
||||
w.eq(SysMenu::getPath, path).or().eq(SysMenu::getPath, routeName)
|
||||
));
|
||||
for (SysMenu sysMenu : sysMenuList) {
|
||||
if (!sysMenu.getMenuId().equals(menuId)) {
|
||||
Long dbParentId = sysMenu.getParentId();
|
||||
String dbPath = sysMenu.getPath();
|
||||
String dbRouteName = StringUtils.isEmpty(sysMenu.getRouteName()) ? dbPath : sysMenu.getRouteName();
|
||||
if (StringUtils.equalsAnyIgnoreCase(path, dbPath) && parentId.equals(dbParentId)) {
|
||||
log.warn("[同级路由冲突] 同级下已存在相同路由路径 '{}',冲突菜单:{}", dbPath, sysMenu.getMenuName());
|
||||
return false;
|
||||
} else if (StringUtils.equalsAnyIgnoreCase(path, dbPath)
|
||||
&& Constants.TOP_PARENT_ID.equals(parentId)
|
||||
&& Constants.TOP_PARENT_ID.equals(dbParentId)) {
|
||||
log.warn("[根目录路由冲突] 根目录下路由 '{}' 必须唯一,已被菜单 '{}' 占用", path, sysMenu.getMenuName());
|
||||
return false;
|
||||
} else if (StringUtils.equalsAnyIgnoreCase(routeName, dbRouteName)
|
||||
&& sysMenu.getMenuType().equals(menu.getMenuType())) {
|
||||
log.warn("[路由名称冲突] 路由名称 '{}' 需全局唯一,已被菜单 '{}' 使用", routeName, sysMenu.getMenuName());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据父节点的ID获取所有子节点
|
||||
*
|
||||
|
||||
@@ -192,6 +192,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
*/
|
||||
@Override
|
||||
public SysOssVo upload(MultipartFile file) {
|
||||
if (ObjectUtil.isNull(file) || file.isEmpty()) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
String originalfileName = file.getOriginalFilename();
|
||||
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
||||
OssClient storage = OssFactory.instance();
|
||||
@@ -216,12 +219,16 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
*/
|
||||
@Override
|
||||
public SysOssVo upload(File file) {
|
||||
if (ObjectUtil.isNull(file) || !file.isFile() || file.length() <= 0) {
|
||||
throw new ServiceException("上传文件不能为空");
|
||||
}
|
||||
String originalfileName = file.getName();
|
||||
String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length());
|
||||
OssClient storage = OssFactory.instance();
|
||||
long length = file.length();
|
||||
UploadResult uploadResult = storage.uploadSuffix(file, suffix);
|
||||
SysOssExt ext1 = new SysOssExt();
|
||||
ext1.setFileSize(file.length());
|
||||
ext1.setFileSize(length);
|
||||
// 保存文件信息
|
||||
return buildResultEntity(originalfileName, suffix, storage.getConfigKey(), uploadResult, ext1);
|
||||
}
|
||||
@@ -270,7 +277,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
|
||||
OssClient storage = OssFactory.instance(oss.getService());
|
||||
// 仅修改桶类型为 private 的URL,临时URL时长为120s
|
||||
if (AccessPolicyType.PRIVATE == storage.getAccessPolicy()) {
|
||||
oss.setUrl(storage.getPrivateUrl(oss.getFileName(), Duration.ofSeconds(120)));
|
||||
oss.setUrl(storage.createPresignedGetUrl(oss.getFileName(), Duration.ofSeconds(120)));
|
||||
}
|
||||
return oss;
|
||||
}
|
||||
|
||||
@@ -508,4 +508,60 @@ public class SysTenantServiceImpl implements ISysTenantService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步租户参数配置
|
||||
*/
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void syncTenantConfig() {
|
||||
// 查询超管 所有参数配置
|
||||
List<SysConfig> configList = TenantHelper.ignore(() -> configMapper.selectList());
|
||||
|
||||
// 所有租户参数配置
|
||||
Map<String, List<SysConfig>> configMap = StreamUtils.groupByKey(configList, TenantEntity::getTenantId);
|
||||
|
||||
// 默认租户字典类型列表
|
||||
List<SysConfig> defaultConfigList = configMap.get(TenantConstants.DEFAULT_TENANT_ID);
|
||||
|
||||
// 获取所有租户编号
|
||||
List<String> tenantIds = baseMapper.selectObjs(
|
||||
new LambdaQueryWrapper<SysTenant>().select(SysTenant::getTenantId)
|
||||
.eq(SysTenant::getStatus, SystemConstants.NORMAL), x -> {
|
||||
return Convert.toStr(x);
|
||||
});
|
||||
// 待入库的字典类型和字典数据
|
||||
List<SysConfig> saveConfigList = new ArrayList<>();
|
||||
// 待同步的租户编号(用于清除对于租户的字典缓存)
|
||||
Set<String> syncTenantIds = new HashSet<>();
|
||||
// 循环所有租户,处理需要同步的数据
|
||||
for (String tenantId : tenantIds) {
|
||||
// 排除默认租户
|
||||
if (TenantConstants.DEFAULT_TENANT_ID.equals(tenantId)) {
|
||||
continue;
|
||||
}
|
||||
// 根据默认租户的字典类型进行数据同步
|
||||
for (SysConfig config : defaultConfigList) {
|
||||
// 获取当前租户的字典类型列表
|
||||
List<String> typeList = StreamUtils.toList(configMap.get(tenantId), SysConfig::getConfigKey);
|
||||
if (!typeList.contains(config.getConfigKey())) {
|
||||
SysConfig type = BeanUtil.toBean(config, SysConfig.class);
|
||||
type.setConfigId(null);
|
||||
type.setTenantId(tenantId);
|
||||
type.setCreateTime(null);
|
||||
type.setUpdateTime(null);
|
||||
syncTenantIds.add(tenantId);
|
||||
saveConfigList.add(type);
|
||||
}
|
||||
}
|
||||
}
|
||||
TenantHelper.ignore(() -> {
|
||||
if (CollUtil.isNotEmpty(saveConfigList)) {
|
||||
configMapper.insertBatch(saveConfigList);
|
||||
}
|
||||
});
|
||||
for (String tenantId : syncTenantIds) {
|
||||
TenantHelper.dynamic(tenantId, () -> CacheUtils.clear(CacheNames.SYS_CONFIG));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
|
||||
w.in(SysUser::getDeptId, ids);
|
||||
}).orderByAsc(SysUser::getUserId);
|
||||
if (StringUtils.isNotBlank(user.getExcludeUserIds())) {
|
||||
wrapper.notIn(SysUser::getUserId, StringUtils.splitList(user.getExcludeUserIds()));
|
||||
wrapper.notIn(SysUser::getUserId, StringUtils.splitTo(user.getExcludeUserIds(), Convert::toLong));
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
@@ -375,7 +375,7 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
|
||||
* 修改用户状态
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param status 帐号状态
|
||||
* @param status 账号状态
|
||||
* @return 结果
|
||||
*/
|
||||
@Override
|
||||
|
||||
@@ -92,4 +92,20 @@ public interface FlowConstant {
|
||||
* 业务编码
|
||||
*/
|
||||
String BUSINESS_CODE = "businessCode";
|
||||
|
||||
/**
|
||||
* 忽略-办理权限校验(true:忽略,false:不忽略)
|
||||
*/
|
||||
String VAR_IGNORE = "ignore";
|
||||
|
||||
/**
|
||||
* 忽略-委派处理(true:忽略,false:不忽略)
|
||||
*/
|
||||
String VAR_IGNORE_DEPUTE = "ignoreDepute";
|
||||
|
||||
/**
|
||||
* 忽略-会签票签处理(true:忽略,false:不忽略)
|
||||
*/
|
||||
String VAR_IGNORE_COOPERATE = "ignoreCooperate";
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public enum ButtonPermissionEnum implements NodeExtEnum {
|
||||
/**
|
||||
* 是否能抄送
|
||||
*/
|
||||
COPY("是否能抄送", "copy", false),
|
||||
COPY("是否能抄送", "copy", true),
|
||||
|
||||
/**
|
||||
* 是否显示退回
|
||||
|
||||
@@ -187,6 +187,7 @@ public class FlwDefinitionController extends BaseController {
|
||||
@RepeatSubmit()
|
||||
@PutMapping("/active/{id}")
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
|
||||
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
|
||||
return R.ok(active ? defService.active(id) : defService.unActive(id));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package org.dromara.workflow.controller;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.idempotent.annotation.RepeatSubmit;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
@@ -75,8 +77,9 @@ public class FlwInstanceController extends BaseController {
|
||||
* @param businessIds 业务id
|
||||
*/
|
||||
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
|
||||
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
|
||||
public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) {
|
||||
return toAjax(flwInstanceService.deleteByBusinessIds(businessIds));
|
||||
return toAjax(flwInstanceService.deleteByBusinessIds(StreamUtils.toList(businessIds, Convert::toStr)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,6 +88,7 @@ public class FlwInstanceController extends BaseController {
|
||||
* @param instanceIds 实例id
|
||||
*/
|
||||
@DeleteMapping("/deleteByInstanceIds/{instanceIds}")
|
||||
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
|
||||
public R<Void> deleteByInstanceIds(@PathVariable List<Long> instanceIds) {
|
||||
return toAjax(flwInstanceService.deleteByInstanceIds(instanceIds));
|
||||
}
|
||||
@@ -95,6 +99,7 @@ public class FlwInstanceController extends BaseController {
|
||||
* @param instanceIds 实例id
|
||||
*/
|
||||
@DeleteMapping("/deleteHisByInstanceIds/{instanceIds}")
|
||||
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
|
||||
public R<Void> deleteHisByInstanceIds(@PathVariable List<Long> instanceIds) {
|
||||
return toAjax(flwInstanceService.deleteHisByInstanceIds(instanceIds));
|
||||
}
|
||||
@@ -106,6 +111,7 @@ public class FlwInstanceController extends BaseController {
|
||||
*/
|
||||
@RepeatSubmit()
|
||||
@PutMapping("/cancelProcessApply")
|
||||
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
|
||||
public R<Void> cancelProcessApply(@RequestBody FlowCancelBo bo) {
|
||||
return toAjax(flwInstanceService.cancelProcessApply(bo));
|
||||
}
|
||||
@@ -118,6 +124,7 @@ public class FlwInstanceController extends BaseController {
|
||||
*/
|
||||
@RepeatSubmit()
|
||||
@PutMapping("/active/{id}")
|
||||
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
|
||||
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
|
||||
return R.ok(active ? insService.active(id) : insService.unActive(id));
|
||||
}
|
||||
@@ -160,6 +167,7 @@ public class FlwInstanceController extends BaseController {
|
||||
*/
|
||||
@RepeatSubmit()
|
||||
@PutMapping("/updateVariable")
|
||||
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
|
||||
public R<Void> updateVariable(@Validated @RequestBody FlowVariableBo bo) {
|
||||
return toAjax(flwInstanceService.updateVariable(bo));
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程spel达式定义
|
||||
* 流程spel表达式定义
|
||||
*
|
||||
* @author Michelle.Chung
|
||||
* @date 2025-07-04
|
||||
@@ -38,7 +38,7 @@ public class FlwSpelController extends BaseController {
|
||||
private final IFlwSpelService flwSpelService;
|
||||
|
||||
/**
|
||||
* 查询流程spel达式定义列表
|
||||
* 查询流程spel表达式定义列表
|
||||
*/
|
||||
@SaCheckPermission("workflow:spel:list")
|
||||
@GetMapping("/list")
|
||||
@@ -47,7 +47,7 @@ public class FlwSpelController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取流程spel达式定义详细信息
|
||||
* 获取流程spel表达式定义详细信息
|
||||
*
|
||||
* @param id 主键
|
||||
*/
|
||||
@@ -58,10 +58,10 @@ public class FlwSpelController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增流程spel达式定义
|
||||
* 新增流程spel表达式定义
|
||||
*/
|
||||
@SaCheckPermission("workflow:spel:add")
|
||||
@Log(title = "流程spel达式定义", businessType = BusinessType.INSERT)
|
||||
@Log(title = "流程spel表达式定义", businessType = BusinessType.INSERT)
|
||||
@RepeatSubmit()
|
||||
@PostMapping()
|
||||
public R<Void> add(@Validated(AddGroup.class) @RequestBody FlowSpelBo bo) {
|
||||
@@ -69,10 +69,10 @@ public class FlwSpelController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改流程spel达式定义
|
||||
* 修改流程spel表达式定义
|
||||
*/
|
||||
@SaCheckPermission("workflow:spel:edit")
|
||||
@Log(title = "流程spel达式定义", businessType = BusinessType.UPDATE)
|
||||
@Log(title = "流程spel表达式定义", businessType = BusinessType.UPDATE)
|
||||
@RepeatSubmit()
|
||||
@PutMapping()
|
||||
public R<Void> edit(@Validated(EditGroup.class) @RequestBody FlowSpelBo bo) {
|
||||
@@ -80,12 +80,12 @@ public class FlwSpelController extends BaseController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除流程spel达式定义
|
||||
* 删除流程spel表达式定义
|
||||
*
|
||||
* @param ids 主键串
|
||||
*/
|
||||
@SaCheckPermission("workflow:spel:remove")
|
||||
@Log(title = "流程spel达式定义", businessType = BusinessType.DELETE)
|
||||
@Log(title = "流程spel表达式定义", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
|
||||
return toAjax(flwSpelService.deleteWithValidByIds(List.of(ids), true));
|
||||
|
||||
@@ -216,6 +216,7 @@ public class FlwTaskController extends BaseController {
|
||||
* @return 结果
|
||||
*/
|
||||
@PostMapping("/urgeTask")
|
||||
@Log(title = "任务管理", businessType = BusinessType.INSERT)
|
||||
public R<Void> urgeTask(@RequestBody FlowUrgeTaskBo bo) {
|
||||
return toAjax(flwTaskService.urgeTask(bo));
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user