mirror of
https://github.com/dromara/RuoYi-Vue-Plus.git
synced 2025-11-24 09:56:47 +08:00
Compare commits
122 Commits
23338995d7
...
v5.5.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
b30ffa952f | ||
|
|
f616c6931c | ||
|
|
26e10293f5 | ||
|
|
60e578f763 | ||
|
|
5cd4d8ca11 | ||
|
|
41a6230b6e | ||
|
|
effda4f6e8 | ||
|
|
af4c38e439 | ||
|
|
fafa8cd573 | ||
|
|
8909b8a7d4 | ||
|
|
8ae9bde731 | ||
|
|
a703cb2ad1 | ||
|
|
a918b880d6 | ||
|
|
e795e315eb | ||
|
|
81869cfeb3 | ||
|
|
fc6f61bc95 | ||
|
|
d44e45ad3b | ||
|
|
00ed9ddd10 | ||
|
|
341fc144a1 | ||
|
|
b6b1b2de18 | ||
|
|
c19f2b9e4e | ||
|
|
3a11f18656 | ||
|
|
5a43212ccc | ||
|
|
f4cfd1c913 | ||
|
|
26ce8f30c9 | ||
|
|
2258962770 | ||
|
|
655e84012c | ||
|
|
f683ef00b8 | ||
|
|
424b2ea164 | ||
|
|
7bb4838132 | ||
|
|
20516758ea | ||
|
|
2d5f84ebc2 | ||
|
|
6bc28e41de | ||
|
|
a4fb3fadaf | ||
|
|
cfa67fcd8c | ||
|
|
e5e8d305d2 | ||
|
|
9d0084409e | ||
|
|
ee02f46dfd | ||
|
|
25de0b3530 | ||
|
|
aa76859a05 | ||
|
|
71b70a59fe | ||
|
|
05c9528549 | ||
|
|
1feb2a3861 | ||
|
|
237e78e80c | ||
|
|
ffc3dcaec9 | ||
|
|
a94e474069 | ||
|
|
40a0e57870 | ||
|
|
c01ed34602 | ||
|
|
26a99003d2 | ||
|
|
93c886d3ed | ||
|
|
9e1027690b | ||
|
|
cc120c06fd | ||
|
|
3827da078a | ||
|
|
70d3505b94 | ||
|
|
a39a69cac5 | ||
|
|
1dbce3ab7c | ||
|
|
9742b1b596 | ||
|
|
d98d11ae2d | ||
|
|
6742dcb33e | ||
|
|
09a51478a5 | ||
|
|
f02601ab2c | ||
|
|
ac56ca0e81 | ||
|
|
0fcf77e2ed | ||
|
|
0f0a3a181e | ||
|
|
e24e2c51e4 | ||
|
|
2b0dd82d3d | ||
|
|
b97f711eb4 | ||
|
|
0250ca4eb8 | ||
|
|
fd5d028e95 | ||
|
|
64100cf1ff | ||
|
|
7e7d857ba5 | ||
|
|
d22b2a10df | ||
|
|
957a4d1fcd | ||
|
|
49ef8378fe | ||
|
|
57dd6831d3 | ||
|
|
8aa60abb1f | ||
|
|
7a9f51fc7a | ||
|
|
159e30c982 | ||
|
|
7334d91d6b | ||
|
|
95c01301f6 | ||
|
|
296466fa13 | ||
|
|
3c8d864b5f | ||
|
|
ea50a57602 | ||
|
|
7e14b98676 | ||
|
|
015b406001 | ||
|
|
098d3347a0 | ||
|
|
08d4493994 | ||
|
|
367d739e2d | ||
|
|
d6688a367d | ||
|
|
0b331796e2 | ||
|
|
456620b638 |
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.4.1" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-monitor-admin:5.5.1" />
|
||||
<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.4.1" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-server:5.5.1" />
|
||||
<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.4.1" />
|
||||
<option name="imageTag" value="ruoyi/ruoyi-snailjob-server:5.5.1" />
|
||||
<option name="buildOnly" value="true" />
|
||||
<option name="sourceFilePath" value="ruoyi-extend/ruoyi-snailjob-server/Dockerfile" />
|
||||
</settings>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
[](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)
|
||||
[]()
|
||||
[]()
|
||||
[]()
|
||||
|
||||
26
pom.xml
26
pom.xml
@@ -13,28 +13,28 @@
|
||||
<description>Dromara RuoYi-Vue-Plus多租户管理系统</description>
|
||||
|
||||
<properties>
|
||||
<revision>5.4.1</revision>
|
||||
<spring-boot.version>3.5.4</spring-boot.version>
|
||||
<revision>5.5.1</revision>
|
||||
<spring-boot.version>3.5.7</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.9</springdoc.version>
|
||||
<springdoc.version>2.8.13</springdoc.version>
|
||||
<therapi-javadoc.version>0.15.0</therapi-javadoc.version>
|
||||
<fastexcel.version>1.2.0</fastexcel.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.12</mybatis-plus.version>
|
||||
<mybatis-plus.version>3.5.14</mybatis-plus.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<hutool.version>5.8.38</hutool.version>
|
||||
<spring-boot-admin.version>3.5.1</spring-boot-admin.version>
|
||||
<redisson.version>3.50.0</redisson.version>
|
||||
<hutool.version>5.8.40</hutool.version>
|
||||
<spring-boot-admin.version>3.5.5</spring-boot-admin.version>
|
||||
<redisson.version>3.51.0</redisson.version>
|
||||
<lock4j.version>2.2.7</lock4j.version>
|
||||
<dynamic-ds.version>4.3.1</dynamic-ds.version>
|
||||
<snailjob.version>1.7.0</snailjob.version>
|
||||
<mapstruct-plus.version>1.4.8</mapstruct-plus.version>
|
||||
<snailjob.version>1.8.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.38</lombok.version>
|
||||
<lombok.version>1.18.40</lombok.version>
|
||||
<bouncycastle.version>1.80</bouncycastle.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<!-- 离线IP地址定位库 -->
|
||||
@@ -42,13 +42,13 @@
|
||||
<!-- OSS 配置 -->
|
||||
<aws.sdk.version>2.28.22</aws.sdk.version>
|
||||
<!-- SMS 配置 -->
|
||||
<sms4j.version>3.3.4</sms4j.version>
|
||||
<sms4j.version>3.3.5</sms4j.version>
|
||||
<!-- 限制框架中的fastjson版本 -->
|
||||
<fastjson.version>1.2.83</fastjson.version>
|
||||
<!-- 面向运行时的D-ORM依赖 -->
|
||||
<anyline.version>8.7.2-20250603</anyline.version>
|
||||
<!-- 工作流配置 -->
|
||||
<warm-flow.version>1.8.0-m3</warm-flow.version>
|
||||
<warm-flow.version>1.8.2</warm-flow.version>
|
||||
|
||||
<!-- 插件版本 -->
|
||||
<maven-jar-plugin.version>3.4.2</maven-jar-plugin.version>
|
||||
|
||||
@@ -131,15 +131,18 @@ public class CaptchaController {
|
||||
String verifyKey = GlobalConstants.CAPTCHA_CODE_KEY + uuid;
|
||||
// 生成验证码
|
||||
CaptchaType captchaType = captchaProperties.getType();
|
||||
boolean isMath = CaptchaType.MATH == captchaType;
|
||||
Integer length = isMath ? captchaProperties.getNumberLength() : captchaProperties.getCharLength();
|
||||
CodeGenerator codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), length);
|
||||
CodeGenerator codeGenerator;
|
||||
if (CaptchaType.MATH == captchaType) {
|
||||
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getNumberLength(), false);
|
||||
} else {
|
||||
codeGenerator = ReflectUtils.newInstance(captchaType.getClazz(), captchaProperties.getCharLength());
|
||||
}
|
||||
AbstractCaptcha captcha = SpringUtils.getBean(captchaProperties.getCategory().getClazz());
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.createCode();
|
||||
// 如果是数学验证码,使用SpEL表达式处理验证码结果
|
||||
String code = captcha.getCode();
|
||||
if (isMath) {
|
||||
if (CaptchaType.MATH == captchaType) {
|
||||
ExpressionParser parser = new SpelExpressionParser();
|
||||
Expression exp = parser.parseExpression(StringUtils.remove(code, "="));
|
||||
code = exp.getValue(String.class);
|
||||
|
||||
@@ -87,7 +87,7 @@ public class SysRegisterService {
|
||||
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
if (!StringUtils.equalsIgnoreCase(code, captcha)) {
|
||||
recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public class PasswordAuthStrategy implements IAuthStrategy {
|
||||
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"));
|
||||
throw new CaptchaExpireException();
|
||||
}
|
||||
if (!code.equalsIgnoreCase(captcha)) {
|
||||
if (!StringUtils.equalsIgnoreCase(code, captcha)) {
|
||||
loginService.recordLogininfor(tenantId, username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"));
|
||||
throw new CaptchaException();
|
||||
}
|
||||
|
||||
@@ -259,13 +259,7 @@ warm-flow:
|
||||
ui: true
|
||||
# 是否显示流程图顶部文字
|
||||
top-text-show: true
|
||||
# 是否渲染节点悬浮提示,默认true
|
||||
node-tooltip: true
|
||||
# 默认Authorization,如果有多个token,用逗号分隔
|
||||
token-name: ${sa-token.token-name},clientid
|
||||
# 流程状态对应的三元色
|
||||
chart-status-color:
|
||||
## 未办理
|
||||
- 62,62,62
|
||||
## 待办理
|
||||
- 255,205,23
|
||||
## 已办理
|
||||
- 157,255,0
|
||||
|
||||
Binary file not shown.
@@ -38,7 +38,7 @@
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
@@ -60,7 +60,7 @@
|
||||
<!-- 循环政策:基于时间创建日志文件 -->
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<!-- 日志文件名格式 -->
|
||||
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log.gz</fileNamePattern>
|
||||
<!-- 日志最大的历史 60天 -->
|
||||
<maxHistory>60</maxHistory>
|
||||
</rollingPolicy>
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
</description>
|
||||
|
||||
<properties>
|
||||
<revision>5.4.1</revision>
|
||||
<revision>5.5.1</revision>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
|
||||
@@ -5,15 +5,12 @@ 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.*;
|
||||
|
||||
/**
|
||||
* 线程池配置
|
||||
@@ -50,7 +47,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 +56,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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -72,5 +72,10 @@ public interface Constants {
|
||||
*/
|
||||
Long TOP_PARENT_ID = 0L;
|
||||
|
||||
/**
|
||||
* 加密头
|
||||
*/
|
||||
String ENCRYPT_HEADER = "ENC_";
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -50,6 +50,11 @@ public class CompleteTaskDTO implements Serializable {
|
||||
*/
|
||||
private String notice;
|
||||
|
||||
/**
|
||||
* 办理人(可不填 用于覆盖当前节点办理人)
|
||||
*/
|
||||
private String handler;
|
||||
|
||||
/**
|
||||
* 流程变量
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
@@ -30,11 +31,21 @@ public class StartProcessDTO implements Serializable {
|
||||
*/
|
||||
private String flowCode;
|
||||
|
||||
/**
|
||||
* 办理人(可不填 用于覆盖当前节点办理人)
|
||||
*/
|
||||
private String handler;
|
||||
|
||||
/**
|
||||
* 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
|
||||
*/
|
||||
private Map<String, Object> variables;
|
||||
|
||||
/**
|
||||
* 流程业务扩展信息
|
||||
*/
|
||||
private FlowInstanceBizExtDTO bizExt;
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
if (variables == null) {
|
||||
return new HashMap<>(16);
|
||||
@@ -42,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,8 +30,10 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
return collection.stream().filter(function).collect(Collectors.toList());
|
||||
return collection.stream()
|
||||
.filter(function)
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,13 +41,26 @@ public class StreamUtils {
|
||||
*
|
||||
* @param collection 需要查询的集合
|
||||
* @param function 过滤方法
|
||||
* @return 找到符合条件的第一个元素,没有则返回null
|
||||
* @return 找到符合条件的第一个元素,没有则返回 Optional.empty()
|
||||
*/
|
||||
public static <E> E findFirst(Collection<E> collection, Predicate<E> function) {
|
||||
public static <E> Optional<E> findFirst(Collection<E> collection, Predicate<E> function) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return null;
|
||||
return Optional.empty();
|
||||
}
|
||||
return collection.stream().filter(function).findFirst().orElse(null);
|
||||
return collection.stream()
|
||||
.filter(function)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到流中满足条件的第一个元素值
|
||||
*
|
||||
* @param collection 需要查询的集合
|
||||
* @param function 过滤方法
|
||||
* @return 找到符合条件的第一个元素,没有则返回 null
|
||||
*/
|
||||
public static <E> E findFirstValue(Collection<E> collection, Predicate<E> function) {
|
||||
return findFirst(collection,function).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,13 +68,26 @@ public class StreamUtils {
|
||||
*
|
||||
* @param collection 需要查询的集合
|
||||
* @param function 过滤方法
|
||||
* @return 找到符合条件的任意一个元素,没有则返回null
|
||||
* @return 找到符合条件的任意一个元素,没有则返回 Optional.empty()
|
||||
*/
|
||||
public static <E> Optional<E> findAny(Collection<E> collection, Predicate<E> function) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return Optional.empty();
|
||||
}
|
||||
return collection.stream().filter(function).findAny();
|
||||
return collection.stream()
|
||||
.filter(function)
|
||||
.findAny();
|
||||
}
|
||||
|
||||
/**
|
||||
* 找到流中任意一个满足条件的元素值
|
||||
*
|
||||
* @param collection 需要查询的集合
|
||||
* @param function 过滤方法
|
||||
* @return 找到符合条件的任意一个元素,没有则返回null
|
||||
*/
|
||||
public static <E> E findAnyValue(Collection<E> collection, Predicate<E> function) {
|
||||
return findAny(collection,function).orElse(null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -85,7 +113,10 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return StringUtils.EMPTY;
|
||||
}
|
||||
return collection.stream().map(function).filter(Objects::nonNull).collect(Collectors.joining(delimiter));
|
||||
return collection.stream()
|
||||
.map(function)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.joining(delimiter));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -99,8 +130,11 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
return collection.stream().filter(Objects::nonNull).sorted(comparing).collect(Collectors.toList());
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.sorted(comparing)
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -117,7 +151,9 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(key, Function.identity(), (l, r) -> l));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +172,25 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection.stream().filter(Objects::nonNull).collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toMap(key, value, (l, r) -> l));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 map 中的数据作为新 Map 的 value ,key 不变
|
||||
* @param map 需要处理的map
|
||||
* @param take 取值函数
|
||||
* @param <K> map中的key类型
|
||||
* @param <E> map中的value类型
|
||||
* @param <V> 新map中的value类型
|
||||
* @return 新的map
|
||||
*/
|
||||
public static <K, E, V> Map<K, V> toMap(Map<K, E> map, BiFunction<K, E, V> take) {
|
||||
if (CollUtil.isEmpty(map)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return toMap(map.entrySet(), Map.Entry::getKey, entry -> take.apply(entry.getKey(), entry.getValue()));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,8 +207,8 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream().filter(Objects::nonNull)
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key, LinkedHashMap::new, Collectors.toList()));
|
||||
}
|
||||
|
||||
@@ -174,8 +228,8 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream().filter(Objects::nonNull)
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.groupingBy(key2, LinkedHashMap::new, Collectors.toList())));
|
||||
}
|
||||
|
||||
@@ -192,11 +246,11 @@ public class StreamUtils {
|
||||
* @return 分类后的map
|
||||
*/
|
||||
public static <E, T, U> Map<T, Map<U, E>> group2Map(Collection<E> collection, Function<E, T> key1, Function<E, U> key2) {
|
||||
if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return MapUtil.newHashMap();
|
||||
}
|
||||
return collection
|
||||
.stream().filter(Objects::nonNull)
|
||||
return collection.stream()
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.groupingBy(key1, LinkedHashMap::new, Collectors.toMap(key2, Function.identity(), (l, r) -> l)));
|
||||
}
|
||||
|
||||
@@ -214,8 +268,7 @@ public class StreamUtils {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newArrayList();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
return collection.stream()
|
||||
.map(function)
|
||||
.filter(Objects::nonNull)
|
||||
// 注意此处不要使用 .toList() 新语法 因为返回的是不可变List 会导致序列化问题
|
||||
@@ -233,11 +286,10 @@ public class StreamUtils {
|
||||
* @return 转化后的Set
|
||||
*/
|
||||
public static <E, T> Set<T> toSet(Collection<E> collection, Function<E, T> function) {
|
||||
if (CollUtil.isEmpty(collection) || function == null) {
|
||||
if (CollUtil.isEmpty(collection)) {
|
||||
return CollUtil.newHashSet();
|
||||
}
|
||||
return collection
|
||||
.stream()
|
||||
return collection.stream()
|
||||
.map(function)
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toSet());
|
||||
@@ -257,26 +309,20 @@ public class StreamUtils {
|
||||
* @return 合并后的map
|
||||
*/
|
||||
public static <K, X, Y, V> Map<K, V> merge(Map<K, X> map1, Map<K, Y> map2, BiFunction<X, Y, V> merge) {
|
||||
if (MapUtil.isEmpty(map1) && MapUtil.isEmpty(map2)) {
|
||||
if (CollUtil.isEmpty(map1) && CollUtil.isEmpty(map2)) {
|
||||
// 如果两个 map 都为空,则直接返回空的 map
|
||||
return MapUtil.newHashMap();
|
||||
} else if (MapUtil.isEmpty(map1)) {
|
||||
map1 = MapUtil.newHashMap();
|
||||
} else if (MapUtil.isEmpty(map2)) {
|
||||
map2 = MapUtil.newHashMap();
|
||||
} else if (CollUtil.isEmpty(map1)) {
|
||||
// 如果 map1 为空,则直接处理返回 map2
|
||||
return toMap(map2.entrySet(), Map.Entry::getKey, entry -> merge.apply(null, entry.getValue()));
|
||||
} else if (CollUtil.isEmpty(map2)) {
|
||||
// 如果 map2 为空,则直接处理返回 map1
|
||||
return toMap(map1.entrySet(), Map.Entry::getKey, entry -> merge.apply(entry.getValue(), null));
|
||||
}
|
||||
Set<K> key = new HashSet<>();
|
||||
key.addAll(map1.keySet());
|
||||
key.addAll(map2.keySet());
|
||||
Map<K, V> map = new HashMap<>();
|
||||
for (K t : key) {
|
||||
X x = map1.get(t);
|
||||
Y y = map2.get(t);
|
||||
V z = merge.apply(x, y);
|
||||
if (z != null) {
|
||||
map.put(t, z);
|
||||
}
|
||||
}
|
||||
return map;
|
||||
Set<K> keySet = new HashSet<>();
|
||||
keySet.addAll(map1.keySet());
|
||||
keySet.addAll(map2.keySet());
|
||||
return toMap(keySet, key -> key, key -> merge.apply(map1.get(key), map2.get(key)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import cn.hutool.core.util.ReflectUtil;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.ibatis.io.Resources;
|
||||
import org.dromara.common.core.constant.Constants;
|
||||
import org.dromara.common.core.utils.ObjectUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.encrypt.annotation.EncryptField;
|
||||
@@ -92,8 +93,12 @@ public class EncryptorManager {
|
||||
* @param encryptContext 加密相关的配置信息
|
||||
*/
|
||||
public String encrypt(String value, EncryptContext encryptContext) {
|
||||
if (StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
|
||||
return value;
|
||||
}
|
||||
IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
|
||||
return encryptor.encrypt(value, encryptContext.getEncode());
|
||||
String encrypt = encryptor.encrypt(value, encryptContext.getEncode());
|
||||
return Constants.ENCRYPT_HEADER + encrypt;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,8 +108,12 @@ public class EncryptorManager {
|
||||
* @param encryptContext 加密相关的配置信息
|
||||
*/
|
||||
public String decrypt(String value, EncryptContext encryptContext) {
|
||||
if (!StringUtils.startsWith(value, Constants.ENCRYPT_HEADER)) {
|
||||
return value;
|
||||
}
|
||||
IEncryptor encryptor = this.registAndGetEncryptor(encryptContext);
|
||||
return encryptor.decrypt(value);
|
||||
String str = StringUtils.removeStart(value, Constants.ENCRYPT_HEADER);
|
||||
return encryptor.decrypt(str);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -28,7 +28,7 @@ public class ExcelBigNumberConvert implements Converter<Long> {
|
||||
|
||||
@Override
|
||||
public CellDataTypeEnum supportExcelTypeKey() {
|
||||
return CellDataTypeEnum.STRING;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -31,12 +31,89 @@ public class CellMergeHandler {
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public List<CellRangeAddress> handle(List<?> list) {
|
||||
List<CellRangeAddress> cellList = new ArrayList<>();
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return cellList;
|
||||
public List<CellRangeAddress> handle(List<?> rows) {
|
||||
// 如果入参为空集合则返回空集
|
||||
if (CollUtil.isEmpty(rows)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Class<?> clazz = list.get(0).getClass();
|
||||
|
||||
// 获取有合并注解的字段
|
||||
Map<Field, FieldColumnIndex> mergeFields = getFieldColumnIndexMap(rows.get(0).getClass());
|
||||
// 如果没有需要合并的字段则返回空集
|
||||
if (CollUtil.isEmpty(mergeFields)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
// 结果集
|
||||
List<CellRangeAddress> result = new ArrayList<>();
|
||||
|
||||
// 生成两两合并单元格
|
||||
Map<Field, RepeatCell> rowRepeatCellMap = new HashMap<>();
|
||||
for (Map.Entry<Field, FieldColumnIndex> item : mergeFields.entrySet()) {
|
||||
Field field = item.getKey();
|
||||
FieldColumnIndex itemValue = item.getValue();
|
||||
int colNum = itemValue.colIndex();
|
||||
CellMerge cellMerge = itemValue.cellMerge();
|
||||
|
||||
for (int i = 0; i < rows.size(); i++) {
|
||||
// 当前行数据
|
||||
Object currentRowObj = rows.get(i);
|
||||
// 当前行数据字段值
|
||||
Object currentRowObjFieldVal = ReflectUtils.invokeGetter(currentRowObj, field.getName());
|
||||
|
||||
// 空值跳过不处理
|
||||
if (currentRowObjFieldVal == null || "".equals(currentRowObjFieldVal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 单元格合并Map是否存在数据,如果不存在则添加当前行的字段值
|
||||
if (!rowRepeatCellMap.containsKey(field)) {
|
||||
rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取 单元格合并Map 中字段值
|
||||
RepeatCell repeatCell = rowRepeatCellMap.get(field);
|
||||
Object cellValue = repeatCell.value();
|
||||
int current = repeatCell.current();
|
||||
|
||||
// 检查是否满足合并条件
|
||||
// currentRowObj 当前行数据
|
||||
// rows.get(i - 1) 上一行数据 注:由于 if (!rowRepeatCellMap.containsKey(field)) 条件的存在,所以该 i 必不可能小于1
|
||||
// cellMerge 当前行字段合并注解
|
||||
boolean merge = isMerge(currentRowObj, rows.get(i - 1), cellMerge);
|
||||
|
||||
// 是否添加到结果集
|
||||
boolean isAddResult = false;
|
||||
// 最新行
|
||||
int lastRow = i + rowIndex - 1;
|
||||
|
||||
// 如果当前行字段值和缓存中的字段值不相等,或不满足合并条件,则替换
|
||||
if (!currentRowObjFieldVal.equals(cellValue) || !merge) {
|
||||
rowRepeatCellMap.put(field, RepeatCell.of(currentRowObjFieldVal, i));
|
||||
isAddResult = true;
|
||||
}
|
||||
|
||||
// 如果最后一行不能合并,检查之前的数据是否需要合并;如果最后一行可以合并,则直接合并到最后
|
||||
if (i == rows.size() - 1) {
|
||||
isAddResult = true;
|
||||
if (i > current) {
|
||||
lastRow = i + rowIndex;
|
||||
}
|
||||
}
|
||||
|
||||
if (isAddResult && i > current) {
|
||||
result.add(new CellRangeAddress(current + rowIndex, lastRow, colNum, colNum));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取带有合并注解的字段列索引和合并注解信息Map集
|
||||
*/
|
||||
private Map<Field, FieldColumnIndex> getFieldColumnIndexMap(Class<?> clazz) {
|
||||
boolean annotationPresent = clazz.isAnnotationPresent(ExcelIgnoreUnannotated.class);
|
||||
Field[] fields = ReflectUtils.getFields(clazz, field -> {
|
||||
if ("serialVersionUID".equals(field.getName())) {
|
||||
@@ -49,86 +126,57 @@ public class CellMergeHandler {
|
||||
});
|
||||
|
||||
// 有注解的字段
|
||||
List<Field> mergeFields = new ArrayList<>();
|
||||
List<Integer> mergeFieldsIndex = new ArrayList<>();
|
||||
Map<Field, FieldColumnIndex> mergeFields = new HashMap<>();
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
Field field = fields[i];
|
||||
if (field.isAnnotationPresent(CellMerge.class)) {
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
mergeFields.add(field);
|
||||
mergeFieldsIndex.add(cm.index() == -1 ? i : cm.index());
|
||||
if (hasTitle) {
|
||||
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
|
||||
rowIndex = Math.max(rowIndex, property.value().length);
|
||||
}
|
||||
if (!field.isAnnotationPresent(CellMerge.class)) {
|
||||
continue;
|
||||
}
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
int index = cm.index() == -1 ? i : cm.index();
|
||||
mergeFields.put(field, FieldColumnIndex.of(index, cm));
|
||||
|
||||
if (hasTitle) {
|
||||
ExcelProperty property = field.getAnnotation(ExcelProperty.class);
|
||||
rowIndex = Math.max(rowIndex, property.value().length);
|
||||
}
|
||||
}
|
||||
|
||||
Map<Field, RepeatCell> map = new HashMap<>();
|
||||
// 生成两两合并单元格
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
Object rowObj = list.get(i);
|
||||
for (int j = 0; j < mergeFields.size(); j++) {
|
||||
Field field = mergeFields.get(j);
|
||||
Object val = ReflectUtils.invokeGetter(rowObj, field.getName());
|
||||
|
||||
int colNum = mergeFieldsIndex.get(j);
|
||||
if (!map.containsKey(field)) {
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
} else {
|
||||
RepeatCell repeatCell = map.get(field);
|
||||
Object cellValue = repeatCell.value();
|
||||
if (cellValue == null || "".equals(cellValue)) {
|
||||
// 空值跳过不合并
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!cellValue.equals(val)) {
|
||||
if ((i - repeatCell.current() > 1)) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.current() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
} else if (i == list.size() - 1) {
|
||||
if (!isMerge(list, i, field)) {
|
||||
// 如果最后一行不能合并,检查之前的数据是否需要合并
|
||||
if (i - repeatCell.current() > 1) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.current() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
} else if (i > repeatCell.current()) {
|
||||
// 如果最后一行可以合并,则直接合并到最后
|
||||
cellList.add(new CellRangeAddress(repeatCell.current() + rowIndex, i + rowIndex, colNum, colNum));
|
||||
}
|
||||
} else if (!isMerge(list, i, field)) {
|
||||
if ((i - repeatCell.current() > 1)) {
|
||||
cellList.add(new CellRangeAddress(repeatCell.current() + rowIndex, i + rowIndex - 1, colNum, colNum));
|
||||
}
|
||||
map.put(field, new RepeatCell(val, i));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return cellList;
|
||||
return mergeFields;
|
||||
}
|
||||
|
||||
private boolean isMerge(List<?> list, int i, Field field) {
|
||||
boolean isMerge = true;
|
||||
CellMerge cm = field.getAnnotation(CellMerge.class);
|
||||
final String[] mergeBy = cm.mergeBy();
|
||||
private boolean isMerge(Object currentRow, Object preRow, CellMerge cellMerge) {
|
||||
final String[] mergeBy = cellMerge.mergeBy();
|
||||
if (StrUtil.isAllNotBlank(mergeBy)) {
|
||||
//比对当前list(i)和list(i - 1)的各个属性值一一比对 如果全为真 则为真
|
||||
//比对当前行和上一行的各个属性值一一比对 如果全为真 则为真
|
||||
for (String fieldName : mergeBy) {
|
||||
final Object valCurrent = ReflectUtil.getFieldValue(list.get(i), fieldName);
|
||||
final Object valPre = ReflectUtil.getFieldValue(list.get(i - 1), fieldName);
|
||||
final Object valCurrent = ReflectUtil.getFieldValue(currentRow, fieldName);
|
||||
final Object valPre = ReflectUtil.getFieldValue(preRow, fieldName);
|
||||
if (!Objects.equals(valPre, valCurrent)) {
|
||||
//依赖字段如有任一不等值,则标记为不可合并
|
||||
isMerge = false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return isMerge;
|
||||
return true;
|
||||
}
|
||||
|
||||
record RepeatCell(Object value, int current) {}
|
||||
/**
|
||||
* 单元格合并
|
||||
*/
|
||||
record RepeatCell(Object value, int current) {
|
||||
static RepeatCell of(Object value, int current) {
|
||||
return new RepeatCell(value, current);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段列索引和合并注解信息
|
||||
*/
|
||||
record FieldColumnIndex(int colIndex, CellMerge cellMerge) {
|
||||
static FieldColumnIndex of(int colIndex, CellMerge cellMerge) {
|
||||
return new FieldColumnIndex(colIndex, cellMerge);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个单元格合并处理器实例
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.io.UnsupportedEncodingException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Excel相关处理
|
||||
@@ -203,6 +204,44 @@ public class ExcelUtil {
|
||||
builder.doWrite(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param headType 带Excel注解的类型
|
||||
* @param os 输出流
|
||||
* @param options Excel下拉可选项
|
||||
* @param consumer 导出助手消费函数
|
||||
*/
|
||||
public static <T> void exportExcel(Class<T> headType, OutputStream os, List<DropDownOptions> options, Consumer<ExcelWriterWrapper<T>> consumer) {
|
||||
try (ExcelWriter writer = FastExcel.write(os, headType)
|
||||
.autoCloseStream(false)
|
||||
// 自动适配
|
||||
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy())
|
||||
// 大数值自动转换 防止失真
|
||||
.registerConverter(new ExcelBigNumberConvert())
|
||||
// 批注必填项处理
|
||||
.registerWriteHandler(new DataWriteHandler(headType))
|
||||
// 添加下拉框操作
|
||||
.registerWriteHandler(new ExcelDownHandler(options))
|
||||
.build()) {
|
||||
// 执行消费函数
|
||||
consumer.accept(ExcelWriterWrapper.of(writer));
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出excel
|
||||
*
|
||||
* @param headType 带Excel注解的类型
|
||||
* @param os 输出流
|
||||
* @param consumer 导出助手消费函数
|
||||
*/
|
||||
public static <T> void exportExcel(Class<T> headType, OutputStream os, Consumer<ExcelWriterWrapper<T>> consumer) {
|
||||
exportExcel(headType, os, null, consumer);
|
||||
}
|
||||
|
||||
/**
|
||||
* 单表多数据模板导出 模板格式为 {.属性}
|
||||
*
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package org.dromara.common.excel.utils;
|
||||
|
||||
import cn.idev.excel.ExcelWriter;
|
||||
import cn.idev.excel.FastExcel;
|
||||
import cn.idev.excel.context.WriteContext;
|
||||
import cn.idev.excel.write.builder.ExcelWriterSheetBuilder;
|
||||
import cn.idev.excel.write.builder.ExcelWriterTableBuilder;
|
||||
import cn.idev.excel.write.metadata.WriteSheet;
|
||||
import cn.idev.excel.write.metadata.WriteTable;
|
||||
import cn.idev.excel.write.metadata.fill.FillConfig;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* ExcelWriterWrapper Excel写出包装器
|
||||
* <br>
|
||||
* 提供了一组与 ExcelWriter 一一对应的写出方法,避免直接提供 ExcelWriter 而导致的一些不可控问题(比如提前关闭了IO流等)
|
||||
*
|
||||
* @author 秋辞未寒
|
||||
* @see ExcelWriter
|
||||
*/
|
||||
public record ExcelWriterWrapper<T>(ExcelWriter excelWriter) {
|
||||
|
||||
public void write(Collection<T> data, WriteSheet writeSheet) {
|
||||
excelWriter.write(data, writeSheet);
|
||||
}
|
||||
|
||||
public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet) {
|
||||
excelWriter.write(supplier.get(), writeSheet);
|
||||
}
|
||||
|
||||
public void write(Collection<T> data, WriteSheet writeSheet, WriteTable writeTable) {
|
||||
excelWriter.write(data, writeSheet, writeTable);
|
||||
}
|
||||
|
||||
public void write(Supplier<Collection<T>> supplier, WriteSheet writeSheet, WriteTable writeTable) {
|
||||
excelWriter.write(supplier.get(), writeSheet, writeTable);
|
||||
}
|
||||
|
||||
public void fill(Object data, WriteSheet writeSheet) {
|
||||
excelWriter.fill(data, writeSheet);
|
||||
}
|
||||
|
||||
public void fill(Object data, FillConfig fillConfig, WriteSheet writeSheet) {
|
||||
excelWriter.fill(data, fillConfig, writeSheet);
|
||||
}
|
||||
|
||||
public void fill(Supplier<Object> supplier, WriteSheet writeSheet) {
|
||||
excelWriter.fill(supplier, writeSheet);
|
||||
}
|
||||
|
||||
public void fill(Supplier<Object> supplier, FillConfig fillConfig, WriteSheet writeSheet) {
|
||||
excelWriter.fill(supplier, fillConfig, writeSheet);
|
||||
}
|
||||
|
||||
public WriteContext writeContext() {
|
||||
return excelWriter.writeContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建一个 ExcelWriterWrapper
|
||||
*
|
||||
* @param excelWriter ExcelWriter
|
||||
* @return ExcelWriterWrapper
|
||||
*/
|
||||
public static <T> ExcelWriterWrapper<T> of(ExcelWriter excelWriter) {
|
||||
return new ExcelWriterWrapper<>(excelWriter);
|
||||
}
|
||||
|
||||
// -------------------------------- sheet start
|
||||
|
||||
public static WriteSheet buildSheet(Integer sheetNo, String sheetName) {
|
||||
return sheetBuilder(sheetNo, sheetName).build();
|
||||
}
|
||||
|
||||
public static WriteSheet buildSheet(Integer sheetNo) {
|
||||
return sheetBuilder(sheetNo).build();
|
||||
}
|
||||
|
||||
public static WriteSheet buildSheet(String sheetName) {
|
||||
return sheetBuilder(sheetName).build();
|
||||
}
|
||||
|
||||
public static WriteSheet buildSheet() {
|
||||
return sheetBuilder().build();
|
||||
}
|
||||
|
||||
public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo, String sheetName) {
|
||||
return FastExcel.writerSheet(sheetNo, sheetName);
|
||||
}
|
||||
|
||||
public static ExcelWriterSheetBuilder sheetBuilder(Integer sheetNo) {
|
||||
return FastExcel.writerSheet(sheetNo);
|
||||
}
|
||||
|
||||
public static ExcelWriterSheetBuilder sheetBuilder(String sheetName) {
|
||||
return FastExcel.writerSheet(sheetName);
|
||||
}
|
||||
|
||||
public static ExcelWriterSheetBuilder sheetBuilder() {
|
||||
return FastExcel.writerSheet();
|
||||
}
|
||||
|
||||
// -------------------------------- sheet end
|
||||
|
||||
// -------------------------------- table start
|
||||
|
||||
public static WriteTable buildTable(Integer tableNo) {
|
||||
return tableBuilder(tableNo).build();
|
||||
}
|
||||
|
||||
public static WriteTable buildTable() {
|
||||
return tableBuilder().build();
|
||||
}
|
||||
|
||||
public static ExcelWriterTableBuilder tableBuilder(Integer tableNo) {
|
||||
return FastExcel.writerTable(tableNo);
|
||||
}
|
||||
|
||||
public static ExcelWriterTableBuilder tableBuilder() {
|
||||
return FastExcel.writerTable();
|
||||
}
|
||||
|
||||
// -------------------------------- table end
|
||||
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.dromara.common.json.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
@@ -28,20 +29,24 @@ import java.util.TimeZone;
|
||||
@AutoConfiguration(before = JacksonAutoConfiguration.class)
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public Module registerJavaTimeModule() {
|
||||
// 全局配置序列化返回 JSON 处理
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
|
||||
javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
|
||||
javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
|
||||
javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
|
||||
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
|
||||
javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
|
||||
return javaTimeModule;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Jackson2ObjectMapperBuilderCustomizer customizer() {
|
||||
return builder -> {
|
||||
// 全局配置序列化返回 JSON 处理
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
javaTimeModule.addSerializer(Long.class, BigNumberSerializer.INSTANCE);
|
||||
javaTimeModule.addSerializer(Long.TYPE, BigNumberSerializer.INSTANCE);
|
||||
javaTimeModule.addSerializer(BigInteger.class, BigNumberSerializer.INSTANCE);
|
||||
javaTimeModule.addSerializer(BigDecimal.class, ToStringSerializer.instance);
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(formatter));
|
||||
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
|
||||
javaTimeModule.addDeserializer(Date.class, new CustomDateDeserializer());
|
||||
builder.modules(javaTimeModule);
|
||||
builder.timeZone(TimeZone.getDefault());
|
||||
log.info("初始化 jackson 配置");
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.dromara.common.json.handler;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||
import org.dromara.common.core.utils.ObjectUtils;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Date;
|
||||
@@ -25,7 +27,11 @@ public class CustomDateDeserializer extends JsonDeserializer<Date> {
|
||||
*/
|
||||
@Override
|
||||
public Date deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return DateUtil.parse(p.getText());
|
||||
DateTime parse = DateUtil.parse(p.getText());
|
||||
if (ObjectUtils.isNull(parse)) {
|
||||
return null;
|
||||
}
|
||||
return parse.toJdkDate();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
}
|
||||
@@ -6,11 +6,9 @@ import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.baomidou.mybatisplus.core.toolkit.reflect.GenericTypeUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.toolkit.Db;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.logging.Log;
|
||||
import org.apache.ibatis.logging.LogFactory;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
@@ -132,7 +130,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoById(Serializable id) {
|
||||
return this.selectVoById(id, this.currentVoClass());
|
||||
return selectVoById(id, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -158,7 +156,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoByIds(Collection<? extends Serializable> idList) {
|
||||
return this.selectVoByIds(idList, this.currentVoClass());
|
||||
return selectVoByIds(idList, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,7 +182,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoByMap(Map<String, Object> map) {
|
||||
return this.selectVoByMap(map, this.currentVoClass());
|
||||
return selectVoByMap(map, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -210,7 +208,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoOne(Wrapper<T> wrapper) {
|
||||
return this.selectVoOne(wrapper, this.currentVoClass());
|
||||
return selectVoOne(wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -221,12 +219,11 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的单个VO对象
|
||||
*/
|
||||
default V selectVoOne(Wrapper<T> wrapper, boolean throwEx) {
|
||||
return this.selectVoOne(wrapper, this.currentVoClass(), throwEx);
|
||||
return selectVoOne(wrapper, this.currentVoClass(), throwEx);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个VO对象,并指定返回的VO对象的类型(自动拼接 limit 1)
|
||||
* 注意不要再自己添加 limit 1 做限制了
|
||||
* 根据条件查询单个VO对象,并指定返回的VO对象的类型
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 返回的VO对象的Class对象
|
||||
@@ -234,12 +231,11 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的单个VO对象,经过类型转换为指定的VO类后返回
|
||||
*/
|
||||
default <C> C selectVoOne(Wrapper<T> wrapper, Class<C> voClass) {
|
||||
return this.selectVoOne(wrapper, voClass, true);
|
||||
return selectVoOne(wrapper, voClass, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单个实体对象,并将其转换为指定的VO对象(自动拼接 limit 1)
|
||||
* 注意不要再自己添加 limit 1 做限制了
|
||||
* 根据条件查询单个实体对象,并将其转换为指定的VO对象
|
||||
*
|
||||
* @param wrapper 查询条件Wrapper
|
||||
* @param voClass 要转换的VO类的Class对象
|
||||
@@ -255,33 +251,13 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
return MapstructUtils.convert(obj, voClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据条件查询单条记录(自动拼接 limit 1 限制返回 1 条数据,不依赖 {@code throwEx} 参数)
|
||||
* 注意不要再自己添加 limit 1 做限制了
|
||||
* <p>
|
||||
* <strong>注意:</strong>
|
||||
* 1. 使用 {@code Page<>(1, 1)} 强制分页查询,确保 SQL 自动添加 {@code LIMIT 1},因此 {@code throwEx} 参数不再生效
|
||||
* 2. 原方法的 {@code throwEx} 逻辑(多条数据抛异常)已被优化掉,因为分页查询不会返回多条记录
|
||||
* </p>
|
||||
*
|
||||
* @param queryWrapper 查询条件(可为 null)
|
||||
* @param throwEx <del>是否抛出异常(已弃用,此参数不再生效)</del>
|
||||
* @return 单条记录或无数据时返回 null
|
||||
*/
|
||||
@Override
|
||||
default T selectOne(@Param(Constants.WRAPPER) Wrapper<T> queryWrapper, boolean throwEx) {
|
||||
// 强制分页查询(LIMIT 1),确保最多返回 1 条记录
|
||||
List<T> list = this.selectList(new Page<>(1, 1), queryWrapper);
|
||||
return CollUtil.isEmpty(list) ? null : list.get(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有VO对象列表
|
||||
*
|
||||
* @return 查询到的VO对象列表
|
||||
*/
|
||||
default List<V> selectVoList() {
|
||||
return this.selectVoList(new QueryWrapper<>(), this.currentVoClass());
|
||||
return selectVoList(new QueryWrapper<>(), this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -318,7 +294,7 @@ public interface BaseMapperPlus<T, V> extends BaseMapper<T> {
|
||||
* @return 查询到的VO对象分页列表
|
||||
*/
|
||||
default <P extends IPage<V>> P selectVoPage(IPage<T> page, Wrapper<T> wrapper) {
|
||||
return this.selectVoPage(page, wrapper, this.currentVoClass());
|
||||
return selectVoPage(page, wrapper, this.currentVoClass());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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消息主题,并提供一个消费者函数来处理接收到的消息
|
||||
*
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package org.dromara.common.web.config;
|
||||
|
||||
import cn.hutool.core.date.DateTime;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.core.utils.ObjectUtils;
|
||||
import org.dromara.common.web.handler.GlobalExceptionHandler;
|
||||
import org.dromara.common.web.interceptor.PlusWebInvokeTimeInterceptor;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
@@ -34,10 +35,11 @@ public class ResourcesConfig implements WebMvcConfigurer {
|
||||
public void addFormatters(FormatterRegistry registry) {
|
||||
// 全局日期格式转换配置
|
||||
registry.addConverter(String.class, Date.class, source -> {
|
||||
if (StringUtils.isBlank(source)) {
|
||||
DateTime parse = DateUtil.parse(source);
|
||||
if (ObjectUtils.isNull(parse)) {
|
||||
return null;
|
||||
}
|
||||
return DateUtil.parse(source);
|
||||
return parse.toJdkDate();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 org.dromara.common.web.utils.UnsignedMathGenerator;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@@ -18,7 +18,7 @@ public enum CaptchaType {
|
||||
/**
|
||||
* 数字
|
||||
*/
|
||||
MATH(UnsignedMathGenerator.class),
|
||||
MATH(MathGenerator.class),
|
||||
|
||||
/**
|
||||
* 字符
|
||||
|
||||
@@ -23,6 +23,7 @@ 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.MethodArgumentTypeMismatchException;
|
||||
import org.springframework.web.servlet.NoHandlerFoundException;
|
||||
|
||||
@@ -123,7 +124,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 +133,13 @@ public class GlobalExceptionHandler {
|
||||
log.error("请求地址'{}',连接中断", requestURI, e);
|
||||
}
|
||||
|
||||
/**
|
||||
* sse 连接超时异常 不需要处理
|
||||
*/
|
||||
@ExceptionHandler(AsyncRequestTimeoutException.class)
|
||||
public void handleRuntimeException(AsyncRequestTimeoutException e) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 拦截未知的运行时异常
|
||||
*/
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package org.dromara.common.web.utils;
|
||||
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.math.Calculator;
|
||||
import cn.hutool.core.util.CharUtil;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 无符号计算生成器
|
||||
*
|
||||
* @author Lion Li
|
||||
*/
|
||||
public class UnsignedMathGenerator implements CodeGenerator {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = -5514819971774091076L;
|
||||
|
||||
private static final String OPERATORS = "+-*";
|
||||
|
||||
/**
|
||||
* 参与计算数字最大长度
|
||||
*/
|
||||
private final int numberLength;
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*/
|
||||
public UnsignedMathGenerator() {
|
||||
this(2);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造
|
||||
*
|
||||
* @param numberLength 参与计算最大数字位数
|
||||
*/
|
||||
public UnsignedMathGenerator(int numberLength) {
|
||||
this.numberLength = numberLength;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String generate() {
|
||||
final int limit = getLimit();
|
||||
int a = RandomUtil.randomInt(limit);
|
||||
int b = RandomUtil.randomInt(limit);
|
||||
String max = Integer.toString(Math.max(a,b));
|
||||
String min = Integer.toString(Math.min(a,b));
|
||||
max = StringUtils.rightPad(max, this.numberLength, CharUtil.SPACE);
|
||||
min = StringUtils.rightPad(min, this.numberLength, CharUtil.SPACE);
|
||||
|
||||
return max + RandomUtil.randomChar(OPERATORS) + min + '=';
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(String code, String userInputCode) {
|
||||
int result;
|
||||
try {
|
||||
result = Integer.parseInt(userInputCode);
|
||||
} catch (NumberFormatException e) {
|
||||
// 用户输入非数字
|
||||
return false;
|
||||
}
|
||||
|
||||
final int calculateResult = (int) Calculator.conversion(code);
|
||||
return result == calculateResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取验证码长度
|
||||
*
|
||||
* @return 验证码长度
|
||||
*/
|
||||
public int getLength() {
|
||||
return this.numberLength * 2 + 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据长度获取参与计算数字最大值
|
||||
*
|
||||
* @return 最大值
|
||||
*/
|
||||
private int getLimit() {
|
||||
return Integer.parseInt("1" + StringUtils.repeat('0', this.numberLength));
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ spring:
|
||||
snail-job:
|
||||
# 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
|
||||
server-host:
|
||||
# 服务端netty的端口号
|
||||
# 服务端端口号
|
||||
server-port: 17888
|
||||
# 合并日志默认保存天数
|
||||
merge-Log-days: 1
|
||||
|
||||
@@ -18,7 +18,7 @@ spring:
|
||||
snail-job:
|
||||
# 服务端节点IP(默认按照`NetUtil.getLocalIpStr()`)
|
||||
server-host:
|
||||
# 服务端netty的端口号
|
||||
# 服务端端口号
|
||||
server-port: 17888
|
||||
# 合并日志默认保存天数
|
||||
merge-Log-days: 1
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.dromara.demo.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaIgnore;
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -14,6 +15,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
@@ -94,6 +96,16 @@ public class TestExcelController {
|
||||
exportExcelService.exportWithOptions(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义导出
|
||||
*
|
||||
* @param response /
|
||||
*/
|
||||
@GetMapping("/customExport")
|
||||
public void customExport(HttpServletResponse response) throws IOException {
|
||||
exportExcelService.customExport(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 多个sheet导出
|
||||
*/
|
||||
|
||||
@@ -2,6 +2,8 @@ package org.dromara.demo.service;
|
||||
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 导出下拉框Excel示例
|
||||
*
|
||||
@@ -15,4 +17,11 @@ public interface IExportExcelService {
|
||||
* @param response /
|
||||
*/
|
||||
void exportWithOptions(HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 自定义导出
|
||||
*
|
||||
* @param response /
|
||||
*/
|
||||
void customExport(HttpServletResponse response) throws IOException;
|
||||
}
|
||||
|
||||
@@ -2,17 +2,21 @@ package org.dromara.demo.service.impl;
|
||||
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.idev.excel.write.metadata.WriteSheet;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.common.core.constant.SystemConstants;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.core.utils.file.FileUtils;
|
||||
import org.dromara.common.excel.core.DropDownOptions;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.excel.utils.ExcelWriterWrapper;
|
||||
import org.dromara.demo.domain.vo.ExportDemoVo;
|
||||
import org.dromara.demo.service.IExportExcelService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -233,4 +237,61 @@ public class ExportExcelServiceImpl implements IExportExcelService {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void customExport(HttpServletResponse response) throws IOException {
|
||||
String filename = ExcelUtil.encodingFilename("自定义导出");
|
||||
FileUtils.setAttachmentResponseHeader(response, filename);
|
||||
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
|
||||
|
||||
ExcelUtil.exportExcel(ExportDemoVo.class, response.getOutputStream(), wrapper -> {
|
||||
// 创建表格数据,业务中一般通过数据库查询
|
||||
List<ExportDemoVo> excelDataList = new ArrayList<>();
|
||||
for (int i = 0; i < 30; i++) {
|
||||
// 模拟数据库中的一条数据
|
||||
ExportDemoVo everyRowData = new ExportDemoVo();
|
||||
everyRowData.setNickName("用户-" + i);
|
||||
everyRowData.setUserStatus(SystemConstants.NORMAL);
|
||||
everyRowData.setGender("1");
|
||||
everyRowData.setPhoneNumber(String.format("175%08d", i));
|
||||
everyRowData.setEmail(String.format("175%08d", i) + "@163.com");
|
||||
everyRowData.setProvinceId(i);
|
||||
everyRowData.setCityId(i);
|
||||
everyRowData.setAreaId(i);
|
||||
excelDataList.add(everyRowData);
|
||||
}
|
||||
|
||||
// 创建表格
|
||||
WriteSheet sheet = ExcelWriterWrapper.sheetBuilder("自定义导出demo")
|
||||
// 合并单元格
|
||||
// .registerWriteHandler(new CellMergeStrategy(excelDataList, true))
|
||||
.build();
|
||||
|
||||
|
||||
wrapper.write(excelDataList, sheet);
|
||||
|
||||
List<ExportDemoVo> excelDataList2 = new ArrayList<>();
|
||||
for (int i = 0; i < 20; i++) {
|
||||
int index = 1000 + i;
|
||||
// 模拟数据库中的一条数据
|
||||
ExportDemoVo everyRowData = new ExportDemoVo();
|
||||
everyRowData.setNickName("用户-" + index);
|
||||
everyRowData.setUserStatus(SystemConstants.NORMAL);
|
||||
everyRowData.setGender("1");
|
||||
everyRowData.setPhoneNumber(String.format("175%08d", index));
|
||||
everyRowData.setEmail(String.format("175%08d", index) + "@163.com");
|
||||
everyRowData.setProvinceId(index);
|
||||
everyRowData.setCityId(index);
|
||||
everyRowData.setAreaId(index);
|
||||
excelDataList2.add(everyRowData);
|
||||
}
|
||||
|
||||
wrapper.write(excelDataList2, sheet);
|
||||
|
||||
// 或者在同一个excel中创建多个表格
|
||||
// WriteSheet sheet2 = ExcelWriterWrapper.sheetBuilder("自定义导出demo2").build();
|
||||
// wrapper.write(excelDataList2, sheet2);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,8 @@ export interface ${BusinessName}Query #if(!${treeCode})extends PageQuery #end{
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
/**
|
||||
* 日期范围参数
|
||||
*/
|
||||
params?: any;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']" />
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
<el-table-column label="${comment}" align="center" prop="${javaField}" />
|
||||
#end
|
||||
#end
|
||||
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
|
||||
<el-table-column label="操作" align="center" fixed="right" class-name="small-padding fixed-width">
|
||||
<template #default="scope">
|
||||
<el-tooltip content="修改" placement="top">
|
||||
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['${moduleName}:${businessName}:edit']"></el-button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="${packageName}.mapper.${ClassName}Mapper">
|
||||
|
||||
</mapper>
|
||||
|
||||
@@ -53,6 +53,13 @@ public class CacheController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 缓存监控列表信息
|
||||
*
|
||||
* @param info 信息
|
||||
* @param dbSize 数据库
|
||||
* @param commandStats 命令统计
|
||||
*/
|
||||
public record CacheListInfoVo(Properties info, Long dbSize, List<Map<String, String>> commandStats) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -179,6 +179,12 @@ public class SysMenuController extends BaseController {
|
||||
return toAjax(menuService.deleteMenuById(menuId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色菜单列表树信息
|
||||
*
|
||||
* @param checkedKeys 选中菜单列表
|
||||
* @param menus 菜单下拉树结构列表
|
||||
*/
|
||||
public record MenuTreeSelectVo(List<Long> checkedKeys, List<Tree<Long>> menus) {
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package org.dromara.system.controller.system;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.hutool.core.lang.tree.Tree;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -13,8 +14,10 @@ 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.SysDeptBo;
|
||||
import org.dromara.system.domain.bo.SysPostBo;
|
||||
import org.dromara.system.domain.vo.SysPostVo;
|
||||
import org.dromara.system.service.ISysDeptService;
|
||||
import org.dromara.system.service.ISysPostService;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
@@ -35,6 +38,7 @@ import java.util.List;
|
||||
public class SysPostController extends BaseController {
|
||||
|
||||
private final ISysPostService postService;
|
||||
private final ISysDeptService deptService;
|
||||
|
||||
/**
|
||||
* 获取岗位列表
|
||||
@@ -134,4 +138,14 @@ public class SysPostController extends BaseController {
|
||||
return R.ok(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取部门树列表
|
||||
*/
|
||||
@SaCheckPermission("system:post:list")
|
||||
@GetMapping("/deptTree")
|
||||
public R<List<Tree<Long>>> deptTree(SysDeptBo dept) {
|
||||
return R.ok(deptService.selectDeptTreeList(dept));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -129,8 +129,20 @@ public class SysProfileController extends BaseController {
|
||||
return R.fail("上传图片异常,请联系管理员");
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户头像信息
|
||||
*
|
||||
* @param imgUrl 头像地址
|
||||
*/
|
||||
public record AvatarVo(String imgUrl) {}
|
||||
|
||||
/**
|
||||
* 用户个人信息
|
||||
*
|
||||
* @param user 用户信息
|
||||
* @param roleGroup 用户所属角色组
|
||||
* @param postGroup 用户所属岗位组
|
||||
*/
|
||||
public record ProfileVo(ProfileUserVo user, String roleGroup, String postGroup) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -235,6 +235,12 @@ public class SysRoleController extends BaseController {
|
||||
return R.ok(selectVo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 角色部门列表树信息
|
||||
*
|
||||
* @param checkedKeys 选中部门列表
|
||||
* @param depts 下拉树结构列表
|
||||
*/
|
||||
public record DeptTreeSelectVo(List<Long> checkedKeys, List<Tree<Long>> depts) {}
|
||||
|
||||
}
|
||||
|
||||
@@ -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("同步租户参数配置成功");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,11 @@ public class MetaVo {
|
||||
*/
|
||||
private String link;
|
||||
|
||||
/**
|
||||
* 激活菜单
|
||||
*/
|
||||
private String activeMenu;
|
||||
|
||||
public MetaVo(String title, String icon) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
@@ -58,4 +63,16 @@ public class MetaVo {
|
||||
}
|
||||
}
|
||||
|
||||
public MetaVo(String title, String icon, Boolean noCache, String link, String activeMenu) {
|
||||
this.title = title;
|
||||
this.icon = icon;
|
||||
this.noCache = noCache;
|
||||
if (StringUtils.ishttp(link)) {
|
||||
this.link = link;
|
||||
}
|
||||
if (StringUtils.startWithAnyIgnoreCase(activeMenu, "/")) {
|
||||
this.activeMenu = activeMenu;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,9 @@ public interface SysDeptMapper extends BaseMapperPlus<SysDept, SysDeptVo> {
|
||||
*/
|
||||
default String buildDeptByRoleSql(Long roleId) {
|
||||
return """
|
||||
select dept_id from sys_role_dept where role_id = %d
|
||||
select srd.dept_id from sys_role_dept srd
|
||||
left join sys_role sr on sr.role_id = srd.role_id
|
||||
where srd.role_id = %d and sr.status = '0'
|
||||
""".formatted(roleId);
|
||||
}
|
||||
|
||||
@@ -47,7 +49,9 @@ public interface SysDeptMapper extends BaseMapperPlus<SysDept, SysDeptVo> {
|
||||
default String buildParentDeptByRoleSql(Long roleId) {
|
||||
return """
|
||||
select parent_id from sys_dept where dept_id in (
|
||||
select dept_id from sys_role_dept where role_id = %d
|
||||
select srd.dept_id from sys_role_dept srd
|
||||
left join sys_role sr on sr.role_id = srd.role_id
|
||||
where srd.role_id = %d and sr.status = '0'
|
||||
)
|
||||
""".formatted(roleId);
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenu, SysMenuVo> {
|
||||
default String buildMenuByUserSql(Long userId) {
|
||||
return """
|
||||
select menu_id from sys_role_menu where role_id in (
|
||||
select role_id from sys_user_role where user_id = %d
|
||||
select sur.role_id from sys_user_role sur
|
||||
left join sys_role sr on sr.role_id = sur.role_id
|
||||
where sur.user_id = %d and sr.status = '0'
|
||||
)
|
||||
""".formatted(userId);
|
||||
}
|
||||
@@ -50,7 +52,9 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenu, SysMenuVo> {
|
||||
*/
|
||||
default String buildMenuByRoleSql(Long roleId) {
|
||||
return """
|
||||
select menu_id from sys_role_menu where role_id = %d
|
||||
select srm.menu_id from sys_role_menu srm
|
||||
left join sys_role sr on sr.role_id = srm.role_id
|
||||
where srm.role_id = %d and sr.status = '0'
|
||||
""".formatted(roleId);
|
||||
}
|
||||
|
||||
@@ -68,7 +72,9 @@ public interface SysMenuMapper extends BaseMapperPlus<SysMenu, SysMenuVo> {
|
||||
default String buildParentMenuByRoleSql(Long roleId) {
|
||||
return """
|
||||
select parent_id from sys_menu where menu_id in (
|
||||
select menu_id from sys_role_menu where role_id = %d
|
||||
select srm.menu_id from sys_role_menu srm
|
||||
left join sys_role sr on sr.role_id = srm.role_id
|
||||
where srm.role_id = %d and sr.status = '0'
|
||||
)
|
||||
""".formatted(roleId);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -161,6 +161,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
});
|
||||
}
|
||||
return baseMapper.selectObjs(new LambdaQueryWrapper<SysMenu>()
|
||||
.select(SysMenu::getMenuId)
|
||||
.in(SysMenu::getMenuId, menuIds)
|
||||
.notIn(CollUtil.isNotEmpty(parentIds), SysMenu::getMenuId, parentIds), x -> {
|
||||
return Convert.toLong(x);
|
||||
@@ -185,7 +186,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
router.setPath(menu.getRouterPath());
|
||||
router.setComponent(menu.getComponentInfo());
|
||||
router.setQuery(menu.getQueryParam());
|
||||
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
|
||||
router.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath(), menu.getRemark()));
|
||||
List<SysMenu> cMenus = menu.getChildren();
|
||||
if (CollUtil.isNotEmpty(cMenus) && SystemConstants.TYPE_DIR.equals(menu.getMenuType())) {
|
||||
router.setAlwaysShow(true);
|
||||
@@ -199,7 +200,7 @@ public class SysMenuServiceImpl implements ISysMenuService {
|
||||
children.setPath(menu.getPath());
|
||||
children.setComponent(menu.getComponent());
|
||||
children.setName(frameName);
|
||||
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath()));
|
||||
children.setMeta(new MetaVo(menu.getMenuName(), menu.getIcon(), StringUtils.equals("1", menu.getIsCache()), menu.getPath(), menu.getRemark()));
|
||||
children.setQuery(menu.getQueryParam());
|
||||
childrenList.add(children);
|
||||
router.setChildren(childrenList);
|
||||
@@ -240,6 +241,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());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -88,4 +88,8 @@ public interface FlowConstant {
|
||||
*/
|
||||
String AUTO_PASS = "autoPass";
|
||||
|
||||
/**
|
||||
* 业务编码
|
||||
*/
|
||||
String BUSINESS_CODE = "businessCode";
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public enum ButtonPermissionEnum implements NodeExtEnum {
|
||||
/**
|
||||
* 是否能抄送
|
||||
*/
|
||||
COPY("是否能抄送", "copy", false),
|
||||
COPY("是否能抄送", "copy", true),
|
||||
|
||||
/**
|
||||
* 是否显示退回
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.dromara.workflow.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 抄送设置枚举
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum CopySettingEnum implements NodeExtEnum {
|
||||
;
|
||||
private final String label;
|
||||
private final String value;
|
||||
private final boolean selected;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package org.dromara.workflow.common.enums;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* 变量枚举
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum VariablesEnum implements NodeExtEnum {
|
||||
;
|
||||
private final String label;
|
||||
private final String value;
|
||||
private final boolean selected;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package org.dromara.workflow.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableLogic;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.common.tenant.core.TenantEntity;
|
||||
|
||||
import java.io.Serial;
|
||||
|
||||
/**
|
||||
* 流程实例业务扩展对象 flow_instance_biz_ext
|
||||
*
|
||||
* @author may
|
||||
* @date 2025-08-05
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@TableName("flow_instance_biz_ext")
|
||||
public class FlowInstanceBizExt extends TenantEntity {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 流程实例ID
|
||||
*/
|
||||
private Long instanceId;
|
||||
|
||||
/**
|
||||
* 业务ID
|
||||
*/
|
||||
private String businessId;
|
||||
|
||||
/**
|
||||
* 业务编码
|
||||
*/
|
||||
private String businessCode;
|
||||
|
||||
/**
|
||||
* 业务标题
|
||||
*/
|
||||
private String businessTitle;
|
||||
|
||||
/**
|
||||
* 删除标志(0代表存在 1代表删除)
|
||||
*/
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
|
||||
}
|
||||
@@ -29,6 +29,11 @@ public class TestLeave extends BaseEntity {
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 申请编号
|
||||
*/
|
||||
private String applyCode;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
|
||||
@@ -53,6 +53,11 @@ public class CompleteTaskBo implements Serializable {
|
||||
*/
|
||||
private String notice;
|
||||
|
||||
/**
|
||||
* 办理人(可不填 用于覆盖当前节点办理人)
|
||||
*/
|
||||
private String handler;
|
||||
|
||||
/**
|
||||
* 流程变量
|
||||
*/
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package org.dromara.workflow.domain.bo;
|
||||
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.workflow.domain.FlowInstanceBizExt;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
@@ -34,11 +36,21 @@ public class StartProcessBo implements Serializable {
|
||||
@NotBlank(message = "流程定义编码不能为空", groups = {AddGroup.class})
|
||||
private String flowCode;
|
||||
|
||||
/**
|
||||
* 办理人(可不填 用于覆盖当前节点办理人)
|
||||
*/
|
||||
private String handler;
|
||||
|
||||
/**
|
||||
* 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
|
||||
*/
|
||||
private Map<String, Object> variables;
|
||||
|
||||
/**
|
||||
* 流程业务扩展信息
|
||||
*/
|
||||
private FlowInstanceBizExt bizExt;
|
||||
|
||||
public Map<String, Object> getVariables() {
|
||||
if (variables == null) {
|
||||
return new HashMap<>(16);
|
||||
@@ -46,4 +58,11 @@ public class StartProcessBo implements Serializable {
|
||||
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
|
||||
return variables;
|
||||
}
|
||||
|
||||
public FlowInstanceBizExt getBizExt() {
|
||||
if (ObjectUtil.isNull(bizExt)) {
|
||||
bizExt = new FlowInstanceBizExt();
|
||||
}
|
||||
return bizExt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,11 @@ public class TestLeaveBo extends BaseEntity {
|
||||
*/
|
||||
private String flowCode;
|
||||
|
||||
/**
|
||||
* 申请编号
|
||||
*/
|
||||
private String applyCode;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package org.dromara.workflow.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
import org.dromara.common.translation.annotation.Translation;
|
||||
import org.dromara.common.translation.constant.TransConstant;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 抄送对象
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Data
|
||||
public class FlowCopyVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 用户id
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 用户名称
|
||||
*/
|
||||
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "userId")
|
||||
private String userName;
|
||||
|
||||
public FlowCopyVo(Long userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -203,6 +203,18 @@ public class FlowHisTaskVo implements Serializable {
|
||||
*/
|
||||
private String runDuration;
|
||||
|
||||
//业务扩展信息开始
|
||||
/**
|
||||
* 业务编码
|
||||
*/
|
||||
private String businessCode;
|
||||
|
||||
/**
|
||||
* 业务标题
|
||||
*/
|
||||
private String businessTitle;
|
||||
//业务扩展信息结束
|
||||
|
||||
/**
|
||||
* 设置创建时间并计算任务运行时长
|
||||
*
|
||||
|
||||
@@ -134,4 +134,16 @@ public class FlowInstanceVo {
|
||||
@Translation(type = FlowConstant.CATEGORY_ID_TO_NAME, mapper = "category")
|
||||
private String categoryName;
|
||||
|
||||
//业务扩展信息开始
|
||||
/**
|
||||
* 业务编码
|
||||
*/
|
||||
private String businessCode;
|
||||
|
||||
/**
|
||||
* 业务标题
|
||||
*/
|
||||
private String businessTitle;
|
||||
//业务扩展信息结束
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 任务视图
|
||||
@@ -185,4 +186,30 @@ public class FlowTaskVo implements Serializable {
|
||||
*/
|
||||
private List<ButtonPermissionVo> buttonList;
|
||||
|
||||
/**
|
||||
* 抄送对象 ID 集合
|
||||
* <p>
|
||||
* 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID
|
||||
*/
|
||||
private List<FlowCopyVo> copyList;
|
||||
|
||||
/**
|
||||
* 自定义参数 Map
|
||||
* <p>
|
||||
* 根据扩展属性中 VariablesEnum 类型的数据生成,存储 key=value 格式的自定义参数
|
||||
*/
|
||||
private Map<String, String> varList;
|
||||
|
||||
//业务扩展信息开始
|
||||
/**
|
||||
* 业务编码
|
||||
*/
|
||||
private String businessCode;
|
||||
|
||||
/**
|
||||
* 业务标题
|
||||
*/
|
||||
private String businessTitle;
|
||||
//业务扩展信息结束
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.dromara.workflow.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Node 扩展属性解析结果 VO
|
||||
* <p>
|
||||
* 用于封装从扩展属性 JSON 中解析出的各类信息,包括按钮权限、抄送对象和自定义参数。
|
||||
*
|
||||
* @author AprilWind
|
||||
*/
|
||||
@Data
|
||||
public class NodeExtVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 按钮权限列表
|
||||
* <p>
|
||||
* 根据扩展属性中 ButtonPermissionEnum 类型的数据生成,每个元素表示一个按钮及其是否勾选。
|
||||
*/
|
||||
private List<ButtonPermissionVo> buttonPermissions;
|
||||
|
||||
/**
|
||||
* 抄送对象 ID 集合
|
||||
* <p>
|
||||
* 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID
|
||||
*/
|
||||
private Set<String> copySettings;
|
||||
|
||||
/**
|
||||
* 自定义参数 Map
|
||||
* <p>
|
||||
* 根据扩展属性中 VariablesEnum 类型的数据生成,存储 key=value 格式的自定义参数
|
||||
*/
|
||||
private Map<String, String> variables;
|
||||
|
||||
}
|
||||
@@ -32,6 +32,12 @@ public class TestLeaveVo implements Serializable {
|
||||
@ExcelProperty(value = "主键")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 申请编号
|
||||
*/
|
||||
@ExcelProperty(value = "申请编号")
|
||||
private String applyCode;
|
||||
|
||||
/**
|
||||
* 请假类型
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,7 @@ import java.util.Map;
|
||||
public class FlowProcessEventHandler {
|
||||
|
||||
/**
|
||||
* 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成,单任务完成等)
|
||||
* 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成等)
|
||||
*
|
||||
* @param flowCode 流程定义编码
|
||||
* @param instance 实例数据
|
||||
|
||||
@@ -1,33 +1,39 @@
|
||||
package org.dromara.workflow.listener;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.TypeReference;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.enums.BusinessStatusEnum;
|
||||
import org.dromara.common.core.service.UserService;
|
||||
import org.dromara.common.core.utils.StreamUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.warm.flow.core.FlowEngine;
|
||||
import org.dromara.warm.flow.core.dto.FlowParams;
|
||||
import org.dromara.warm.flow.core.entity.Definition;
|
||||
import org.dromara.warm.flow.core.entity.Instance;
|
||||
import org.dromara.warm.flow.core.entity.Task;
|
||||
import org.dromara.warm.flow.core.listener.GlobalListener;
|
||||
import org.dromara.warm.flow.core.listener.ListenerVariable;
|
||||
import org.dromara.warm.flow.core.service.InsService;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.constant.FlowConstant;
|
||||
import org.dromara.workflow.common.enums.TaskStatusEnum;
|
||||
import org.dromara.workflow.domain.bo.FlowCopyBo;
|
||||
import org.dromara.workflow.domain.vo.NodeExtVo;
|
||||
import org.dromara.workflow.handler.FlowProcessEventHandler;
|
||||
import org.dromara.workflow.service.IFlwCommonService;
|
||||
import org.dromara.workflow.service.IFlwInstanceService;
|
||||
import org.dromara.workflow.service.IFlwNodeExtService;
|
||||
import org.dromara.workflow.service.IFlwTaskService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 全局任务办理监听
|
||||
@@ -41,10 +47,11 @@ import java.util.Map;
|
||||
public class WorkflowGlobalListener implements GlobalListener {
|
||||
|
||||
private final IFlwTaskService flwTaskService;
|
||||
private final IFlwInstanceService instanceService;
|
||||
private final IFlwInstanceService flwInstanceService;
|
||||
private final FlowProcessEventHandler flowProcessEventHandler;
|
||||
private final IFlwCommonService flwCommonService;
|
||||
private final InsService insService;
|
||||
private final IFlwNodeExtService nodeExtService;
|
||||
private final UserService userService;
|
||||
|
||||
/**
|
||||
* 创建监听器,任务创建时执行
|
||||
@@ -63,6 +70,25 @@ public class WorkflowGlobalListener implements GlobalListener {
|
||||
*/
|
||||
@Override
|
||||
public void start(ListenerVariable listenerVariable) {
|
||||
String ext = listenerVariable.getNode().getExt();
|
||||
if (StringUtils.isNotBlank(ext)) {
|
||||
Map<String, Object> variable = listenerVariable.getVariable();
|
||||
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable);
|
||||
Set<String> copyList = nodeExt.getCopySettings();
|
||||
if (CollUtil.isNotEmpty(copyList)) {
|
||||
List<FlowCopyBo> list = StreamUtils.toList(copyList, x -> {
|
||||
FlowCopyBo bo = new FlowCopyBo();
|
||||
Long id = Convert.toLong(x);
|
||||
bo.setUserId(id);
|
||||
bo.setUserName(userService.selectUserNameById(id));
|
||||
return bo;
|
||||
});
|
||||
variable.put(FlowConstant.FLOW_COPY_LIST, list);
|
||||
}
|
||||
if (CollUtil.isNotEmpty(nodeExt.getVariables())) {
|
||||
variable.putAll(nodeExt.getVariables());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,7 +158,7 @@ public class WorkflowGlobalListener implements GlobalListener {
|
||||
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, BusinessStatusEnum.BACK.getStatus(), params, false);
|
||||
// 修改流程实例状态
|
||||
instance.setFlowStatus(BusinessStatusEnum.BACK.getStatus());
|
||||
insService.updateById(instance);
|
||||
FlowEngine.insService().updateById(instance);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,19 +180,18 @@ public class WorkflowGlobalListener implements GlobalListener {
|
||||
}
|
||||
|
||||
if (variable.containsKey(FlowConstant.FLOW_COPY_LIST)) {
|
||||
List<FlowCopyBo> flowCopyList = MapUtil.get(variable, FlowConstant.FLOW_COPY_LIST, new TypeReference<>() {});
|
||||
List<FlowCopyBo> flowCopyList = MapUtil.get(variable, FlowConstant.FLOW_COPY_LIST, new TypeReference<>() {
|
||||
});
|
||||
// 添加抄送人
|
||||
flwTaskService.setCopy(task, flowCopyList);
|
||||
}
|
||||
if (variable.containsKey(FlowConstant.MESSAGE_TYPE)) {
|
||||
List<String> messageType = MapUtil.get(variable, FlowConstant.MESSAGE_TYPE, new TypeReference<>() {});
|
||||
List<String> messageType = MapUtil.get(variable, FlowConstant.MESSAGE_TYPE, new TypeReference<>() {
|
||||
});
|
||||
String notice = MapUtil.getStr(variable, FlowConstant.MESSAGE_NOTICE);
|
||||
// 消息通知
|
||||
if (CollUtil.isNotEmpty(messageType)) {
|
||||
flwCommonService.sendMessage(definition.getFlowName(), instance.getId(), messageType, notice);
|
||||
}
|
||||
flwCommonService.sendMessage(definition.getFlowName(), instance.getId(), messageType, notice);
|
||||
}
|
||||
insService.removeVariables(instance.getId(),
|
||||
FlowEngine.insService().removeVariables(instance.getId(),
|
||||
FlowConstant.FLOW_COPY_LIST,
|
||||
FlowConstant.MESSAGE_TYPE,
|
||||
FlowConstant.MESSAGE_NOTICE,
|
||||
@@ -190,7 +215,7 @@ public class WorkflowGlobalListener implements GlobalListener {
|
||||
if (flwTaskService.isTaskEnd(instanceId)) {
|
||||
String status = BusinessStatusEnum.FINISH.getStatus();
|
||||
// 更新流程状态为已完成
|
||||
instanceService.updateStatus(instanceId, status);
|
||||
flwInstanceService.updateStatus(instanceId, status);
|
||||
log.info("流程已结束,状态更新为: {}", status);
|
||||
return status;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package org.dromara.workflow.mapper;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
import org.dromara.workflow.domain.FlowInstanceBizExt;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 流程实例业务扩展Mapper接口
|
||||
*
|
||||
* @author may
|
||||
* @date 2025-08-05
|
||||
*/
|
||||
public interface FlwInstanceBizExtMapper extends BaseMapperPlus<FlowInstanceBizExt, FlowInstanceBizExt> {
|
||||
|
||||
/**
|
||||
* 根据 instanceId 保存或更新流程实例业务扩展
|
||||
*
|
||||
* @param entity 流程实例业务扩展实体
|
||||
* @return 操作是否成功
|
||||
*/
|
||||
default int saveOrUpdateByInstanceId(FlowInstanceBizExt entity) {
|
||||
// 查询是否存在
|
||||
FlowInstanceBizExt exist = this.selectOne(new LambdaQueryWrapper<FlowInstanceBizExt>()
|
||||
.eq(FlowInstanceBizExt::getInstanceId, entity.getInstanceId()));
|
||||
|
||||
if (ObjectUtil.isNotNull(exist)) {
|
||||
// 存在就带上主键更新
|
||||
entity.setId(exist.getId());
|
||||
return updateById(entity);
|
||||
} else {
|
||||
// 不存在就插入
|
||||
return insert(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照流程实例ID删除单个流程实例业务扩展
|
||||
*
|
||||
* @param instanceId 流程实例ID
|
||||
* @return 删除的行数
|
||||
*/
|
||||
default int deleteByInstId(Long instanceId) {
|
||||
return this.delete(new LambdaQueryWrapper<FlowInstanceBizExt>()
|
||||
.eq(FlowInstanceBizExt::getInstanceId, instanceId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 按照流程实例ID批量删除流程实例业务扩展
|
||||
*
|
||||
* @param instanceIds 流程实例ID列表
|
||||
* @return 删除的行数
|
||||
*/
|
||||
default int deleteByInstIds(List<Long> instanceIds) {
|
||||
return this.delete(new LambdaQueryWrapper<FlowInstanceBizExt>()
|
||||
.in(FlowInstanceBizExt::getInstanceId, instanceIds));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
package org.dromara.workflow.service;
|
||||
|
||||
import org.dromara.workflow.domain.vo.ButtonPermissionVo;
|
||||
import org.dromara.workflow.domain.vo.NodeExtVo;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 流程节点扩展属性 服务层
|
||||
@@ -12,11 +12,24 @@ import java.util.List;
|
||||
public interface IFlwNodeExtService {
|
||||
|
||||
/**
|
||||
* 从扩展属性构建按钮权限列表:根据 ext 中记录的权限值,标记每个按钮是否勾选
|
||||
* 解析扩展属性 JSON 并构建 Node 扩展属性对象
|
||||
* <p>
|
||||
* 根据传入的 JSON 字符串,将扩展属性分为三类:
|
||||
* 1. ButtonPermissionEnum:解析为按钮权限列表,标记每个按钮是否勾选
|
||||
* 2. CopySettingEnum:解析为抄送对象 ID 集合
|
||||
* 3. VariablesEnum:解析为自定义参数 Map
|
||||
*
|
||||
* @param ext 扩展属性 JSON 字符串
|
||||
* @return 按钮权限 VO 列表
|
||||
* <p>示例 JSON:
|
||||
* [
|
||||
* {"code": "ButtonPermissionEnum", "value": "back,termination"},
|
||||
* {"code": "CopySettingEnum", "value": "1"},
|
||||
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
|
||||
* ]
|
||||
*
|
||||
* @param ext 扩展属性 JSON 字符串
|
||||
* @param variable 流程变量
|
||||
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
|
||||
*/
|
||||
List<ButtonPermissionVo> buildButtonPermissionsFromExt(String ext);
|
||||
NodeExtVo parseNodeExt(String ext, Map<String, Object> variable);
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
|
||||
import org.dromara.warm.flow.ui.service.ChartExtService;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.constant.FlowConstant;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -48,6 +49,8 @@ public class FlwChartExtServiceImpl implements ChartExtService {
|
||||
private final DeptService deptService;
|
||||
private final FlowHisTaskMapper flowHisTaskMapper;
|
||||
private final DictService dictService;
|
||||
@Value("${warm-flow.node-tooltip:true}")
|
||||
private boolean nodeTooltip;
|
||||
|
||||
/**
|
||||
* 设置流程图提示信息
|
||||
@@ -56,6 +59,11 @@ public class FlwChartExtServiceImpl implements ChartExtService {
|
||||
*/
|
||||
@Override
|
||||
public void execute(DefJson defJson) {
|
||||
// 配置关闭,直接返回,不渲染悬浮窗
|
||||
if (!nodeTooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 根据流程实例ID查询所有相关的历史任务列表
|
||||
List<FlowHisTask> flowHisTasks = this.getHisTaskGroupedByNode(defJson.getInstance().getId());
|
||||
if (CollUtil.isEmpty(flowHisTasks)) {
|
||||
@@ -103,6 +111,11 @@ public class FlwChartExtServiceImpl implements ChartExtService {
|
||||
*/
|
||||
@Override
|
||||
public void initPromptContent(DefJson defJson) {
|
||||
// 配置关闭,直接返回,不渲染悬浮窗
|
||||
if (!nodeTooltip) {
|
||||
return;
|
||||
}
|
||||
|
||||
defJson.setTopText("流程名称: " + defJson.getFlowName());
|
||||
defJson.getNodeList().forEach(nodeJson -> {
|
||||
nodeJson.setPromptContent(
|
||||
@@ -152,8 +165,10 @@ public class FlwChartExtServiceImpl implements ChartExtService {
|
||||
/**
|
||||
* 处理节点的扩展信息,构建用于流程图悬浮提示的内容
|
||||
*
|
||||
* @param nodeJson 当前节点对象
|
||||
* @param taskList 当前节点对应的历史审批任务列表
|
||||
* @param nodeJson 当前流程节点对象,包含节点基础信息和提示内容容器
|
||||
* @param taskList 当前节点关联的历史审批任务列表,用于生成提示信息
|
||||
* @param userMap 用户信息映射表,key 为用户ID,value 为用户DTO对象,用于获取审批人信息
|
||||
* @param dictType 数据字典映射表,key 为字典项编码,value 为对应显示值,用于翻译审批状态等
|
||||
*/
|
||||
private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap, Map<String, String> dictType) {
|
||||
|
||||
|
||||
@@ -11,9 +11,8 @@ import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mail.utils.MailUtils;
|
||||
import org.dromara.common.sse.dto.SseMessageDto;
|
||||
import org.dromara.common.sse.utils.SseMessageUtils;
|
||||
import org.dromara.warm.flow.core.FlowEngine;
|
||||
import org.dromara.warm.flow.core.entity.Node;
|
||||
import org.dromara.warm.flow.core.enums.SkipType;
|
||||
import org.dromara.warm.flow.core.service.NodeService;
|
||||
import org.dromara.warm.flow.orm.entity.FlowTask;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.enums.MessageTypeEnum;
|
||||
@@ -21,8 +20,10 @@ import org.dromara.workflow.service.IFlwCommonService;
|
||||
import org.dromara.workflow.service.IFlwTaskService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
/**
|
||||
@@ -35,19 +36,27 @@ import java.util.stream.Collectors;
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
private final NodeService nodeService;
|
||||
|
||||
private static final String DEFAULT_SUBJECT = "单据审批提醒";
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* 根据流程实例发送消息给当前处理人
|
||||
*
|
||||
* @param flowName 流程定义名称
|
||||
* @param messageType 消息类型
|
||||
* @param message 消息内容,为空则发送默认配置的消息内容
|
||||
* @param instId 流程实例ID
|
||||
* @param messageType 消息类型列表
|
||||
* @param message 消息内容,为空则使用默认消息
|
||||
*/
|
||||
@Override
|
||||
public void sendMessage(String flowName, Long instId, List<String> messageType, String message) {
|
||||
if (CollUtil.isEmpty(messageType)) {
|
||||
return;
|
||||
}
|
||||
IFlwTaskService flwTaskService = SpringUtils.getBean(IFlwTaskService.class);
|
||||
List<FlowTask> list = flwTaskService.selectByInstId(instId);
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
if (StringUtils.isBlank(message)) {
|
||||
message = "有新的【" + flowName + "】单据已经提交至您,请您及时处理。";
|
||||
}
|
||||
@@ -55,19 +64,25 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
if (CollUtil.isEmpty(userList)) {
|
||||
return;
|
||||
}
|
||||
sendMessage(messageType, message, "单据审批提醒", userList);
|
||||
sendMessage(messageType, message, DEFAULT_SUBJECT, userList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* 发送消息给指定用户列表
|
||||
*
|
||||
* @param messageType 消息类型
|
||||
* @param messageType 消息类型列表
|
||||
* @param message 消息内容
|
||||
* @param subject 邮件标题
|
||||
* @param userList 接收用户
|
||||
* @param userList 接收用户列表
|
||||
*/
|
||||
@Override
|
||||
public void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList) {
|
||||
if (CollUtil.isEmpty(messageType) || CollUtil.isEmpty(userList)) {
|
||||
return;
|
||||
}
|
||||
List<Long> userIds = new ArrayList<>(StreamUtils.toSet(userList, UserDTO::getUserId));
|
||||
Set<String> emails = StreamUtils.toSet(userList, UserDTO::getEmail);
|
||||
|
||||
for (String code : messageType) {
|
||||
MessageTypeEnum messageTypeEnum = MessageTypeEnum.getByCode(code);
|
||||
if (ObjectUtil.isEmpty(messageTypeEnum)) {
|
||||
@@ -76,13 +91,11 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
switch (messageTypeEnum) {
|
||||
case SYSTEM_MESSAGE -> {
|
||||
SseMessageDto dto = new SseMessageDto();
|
||||
dto.setUserIds(StreamUtils.toList(userList, UserDTO::getUserId).stream().distinct().collect(Collectors.toList()));
|
||||
dto.setUserIds(userIds);
|
||||
dto.setMessage(message);
|
||||
SseMessageUtils.publishMessage(dto);
|
||||
}
|
||||
case EMAIL_MESSAGE -> {
|
||||
MailUtils.sendText(StreamUtils.join(userList, UserDTO::getEmail), subject, message);
|
||||
}
|
||||
case EMAIL_MESSAGE -> MailUtils.sendText(emails, subject, message);
|
||||
case SMS_MESSAGE -> {
|
||||
//todo 短信发送
|
||||
}
|
||||
@@ -100,8 +113,7 @@ public class FlwCommonServiceImpl implements IFlwCommonService {
|
||||
*/
|
||||
@Override
|
||||
public String applyNodeCode(Long definitionId) {
|
||||
Node startNode = nodeService.getStartNode(definitionId);
|
||||
Node nextNode = nodeService.getNextNode(definitionId, startNode.getNodeCode(), null, SkipType.PASS.getKey());
|
||||
return nextNode.getNodeCode();
|
||||
List<Node> firstBetweenNode = FlowEngine.nodeService().getFirstBetweenNode(definitionId, new HashMap<>());
|
||||
return firstBetweenNode.get(0).getNodeCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,31 @@
|
||||
package org.dromara.workflow.service.impl;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.lang.Dict;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.domain.dto.DictTypeDTO;
|
||||
import org.dromara.common.core.service.DictService;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.warm.flow.core.FlowEngine;
|
||||
import org.dromara.warm.flow.core.utils.CollUtil;
|
||||
import org.dromara.warm.flow.core.utils.ExpressionUtil;
|
||||
import org.dromara.warm.flow.ui.service.NodeExtService;
|
||||
import org.dromara.warm.flow.ui.vo.NodeExt;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.enums.ButtonPermissionEnum;
|
||||
import org.dromara.workflow.common.enums.CopySettingEnum;
|
||||
import org.dromara.workflow.common.enums.NodeExtEnum;
|
||||
import org.dromara.workflow.common.enums.VariablesEnum;
|
||||
import org.dromara.workflow.domain.vo.ButtonPermissionVo;
|
||||
import org.dromara.workflow.domain.vo.NodeExtVo;
|
||||
import org.dromara.workflow.service.IFlwNodeExtService;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* 流程设计器-节点扩展属性
|
||||
@@ -36,14 +41,35 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
|
||||
/**
|
||||
* 存储不同 dictType 对应的配置信息
|
||||
*/
|
||||
private static final Map<String, ButtonPermission> CHILD_NODE_MAP = new HashMap<>();
|
||||
|
||||
record ButtonPermission(String label, Integer type, Boolean must, Boolean multiple) {
|
||||
}
|
||||
private static final Map<String, Map<String, Object>> CHILD_NODE_MAP;
|
||||
|
||||
static {
|
||||
CHILD_NODE_MAP.put(ButtonPermissionEnum.class.getSimpleName(),
|
||||
new ButtonPermission("权限按钮", 4, false, true));
|
||||
CHILD_NODE_MAP = Map.of(
|
||||
CopySettingEnum.class.getSimpleName(),
|
||||
Map.of(
|
||||
"label", "抄送对象",
|
||||
"type", 5,
|
||||
"must", false,
|
||||
"multiple", false,
|
||||
"desc", "设置该节点的抄送办理人"
|
||||
),
|
||||
VariablesEnum.class.getSimpleName(),
|
||||
Map.of(
|
||||
"label", "自定义参数",
|
||||
"type", 2,
|
||||
"must", false,
|
||||
"multiple", false,
|
||||
"desc", "节点执行时可设置自定义参数,多个参数以逗号分隔,如:key1=value1,key2=value2"
|
||||
),
|
||||
ButtonPermissionEnum.class.getSimpleName(),
|
||||
Map.of(
|
||||
"label", "权限按钮",
|
||||
"type", 4,
|
||||
"must", false,
|
||||
"multiple", true,
|
||||
"desc", "控制该节点的按钮权限"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private final DictService dictService;
|
||||
@@ -56,6 +82,9 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
|
||||
@Override
|
||||
public List<NodeExt> getNodeExt() {
|
||||
List<NodeExt> nodeExtList = new ArrayList<>();
|
||||
// 构建基础设置页面
|
||||
nodeExtList.add(buildNodeExt("wf_basic_tab", "基础设置", 1,
|
||||
List.of(CopySettingEnum.class, VariablesEnum.class)));
|
||||
// 构建按钮权限页面
|
||||
nodeExtList.add(buildNodeExt("wf_button_tab", "权限", 2,
|
||||
List.of(ButtonPermissionEnum.class)));
|
||||
@@ -105,9 +134,20 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
|
||||
return null;
|
||||
}
|
||||
String simpleName = enumClass.getSimpleName();
|
||||
NodeExt.ChildNode childNode = buildChildNodeMap(simpleName);
|
||||
NodeExt.ChildNode childNode = new NodeExt.ChildNode();
|
||||
Map<String, Object> map = CHILD_NODE_MAP.get(simpleName);
|
||||
// 编码,此json中唯
|
||||
childNode.setCode(simpleName);
|
||||
// label名称
|
||||
childNode.setLabel(Convert.toStr(map.get("label")));
|
||||
// 1:输入框 2:文本域 3:下拉框 4:选择框 5:用户选择器
|
||||
childNode.setType(Convert.toInt(map.get("type"), 1));
|
||||
// 是否必填
|
||||
childNode.setMust(Convert.toBool(map.get("must"), false));
|
||||
// 是否多选
|
||||
childNode.setMultiple(Convert.toBool(map.get("multiple"), true));
|
||||
// 描述
|
||||
childNode.setDesc(Convert.toStr(map.get("desc"), null));
|
||||
// 字典,下拉框和复选框时用到
|
||||
childNode.setDict(Arrays.stream(enumClass.getEnumConstants())
|
||||
.map(NodeExtEnum.class::cast)
|
||||
@@ -128,12 +168,18 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
|
||||
if (ObjectUtil.isNull(dictTypeDTO)) {
|
||||
return null;
|
||||
}
|
||||
NodeExt.ChildNode childNode = buildChildNodeMap(dictType);
|
||||
NodeExt.ChildNode childNode = new NodeExt.ChildNode();
|
||||
// 编码,此json中唯一
|
||||
childNode.setCode(dictType);
|
||||
// label名称
|
||||
childNode.setLabel(dictTypeDTO.getDictName());
|
||||
// 描述
|
||||
// 1:输入框 2:文本域 3:下拉框 4:选择框 5:用户选择器
|
||||
childNode.setType(3);
|
||||
// 是否必填
|
||||
childNode.setMust(false);
|
||||
// 是否多选
|
||||
childNode.setMultiple(true);
|
||||
// 描述 (可根据描述参数解析更多配置,如type,must,multiple等)
|
||||
childNode.setDesc(dictTypeDTO.getRemark());
|
||||
// 字典,下拉框和复选框时用到
|
||||
childNode.setDict(dictService.getDictData(dictType)
|
||||
@@ -144,100 +190,165 @@ public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 CHILD_NODE_MAP 中的配置信息,构建一个基本的 ChildNode 对象
|
||||
* 该方法用于设置 ChildNode 的常规属性,例如 label、type、是否必填、是否多选等
|
||||
* 解析扩展属性 JSON 并构建 Node 扩展属性对象
|
||||
* <p>
|
||||
* 根据传入的 JSON 字符串,将扩展属性分为三类:
|
||||
* 1. ButtonPermissionEnum:解析为按钮权限列表,标记每个按钮是否勾选
|
||||
* 2. CopySettingEnum:解析为抄送对象 ID 集合
|
||||
* 3. VariablesEnum:解析为自定义参数 Map
|
||||
*
|
||||
* @param key CHILD_NODE_MAP 的 key
|
||||
* @return 返回构建好的 ChildNode 对象
|
||||
*/
|
||||
private NodeExt.ChildNode buildChildNodeMap(String key) {
|
||||
NodeExt.ChildNode childNode = new NodeExt.ChildNode();
|
||||
ButtonPermission bp = CHILD_NODE_MAP.get(key);
|
||||
if (bp == null) {
|
||||
childNode.setType(1);
|
||||
childNode.setMust(false);
|
||||
childNode.setMultiple(true);
|
||||
return childNode;
|
||||
}
|
||||
// label名称
|
||||
childNode.setLabel(bp.label());
|
||||
// 1:输入框 2:输入框 3:下拉框 4:选择框
|
||||
childNode.setType(bp.type());
|
||||
// 是否必填
|
||||
childNode.setMust(bp.must());
|
||||
// 是否多选
|
||||
childNode.setMultiple(bp.multiple());
|
||||
return childNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从扩展属性构建按钮权限列表:根据 ext 中记录的权限值,标记每个按钮是否勾选
|
||||
* <p>示例 JSON:
|
||||
* [
|
||||
* {"code": "ButtonPermissionEnum", "value": "back,termination"},
|
||||
* {"code": "CopySettingEnum", "value": "1,3,4,#{@spelRuleComponent.selectDeptLeaderById(#deptId", "#roleId)}"},
|
||||
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
|
||||
* ]
|
||||
*
|
||||
* @param ext 扩展属性 JSON 字符串
|
||||
* @return 按钮权限 VO 列表
|
||||
* @param ext 扩展属性 JSON 字符串
|
||||
* @param variable 流程变量
|
||||
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
|
||||
*/
|
||||
@Override
|
||||
public List<ButtonPermissionVo> buildButtonPermissionsFromExt(String ext) {
|
||||
// 解析 ext 为 Map<code, Set<value>>,用于标记权限
|
||||
Map<String, Set<String>> permissionMap = JsonUtils.parseArray(ext, ButtonPermissionVo.class)
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
ButtonPermissionVo::getCode,
|
||||
item -> StringUtils.splitList(item.getValue()).stream()
|
||||
.map(String::trim)
|
||||
.filter(StrUtil::isNotBlank)
|
||||
.collect(Collectors.toSet()),
|
||||
(a, b) -> b,
|
||||
HashMap::new
|
||||
));
|
||||
public NodeExtVo parseNodeExt(String ext, Map<String, Object> variable) {
|
||||
NodeExtVo nodeExtVo = new NodeExtVo();
|
||||
|
||||
// 构建按钮权限列表,标记哪些按钮在 permissionMap 中出现(表示已勾选)
|
||||
return buildPermissionsFromSources(permissionMap, List.of(ButtonPermissionEnum.class));
|
||||
// 解析 JSON 为 Dict 列表
|
||||
List<Dict> nodeExtMap = JsonUtils.parseArrayMap(ext);
|
||||
if (ObjectUtil.isEmpty(nodeExtMap)) {
|
||||
return nodeExtVo;
|
||||
}
|
||||
|
||||
for (Dict nodeExt : nodeExtMap) {
|
||||
String code = nodeExt.getStr("code");
|
||||
String value = nodeExt.getStr("value");
|
||||
|
||||
if (ButtonPermissionEnum.class.getSimpleName().equals(code)) {
|
||||
// 解析按钮权限
|
||||
// 将 value 拆分为 Set<String>,便于精确匹配
|
||||
Set<String> buttonSet = StringUtils.str2Set(value, StringUtils.SEPARATOR);
|
||||
|
||||
// 获取按钮字典配置
|
||||
NodeExt.ChildNode childNode = buildChildNode(ButtonPermissionEnum.class);
|
||||
|
||||
// 构建 ButtonPermissionVo 列表
|
||||
List<ButtonPermissionVo> buttonList = Optional.ofNullable(childNode)
|
||||
.map(NodeExt.ChildNode::getDict)
|
||||
.orElse(List.of())
|
||||
.stream()
|
||||
.map(dict -> new ButtonPermissionVo(dict.getValue(), buttonSet.contains(dict.getValue())))
|
||||
.toList();
|
||||
|
||||
nodeExtVo.setButtonPermissions(buttonList);
|
||||
|
||||
} else if (CopySettingEnum.class.getSimpleName().equals(code)) {
|
||||
List<String> permissions = spelSmartSplit(value).stream()
|
||||
.map(s -> {
|
||||
List<String> result = ExpressionUtil.evalVariable(s, variable);
|
||||
if (CollUtil.isNotEmpty(result)) {
|
||||
return result;
|
||||
}
|
||||
return Collections.singletonList(s);
|
||||
}).filter(Objects::nonNull)
|
||||
.flatMap(List::stream)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
List<String> copySettings = FlowEngine.permissionHandler().convertPermissions(permissions);
|
||||
// 解析抄送对象 ID 集合
|
||||
nodeExtVo.setCopySettings(new HashSet<>(copySettings));
|
||||
|
||||
} else if (VariablesEnum.class.getSimpleName().equals(code)) {
|
||||
// 解析自定义参数
|
||||
// 将 key=value 字符串拆分为 Map
|
||||
Map<String, String> variables = Arrays.stream(StringUtils.split(value, StringUtils.SEPARATOR))
|
||||
.map(s -> StringUtils.split(s, "="))
|
||||
.filter(arr -> arr.length == 2)
|
||||
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
|
||||
|
||||
nodeExtVo.setVariables(variables);
|
||||
} else {
|
||||
// 未知扩展类型,记录日志
|
||||
log.warn("未知扩展类型:code={}, value={}", code, value);
|
||||
}
|
||||
}
|
||||
return nodeExtVo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将权限映射与按钮权限来源(枚举类或字典类型)进行匹配,生成权限视图列表
|
||||
* <p>
|
||||
* 使用说明:
|
||||
* - sources 支持传入多个来源类型,支持 NodeExtEnum 枚举类 或 字典类型字符串(dictType)
|
||||
* - 若需要扩展更多按钮权限,只需在 sources 中新增对应的枚举类或字典类型
|
||||
* <p>
|
||||
* 示例:
|
||||
* buildPermissionsFromSources(permissionMap, List.of(ButtonPermissionEnum.class, "custom_button_dict"));
|
||||
*
|
||||
* @param permissionMap 权限映射
|
||||
* @param sources 枚举类或字典类型列表
|
||||
* @return 按钮权限视图对象列表
|
||||
* 按逗号分割字符串,但保留 #{...} 表达式和字符串常量中的逗号
|
||||
*/
|
||||
@SuppressWarnings("unchecked cast")
|
||||
private List<ButtonPermissionVo> buildPermissionsFromSources(Map<String, Set<String>> permissionMap, List<Object> sources) {
|
||||
return sources.stream()
|
||||
.flatMap(source -> {
|
||||
if (source instanceof Class<?> clazz && NodeExtEnum.class.isAssignableFrom(clazz)) {
|
||||
Set<String> selectedSet = permissionMap.getOrDefault(clazz.getSimpleName(), Collections.emptySet());
|
||||
return extractDictItems(this.buildChildNode((Class<? extends NodeExtEnum>) clazz), selectedSet).stream();
|
||||
} else if (source instanceof String dictType) {
|
||||
Set<String> selectedSet = permissionMap.getOrDefault(dictType, Collections.emptySet());
|
||||
return extractDictItems(this.buildChildNode(dictType), selectedSet).stream();
|
||||
private static List<String> spelSmartSplit(String str) {
|
||||
List<String> result = new ArrayList<>();
|
||||
if (str == null || str.trim().isEmpty()) {
|
||||
return result;
|
||||
}
|
||||
|
||||
StringBuilder token = new StringBuilder();
|
||||
// #{...} 的嵌套深度
|
||||
int depth = 0;
|
||||
// 是否在字符串常量中(" 或 ')
|
||||
boolean inString = false;
|
||||
// 当前字符串引号类型
|
||||
char stringQuote = 0;
|
||||
|
||||
for (int i = 0; i < str.length(); i++) {
|
||||
char c = str.charAt(i);
|
||||
|
||||
// 检测进入 SpEL 表达式 #{...}
|
||||
if (!inString && c == '#' && depth == 0 && checkNext(str, i, '{')) {
|
||||
depth++;
|
||||
token.append("#{");
|
||||
// 跳过 {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 在表达式中遇到 { 或 } 改变嵌套深度
|
||||
if (!inString && depth > 0) {
|
||||
if (c == '{') {
|
||||
depth++;
|
||||
} else if (c == '}') {
|
||||
depth--;
|
||||
}
|
||||
return Stream.empty();
|
||||
}).toList();
|
||||
token.append(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检测字符串开始/结束
|
||||
if (depth > 0 && (c == '"' || c == '\'')) {
|
||||
if (!inString) {
|
||||
inString = true;
|
||||
stringQuote = c;
|
||||
} else if (stringQuote == c) {
|
||||
inString = false;
|
||||
}
|
||||
token.append(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 外层逗号才分割
|
||||
if (c == ',' && depth == 0 && !inString) {
|
||||
String part = token.toString().trim();
|
||||
if (!part.isEmpty()) {
|
||||
result.add(part);
|
||||
}
|
||||
token.setLength(0);
|
||||
continue;
|
||||
}
|
||||
|
||||
token.append(c);
|
||||
}
|
||||
|
||||
// 添加最后一个
|
||||
String part = token.toString().trim();
|
||||
if (!part.isEmpty()) {
|
||||
result.add(part);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从节点子项中提取字典项,并构建按钮权限视图对象列表
|
||||
*
|
||||
* @param childNode 子节点
|
||||
* @param selectedSet 已选中的值集
|
||||
* @return 按钮权限视图对象列表
|
||||
*/
|
||||
private List<ButtonPermissionVo> extractDictItems(NodeExt.ChildNode childNode, Set<String> selectedSet) {
|
||||
return Optional.ofNullable(childNode)
|
||||
.map(NodeExt.ChildNode::getDict)
|
||||
.orElse(List.of())
|
||||
.stream()
|
||||
.map(dict -> new ButtonPermissionVo(dict.getValue(), selectedSet.contains(dict.getValue())))
|
||||
.toList();
|
||||
private static boolean checkNext(String str, int index, char expected) {
|
||||
return index + 1 < str.length() && str.charAt(index + 1) == expected;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -36,15 +36,22 @@ import org.dromara.warm.flow.core.service.*;
|
||||
import org.dromara.warm.flow.core.utils.ExpressionUtil;
|
||||
import org.dromara.warm.flow.core.utils.MapUtil;
|
||||
import org.dromara.warm.flow.orm.entity.*;
|
||||
import org.dromara.warm.flow.orm.mapper.*;
|
||||
import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
|
||||
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
|
||||
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
|
||||
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.constant.FlowConstant;
|
||||
import org.dromara.workflow.common.enums.TaskAssigneeType;
|
||||
import org.dromara.workflow.common.enums.TaskStatusEnum;
|
||||
import org.dromara.workflow.domain.FlowInstanceBizExt;
|
||||
import org.dromara.workflow.domain.bo.*;
|
||||
import org.dromara.workflow.domain.vo.FlowCopyVo;
|
||||
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
|
||||
import org.dromara.workflow.domain.vo.FlowTaskVo;
|
||||
import org.dromara.workflow.domain.vo.NodeExtVo;
|
||||
import org.dromara.workflow.mapper.FlwCategoryMapper;
|
||||
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
|
||||
import org.dromara.workflow.mapper.FlwTaskMapper;
|
||||
import org.dromara.workflow.service.IFlwCommonService;
|
||||
import org.dromara.workflow.service.IFlwNodeExtService;
|
||||
@@ -85,7 +92,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
private final IFlwTaskAssigneeService flwTaskAssigneeService;
|
||||
private final IFlwCommonService flwCommonService;
|
||||
private final IFlwNodeExtService flwNodeExtService;
|
||||
private final FlowDefinitionMapper flowDefinitionMapper;
|
||||
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
|
||||
|
||||
/**
|
||||
* 启动任务
|
||||
@@ -99,6 +106,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
if (StringUtils.isBlank(businessId)) {
|
||||
throw new ServiceException("启动工作流时必须包含业务ID");
|
||||
}
|
||||
|
||||
// 启动流程实例(提交申请)
|
||||
Map<String, Object> variables = startProcessBo.getVariables();
|
||||
// 流程发起人
|
||||
@@ -107,9 +115,14 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
variables.put(INITIATOR_DEPT_ID, LoginHelper.getDeptId());
|
||||
// 业务id
|
||||
variables.put(BUSINESS_ID, businessId);
|
||||
FlowInstanceBizExt bizExt = startProcessBo.getBizExt();
|
||||
|
||||
// 获取已有流程实例
|
||||
FlowInstance flowInstance = flowInstanceMapper.selectOne(new LambdaQueryWrapper<>(FlowInstance.class)
|
||||
.eq(FlowInstance::getBusinessId, businessId));
|
||||
|
||||
if (ObjectUtil.isNotNull(flowInstance)) {
|
||||
// 已存在流程
|
||||
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
|
||||
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId()));
|
||||
taskService.mergeVariable(flowInstance, variables);
|
||||
@@ -117,15 +130,19 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
StartProcessReturnDTO dto = new StartProcessReturnDTO();
|
||||
dto.setProcessInstanceId(taskList.get(0).getInstanceId());
|
||||
dto.setTaskId(taskList.get(0).getId());
|
||||
// 保存流程实例业务信息
|
||||
this.buildFlowInstanceBizExt(flowInstance, bizExt);
|
||||
return dto;
|
||||
}
|
||||
|
||||
// 将流程定义内的扩展参数设置到变量中
|
||||
Definition definition = FlowEngine.defService().getPublishByFlowCode(startProcessBo.getFlowCode());
|
||||
Dict dict = JsonUtils.parseMap(definition.getExt());
|
||||
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(FlowConstant.AUTO_PASS);
|
||||
variables.put(FlowConstant.AUTO_PASS, autoPass);
|
||||
|
||||
variables.put(FlowConstant.BUSINESS_CODE, this.generateBusinessCode(bizExt));
|
||||
FlowParams flowParams = FlowParams.build()
|
||||
.handler(startProcessBo.getHandler())
|
||||
.flowCode(startProcessBo.getFlowCode())
|
||||
.variable(startProcessBo.getVariables())
|
||||
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
|
||||
@@ -135,6 +152,8 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
} catch (Exception e) {
|
||||
throw new ServiceException(e.getMessage());
|
||||
}
|
||||
// 保存流程实例业务信息
|
||||
this.buildFlowInstanceBizExt(instance, bizExt);
|
||||
// 申请人执行流程
|
||||
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
|
||||
if (taskList.size() > 1) {
|
||||
@@ -146,6 +165,31 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
return dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成业务编号,如果已有则直接返回已有值
|
||||
*/
|
||||
private String generateBusinessCode(FlowInstanceBizExt bizExt) {
|
||||
if (StringUtils.isBlank(bizExt.getBusinessCode())) {
|
||||
// TODO: 按照自己业务规则生成编号
|
||||
String businessCode = Convert.toStr(System.currentTimeMillis());
|
||||
bizExt.setBusinessCode(businessCode);
|
||||
return businessCode;
|
||||
}
|
||||
return bizExt.getBusinessCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建流程实例业务信息
|
||||
*
|
||||
* @param instance 流程实例
|
||||
* @param bizExt 流程业务扩展信息
|
||||
*/
|
||||
private void buildFlowInstanceBizExt(Instance instance, FlowInstanceBizExt bizExt) {
|
||||
bizExt.setInstanceId(instance.getId());
|
||||
bizExt.setBusinessId(instance.getBusinessId());
|
||||
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
|
||||
}
|
||||
|
||||
/**
|
||||
* 办理任务
|
||||
*
|
||||
@@ -186,6 +230,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
}
|
||||
// 构建流程参数,包括变量、跳转类型、消息、处理人、权限等信息
|
||||
FlowParams flowParams = FlowParams.build()
|
||||
.handler(completeTaskBo.getHandler())
|
||||
.variable(variables)
|
||||
.skipType(SkipType.PASS.getKey())
|
||||
.message(completeTaskBo.getMessage())
|
||||
@@ -204,10 +249,10 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
/**
|
||||
* 流程办理
|
||||
*
|
||||
* @param taskId 任务ID
|
||||
* @param flowParams 参数
|
||||
* @param instanceId 实例ID
|
||||
* @param autoPass 自动审批
|
||||
* @param taskId 任务ID
|
||||
* @param flowParams 参数
|
||||
* @param instanceId 实例ID
|
||||
* @param autoPass 自动审批
|
||||
*/
|
||||
private void skipTask(Long taskId, FlowParams flowParams, Long instanceId, Boolean autoPass) {
|
||||
// 执行任务跳转,并根据返回的处理人设置下一步处理人
|
||||
@@ -337,7 +382,6 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
|
||||
queryWrapper.eq("t.node_type", NodeType.BETWEEN.getKey());
|
||||
queryWrapper.in("t.approver", LoginHelper.getUserIdStr());
|
||||
queryWrapper.orderByDesc("t.create_time").orderByDesc("t.update_time");
|
||||
Page<FlowHisTaskVo> page = flwTaskMapper.getListFinishTask(pageQuery.build(), queryWrapper);
|
||||
return TableDataInfo.build(page);
|
||||
}
|
||||
@@ -412,7 +456,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowTaskBo.getCategory()));
|
||||
wrapper.in("t.category", StreamUtils.toList(categoryIds, Convert::toStr));
|
||||
}
|
||||
wrapper.orderByDesc("t.create_time");
|
||||
wrapper.orderByDesc("t.create_time").orderByDesc("t.update_time");
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
@@ -440,9 +484,9 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
|
||||
Map<String, Object> variable = new HashMap<>();
|
||||
// 消息类型
|
||||
variable.put("messageType", messageType);
|
||||
variable.put(FlowConstant.MESSAGE_TYPE, messageType);
|
||||
// 消息通知
|
||||
variable.put("notice", notice);
|
||||
variable.put(FlowConstant.MESSAGE_NOTICE, notice);
|
||||
|
||||
FlowParams flowParams = FlowParams.build()
|
||||
.nodeCode(bo.getNodeCode())
|
||||
@@ -463,8 +507,8 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
/**
|
||||
* 获取可驳回的前置节点
|
||||
*
|
||||
* @param taskId 任务id
|
||||
* @param nowNodeCode 当前节点
|
||||
* @param taskId 任务id
|
||||
* @param nowNodeCode 当前节点
|
||||
*/
|
||||
@Override
|
||||
public List<Node> getBackTaskNode(Long taskId, String nowNodeCode) {
|
||||
@@ -554,8 +598,24 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
if (ObjectUtil.isNull(flowNode)) {
|
||||
throw new NullPointerException("当前【" + flowTaskVo.getNodeCode() + "】节点编码不存在");
|
||||
}
|
||||
NodeExtVo nodeExtVo = flwNodeExtService.parseNodeExt(flowNode.getExt(), instance.getVariableMap());
|
||||
//设置按钮权限
|
||||
flowTaskVo.setButtonList(flwNodeExtService.buildButtonPermissionsFromExt(flowNode.getExt()));
|
||||
if (CollUtil.isNotEmpty(nodeExtVo.getButtonPermissions())) {
|
||||
flowTaskVo.setButtonList(nodeExtVo.getButtonPermissions());
|
||||
} else {
|
||||
flowTaskVo.setButtonList(new ArrayList<>());
|
||||
}
|
||||
if (CollUtil.isNotEmpty(nodeExtVo.getCopySettings())) {
|
||||
List<FlowCopyVo> list = StreamUtils.toList(nodeExtVo.getCopySettings(), x -> new FlowCopyVo(Convert.toLong(x)));
|
||||
flowTaskVo.setCopyList(list);
|
||||
} else {
|
||||
flowTaskVo.setCopyList(new ArrayList<>());
|
||||
}
|
||||
if (CollUtil.isNotEmpty(nodeExtVo.getVariables())) {
|
||||
flowTaskVo.setVarList(nodeExtVo.getVariables());
|
||||
} else {
|
||||
flowTaskVo.setVarList(new HashMap<>());
|
||||
}
|
||||
flowTaskVo.setNodeRatio(flowNode.getNodeRatio());
|
||||
flowTaskVo.setApplyNode(flowNode.getNodeCode().equals(flwCommonService.applyNodeCode(task.getDefinitionId())));
|
||||
return flowTaskVo;
|
||||
@@ -585,14 +645,14 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
//办理人变量替换
|
||||
ExpressionUtil.evalVariable(buildNextTaskList, FlowParams.build().variable(mergeVariable));
|
||||
for (FlowNode flowNode : nextFlowNodes) {
|
||||
Task first = StreamUtils.findFirst(buildNextTaskList, t -> t.getNodeCode().equals(flowNode.getNodeCode()));
|
||||
if (ObjectUtil.isNotNull(first) && CollUtil.isNotEmpty(first.getPermissionList())) {
|
||||
List<UserDTO> users = flwTaskAssigneeService.fetchUsersByStorageIds(StringUtils.joinComma(first.getPermissionList()));
|
||||
if (CollUtil.isNotEmpty(users)) {
|
||||
flowNode.setPermissionFlag(StreamUtils.join(users, e -> Convert.toStr(e.getUserId())));
|
||||
}
|
||||
}
|
||||
|
||||
StreamUtils.findFirst(buildNextTaskList, t -> t.getNodeCode().equals(flowNode.getNodeCode()))
|
||||
.ifPresent(first -> {
|
||||
List<UserDTO> users;
|
||||
if (CollUtil.isNotEmpty(first.getPermissionList())
|
||||
&& CollUtil.isNotEmpty(users = flwTaskAssigneeService.fetchUsersByStorageIds(StringUtils.joinComma(first.getPermissionList())))) {
|
||||
flowNode.setPermissionFlag(StreamUtils.join(users, e -> Convert.toStr(e.getUserId())));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return nextFlowNodes;
|
||||
@@ -678,7 +738,7 @@ public class FlwTaskServiceImpl implements IFlwTaskService {
|
||||
Long taskId = bo.getTaskId();
|
||||
Task task = taskService.getById(taskId);
|
||||
FlowNode flowNode = getByNodeCode(task.getNodeCode(), task.getDefinitionId());
|
||||
if ("addSignature".equals(taskOperation) || "reductionSignature".equals(taskOperation)) {
|
||||
if (ADD_SIGNATURE.equals(taskOperation) || REDUCTION_SIGNATURE.equals(taskOperation)) {
|
||||
if (flowNode.getNodeRatio().compareTo(BigDecimal.ZERO) == 0) {
|
||||
throw new ServiceException(task.getNodeName() + "不是会签节点!");
|
||||
}
|
||||
|
||||
@@ -4,17 +4,16 @@ import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.common.core.domain.dto.CompleteTaskDTO;
|
||||
import org.dromara.common.core.domain.dto.StartProcessDTO;
|
||||
import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
|
||||
import org.dromara.common.core.domain.event.ProcessTaskEvent;
|
||||
import org.dromara.common.core.domain.event.ProcessDeleteEvent;
|
||||
import org.dromara.common.core.domain.event.ProcessEvent;
|
||||
import org.dromara.common.core.domain.event.ProcessTaskEvent;
|
||||
import org.dromara.common.core.enums.BusinessStatusEnum;
|
||||
import org.dromara.common.core.exception.ServiceException;
|
||||
import org.dromara.common.core.service.WorkflowService;
|
||||
@@ -24,6 +23,7 @@ import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.constant.FlowConstant;
|
||||
import org.dromara.workflow.domain.TestLeave;
|
||||
import org.dromara.workflow.domain.bo.TestLeaveBo;
|
||||
import org.dromara.workflow.domain.vo.TestLeaveVo;
|
||||
@@ -108,6 +108,7 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
|
||||
long day = DateUtil.betweenDay(bo.getStartDate(), bo.getEndDate(), true);
|
||||
// 截止日期也算一天
|
||||
bo.setLeaveDays((int) day + 1);
|
||||
bo.setApplyCode(System.currentTimeMillis() + StrUtil.EMPTY);
|
||||
TestLeave add = MapstructUtils.convert(bo, TestLeave.class);
|
||||
if (StringUtils.isBlank(add.getStatus())) {
|
||||
add.setStatus(BusinessStatusEnum.DRAFT.getStatus());
|
||||
@@ -125,6 +126,9 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
|
||||
long day = DateUtil.betweenDay(bo.getStartDate(), bo.getEndDate(), true);
|
||||
// 截止日期也算一天
|
||||
bo.setLeaveDays((int) day + 1);
|
||||
if (ObjectUtil.isNull(bo.getId())) {
|
||||
bo.setApplyCode(System.currentTimeMillis() + StrUtil.EMPTY);
|
||||
}
|
||||
TestLeave leave = MapstructUtils.convert(bo, TestLeave.class);
|
||||
boolean flag = baseMapper.insertOrUpdate(leave);
|
||||
if (flag) {
|
||||
@@ -132,12 +136,14 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
|
||||
// 后端发起需要忽略权限
|
||||
bo.getParams().put("ignore", true);
|
||||
|
||||
StartProcessDTO startProcess = new StartProcessDTO();
|
||||
startProcess.setBusinessId(leave.getId().toString());
|
||||
startProcess.setFlowCode(StringUtils.isEmpty(bo.getFlowCode()) ? "leave1" : bo.getFlowCode());
|
||||
startProcess.setVariables(bo.getParams());
|
||||
// 后端发起 如果没有登录用户 比如定时任务 可以手动设置一个处理人id
|
||||
// startProcess.setHandler("0");
|
||||
|
||||
boolean flag1 = workflowService.startCompleteTask(new StartProcessDTO() {{
|
||||
setBusinessId(leave.getId().toString());
|
||||
setFlowCode(StringUtils.isEmpty(bo.getFlowCode()) ? "leave1" : bo.getFlowCode());
|
||||
setVariables(bo.getParams());
|
||||
}});
|
||||
boolean flag1 = workflowService.startCompleteTask(startProcess);
|
||||
if (!flag1) {
|
||||
throw new ServiceException("流程发起异常");
|
||||
}
|
||||
@@ -188,6 +194,10 @@ public class TestLeaveServiceImpl implements ITestLeaveService {
|
||||
String message = Convert.toStr(params.get("message"));
|
||||
}
|
||||
if (processEvent.getSubmit()) {
|
||||
if (StringUtils.isBlank(testLeave.getApplyCode())) {
|
||||
String businessCode = MapUtil.getStr(params, FlowConstant.BUSINESS_CODE, StrUtil.EMPTY);
|
||||
testLeave.setApplyCode(businessCode);
|
||||
}
|
||||
testLeave.setStatus(BusinessStatusEnum.WAITING.getStatus());
|
||||
}
|
||||
baseMapper.updateById(testLeave);
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.warm.flow.orm.entity.FlowInstance;
|
||||
import org.dromara.workflow.common.ConditionalOnEnable;
|
||||
import org.dromara.workflow.common.enums.MessageTypeEnum;
|
||||
import org.dromara.workflow.domain.FlowInstanceBizExt;
|
||||
import org.dromara.workflow.domain.bo.CompleteTaskBo;
|
||||
import org.dromara.workflow.domain.bo.StartProcessBo;
|
||||
import org.dromara.workflow.service.IFlwDefinitionService;
|
||||
@@ -161,16 +162,21 @@ public class WorkflowServiceImpl implements WorkflowService {
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public boolean startCompleteTask(StartProcessDTO startProcess) {
|
||||
try {
|
||||
StartProcessReturnDTO result = flwTaskService.startWorkFlow(new StartProcessBo() {{
|
||||
setBusinessId(startProcess.getBusinessId());
|
||||
setFlowCode(startProcess.getFlowCode());
|
||||
setVariables(startProcess.getVariables());
|
||||
}});
|
||||
boolean flag = flwTaskService.completeTask(new CompleteTaskBo() {{
|
||||
setTaskId(result.getTaskId());
|
||||
setMessageType(Collections.singletonList(MessageTypeEnum.SYSTEM_MESSAGE.getCode()));
|
||||
setVariables(startProcess.getVariables());
|
||||
}});
|
||||
StartProcessBo processBo = new StartProcessBo();
|
||||
processBo.setBusinessId(startProcess.getBusinessId());
|
||||
processBo.setFlowCode(startProcess.getFlowCode());
|
||||
processBo.setVariables(startProcess.getVariables());
|
||||
processBo.setHandler(startProcess.getHandler());
|
||||
processBo.setBizExt(BeanUtil.toBean(startProcess.getBizExt(), FlowInstanceBizExt.class));
|
||||
|
||||
StartProcessReturnDTO result = flwTaskService.startWorkFlow(processBo);
|
||||
CompleteTaskBo taskBo = new CompleteTaskBo();
|
||||
taskBo.setTaskId(result.getTaskId());
|
||||
taskBo.setMessageType(Collections.singletonList(MessageTypeEnum.SYSTEM_MESSAGE.getCode()));
|
||||
taskBo.setVariables(startProcess.getVariables());
|
||||
taskBo.setHandler(startProcess.getHandler());
|
||||
|
||||
boolean flag = flwTaskService.completeTask(taskBo);
|
||||
if (!flag) {
|
||||
throw new ServiceException("流程发起异常");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!DOCTYPE mapper
|
||||
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
|
||||
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="org.dromara.workflow.mapper.FlwInstanceBizExtMapper">
|
||||
|
||||
</mapper>
|
||||
@@ -27,9 +27,12 @@
|
||||
fd.version,
|
||||
fd.form_custom,
|
||||
fd.form_path,
|
||||
fd.category
|
||||
fd.category,
|
||||
biz.business_code,
|
||||
biz.business_title
|
||||
from flow_instance fi
|
||||
left join flow_definition fd on fi.definition_id = fd.id
|
||||
left join flow_instance_biz_ext biz on biz.instance_id = fi.id
|
||||
${ew.getCustomSqlSegment}
|
||||
</select>
|
||||
|
||||
|
||||
@@ -30,11 +30,14 @@
|
||||
COALESCE(t.form_path, d.form_path) as form_path,
|
||||
d.version,
|
||||
uu.processed_by,
|
||||
uu.type
|
||||
uu.type,
|
||||
biz.business_code,
|
||||
biz.business_title
|
||||
from flow_task t
|
||||
left join flow_user uu on uu.associated = t.id
|
||||
left join flow_definition d on t.definition_id = d.id
|
||||
left join flow_instance i on t.instance_id = i.id
|
||||
left join flow_instance_biz_ext biz on biz.instance_id = i.id
|
||||
where t.node_type = 1
|
||||
and t.del_flag = '0'
|
||||
and uu.del_flag = '0'
|
||||
@@ -71,10 +74,13 @@
|
||||
c.flow_name,
|
||||
c.flow_code,
|
||||
c.category,
|
||||
c.version
|
||||
c.version,
|
||||
biz.business_code,
|
||||
biz.business_title
|
||||
from flow_his_task a
|
||||
left join flow_instance b on a.instance_id = b.id
|
||||
left join flow_definition c on a.definition_id = c.id
|
||||
left join flow_instance_biz_ext biz on biz.instance_id = b.id
|
||||
where a.del_flag ='0'
|
||||
and b.del_flag = '0'
|
||||
and c.del_flag = '0'
|
||||
@@ -100,11 +106,14 @@
|
||||
d.flow_name,
|
||||
d.flow_code,
|
||||
d.category,
|
||||
d.version
|
||||
d.version,
|
||||
biz.business_code,
|
||||
biz.business_title
|
||||
from flow_user a
|
||||
left join flow_his_task b on a.associated = b.task_id
|
||||
left join flow_instance c on b.instance_id = c.id
|
||||
left join flow_definition d on c.definition_id=d.id
|
||||
left join flow_instance_biz_ext biz on biz.instance_id = c.id
|
||||
where a.type = '4'
|
||||
and a.del_flag = '0'
|
||||
and b.del_flag = '0'
|
||||
|
||||
@@ -99,7 +99,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-server1:
|
||||
image: ruoyi/ruoyi-server:5.4.1
|
||||
image: ruoyi/ruoyi-server:5.5.1
|
||||
container_name: ruoyi-server1
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -115,7 +115,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-server2:
|
||||
image: ruoyi/ruoyi-server:5.4.1
|
||||
image: ruoyi/ruoyi-server:5.5.1
|
||||
container_name: ruoyi-server2
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -131,7 +131,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-monitor-admin:
|
||||
image: ruoyi/ruoyi-monitor-admin:5.4.1
|
||||
image: ruoyi/ruoyi-monitor-admin:5.5.1
|
||||
container_name: ruoyi-monitor-admin
|
||||
environment:
|
||||
# 时区上海
|
||||
@@ -143,7 +143,7 @@ services:
|
||||
network_mode: "host"
|
||||
|
||||
ruoyi-snailjob-server:
|
||||
image: ruoyi/ruoyi-snailjob-server:5.4.1
|
||||
image: ruoyi/ruoyi-snailjob-server:5.5.1
|
||||
container_name: ruoyi-snailjob-server
|
||||
environment:
|
||||
# 时区上海
|
||||
|
||||
@@ -4,18 +4,30 @@ error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
events {
|
||||
# 可以根据业务并发量适当调高
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
# 高效传输文件
|
||||
sendfile on;
|
||||
# 长连接超时时间
|
||||
keepalive_timeout 65;
|
||||
# 单连接最大请求数,提高长连接复用率
|
||||
keepalive_requests 100000;
|
||||
# 限制body大小
|
||||
client_max_body_size 100m;
|
||||
client_header_buffer_size 32k;
|
||||
client_body_buffer_size 512k;
|
||||
# 开启静态资源压缩
|
||||
gzip_static on;
|
||||
# 连接数限制 (防御类配置) 10m 一般够用了,能存储上万 IP 的计数
|
||||
limit_conn_zone $binary_remote_addr zone=perip:10m;
|
||||
limit_conn_zone $server_name zone=perserver:10m;
|
||||
# 隐藏 nginx 版本号,防止暴露版本信息
|
||||
server_tokens off;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
@@ -50,7 +62,7 @@ http {
|
||||
#ssl_certificate_key /etc/nginx/cert/xxx.local.key; # /etc/nginx/cert/ 为docker映射路径 不允许更改
|
||||
#ssl_session_timeout 5m;
|
||||
#ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
|
||||
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
#ssl_protocols TLSv1.3 TLSv1.2 TLSv1.1 TLSv1;
|
||||
#ssl_prefer_server_ciphers on;
|
||||
# https配置参考 end
|
||||
|
||||
@@ -76,17 +88,29 @@ http {
|
||||
}
|
||||
|
||||
location /prod-api/ {
|
||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
||||
proxy_set_header Host $http_host;
|
||||
# 获取客户端真实 IP
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# 设置后端响应超时时间(这里是 24 小时,适合长连接/SSE)
|
||||
proxy_read_timeout 86400s;
|
||||
# sse 与 websocket参数
|
||||
# SSE (Server-Sent Events) 与 WebSocket 支持参数
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
# 禁用代理缓冲,数据直接传给客户端
|
||||
proxy_buffering off;
|
||||
# 禁用代理缓存
|
||||
proxy_cache off;
|
||||
# 按 IP 限制连接数(防 CC 攻击) 小型站:10~20 就够 中型站:50~100
|
||||
limit_conn perip 20;
|
||||
|
||||
# 按 Server 限制总并发连接数 根据服务器的最大并发处理能力来定 太小会限制合法用户访问,太大会占满服务器资源
|
||||
limit_conn perserver 500;
|
||||
proxy_pass http://server/;
|
||||
}
|
||||
|
||||
@@ -94,23 +118,37 @@ http {
|
||||
# 解决方案1 将 admin 服务 也配置成 https
|
||||
# 解决方案2 将菜单配置为外链访问 走独立页面 http 访问
|
||||
location /admin/ {
|
||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
||||
proxy_set_header Host $http_host;
|
||||
# 获取客户端真实 IP
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# 禁用代理缓冲,数据直接传给客户端
|
||||
proxy_buffering off;
|
||||
# 禁用代理缓存
|
||||
proxy_cache off;
|
||||
proxy_pass http://monitor-admin/admin/;
|
||||
}
|
||||
|
||||
location /snail-job/ {
|
||||
# 设置客户端请求头中的 Host 信息(保持原始 Host)
|
||||
proxy_set_header Host $http_host;
|
||||
# 获取客户端真实 IP
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# 自定义头 REMOTE-HOST,记录客户端 IP
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
# 获取完整的客户端 IP 链(经过多级代理时)
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
# sse 与 websocket参数
|
||||
# SSE (Server-Sent Events) 与 WebSocket 支持参数
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
# 禁用代理缓冲,直接传输给客户端
|
||||
proxy_buffering off;
|
||||
# 禁用代理缓存
|
||||
proxy_cache off;
|
||||
proxy_pass http://snailjob-server/snail-job/;
|
||||
}
|
||||
|
||||
@@ -1,75 +1,129 @@
|
||||
{
|
||||
"flowCode" : "leave1",
|
||||
"flowName" : "请假申请-普通",
|
||||
"category" : "100",
|
||||
"version" : "1",
|
||||
"formCustom" : "N",
|
||||
"formPath" : "/workflow/leaveEdit/index",
|
||||
"nodeList" : [ {
|
||||
"nodeType" : 0,
|
||||
"nodeCode" : "d5ee3ddf-3968-4379-a86f-9ceabde5faac",
|
||||
"nodeName" : "开始",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "200,200|200,200",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "d5ee3ddf-3968-4379-a86f-9ceabde5faac",
|
||||
"nextNodeCode" : "dd515cdd-59f6-446f-94ca-25ca062afb42",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "220,200;310,200"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "dd515cdd-59f6-446f-94ca-25ca062afb42",
|
||||
"nodeName" : "申请人",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "360,200|360,200",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "dd515cdd-59f6-446f-94ca-25ca062afb42",
|
||||
"nextNodeCode" : "78fa8e5b-e809-44ed-978a-41092409ebcf",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "410,200;490,200"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "78fa8e5b-e809-44ed-978a-41092409ebcf",
|
||||
"nodeName" : "组长",
|
||||
"permissionFlag" : "role:1",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "540,200|540,200",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "78fa8e5b-e809-44ed-978a-41092409ebcf",
|
||||
"nextNodeCode" : "a8abf15f-b83e-428a-86cc-033555ea9bbe",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "590,200;670,200"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "a8abf15f-b83e-428a-86cc-033555ea9bbe",
|
||||
"nodeName" : "部门主管",
|
||||
"permissionFlag" : "role:3@@role:4",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "720,200|720,200",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "a8abf15f-b83e-428a-86cc-033555ea9bbe",
|
||||
"nextNodeCode" : "8b82b7d7-8660-455e-b880-d6d22ea3eb6d",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "770,200;880,200"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 2,
|
||||
"nodeCode" : "8b82b7d7-8660-455e-b880-d6d22ea3eb6d",
|
||||
"nodeName" : "结束",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "900,200|900,200",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]"
|
||||
} ]
|
||||
"nodeList": [
|
||||
{
|
||||
"nodeType": "0",
|
||||
"nodeCode": "d5ee3ddf-3968-4379-a86f-9ceabde5faac",
|
||||
"nodeName": "开始",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "200,200|200,200",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "d5ee3ddf-3968-4379-a86f-9ceabde5faac",
|
||||
"nextNodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42",
|
||||
"coordinate": "220,200;310,200"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42",
|
||||
"nodeName": "申请人",
|
||||
"permissionFlag": "",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,copy\"}]",
|
||||
"coordinate": "360,200|360,200",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "dd515cdd-59f6-446f-94ca-25ca062afb42",
|
||||
"nextNodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf",
|
||||
"coordinate": "410,200;490,200"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf",
|
||||
"nodeName": "组长",
|
||||
"permissionFlag": "role:1",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,copy,transfer,trust,file\"}]",
|
||||
"coordinate": "540,200|540,200",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "78fa8e5b-e809-44ed-978a-41092409ebcf",
|
||||
"nextNodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe",
|
||||
"coordinate": "590,200;670,200"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe",
|
||||
"nodeName": "部门主管",
|
||||
"permissionFlag": "role:3@@role:4",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,copy,transfer,trust,file\"}]",
|
||||
"coordinate": "720,200|720,200",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "a8abf15f-b83e-428a-86cc-033555ea9bbe",
|
||||
"nextNodeCode": "8b82b7d7-8660-455e-b880-d6d22ea3eb6d",
|
||||
"coordinate": "770,200;880,200"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "2",
|
||||
"nodeCode": "8b82b7d7-8660-455e-b880-d6d22ea3eb6d",
|
||||
"nodeName": "结束",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "900,200|900,200",
|
||||
"version": "1",
|
||||
"skipList": []
|
||||
}
|
||||
],
|
||||
"flowCode": "leave1",
|
||||
"flowName": "请假申请-普通",
|
||||
"modelValue": "CLASSICS",
|
||||
"category": "103",
|
||||
"version": "1",
|
||||
"formCustom": "N",
|
||||
"formPath": "/workflow/leaveEdit/index",
|
||||
"listenerType": null,
|
||||
"listenerPath": null
|
||||
}
|
||||
|
||||
@@ -1,111 +1,187 @@
|
||||
{
|
||||
"flowCode" : "leave2",
|
||||
"flowName" : "请假申请-排他网关",
|
||||
"category" : "100",
|
||||
"version" : "1",
|
||||
"formCustom" : "N",
|
||||
"formPath" : "/workflow/leaveEdit/index",
|
||||
"nodeList" : [ {
|
||||
"nodeType" : 0,
|
||||
"nodeCode" : "cef3895c-f7d8-4598-8bf3-8ec2ef6ce84a",
|
||||
"nodeName" : "开始",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "300,240|300,240",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "cef3895c-f7d8-4598-8bf3-8ec2ef6ce84a",
|
||||
"nextNodeCode" : "fdcae93b-b69c-498a-b231-09255e74bcbd",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "320,240;390,240"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "fdcae93b-b69c-498a-b231-09255e74bcbd",
|
||||
"nodeName" : "申请人",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "440,240|440,240",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "fdcae93b-b69c-498a-b231-09255e74bcbd",
|
||||
"nextNodeCode" : "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "490,240;535,240"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 3,
|
||||
"nodeCode" : "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "560,240",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"nextNodeCode" : "b3528155-dcb7-4445-bbdf-3d00e3499e86",
|
||||
"skipType" : "PASS",
|
||||
"skipCondition" : "le@@leaveDays|2",
|
||||
"coordinate" : "560,265;560,320;670,320"
|
||||
}, {
|
||||
"nowNodeCode" : "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"nextNodeCode" : "5ed2362b-fc0c-4d52-831f-95208b830605",
|
||||
"skipName" : "大于两天",
|
||||
"skipType" : "PASS",
|
||||
"skipCondition" : "gt@@leaveDays|2",
|
||||
"coordinate" : "560,215;560,160;670,160|560,187"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "b3528155-dcb7-4445-bbdf-3d00e3499e86",
|
||||
"nodeName" : "组长",
|
||||
"permissionFlag" : "3@@4",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "720,320|720,320",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "b3528155-dcb7-4445-bbdf-3d00e3499e86",
|
||||
"nextNodeCode" : "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "770,320;860,320;860,280"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"nodeName" : "总经理",
|
||||
"permissionFlag" : "role:1",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "860,240|860,240",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"nextNodeCode" : "40aa65fd-0712-4d23-b6f7-d0432b920fd1",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "910,240;980,240"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 2,
|
||||
"nodeCode" : "40aa65fd-0712-4d23-b6f7-d0432b920fd1",
|
||||
"nodeName" : "结束",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "1000,240|1000,240",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]"
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "5ed2362b-fc0c-4d52-831f-95208b830605",
|
||||
"nodeName" : "部门领导",
|
||||
"permissionFlag" : "role:1",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "720,160|720,160",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination\"}]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "5ed2362b-fc0c-4d52-831f-95208b830605",
|
||||
"nextNodeCode" : "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "770,160;860,160;860,200"
|
||||
} ]
|
||||
} ]
|
||||
"nodeList": [
|
||||
{
|
||||
"nodeType": "0",
|
||||
"nodeCode": "cef3895c-f7d8-4598-8bf3-8ec2ef6ce84a",
|
||||
"nodeName": "开始",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "300,240|300,240",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "cef3895c-f7d8-4598-8bf3-8ec2ef6ce84a",
|
||||
"nextNodeCode": "fdcae93b-b69c-498a-b231-09255e74bcbd",
|
||||
"coordinate": "320,240;390,240"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "fdcae93b-b69c-498a-b231-09255e74bcbd",
|
||||
"nodeName": "申请人",
|
||||
"permissionFlag": "",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]",
|
||||
"coordinate": "440,240|440,240",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "fdcae93b-b69c-498a-b231-09255e74bcbd",
|
||||
"nextNodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"coordinate": "490,240;535,240"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "3",
|
||||
"nodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "560,240",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": "le@@leaveDays|2",
|
||||
"skipName": null,
|
||||
"nowNodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"nextNodeCode": "b3528155-dcb7-4445-bbdf-3d00e3499e86",
|
||||
"coordinate": "560,265;560,320;670,320"
|
||||
},
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": "gt@@leaveDays|2",
|
||||
"skipName": "大于两天",
|
||||
"nowNodeCode": "7b8c7ead-7dc8-4951-a7f3-f0c41995909e",
|
||||
"nextNodeCode": "5ed2362b-fc0c-4d52-831f-95208b830605",
|
||||
"coordinate": "560,215;560,160;670,160|560,187"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "b3528155-dcb7-4445-bbdf-3d00e3499e86",
|
||||
"nodeName": "组长",
|
||||
"permissionFlag": "3@@4",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]",
|
||||
"coordinate": "720,320|720,320",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "b3528155-dcb7-4445-bbdf-3d00e3499e86",
|
||||
"nextNodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"coordinate": "770,320;860,320;860,280"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"nodeName": "总经理",
|
||||
"permissionFlag": "role:1",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]",
|
||||
"coordinate": "860,240|860,240",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"nextNodeCode": "40aa65fd-0712-4d23-b6f7-d0432b920fd1",
|
||||
"coordinate": "910,240;980,240"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "2",
|
||||
"nodeCode": "40aa65fd-0712-4d23-b6f7-d0432b920fd1",
|
||||
"nodeName": "结束",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "1000,240|1000,240",
|
||||
"version": "1",
|
||||
"skipList": []
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "5ed2362b-fc0c-4d52-831f-95208b830605",
|
||||
"nodeName": "部门领导",
|
||||
"permissionFlag": "role:1",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]",
|
||||
"coordinate": "720,160|720,160",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "5ed2362b-fc0c-4d52-831f-95208b830605",
|
||||
"nextNodeCode": "c9fa6d7d-2a74-4e78-b947-0cad8a6af869",
|
||||
"nextNodeType": "1",
|
||||
"coordinate": "770,160;860,160;860,200"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"flowCode": "leave2",
|
||||
"flowName": "请假申请-排他网关",
|
||||
"modelValue": "CLASSICS",
|
||||
"category": "103",
|
||||
"version": "1",
|
||||
"formCustom": "N",
|
||||
"formPath": "/workflow/leaveEdit/index",
|
||||
"listenerType": null,
|
||||
"listenerPath": null
|
||||
}
|
||||
|
||||
@@ -1,121 +1,211 @@
|
||||
{
|
||||
"flowCode" : "leave3",
|
||||
"flowName" : "请假申请-并行网关",
|
||||
"category" : "100",
|
||||
"version" : "1",
|
||||
"formCustom" : "N",
|
||||
"formPath" : "/workflow/leaveEdit/index",
|
||||
"nodeList" : [ {
|
||||
"nodeType" : 0,
|
||||
"nodeCode" : "a80ecf9f-f465-4ae5-a429-e30ec5d0f957",
|
||||
"nodeName" : "开始",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "380,220|380,220",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "a80ecf9f-f465-4ae5-a429-e30ec5d0f957",
|
||||
"nextNodeCode" : "b7bbb571-06de-455c-8083-f83c07bf0b99",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "400,220;470,220"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "b7bbb571-06de-455c-8083-f83c07bf0b99",
|
||||
"nodeName" : "申请人",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "520,220|520,220",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "b7bbb571-06de-455c-8083-f83c07bf0b99",
|
||||
"nextNodeCode" : "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "570,220;655,220"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 4,
|
||||
"nodeCode" : "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "680,220",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"nextNodeCode" : "4b7743cd-940c-431b-926f-e7b614fbf1fe",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "680,195;680,140;750,140"
|
||||
}, {
|
||||
"nowNodeCode" : "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"nextNodeCode" : "762cb975-37d8-4276-b6db-79a4c3606394",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "680,245;680,300;750,300"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "4b7743cd-940c-431b-926f-e7b614fbf1fe",
|
||||
"nodeName" : "市场部",
|
||||
"permissionFlag" : "role:1",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "800,140|800,140",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "4b7743cd-940c-431b-926f-e7b614fbf1fe",
|
||||
"nextNodeCode" : "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "850,140;920,140;920,195"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 4,
|
||||
"nodeCode" : "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "920,220",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"nextNodeCode" : "23e7429e-2b47-4431-b93e-40db7c431ce6",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "945,220;975,220;975,220;960,220;960,220;990,220"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "23e7429e-2b47-4431-b93e-40db7c431ce6",
|
||||
"nodeName" : "CEO",
|
||||
"permissionFlag" : "1",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "1040,220|1040,220",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "23e7429e-2b47-4431-b93e-40db7c431ce6",
|
||||
"nextNodeCode" : "f5ace37f-5a5e-4e64-a6f6-913ab9a71cd1",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "1090,220;1140,220"
|
||||
} ]
|
||||
}, {
|
||||
"nodeType" : 2,
|
||||
"nodeCode" : "f5ace37f-5a5e-4e64-a6f6-913ab9a71cd1",
|
||||
"nodeName" : "结束",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "1160,220|1160,220",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]"
|
||||
}, {
|
||||
"nodeType" : 1,
|
||||
"nodeCode" : "762cb975-37d8-4276-b6db-79a4c3606394",
|
||||
"nodeName" : "综合部",
|
||||
"permissionFlag" : "role:3@@role:4",
|
||||
"nodeRatio" : 0.000,
|
||||
"coordinate" : "800,300|800,300",
|
||||
"formCustom" : "N",
|
||||
"ext" : "[]",
|
||||
"skipList" : [ {
|
||||
"nowNodeCode" : "762cb975-37d8-4276-b6db-79a4c3606394",
|
||||
"nextNodeCode" : "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"skipType" : "PASS",
|
||||
"coordinate" : "850,300;920,300;920,245"
|
||||
} ]
|
||||
} ]
|
||||
"nodeList": [
|
||||
{
|
||||
"nodeType": "0",
|
||||
"nodeCode": "a80ecf9f-f465-4ae5-a429-e30ec5d0f957",
|
||||
"nodeName": "开始",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "380,220|380,220",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "a80ecf9f-f465-4ae5-a429-e30ec5d0f957",
|
||||
"nextNodeCode": "b7bbb571-06de-455c-8083-f83c07bf0b99",
|
||||
"coordinate": "400,220;470,220"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "b7bbb571-06de-455c-8083-f83c07bf0b99",
|
||||
"nodeName": "申请人",
|
||||
"permissionFlag": "",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file\"}]",
|
||||
"coordinate": "520,220|520,220",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "b7bbb571-06de-455c-8083-f83c07bf0b99",
|
||||
"nextNodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"coordinate": "570,220;655,220"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "4",
|
||||
"nodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "680,220",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"nextNodeCode": "4b7743cd-940c-431b-926f-e7b614fbf1fe",
|
||||
"coordinate": "680,195;680,140;750,140"
|
||||
},
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "84d7ed24-bb44-4ba1-bf1f-e6f5092d3f0a",
|
||||
"nextNodeCode": "762cb975-37d8-4276-b6db-79a4c3606394",
|
||||
"coordinate": "680,245;680,300;750,300"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "4b7743cd-940c-431b-926f-e7b614fbf1fe",
|
||||
"nodeName": "市场部",
|
||||
"permissionFlag": "role:1",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]",
|
||||
"coordinate": "800,140|800,140",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "4b7743cd-940c-431b-926f-e7b614fbf1fe",
|
||||
"nextNodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"coordinate": "850,140;920,140;920,195"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "4",
|
||||
"nodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "920,220",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"nextNodeCode": "23e7429e-2b47-4431-b93e-40db7c431ce6",
|
||||
"coordinate": "945,220;975,220;975,220;960,220;960,220;990,220"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "23e7429e-2b47-4431-b93e-40db7c431ce6",
|
||||
"nodeName": "CEO",
|
||||
"permissionFlag": "1",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]",
|
||||
"coordinate": "1040,220|1040,220",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "23e7429e-2b47-4431-b93e-40db7c431ce6",
|
||||
"nextNodeCode": "f5ace37f-5a5e-4e64-a6f6-913ab9a71cd1",
|
||||
"coordinate": "1090,220;1140,220"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"nodeType": "2",
|
||||
"nodeCode": "f5ace37f-5a5e-4e64-a6f6-913ab9a71cd1",
|
||||
"nodeName": "结束",
|
||||
"permissionFlag": null,
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": null,
|
||||
"listenerPath": null,
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[]",
|
||||
"coordinate": "1160,220|1160,220",
|
||||
"version": "1",
|
||||
"skipList": []
|
||||
},
|
||||
{
|
||||
"nodeType": "1",
|
||||
"nodeCode": "762cb975-37d8-4276-b6db-79a4c3606394",
|
||||
"nodeName": "综合部",
|
||||
"permissionFlag": "role:3@@role:4",
|
||||
"nodeRatio": "0.000",
|
||||
"anyNodeSkip": null,
|
||||
"listenerType": "",
|
||||
"listenerPath": "",
|
||||
"formCustom": "N",
|
||||
"formPath": null,
|
||||
"ext": "[{\"code\":\"ButtonPermissionEnum\",\"value\":\"back,termination,file,transfer,trust,copy\"}]",
|
||||
"coordinate": "800,300|800,300",
|
||||
"version": "1",
|
||||
"skipList": [
|
||||
{
|
||||
"skipType": "PASS",
|
||||
"skipCondition": null,
|
||||
"skipName": null,
|
||||
"nowNodeCode": "762cb975-37d8-4276-b6db-79a4c3606394",
|
||||
"nextNodeCode": "b66b6563-f9fe-41cc-a782-f7837bb6f3d2",
|
||||
"nextNodeType": "4",
|
||||
"coordinate": "850,300;920,300;920,245"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"flowCode": "leave3",
|
||||
"flowName": "请假申请-并行网关",
|
||||
"modelValue": "CLASSICS",
|
||||
"category": "103",
|
||||
"version": "1",
|
||||
"formCustom": "N",
|
||||
"formPath": "/workflow/leaveEdit/index",
|
||||
"listenerType": null,
|
||||
"listenerPath": null
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user