Compare commits

..

No commits in common. "master" and "v2026.01(jdk8/11)" have entirely different histories.

3120 changed files with 22044 additions and 237845 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 41 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

View File

@ -31,8 +31,8 @@
| 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 | | 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 |
| 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 | | 【精简版】[yudao-cloud-mini](https://gitee.com/yudaocode/yudao-cloud-mini) | [`master`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/yudaocode/yudao-cloud-mini/tree/master-jdk17/) 分支 |
* 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能 * 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、IM 即时通讯、AI 大模型、IoT 物联网等功能 * 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】 可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
@ -105,7 +105,7 @@
团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。 团队包含专业的项目经理、架构师、前端工程师、后端工程师、测试工程师、运维工程师,可以提供全流程的外包服务。
项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 即时通讯、微信公众号、微信小程序等等。 项目可以是商城、SCRM 系统、OA 系统、物流系统、ERP 系统、CMS 系统、HIS 系统、支付系统、IM 聊天、微信公众号、微信小程序等等。
## 🐼 内置功能 ## 🐼 内置功能
@ -115,7 +115,7 @@
* 通用模块(必选):系统功能、基础设施 * 通用模块(必选):系统功能、基础设施
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心 * 通用模块(可选):工作流程、支付系统、数据报表、会员中心
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏 * 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。 > 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
> >
@ -273,28 +273,12 @@
![功能图](/.image/common/erp-feature.png) ![功能图](/.image/common/erp-feature.png)
### WMS 系统
演示地址:<https://cloud.iocoder.cn/wms-preview/>
![功能图](/.image/common/wms-feature.png)
![功能图](/.image/common/wms-preview.png)
### CRM 系统 ### CRM 系统
演示地址:<https://cloud.iocoder.cn/crm-preview/> 演示地址:<https://cloud.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png) ![功能图](/.image/common/crm-feature.png)
### MES 系统
演示地址:<https://cloud.iocoder.cn/mes-preview/>
![功能图](/.image/common/mes-feature.png)
![功能图](/.image/common/mes-preview.png)
### AI 大模型 ### AI 大模型
演示地址:<https://cloud.iocoder.cn/ai-preview/> 演示地址:<https://cloud.iocoder.cn/ai-preview/>
@ -303,27 +287,6 @@
![功能图](/.image/common/ai-preview.gif) ![功能图](/.image/common/ai-preview.gif)
### IoT 物联网
演示地址:<https://cloud.iocoder.cn/iot/build>
![功能图](/.image/common/iot-feature.png)
![预览图](/.image/common/iot-preview.png)
### IM 即时通讯
演示地址Cloud<https://cloud.iocoder.cn/im-preview/>
演示地址Vue3 + Element Plus<http://dashboard-vue3.yudao.iocoder.cn>
![功能图](/.image/common/im-feature.png)
| 聊天界面 | 聊天管理 |
| --- | --- |
| ![聊天界面](/.image/common/im-preview-home.png) | ![聊天管理](/.image/common/im-preview-manager.png) |
## 🐨 技术栈 ## 🐨 技术栈
### 微服务 ### 微服务
@ -341,11 +304,7 @@
| `yudao-module-mall` | 商城系统的 Module 模块 | | `yudao-module-mall` | 商城系统的 Module 模块 |
| `yudao-module-erp` | ERP 系统的 Module 模块 | | `yudao-module-erp` | ERP 系统的 Module 模块 |
| `yudao-module-crm` | CRM 系统的 Module 模块 | | `yudao-module-crm` | CRM 系统的 Module 模块 |
| `yudao-module-mes` | MES 系统的 Module 模块 |
| `yudao-module-wms` | WMS 系统的 Module 模块 |
| `yudao-module-im` | IM 即时通讯的 Module 模块 |
| `yudao-module-ai` | AI 大模型的 Module 模块 | | `yudao-module-ai` | AI 大模型的 Module 模块 |
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
| `yudao-module-mp` | 微信公众号的 Module 模块 | | `yudao-module-mp` | 微信公众号的 Module 模块 |
| `yudao-module-report` | 大屏报表 Module 模块 | | `yudao-module-report` | 大屏报表 Module 模块 |

View File

@ -24,12 +24,9 @@
<module>yudao-module-mall</module> <module>yudao-module-mall</module>
<module>yudao-module-erp</module> <module>yudao-module-erp</module>
<module>yudao-module-crm</module> <module>yudao-module-crm</module>
<module>yudao-module-iot</module>
<module>yudao-module-mes</module>
<module>yudao-module-wms</module>
<module>yudao-module-im</module>
<!-- 友情提示:基于 Spring AI 实现 LLM 大模型的接入,需要使用 JDK17 版本,详细可见 https://doc.iocoder.cn/ai/build/ --> <!-- 友情提示:基于 Spring AI 实现 LLM 大模型的接入,需要使用 JDK17 版本,详细可见 https://doc.iocoder.cn/ai/build/ -->
<!-- <module>yudao-module-ai</module>--> <!-- <module>yudao-module-ai</module>-->
<module>yudao-module-iot</module>
</modules> </modules>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
@ -37,7 +34,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>2026.05-jdk8-SNAPSHOT</revision> <revision>2026.01-jdk8-SNAPSHOT</revision>
<!-- Maven 相关 --> <!-- Maven 相关 -->
<java.version>1.8</java.version> <java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
@ -46,7 +43,7 @@
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.7.2</flatten-maven-plugin.version>
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) --> <!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) -->
<lombok.version>1.18.46</lombok.version> <lombok.version>1.18.42</lombok.version>
<spring.boot.version>2.7.18</spring.boot.version> <spring.boot.version>2.7.18</spring.boot.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -62,10 +62,6 @@ def load_and_clean(sql_file: str) -> str:
content = open(sql_file, encoding="utf-8").read() content = open(sql_file, encoding="utf-8").read()
for replace_pair in REPLACE_PAIR_LIST: for replace_pair in REPLACE_PAIR_LIST:
content = content.replace(*replace_pair) content = content.replace(*replace_pair)
# 移除所有 CHARACTER SET / COLLATE 变体 (utf8mb3、utf8 等)
content = re.sub(r" CHARACTER SET \w+ COLLATE \w+", "", content)
content = re.sub(r" CHARACTER SET \w+", "", content)
content = re.sub(r" COLLATE \w+", "", content)
# 移除索引字段的前缀长度定义,例如: `name`(32) -> `name` # 移除索引字段的前缀长度定义,例如: `name`(32) -> `name`
# 移除索引定义上的 USING BTREE COMMENT 部分 # 移除索引定义上的 USING BTREE COMMENT 部分
# 相关 issuehttps://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3 # 相关 issuehttps://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
@ -77,18 +73,11 @@ def load_and_clean(sql_file: str) -> str:
class Convertor(ABC): class Convertor(ABC):
# 不同数据库的关键字不完全一致;子类按需声明需要转义的列名。
reserved_column_names = set()
def __init__(self, src: str, db_type) -> None: def __init__(self, src: str, db_type) -> None:
self.src = src self.src = src
self.db_type = db_type self.db_type = db_type
self.content = load_and_clean(self.src) self.content = load_and_clean(self.src)
# original_content 保留原始 COMMENT 信息,用于注释提取 self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
self.original_content = open(src, encoding="utf-8").read()
# 剥离列级 COMMENT 以避免 COMMENT 值内的分号截断 CREATE TABLE 正则
content_no_comment = re.sub(r" COMMENT '(?:[^'\\]|\\.)*'", "", self.content)
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", content_no_comment)
@abstractmethod @abstractmethod
def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str: def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str:
@ -182,31 +171,6 @@ class Convertor(ABC):
""" """
return "" return ""
def escape_column_name(self, name: str) -> str:
"""转义目标库保留字列名,例如 Oracle / Kingbase 的 level。"""
column_name = name.lower()
if column_name in self.reserved_column_names:
return f'"{column_name}"'
return column_name
def escape_insert_columns(self, insert_script: str) -> str:
"""INSERT 显式列清单需要和 CREATE / COMMENT 使用同一套列名转义。"""
match = re.match(
r"(INSERT INTO\s+\S+\s*\()([^)]+)(\)\s+VALUES\s+[\s\S]*)",
insert_script,
flags=re.IGNORECASE,
)
if not match:
return insert_script
columns = [
self.escape_column_name(column.strip())
for column in match.group(2).split(",")
]
return f"{match.group(1)}{', '.join(columns)}{match.group(3)}"
@staticmethod @staticmethod
def inserts(table_name: str, script_content: str) -> Generator: def inserts(table_name: str, script_content: str) -> Generator:
PREFIX = f"INSERT INTO `{table_name}`" PREFIX = f"INSERT INTO `{table_name}`"
@ -218,8 +182,7 @@ class Convertor(ABC):
head = head.strip().replace("`", "").lower() head = head.strip().replace("`", "").lower()
tail = tail.strip().replace(r"\"", '"') tail = tail.strip().replace(r"\"", '"')
# tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'") # tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'")
col_part = f" {head}" if head else "" yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
@staticmethod @staticmethod
def index(ddl: Dict) -> Generator: def index(ddl: Dict) -> Generator:
@ -232,55 +195,18 @@ class Convertor(ABC):
Generator[str]: create index 语句 Generator[str]: create index 语句
""" """
for no, index in enumerate(ddl.get("index", []), 1): def generate_columns(columns):
columns = ", ".join(Convertor.index_columns(index.get("columns", []))) keys = [
if not columns: f"{col['name'].lower()}{' ' + col['order'].lower() if col['order'] != 'ASC' else ''}"
continue for col in columns[0]
]
return ", ".join(keys)
for no, index in enumerate(ddl["index"], 1):
columns = generate_columns(index["columns"])
table_name = ddl["table_name"].lower() table_name = ddl["table_name"].lower()
yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})" yield f"CREATE INDEX idx_{table_name}_{no:02d} ON {table_name} ({columns})"
@staticmethod
def index_columns(columns) -> list:
"""兼容 simple-ddl-parser 不同版本的索引列结构。"""
keys = []
def append(name, order="ASC"):
if not name:
return
column_name = str(name).strip("`").lower()
column_order = str(order or "ASC").upper()
if column_order == "DESC":
keys.append(f"{column_name} desc")
else:
keys.append(column_name)
def visit(value):
# 普通索引常见结构:[[{'name': 'user_id', 'order': 'ASC'}]]
if isinstance(value, (list, tuple)):
for item in value:
visit(item)
return
if isinstance(value, dict):
name = value.get("name")
if isinstance(name, (dict, list, tuple)):
visit(name)
return
append(name, value.get("order", "ASC"))
return
# 唯一索引在部分版本中会被解析成 ['mobile', 'ASC', 'tenant_id', 'ASC']。
if isinstance(value, str):
token = value.strip("`")
order = token.upper()
if order in ("ASC", "DESC"):
if order == "DESC" and keys and not keys[-1].endswith(" desc"):
keys[-1] = f"{keys[-1]} desc"
return
append(token)
visit(columns)
return keys
@staticmethod @staticmethod
def unique_index(ddl: Dict) -> Generator: def unique_index(ddl: Dict) -> Generator:
if "constraints" in ddl and "uniques" in ddl["constraints"]: if "constraints" in ddl and "uniques" in ddl["constraints"]:
@ -288,9 +214,7 @@ class Convertor(ABC):
for uk in uk_list: for uk in uk_list:
table_name = ddl["table_name"] table_name = ddl["table_name"]
uk_name = uk["constraint_name"] uk_name = uk["constraint_name"]
uk_columns = Convertor.index_columns(uk["columns"]) uk_columns = uk["columns"]
if not uk_columns:
continue
yield table_name, uk_name, uk_columns yield table_name, uk_name, uk_columns
@staticmethod @staticmethod
@ -303,8 +227,7 @@ class Convertor(ABC):
yield field, comment_string yield field, comment_string
def table_comment(self, table_sql: str) -> str: def table_comment(self, table_sql: str) -> str:
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号 match = re.search(r"COMMENT \='([^']+)';", table_sql)
match = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", table_sql)
return match.group(1) if match else None return match.group(1) if match else None
def print(self): def print(self):
@ -328,9 +251,7 @@ class Convertor(ABC):
error_scripts = [] error_scripts = []
for table_sql in self.table_script_list: for table_sql in self.table_script_list:
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符 ddl = DDLParser(table_sql.replace("`", "")).run()
table_sql_for_parse = re.sub(r"\s+COMMENT\s+'[^']*'", "", table_sql)
ddl = DDLParser(table_sql_for_parse.replace("`", "")).run()
# 如果parse失败, 需要跟进 # 如果parse失败, 需要跟进
if len(ddl) == 0: if len(ddl) == 0:
@ -345,23 +266,17 @@ class Convertor(ABC):
continue continue
# 解析注释 # 解析注释
# 从原始 SQL 提取注释(支持中文等 DDLParser 不能处理的内容)
# 按表名定位完整建表段(以「换行 + )」起始的 ENGINE 行作为终止),避免被列 COMMENT 内的分号截断
orig_match = re.search(
rf"CREATE TABLE\s+`{re.escape(table_name)}`[\s\S]*?\n\)[^;]*;",
self.original_content,
flags=re.IGNORECASE,
)
orig_table_sql = orig_match.group() if orig_match else table_sql
comments_dict = dict(Convertor.filed_comments(orig_table_sql))
for column in table_ddl["columns"]: for column in table_ddl["columns"]:
column["comment"] = comments_dict.get(column["name"], "") column["comment"] = bytes(column["comment"], "utf-8").decode(
table_ddl["comment"] = self.table_comment(orig_table_sql) or "" r"unicode_escape"
)[1:-1]
table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode(
r"unicode_escape"
)[1:-1]
# 为每个表生成个6个基本部分 # 为每个表生成个6个基本部分
create = self.gen_create(table_ddl) create = self.gen_create(table_ddl)
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"]) pk = self.gen_pk(table_name)
pk = self.gen_pk(table_name) if has_id else ""
uk = self.gen_uk(table_ddl) uk = self.gen_uk(table_ddl)
index = self.gen_index(table_ddl) index = self.gen_index(table_ddl)
comment = self.gen_comment(table_ddl) comment = self.gen_comment(table_ddl)
@ -405,31 +320,25 @@ class PostgreSQLConvertor(Convertor):
if type == "varchar": if type == "varchar":
return f"varchar({size})" return f"varchar({size})"
if type in ("int", "int unsigned", "int unsigned zerofill"): if type in ("int", "int unsigned"):
return "int4" return "int4"
if type in ("bigint", "bigint unsigned"): if type in ("bigint", "bigint unsigned"):
return "int8" return "int8"
if type in ("tinyint", "smallint", "tinyint unsigned"): if type == "datetime":
return "int2"
if type in ("datetime", "timestamp null"):
return "timestamp" return "timestamp"
if type == "date":
return "date"
if type == "json":
return "jsonb"
if type == "double":
return "double precision"
if type == "timestamp": if type == "timestamp":
return f"timestamp({size})" if size else "timestamp" return f"timestamp({size})"
if type == "bit": if type == "bit":
return "bool" return "bool"
if type in ("tinyint", "smallint"):
return "int2"
if type in ("text", "longtext"): if type in ("text", "longtext"):
return "text" return "text"
if type in ("blob", "mediumblob", "longblob"): if type in ("blob", "mediumblob"):
return "bytea" return "bytea"
if type == "decimal": if type == "decimal":
return ( return (
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric" f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
) )
def gen_create(self, ddl: Dict) -> str: def gen_create(self, ddl: Dict) -> str:
@ -442,13 +351,9 @@ class PostgreSQLConvertor(Convertor):
type = col["type"].lower() type = col["type"].lower()
full_type = self.translate_type(type, col["size"]) full_type = self.translate_type(type, col["size"])
if full_type is None:
raise NotImplementedError(
f"未支持的类型: '{col['type']}' (列: {name}, 表: {ddl['table_name']})"
)
nullable = "NULL" if col["nullable"] else "NOT NULL" nullable = "NULL" if col["nullable"] else "NOT NULL"
default = f"DEFAULT {col['default']}" if col["default"] is not None else "" default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" return f"{name} {full_type} {nullable} {default}"
table_name = ddl["table_name"].lower() table_name = ddl["table_name"].lower()
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
@ -473,7 +378,7 @@ CREATE TABLE {table_name} (
for column in table_ddl["columns"]: for column in table_ddl["columns"]:
table_comment = column["comment"] table_comment = column["comment"]
script += ( script += (
f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';"
+ "\n" + "\n"
) )
@ -502,9 +407,6 @@ CREATE TABLE {table_name} (
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence""" """生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
inserts = list(Convertor.inserts(table_name, self.content)) inserts = list(Convertor.inserts(table_name, self.content))
inserts = [self.escape_insert_columns(s) for s in inserts]
# 转换 MySQL 字符串转义为 PostgreSQL 格式:\\ -> \\' -> ''
inserts = [re.sub(r"\\\\|\\'", lambda m: "\\" if m.group() == "\\\\" else "''", s) for s in inserts]
## 生成 insert 脚本 ## 生成 insert 脚本
script = "" script = ""
last_id = 0 last_id = 0
@ -550,8 +452,6 @@ INSERT INTO dual VALUES (1);
class OracleConvertor(Convertor): class OracleConvertor(Convertor):
reserved_column_names = {"level", "size"}
def __init__(self, src): def __init__(self, src):
super().__init__(src, "Oracle") super().__init__(src, "Oracle")
@ -596,8 +496,10 @@ class OracleConvertor(Convertor):
# Oracle的 INSERT '' 不能通过NOT NULL校验因此对文字类型字段覆写为 NULL # Oracle的 INSERT '' 不能通过NOT NULL校验因此对文字类型字段覆写为 NULL
nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable nullable = "NULL" if type in ("varchar", "text", "longtext") else nullable
default = f"DEFAULT {col['default']}" if col["default"] is not None else "" default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
# Oracle 中 size 不能作为字段名
field_name = '"size"' if name == "size" else name
# Oracle DEFAULT 定义在 NULLABLE 之前 # Oracle DEFAULT 定义在 NULLABLE 之前
return f"{self.escape_column_name(name)} {full_type} {default} {nullable}" return f"{field_name} {full_type} {default} {nullable}"
table_name = ddl["table_name"].lower() table_name = ddl["table_name"].lower()
columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]] columns = [f"{generate_column(col).strip()}" for col in ddl["columns"]]
@ -622,7 +524,7 @@ CREATE TABLE {table_name} (
for column in table_ddl["columns"]: for column in table_ddl["columns"]:
table_comment = column["comment"] table_comment = column["comment"]
script += ( script += (
f"COMMENT ON COLUMN {table_ddl['table_name']}.{self.escape_column_name(column['name'])} IS '{table_comment}';" f"COMMENT ON COLUMN {table_ddl['table_name']}.{column['name']} IS '{table_comment}';"
+ "\n" + "\n"
) )
@ -654,7 +556,6 @@ CREATE TABLE {table_name} (
"""拷贝 INSERT 语句""" """拷贝 INSERT 语句"""
inserts = [] inserts = []
for insert_script in Convertor.inserts(table_name, self.content): for insert_script in Convertor.inserts(table_name, self.content):
insert_script = self.escape_insert_columns(insert_script)
# 对日期数据添加 TO_DATE 转换 # 对日期数据添加 TO_DATE 转换
insert_script = re.sub( insert_script = re.sub(
r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')", r"('\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}')",
@ -976,8 +877,6 @@ SET IDENTITY_INSERT {table_name.lower()} OFF;
class KingbaseConvertor(PostgreSQLConvertor): class KingbaseConvertor(PostgreSQLConvertor):
reserved_column_names = {"level"}
def __init__(self, src): def __init__(self, src):
super().__init__(src) super().__init__(src)
self.db_type = "Kingbase" self.db_type = "Kingbase"
@ -996,7 +895,7 @@ class KingbaseConvertor(PostgreSQLConvertor):
if full_type == "text": if full_type == "text":
nullable = "NULL" nullable = "NULL"
default = f"DEFAULT {col['default']}" if col["default"] is not None else "" default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
return f"{self.escape_column_name(name)} {full_type} {nullable} {default}" return f"{name} {full_type} {nullable} {default}"
table_name = ddl["table_name"].lower() table_name = ddl["table_name"].lower()
columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]] columns = [f"{_generate_column(col).strip()}" for col in ddl["columns"]]
@ -1016,8 +915,6 @@ CREATE TABLE {table_name} (
class OpengaussConvertor(KingbaseConvertor): class OpengaussConvertor(KingbaseConvertor):
reserved_column_names = set()
def __init__(self, src): def __init__(self, src):
super().__init__(src) super().__init__(src)
self.db_type = "OpenGauss" self.db_type = "OpenGauss"

View File

@ -14,30 +14,30 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>2026.05-jdk8-SNAPSHOT</revision> <revision>2026.01-jdk8-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version> <spring.framework.version>5.3.39</spring.framework.version>
<spring.security.version>5.8.16</spring.security.version> <spring.security.version>5.8.16</spring.security.version>
<spring.boot.version>2.7.18</spring.boot.version> <spring.boot.version>2.7.18</spring.boot.version>
<spring.cloud.version>2021.0.9</spring.cloud.version> <!-- Spring Boot 2.X 最多使用 2021.0.9 版本 --> <spring.cloud.version>2021.0.9</spring.cloud.version>
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version> <!-- Spring Boot 2.X 最多使用 2021.0.6.2 版本 --> <spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version>
<!-- Web 相关 --> <!-- Web 相关 -->
<servlet.versoin>2.5</servlet.versoin> <servlet.versoin>2.5</servlet.versoin>
<springdoc.version>1.8.0</springdoc.version> <springdoc.version>1.8.0</springdoc.version>
<knife4j.version>4.5.0</knife4j.version> <knife4j.version>4.5.0</knife4j.version>
<!-- DB 相关 --> <!-- DB 相关 -->
<druid.version>1.2.28</druid.version> <druid.version>1.2.27</druid.version>
<mybatis.version>3.5.19</mybatis.version> <mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version> <mybatis-plus.version>3.5.15</mybatis-plus.version>
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version> <mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
<dynamic-datasource.version>4.5.0</dynamic-datasource.version> <dynamic-datasource.version>4.5.0</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version> <easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>4.4.0</redisson.version> <redisson.version>3.52.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version> <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version> <kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version> <opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<taos.version>3.8.3</taos.version> <taos.version>3.7.9</taos.version>
<!-- 消息队列 --> <!-- 消息队列 -->
<rocketmq-spring.version>2.3.5</rocketmq-spring.version> <rocketmq-spring.version>2.3.5</rocketmq-spring.version>
<!-- RPC 相关 --> <!-- RPC 相关 -->
@ -55,44 +55,38 @@
<jedis-mock.version>1.1.12</jedis-mock.version> <jedis-mock.version>1.1.12</jedis-mock.version>
<mockito-inline.version>4.11.0</mockito-inline.version> <mockito-inline.version>4.11.0</mockito-inline.version>
<!-- Bpm 工作流相关 --> <!-- Bpm 工作流相关 -->
<flowable.version>6.8.1</flowable.version> <flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 --> <!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version> <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.22.2</jsoup.version> <jsoup.version>1.21.2</jsoup.version>
<sensitive-word.version>0.29.5</sensitive-word.version> <lombok.version>1.18.42</lombok.version>
<pinyin4j.version>2.5.1</pinyin4j.version>
<lombok.version>1.18.46</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.44</hutool-5.version> <hutool-5.version>5.8.42</hutool-5.version>
<fastexcel.version>1.3.0</fastexcel.version> <fastexcel.version>1.3.0</fastexcel.version>
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! --> <velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<guava.version>33.6.0-jre</guava.version> <guava.version>33.5.0-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.13.0</commons-net.version> <commons-net.version>3.12.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version> <commons-lang3.version>3.20.0</commons-lang3.version>
<jsch.version>2.28.2</jsch.version> <jsch.version>2.27.7</jsch.version>
<tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X会报 JDK8 不支持 --> <tika-core.version>2.9.3</tika-core.version> <!-- JDK8 不能从 2.9.3 升级到 3.X会报 JDK8 不支持 -->
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version> <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<reflections.version>0.10.2</reflections.version> <reflections.version>0.10.2</reflections.version>
<netty.version>4.2.14.Final</netty.version> <netty.version>4.2.9.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version> <mqtt.version>1.2.5</mqtt.version>
<vertx.version>4.5.26</vertx.version> <vertx.version>4.5.22</vertx.version>
<okhttp.version>4.12.0</okhttp.version> <okhttp.version>4.12.0</okhttp.version>
<californium.version>3.14.0</californium.version> <californium.version>3.12.0</californium.version>
<j2mod.version>3.3.0</j2mod.version>
<httpclient5.version>5.5.2</httpclient5.version> <!-- WxJava 4.8.x 需要 HttpClient5 5.4+Spring Boot 2.7 默认 5.1.4 不兼容 -->
<httpcore5.version>5.3.6</httpcore5.version> <!-- 配套 httpclient5 5.5.2Spring Boot 2.7 默认 5.1.5 不兼容 -->
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<awssdk.version>2.44.0</awssdk.version> <awssdk.version>2.40.15</awssdk.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version> <justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.3.4</jimureport.version> <jimureport.version>2.1.3</jimureport.version>
<jimubi.version>2.3.2</jimubi.version> <jimubi.version>2.3.0</jimubi.version>
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version> <weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
<bouncycastle.version>1.80</bouncycastle.version> <alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
<alipay-sdk-java.version>4.40.806.ALL</alipay-sdk-java.version>
<!-- 专属于 JDK8 安全漏洞升级 --> <!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 --> <logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties> </properties>
@ -313,7 +307,7 @@
<exclusion> <exclusion>
<groupId>org.redisson</groupId> <groupId>org.redisson</groupId>
<!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 --> <!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
<artifactId>redisson-spring-data-40</artifactId> <!-- Redisson 4.x 默认依赖 spring-data-40排除后使用 spring-data-27 适配 Spring Boot 2.7 --> <artifactId>redisson-spring-data-35</artifactId>
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
@ -627,18 +621,6 @@
<version>${jsoup.version}</version> <version>${jsoup.version}</version>
</dependency> </dependency>
<dependency>
<groupId>com.github.houbb</groupId>
<artifactId>sensitive-word</artifactId> <!-- 敏感词检测trie 树高效匹配 -->
<version>${sensitive-word.version}</version>
</dependency>
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId> <!-- 汉字转拼音:作为 hutool PinyinUtil 的底层引擎 -->
<version>${pinyin4j.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.reflections</groupId> <groupId>org.reflections</groupId>
<artifactId>reflections</artifactId> <artifactId>reflections</artifactId>
@ -689,30 +671,6 @@
<version>${californium.version}</version> <version>${californium.version}</version>
</dependency> </dependency>
<!-- Modbus 相关 -->
<dependency>
<groupId>com.ghgande</groupId>
<artifactId>j2mod</artifactId>
<version>${j2mod.version}</version>
</dependency>
<!-- WxJava 4.8.x 需要 HttpClient5 5.4+,覆盖 Spring Boot 2.7 默认的 5.1.4 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>${httpclient5.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5</artifactId>
<version>${httpcore5.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.core5</groupId>
<artifactId>httpcore5-h2</artifactId>
<version>${httpcore5.version}</version>
</dependency>
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<dependency> <dependency>
<groupId>software.amazon.awssdk</groupId> <groupId>software.amazon.awssdk</groupId>
@ -750,24 +708,6 @@
</exclusions> </exclusions>
</dependency> </dependency>
<!-- 锁定 weixin-java 传递依赖,避免 Maven 版本范围自动升级到 1.80.2 后 Fat Jar 启动失败。
反馈https://t.zsxq.com/pCVBo -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcutil-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.binarywang</groupId> <groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId> <artifactId>weixin-java-pay</artifactId>

View File

@ -53,8 +53,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>jakarta.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 --> <scope>provided</scope> <!-- 设置为 provided只有工具类需要使用到 -->
</dependency> </dependency>
@ -125,8 +125,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.validation</groupId> <groupId>jakarta.validation</groupId>
<artifactId>validation-api</artifactId> <artifactId>jakarta.validation-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided主要是 PageParam 使用到 --> <scope>provided</scope> <!-- 设置为 provided主要是 PageParam 使用到 -->
</dependency> </dependency>

View File

@ -124,22 +124,6 @@ public class CollectionUtils {
return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet()); return from.stream().filter(filter).map(func).filter(Objects::nonNull).collect(Collectors.toSet());
} }
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func) {
if (CollUtil.isEmpty(from)) {
return new LinkedHashSet<>();
}
return from.stream().map(func).filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
public static <T, U> Set<U> convertLinkedSet(Collection<T> from, Function<T, U> func, Predicate<T> filter) {
if (CollUtil.isEmpty(from)) {
return new LinkedHashSet<>();
}
return from.stream().filter(filter).map(func).filter(Objects::nonNull)
.collect(Collectors.toCollection(LinkedHashSet::new));
}
public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) { public static <T, K> Map<K, T> convertMapByFilter(Collection<T> from, Predicate<T> filter, Function<T, K> keyFunc) {
if (CollUtil.isEmpty(from)) { if (CollUtil.isEmpty(from)) {
return new HashMap<>(); return new HashMap<>();
@ -365,37 +349,4 @@ public class CollectionUtils {
return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value); return (LinkedHashSet<T>) toCollection(LinkedHashSet.class, elementType, value);
} }
public static boolean dfs(Long node, Map<Long, Set<Long>> graph) {
return dfs(node, graph, new HashSet<>(), new HashSet<>());
}
private static boolean dfs(Long node, Map<Long, Set<Long>> graph, Set<Long> visited, Set<Long> inStack) {
if (inStack.contains(node)) {
return true;
}
if (visited.contains(node)) {
return false;
}
visited.add(node);
inStack.add(node);
Set<Long> neighbors = graph.getOrDefault(node, Collections.emptySet());
for (Long neighbor : neighbors) {
if (dfs(neighbor, graph, visited, inStack)) {
return true;
}
}
inStack.remove(node);
return false;
}
/**
* head tail Listhead tail
*/
public static <T> List<T> of(T head, Collection<T> tail) {
List<T> list = new ArrayList<>();
list.add(head);
CollUtil.addAll(list, tail);
return list;
}
} }

View File

@ -236,23 +236,6 @@ public class LocalDateTimeUtils {
return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN); return LocalDateTime.now().with(TemporalAdjusters.firstDayOfYear()).with(LocalTime.MIN);
} }
/**
* N 0
* <p>
* getLatestDays(3) [ 00:00, 00:00, 00:00]
*
* @param days
* @return LocalDateTime
*/
public static List<LocalDateTime> getLatestDays(int days) {
LocalDateTime today = getToday();
List<LocalDateTime> dates = new ArrayList<>(days);
for (int i = days - 1; i >= 0; i--) {
dates.add(today.minusDays(i));
}
return dates;
}
public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime, public static List<LocalDateTime[]> getDateRangeList(LocalDateTime startTime,
LocalDateTime endTime, LocalDateTime endTime,
Integer interval) { Integer interval) {
@ -319,21 +302,6 @@ public class LocalDateTimeUtils {
return timeRanges; return timeRanges;
} }
/**
*
*
* @param startDate
* @param days
* @return
*/
public static List<LocalDate> getDateList(LocalDate startDate, int days) {
List<LocalDate> dateList = new ArrayList<>(days);
for (int i = 0; i < days; i++) {
dateList.add(startDate.plusDays(i));
}
return dateList;
}
/** /**
* *
* *
@ -367,27 +335,6 @@ public class LocalDateTimeUtils {
} }
} }
/**
*
*
* @param date
* @return
*/
public static LocalDate getQuarterStart(LocalDate date) {
Month firstMonthOfQuarter = date.getMonth().firstMonthOfQuarter();
return LocalDate.of(date.getYear(), firstMonthOfQuarter, 1);
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getWeekStart(LocalDate date) {
return date.with(DayOfWeek.MONDAY);
}
/** /**
* {@link LocalDateTime} Unix 1970-01-01T00:00:00Z * {@link LocalDateTime} Unix 1970-01-01T00:00:00Z
* *

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.framework.common.util.http; package cn.iocoder.yudao.framework.common.util.http;
import cn.hutool.core.codec.Base64; import cn.hutool.core.codec.Base64;
import cn.hutool.core.map.TableMap;
import cn.hutool.core.net.url.UrlBuilder; import cn.hutool.core.net.url.UrlBuilder;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest; import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse; import cn.hutool.http.HttpResponse;
@ -9,7 +11,6 @@ import lombok.SneakyThrows;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
import org.springframework.web.util.UriUtils;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.net.URI; import java.net.URI;
@ -38,10 +39,8 @@ public class HttpUtils {
} }
/** /**
* URL query parameter * URL
* + query parameter URL path
* *
* @see #decodeUrlPath(String)
* @param value * @param value
* @return * @return
*/ */
@ -50,75 +49,14 @@ public class HttpUtils {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name()); return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
} }
/** @SuppressWarnings("unchecked")
* URL
* {@link #decodeUtf8(String)} + +
* URL path
*
* @param path URL
* @return
*/
@SneakyThrows
public static String decodeUrlPath(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
// 先将 + 替换为 %2B避免被 URLDecoder 解码为空格
String encoded = path.replace("+", "%2B");
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
}
/**
* URL /
*
* @param path URL 20250602/xxx.pdf
* @return
*/
public static String encodeUrlPath(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
String[] segments = path.split(StrUtil.SLASH, -1);
StringBuilder result = new StringBuilder(path.length());
for (int i = 0; i < segments.length; i++) {
if (i > 0) {
result.append(StrUtil.SLASH);
}
result.append(encodeUrlPathSegment(segments[i]));
}
return result.toString();
}
/**
* URL
*
* @param segment URL
* @return
*/
public static String encodeUrlPathSegment(String segment) {
return UriUtils.encodePathSegment(segment, StandardCharsets.UTF_8);
}
public static String removeUrlPathQueryAndFragment(String path) {
if (StrUtil.isEmpty(path)) {
return path;
}
int endIndex = path.length();
int queryIndex = path.indexOf('?');
if (queryIndex >= 0) {
endIndex = queryIndex;
}
int fragmentIndex = path.indexOf('#');
if (fragmentIndex >= 0 && fragmentIndex < endIndex) {
endIndex = fragmentIndex;
}
return path.substring(0, endIndex);
}
public static String replaceUrlQuery(String url, String key, String value) { public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset()); UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
// 先移除;再添加 // 先移除
builder.getQuery().remove(key); TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
ReflectUtil.getFieldValue(builder.getQuery(), "query");
query.remove(key);
// 后添加
builder.addQuery(key, value); builder.addQuery(key, value);
return builder.build(); return builder.build();
} }
@ -255,14 +193,4 @@ public class HttpUtils {
} }
} }
/**
* WebSocket URL HTTP URLws:// → http://wss:// → https://;其它格式原样保留
*
* @param url URL
* @return URL
*/
public static String wsUrlToHttp(String url) {
return StrUtil.startWithIgnoreCase(url, "ws") ? "http" + url.substring(2) : url;
}
} }

View File

@ -22,7 +22,6 @@ import java.lang.reflect.Type;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* JSON * JSON
@ -174,41 +173,6 @@ public class JsonUtils {
} }
} }
/**
* JSON Map null
*
* @param text JSON
* @return Map
*/
public static Map<String, Object> parseMap(String text) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, new TypeReference<Map<String, Object>>() {});
} catch (IOException e) {
return null;
}
}
/**
* JSON null
*
* @param text
* @param clazz
* @return
*/
public static <T> T parseObjectQuietly(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) {
return null;
}
try {
return objectMapper.readValue(text, clazz);
} catch (IOException e) {
return null;
}
}
public static <T> List<T> parseArray(String text, Class<T> clazz) { public static <T> List<T> parseArray(String text, Class<T> clazz) {
if (StrUtil.isEmpty(text)) { if (StrUtil.isEmpty(text)) {
return new ArrayList<>(); return new ArrayList<>();
@ -253,14 +217,6 @@ public class JsonUtils {
} }
} }
public static String getText(JsonNode node, String fieldName) {
if (node == null) {
return null;
}
JsonNode value = node.get(fieldName);
return value != null && !value.isNull() ? value.asText() : null;
}
public static boolean isJson(String text) { public static boolean isJson(String text) {
return JSONUtil.isTypeJSON(text); return JSONUtil.isTypeJSON(text);
} }

View File

@ -60,11 +60,6 @@ public class ObjectUtils {
return Arrays.asList(array).contains(obj); return Arrays.asList(array).contains(obj);
} }
@SafeVarargs
public static <T> boolean notEqualsAny(T obj, T... array) {
return !Arrays.asList(array).contains(obj);
}
public static boolean isNotAllEmpty(Object... objs) { public static boolean isNotAllEmpty(Object... objs) {
return !ObjectUtil.isAllEmpty(objs); return !ObjectUtil.isAllEmpty(objs);
} }

View File

@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.common.util.string;
import cn.hutool.core.text.StrPool; import cn.hutool.core.text.StrPool;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.pinyin.PinyinUtil;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
import java.util.Arrays; import java.util.Arrays;
@ -79,25 +78,6 @@ public class StrUtils {
.collect(Collectors.joining("\n")); .collect(Collectors.joining("\n"));
} }
/**
* 便 / /
*
* "lao zhang"ZhangSan "zhangsan"
* / / null
*
* hutool-extra {@link PinyinUtil}
* pinyin4j / TinyPinyin / Bopomofo4j NoClassDefFoundError
*
* @param str
* @return
*/
public static String toPinyin(String str) {
if (StrUtil.isBlank(str)) {
return null;
}
return PinyinUtil.getPinyin(str);
}
/** /**
* *
* *

View File

@ -1,82 +0,0 @@
package cn.iocoder.yudao.framework.common.util.http;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link HttpUtils}
*/
public class HttpUtilsTest {
@Test
public void testEncodeUrlPath() {
// 准备参数
String path = "avatar/中文 100%+文件.jpg";
// 调用
String result = HttpUtils.encodeUrlPath(path);
// 断言
assertEquals("avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg", result);
}
@Test
public void testDecodeUrlPath() {
// 准备参数:+ 是路径字符,不应该按 query parameter 语义解码为空格
String path = "avatar/%E4%B8%AD%E6%96%87%20100%25+%E6%96%87%E4%BB%B6.jpg";
// 调用
String result = HttpUtils.decodeUrlPath(path);
// 断言
assertEquals("avatar/中文 100%+文件.jpg", result);
}
@Test
public void testRemoveUrlPathQueryAndFragment() {
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg?token=1#preview"));
assertEquals("avatar/test.jpg", HttpUtils.removeUrlPathQueryAndFragment("avatar/test.jpg#preview?token=1"));
}
@Test
public void testReplaceUrlQuery_replace() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1&b=2";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "3");
// 断言:被替换的 key 会移到末尾,原顺序的其它参数保留
assertEquals("https://www.iocoder.cn/path?b=2&a=3", result);
}
@Test
public void testReplaceUrlQuery_add() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "b", "2");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1&b=2", result);
}
@Test
public void testReplaceUrlQuery_noQuery() {
// 准备参数:原 URL 没有 query
String url = "https://www.iocoder.cn/path";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "1");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1", result);
}
@Test
public void testReplaceUrlQuery_emptyValue() {
// 准备参数value 为空字符串
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "");
// 断言:保留 keyvalue 为空
assertEquals("https://www.iocoder.cn/path?a=", result);
}
}

View File

@ -8,7 +8,6 @@ import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import lombok.NonNull; import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList; import java.util.ArrayList;
@ -26,46 +25,44 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author * @author
*/ */
@Slf4j @Slf4j
@UtilityClass
public class AreaUtils { public class AreaUtils {
/**
* SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static AreaUtils INSTANCE = new AreaUtils();
/** /**
* Area 访 * Area 访
*/ */
private static Map<Integer, Area> areas; private static Map<Integer, Area> areas;
static { private AreaUtils() {
init(); long now = System.currentTimeMillis();
} areas = new HashMap<>();
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0,
/** null, new ArrayList<>()));
* // 从 csv 中加载数据
*/ List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
private static void init() { rows.remove(0); // 删除 header
try { for (CsvRow row : rows) {
long now = System.currentTimeMillis(); // 创建 Area 对象
areas = new HashMap<>(); Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)),
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>())); null, new ArrayList<>());
// 从 csv 中加载数据 // 添加到 areas 中
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows(); areas.put(area.getId(), area);
rows.remove(0); // 删除 header
for (CsvRow row : rows) {
Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)), null, new ArrayList<>());
areas.put(area.getId(), area);
}
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
area.setParent(parent);
parent.getChildren().add(area);
}
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (Exception e) {
throw new RuntimeException("AreaUtils 初始化失败", e);
} }
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
area.setParent(parent);
parent.getChildren().add(area);
}
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} }
/** /**

View File

@ -3,10 +3,11 @@ package cn.iocoder.yudao.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil; import cn.hutool.core.io.resource.ResourceUtil;
import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.Area;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.lionsoul.ip2region.xdb.Searcher; import org.lionsoul.ip2region.xdb.Searcher;
import java.io.IOException;
/** /**
* IP * IP
* *
@ -15,29 +16,30 @@ import org.lionsoul.ip2region.xdb.Searcher;
* @author wanglhup * @author wanglhup
*/ */
@Slf4j @Slf4j
@UtilityClass
public class IPUtils { public class IPUtils {
/**
* SEARCHER
*/
@SuppressWarnings("InstantiationOfUtilityClass")
private final static IPUtils INSTANCE = new IPUtils();
/** /**
* IP * IP
*/ */
private static Searcher SEARCHER; private static Searcher SEARCHER;
static {
init();
}
/** /**
* *
*/ */
private static void init() { private IPUtils() {
try { try {
long now = System.currentTimeMillis(); long now = System.currentTimeMillis();
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb"); byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
SEARCHER = Searcher.newWithBuffer(bytes); SEARCHER = Searcher.newWithBuffer(bytes);
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now); log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (Exception e) { } catch (IOException e) {
throw new RuntimeException("IPUtils 初始化失败", e); log.error("启动加载 IPUtils 失败", e);
} }
} }

View File

@ -5,12 +5,7 @@ import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum; import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/** /**
* {@link AreaUtils} * {@link AreaUtils}
@ -36,46 +31,6 @@ public class AreaUtilsTest {
assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区"); assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区");
assertEquals(AreaUtils.format(1), "中国"); assertEquals(AreaUtils.format(1), "中国");
assertEquals(AreaUtils.format(2), "蒙古"); assertEquals(AreaUtils.format(2), "蒙古");
// 中国台湾省:省/市/区三级
assertEquals(AreaUtils.format(710101), "台湾省 台北市 中正区");
// 自定义分隔符
assertEquals(AreaUtils.format(110105, "/"), "北京市/北京市/朝阳区");
// 不存在的编号
assertNull(AreaUtils.format(-1));
}
@Test
public void testParseArea() {
// 调用:通过路径解析得到地区
Area area = AreaUtils.parseArea("北京市/北京市/朝阳区");
// 断言
assertNotNull(area);
assertEquals(area.getId(), 110105);
// 路径不存在时返回 null
assertNull(AreaUtils.parseArea("不存在/路径"));
}
@Test
public void testGetParentIdByType() {
// 调用:朝阳区向上找省
Integer provinceId = AreaUtils.getParentIdByType(110105, AreaTypeEnum.PROVINCE);
// 断言
assertEquals(provinceId, 110000);
// 自身就是目标类型
assertEquals(AreaUtils.getParentIdByType(110000, AreaTypeEnum.PROVINCE), 110000);
// 不存在的编号返回 null
assertNull(AreaUtils.getParentIdByType(-1, AreaTypeEnum.PROVINCE));
}
@Test
public void testGetByType() {
// 调用:获取所有省份
List<Area> provinces = AreaUtils.getByType(AreaTypeEnum.PROVINCE, area -> area);
// 断言:包含北京、台湾、香港、澳门
assertTrue(provinces.stream().anyMatch(area -> "北京市".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "台湾省".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "香港特别行政区".equals(area.getName())));
assertTrue(provinces.stream().anyMatch(area -> "澳门特别行政区".equals(area.getName())));
} }
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.tenant.config; package cn.iocoder.yudao.framework.tenant.config;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi; import cn.iocoder.yudao.framework.common.biz.system.tenant.TenantCommonApi;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
@ -22,7 +23,6 @@ import cn.iocoder.yudao.framework.web.config.WebProperties;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor;
import javax.annotation.Resource;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@ -30,6 +30,7 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties
import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.BatchStrategies; import org.springframework.data.redis.cache.BatchStrategies;
import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheConfiguration;
@ -44,7 +45,11 @@ import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import org.springframework.web.util.pattern.PathPattern; import org.springframework.web.util.pattern.PathPattern;
import java.util.*; import javax.annotation.Resource;
import java.util.HashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -58,6 +63,13 @@ public class YudaoTenantAutoConfiguration {
@Bean @Bean
public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) { public TenantFrameworkService tenantFrameworkService(TenantCommonApi tenantApi) {
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC6YZF
try {
TenantCommonApi tenantApiImpl = SpringUtil.getBean("tenantApiImpl", TenantCommonApi.class);
if (tenantApiImpl != null) {
tenantApi = tenantApiImpl;
}
} catch (Exception ignored) {}
return new TenantFrameworkServiceImpl(tenantApi); return new TenantFrameworkServiceImpl(tenantApi);
} }
@ -155,9 +167,20 @@ public class YudaoTenantAutoConfiguration {
// ========== MQ ========== // ========== MQ ==========
@Bean /**
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() { * Redis
return new TenantRedisMessageInterceptor(); *
* TenantRedisMessageInterceptor Bean RedisMessageInterceptor
*/
@Configuration
@ConditionalOnClass(name = "cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate")
public static class TenantRedisMQAutoConfiguration {
@Bean
public TenantRedisMessageInterceptor tenantRedisMessageInterceptor() {
return new TenantRedisMessageInterceptor();
}
} }
@Bean @Bean
@ -175,6 +198,7 @@ public class YudaoTenantAutoConfiguration {
// ========== Job ========== // ========== Job ==========
@Bean @Bean
@ConditionalOnClass(name = "com.xxl.job.core.handler.annotation.XxlJob")
public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) { public TenantJobAspect tenantJobAspect(TenantFrameworkService tenantFrameworkService) {
return new TenantJobAspect(tenantFrameworkService); return new TenantJobAspect(tenantFrameworkService);
} }
@ -192,12 +216,7 @@ public class YudaoTenantAutoConfiguration {
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TenantRedisCacheManager 对象 // 创建 TenantRedisCacheManager 对象
TenantRedisCacheManager cacheManager = new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, return new TenantRedisCacheManager(cacheWriter, redisCacheConfiguration, tenantProperties.getIgnoreCaches());
tenantProperties.getIgnoreCaches());
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
cacheManager.setTransactionAware(true);
return cacheManager;
} }
} }

View File

@ -42,8 +42,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>jakarta.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>jakarta.servlet-api</artifactId>
</dependency> </dependency>
<!-- RPC 相关 --> <!-- RPC 相关 -->

View File

@ -42,8 +42,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>jakarta.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 ExcelUtils 使用 --> <scope>provided</scope> <!-- 设置为 provided只有 ExcelUtils 使用 -->
</dependency> </dependency>

View File

@ -41,8 +41,8 @@
<!-- 工具类相关 --> <!-- 工具类相关 -->
<dependency> <dependency>
<groupId>javax.validation</groupId> <groupId>jakarta.validation</groupId>
<artifactId>validation-api</artifactId> <artifactId>jakarta.validation-api</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -35,8 +35,8 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>javax.servlet</groupId> <groupId>jakarta.servlet</groupId>
<artifactId>javax.servlet-api</artifactId> <artifactId>jakarta.servlet-api</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 TraceFilter 使用 --> <scope>provided</scope> <!-- 设置为 provided只有 TraceFilter 使用 -->
</dependency> </dependency>

View File

@ -12,6 +12,7 @@ import cn.iocoder.yudao.framework.mq.redis.core.stream.AbstractRedisStreamMessag
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RedissonClient; import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -69,8 +70,7 @@ public class YudaoRedisMQConsumerAutoConfiguration {
public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners, public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate, RedisMQTemplate redisTemplate,
RedissonClient redissonClient) { RedissonClient redissonClient) {
return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient, return new RedisPendingMessageResendJob(listeners, redisTemplate, redissonClient);
RedisPendingMessageResendJob.DEFAULT_RESEND_LOCK_KEY);
} }
/** /**
@ -81,8 +81,7 @@ public class YudaoRedisMQConsumerAutoConfiguration {
public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners, public RedisStreamMessageCleanupJob redisStreamMessageCleanupJob(List<AbstractRedisStreamMessageListener<?>> listeners,
RedisMQTemplate redisTemplate, RedisMQTemplate redisTemplate,
RedissonClient redissonClient) { RedissonClient redissonClient) {
return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient, return new RedisStreamMessageCleanupJob(listeners, redisTemplate, redissonClient);
RedisStreamMessageCleanupJob.DEFAULT_CLEANUP_LOCK_KEY);
} }
/** /**

View File

@ -23,9 +23,7 @@ import java.util.Objects;
@AllArgsConstructor @AllArgsConstructor
public class RedisPendingMessageResendJob { public class RedisPendingMessageResendJob {
public static final String DEFAULT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock"; private static final String LOCK_KEY = "redis:stream:pending-message-resend:lock";
public static final String IOT_RESEND_LOCK_KEY = "redis:stream:pending-message-resend:lock:iot";
/** /**
* 5 * 5
@ -38,26 +36,22 @@ public class RedisPendingMessageResendJob {
private final List<AbstractRedisStreamMessageListener<?>> listeners; private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate; private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient; private final RedissonClient redissonClient;
private final String resendLockKey;
/** /**
* , 35 * , 35
*/ */
@Scheduled(cron = "35 * * * * ?") @Scheduled(cron = "35 * * * * ?")
public void messageResend() { public void messageResend() {
RLock lock = redissonClient.getLock(resendLockKey); RLock lock = redissonClient.getLock(LOCK_KEY);
// 尝试加锁
if (lock.tryLock()) { if (lock.tryLock()) {
try { try {
execute(); execute();
} catch (Exception ex) { } catch (Exception ex) {
log.error("[messageResend][执行异常][lockKey={}]", resendLockKey, ex); log.error("[messageResend][执行异常]", ex);
} finally { } finally {
if (lock.isHeldByCurrentThread()) { lock.unlock();
lock.unlock();
}
} }
} else {
log.debug("[messageResend][未获取到锁,跳过本轮][lockKey={}]", resendLockKey);
} }
} }

View File

@ -23,16 +23,7 @@ import java.util.List;
@AllArgsConstructor @AllArgsConstructor
public class RedisStreamMessageCleanupJob { public class RedisStreamMessageCleanupJob {
/** private static final String LOCK_KEY = "redis:stream:message-cleanup:lock";
* MQSpring AbstractRedisStreamMessageListener使
*/
public static final String DEFAULT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock";
/**
* IoT Redis 线使 {@link #DEFAULT_CLEANUP_LOCK_KEY}
* XTRIM Stream
*/
public static final String IOT_CLEANUP_LOCK_KEY = "redis:stream:message-cleanup:lock:iot";
/** /**
* 10000 * 10000
@ -42,29 +33,22 @@ public class RedisStreamMessageCleanupJob {
private final List<AbstractRedisStreamMessageListener<?>> listeners; private final List<AbstractRedisStreamMessageListener<?>> listeners;
private final RedisMQTemplate redisTemplate; private final RedisMQTemplate redisTemplate;
private final RedissonClient redissonClient; private final RedissonClient redissonClient;
/**
* Redisson Bean
*/
private final String cleanupLockKey;
/** /**
* *
*/ */
@Scheduled(cron = "0 0 * * * ?") @Scheduled(cron = "0 0 * * * ?")
public void cleanup() { public void cleanup() {
RLock lock = redissonClient.getLock(cleanupLockKey); RLock lock = redissonClient.getLock(LOCK_KEY);
// 尝试加锁
if (lock.tryLock()) { if (lock.tryLock()) {
try { try {
execute(); execute();
} catch (Exception ex) { } catch (Exception ex) {
log.error("[cleanup][执行异常][lockKey={}]", cleanupLockKey, ex); log.error("[cleanup][执行异常]", ex);
} finally { } finally {
if (lock.isHeldByCurrentThread()) { lock.unlock();
lock.unlock();
}
} }
} else {
log.debug("[cleanup][未获取到锁,跳过本轮][lockKey={}]", cleanupLockKey);
} }
} }
@ -75,8 +59,8 @@ public class RedisStreamMessageCleanupJob {
StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream(); StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
listeners.forEach(listener -> { listeners.forEach(listener -> {
try { try {
// 使用 XTRIM MAXLEN 精确裁剪approximate=false避免 ~ 模式下长期明显高于上限 // 使用 XTRIM 命令清理消息,只保留最近的 MAX_LEN 条消息
Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, false); Long trimCount = ops.trim(listener.getStreamKey(), MAX_COUNT, true);
if (trimCount != null && trimCount > 0) { if (trimCount != null && trimCount > 0) {
log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount); log.info("[execute][Stream({}) 清理消息数量({})]", listener.getStreamKey(), trimCount);
} }

View File

@ -94,13 +94,6 @@
<groupId>com.fhs-opensource</groupId> <groupId>com.fhs-opensource</groupId>
<artifactId>easy-trans-mybatis-plus-extend</artifactId> <artifactId>easy-trans-mybatis-plus-extend</artifactId>
</dependency> </dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -20,49 +20,51 @@ public enum DbTypeEnum {
/** /**
* H2 * H2
*
* H2 find_in_set
*/ */
H2(DbType.H2, "H2", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"), H2(DbType.H2, "H2", ""),
/** /**
* MySQL * MySQL
*/ */
MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET(#{value}, #{column}) <> 0"), MY_SQL(DbType.MYSQL, "MySQL", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/** /**
* Oracle * Oracle
*/ */
ORACLE(DbType.ORACLE, "Oracle", "INSTR(',' || #{column} || ',', ',' || #{value} || ',') > 0"), ORACLE(DbType.ORACLE, "Oracle", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/** /**
* PostgreSQL * PostgreSQL
* *
* openGauss 使 ProductName PostgreSQL * openGauss 使 ProductName PostgreSQL
*/ */
POSTGRE_SQL(DbType.POSTGRE_SQL, "PostgreSQL", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"), POSTGRE_SQL(DbType.POSTGRE_SQL,"PostgreSQL", "POSITION('#{value}' IN #{column}) <> 0"),
/** /**
* SQL Server * SQL Server
*/ */
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"), SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/** /**
* SQL Server 2005 * SQL Server 2005
*/ */
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + CAST(#{value} AS varchar(255)) + ',', ',' + #{column} + ',') > 0"), SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/** /**
* *
*/ */
DM(DbType.DM, "DM DBMS", "FIND_IN_SET(#{value}, #{column}) <> 0"), DM(DbType.DM, "DM DBMS", "FIND_IN_SET('#{value}', #{column}) <> 0"),
/** /**
* *
*/ */
KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION(',' || CAST(#{value} AS VARCHAR) || ',' IN ',' || #{column} || ',') > 0"), KINGBASE_ES(DbType.KINGBASE_ES, "KingbaseES", "POSITION('#{value}' IN #{column}) <> 0"),
/** /**
* OceanBase * OceanBase
*/ */
OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET(#{value}, #{column}) <> 0") OCEAN_BASE(DbType.OCEAN_BASE, "OceanBase", "FIND_IN_SET('#{value}', #{column}) <> 0")
; ;
@ -93,9 +95,7 @@ public enum DbTypeEnum {
} }
public static String getFindInSetTemplate(DbType dbType) { public static String getFindInSetTemplate(DbType dbType) {
return Optional.ofNullable(MAP_BY_MP.get(dbType)) return Optional.of(MAP_BY_MP.get(dbType).getFindInSetTemplate())
.map(DbTypeEnum::getFindInSetTemplate)
.filter(StrUtil::isNotBlank)
.orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported")); .orElseThrow(() -> new IllegalArgumentException("FIND_IN_SET not supported"));
} }
} }

View File

@ -119,31 +119,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3)); return selectOne(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
} }
/**
* 使 FOR UPDATE
*
*
*
* @param queryWrapper
* @return
*/
default T selectOneForUpdate(LambdaQueryWrapper<T> queryWrapper) {
return selectOne(queryWrapper.last("FOR UPDATE"));
}
default T selectOneForUpdate(SFunction<T, ?> field, Object value) {
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field, value));
}
default T selectOneForUpdate(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
}
default T selectOneForUpdate(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2,
SFunction<T, ?> field3, Object value3) {
return selectOneForUpdate(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2).eq(field3, value3));
}
/** /**
* 1 * 1
* *
@ -170,17 +145,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return CollUtil.getFirst(list); return CollUtil.getFirst(list);
} }
/**
*
* <p>
* 使 selectOne
*
* @param queryWrapper
* @return null
*/
default T selectLastOne(LambdaQueryWrapper<T> queryWrapper) {
return CollUtil.getLast(selectList(queryWrapper));
}
default Long selectCount() { default Long selectCount() {
return selectCount(new QueryWrapper<>()); return selectCount(new QueryWrapper<>());

View File

@ -24,12 +24,6 @@ public class LambdaQueryWrapperX<T> extends LambdaQueryWrapper<T> {
} }
return this; return this;
} }
public LambdaQueryWrapperX<T> likeRightIfPresent(SFunction<T, ?> column, String val) {
if (StringUtils.hasText(val)) {
return (LambdaQueryWrapperX<T>) super.likeRight(column, val);
}
return this;
}
public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) { public LambdaQueryWrapperX<T> inIfPresent(SFunction<T, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {

View File

@ -27,13 +27,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this; return this;
} }
public <S> MPJLambdaWrapperX<T> likeRightIfPresent(SFunction<S, ?> column, String val) {
if (StringUtils.hasText(val)) {
return (MPJLambdaWrapperX<T>) super.likeRight(column, val);
}
return this;
}
public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) { public <S> MPJLambdaWrapperX<T> inIfPresent(SFunction<S, ?> column, Collection<?> values) {
if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) { if (ObjectUtil.isAllNotEmpty(values) && !ArrayUtil.isEmpty(values)) {
return (MPJLambdaWrapperX<T>) super.in(column, values); return (MPJLambdaWrapperX<T>) super.in(column, values);
@ -109,6 +102,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
return this; return this;
} }
// ========== 重写父类方法,方便链式调用 ========== // ========== 重写父类方法,方便链式调用 ==========
@Override @Override

View File

@ -25,13 +25,6 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
return this; return this;
} }
public QueryWrapperX<T> likeRightIfPresent(String column, String val) {
if (StringUtils.hasText(val)) {
return (QueryWrapperX<T>) super.likeRight(column, val);
}
return this;
}
public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) { public QueryWrapperX<T> inIfPresent(String column, Collection<?> values) {
if (!CollectionUtils.isEmpty(values)) { if (!CollectionUtils.isEmpty(values)) {
return (QueryWrapperX<T>) super.in(column, values); return (QueryWrapperX<T>) super.in(column, values);
@ -102,13 +95,13 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
} }
public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) { public QueryWrapperX<T> betweenIfPresent(String column, Object[] values) {
if (values != null && values.length != 0 && values[0] != null && values[1] != null) { if (values!= null && values.length != 0 && values[0] != null && values[1] != null) {
return (QueryWrapperX<T>) super.between(column, values[0], values[1]); return (QueryWrapperX<T>) super.between(column, values[0], values[1]);
} }
if (values != null && values.length != 0 && values[0] != null) { if (values!= null && values.length != 0 && values[0] != null) {
return (QueryWrapperX<T>) ge(column, values[0]); return (QueryWrapperX<T>) ge(column, values[0]);
} }
if (values != null && values.length != 0 && values[1] != null) { if (values!= null && values.length != 0 && values[1] != null) {
return (QueryWrapperX<T>) le(column, values[1]); return (QueryWrapperX<T>) le(column, values[1]);
} }
return this; return this;

View File

@ -23,7 +23,6 @@ import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.regex.Pattern;
/** /**
* MyBatis * MyBatis
@ -32,12 +31,6 @@ public class MyBatisUtils {
private static final String MYSQL_ESCAPE_CHARACTER = "`"; private static final String MYSQL_ESCAPE_CHARACTER = "`";
private static final Pattern SAFE_COLUMN_NAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)*$");
private static final String FIND_IN_SET_VALUE_PLACEHOLDER = "#{value}";
private static final String FIND_IN_SET_COLUMN_PLACEHOLDER = "#{column}";
public static <T> Page<T> buildPage(PageParam pageParam) { public static <T> Page<T> buildPage(PageParam pageParam) {
return buildPage(pageParam, null); return buildPage(pageParam, null);
} }
@ -49,11 +42,8 @@ public class MyBatisUtils {
// 排序字段 // 排序字段
if (CollUtil.isNotEmpty(sortingFields)) { if (CollUtil.isNotEmpty(sortingFields)) {
for (SortingField sortingField : sortingFields) { for (SortingField sortingField : sortingFields) {
String columnName = buildSafeOrderColumn(sortingField.getField()); page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder()))
if (columnName == null) { .setColumn(StrUtil.toUnderlineCase(sortingField.getField())));
continue;
}
page.addOrder(new OrderItem().setAsc(isAscOrder(sortingField.getOrder())).setColumn(columnName));
} }
} }
return page; return page;
@ -67,29 +57,23 @@ public class MyBatisUtils {
if (wrapper instanceof QueryWrapper) { if (wrapper instanceof QueryWrapper) {
QueryWrapper<T> query = (QueryWrapper<T>) wrapper; QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
for (SortingField sortingField : sortingFields) { for (SortingField sortingField : sortingFields) {
String columnName = buildSafeOrderColumn(sortingField.getField()); query.orderBy(true,
if (columnName == null) { SortingField.ORDER_ASC.equals(sortingField.getOrder()),
continue; StrUtil.toUnderlineCase(sortingField.getField()));
}
query.orderBy(true, isAscOrder(sortingField.getOrder()), columnName);
} }
} else if (wrapper instanceof LambdaQueryWrapper) { } else if (wrapper instanceof LambdaQueryWrapper) {
// LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY // LambdaQueryWrapper 不直接支持字符串字段排序,使用 last 方法拼接 ORDER BY
LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper; LambdaQueryWrapper<T> lambdaQuery = (LambdaQueryWrapper<T>) wrapper;
StringBuilder orderBy = new StringBuilder(); StringBuilder orderBy = new StringBuilder();
for (SortingField sortingField : sortingFields) { for (SortingField sortingField : sortingFields) {
String columnName = buildSafeOrderColumn(sortingField.getField());
if (columnName == null) {
continue;
}
if (StrUtil.isNotEmpty(orderBy)) { if (StrUtil.isNotEmpty(orderBy)) {
orderBy.append(", "); orderBy.append(", ");
} }
orderBy.append(columnName).append(" ").append(getOrderDirection(sortingField.getOrder())); orderBy.append(StrUtil.toUnderlineCase(sortingField.getField()))
} .append(" ")
if (StrUtil.isNotEmpty(orderBy)) { .append(SortingField.ORDER_ASC.equals(sortingField.getOrder()) ? "ASC" : "DESC");
lambdaQuery.last("ORDER BY " + orderBy);
} }
lambdaQuery.last("ORDER BY " + orderBy);
// 另外个思路https://blog.csdn.net/m0_59084856/article/details/138450913 // 另外个思路https://blog.csdn.net/m0_59084856/article/details/138450913
} else { } else {
throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName()); throw new IllegalArgumentException("Unsupported wrapper type: " + wrapper.getClass().getName());
@ -97,22 +81,6 @@ public class MyBatisUtils {
} }
public static boolean isAscOrder(String order) {
return SortingField.ORDER_ASC.equals(order);
}
public static String getOrderDirection(String order) {
return isAscOrder(order) ? "ASC" : "DESC";
}
private static String buildSafeOrderColumn(String field) {
String columnName = StrUtil.toUnderlineCase(field);
if (StrUtil.isEmpty(columnName) || !SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches()) {
return null;
}
return columnName;
}
/** /**
* *
* MybatisPlusInterceptor * MybatisPlusInterceptor
@ -161,43 +129,15 @@ public class MyBatisUtils {
/** /**
* find_in_set * find_in_set
* *
* @param columnName * @param column
* @param value ()
* @return sql * @return sql
*/ */
public static String findInSet(String columnName) { public static String findInSet(String column, Object value) {
return findInSet(columnName, 0);
}
/**
* find_in_set apply
*
* @param columnName
* @param paramIndex apply
* @return sql
*/
public static String findInSetWithParamIndex(String columnName, int paramIndex) {
return findInSet(columnName, paramIndex);
}
private static String findInSet(String columnName, int paramIndex) {
DbType dbType = JdbcUtils.getDbType(); DbType dbType = JdbcUtils.getDbType();
return findInSet(dbType, columnName, paramIndex);
}
static String findInSet(DbType dbType, String columnName, int paramIndex) {
if (!isSafeColumnName(columnName)) {
throw new IllegalArgumentException("Invalid column name: " + columnName);
}
if (paramIndex < 0) {
throw new IllegalArgumentException("Invalid param index: " + paramIndex);
}
return DbTypeEnum.getFindInSetTemplate(dbType) return DbTypeEnum.getFindInSetTemplate(dbType)
.replace(FIND_IN_SET_COLUMN_PLACEHOLDER, columnName) .replace("#{column}", column)
.replace(FIND_IN_SET_VALUE_PLACEHOLDER, "{" + paramIndex + "}"); .replace("#{value}", StrUtil.toString(value));
}
private static boolean isSafeColumnName(String columnName) {
return StrUtil.isNotEmpty(columnName) && SAFE_COLUMN_NAME_PATTERN.matcher(columnName).matches();
} }
/** /**

View File

@ -1,173 +0,0 @@
package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.junit.jupiter.api.Test;
import java.util.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* {@link MyBatisUtils}
*/
public class MyBatisUtilsTest {
@Test
public void testBuildPage_sortingFields() {
// 准备参数
PageParam pageParam = new PageParam();
pageParam.setPageNo(2);
pageParam.setPageSize(20);
List<SortingField> sortingFields = Arrays.asList(
new SortingField("userName", SortingField.ORDER_ASC),
new SortingField("u.id", SortingField.ORDER_DESC),
new SortingField("name desc", SortingField.ORDER_DESC));
// 调用
Page<Object> page = MyBatisUtils.buildPage(pageParam, sortingFields);
// 断言
assertEquals(2, page.getCurrent());
assertEquals(20, page.getSize());
assertEquals(2, page.orders().size());
assertOrderItem(page.orders().get(0), "user_name", true);
assertOrderItem(page.orders().get(1), "u.id", false);
}
@Test
public void testAddOrder_queryWrapper() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
List<SortingField> sortingFields = Arrays.asList(
new SortingField("userName", SortingField.ORDER_ASC),
new SortingField("u.id", SortingField.ORDER_DESC),
new SortingField("name;drop", SortingField.ORDER_ASC));
// 调用
MyBatisUtils.addOrder(query, sortingFields);
// 断言
assertEquals(" ORDER BY user_name ASC,u.id DESC", query.getSqlSegment());
}
@Test
public void testAddOrder_lambdaQueryWrapper() {
// 准备参数
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
List<SortingField> sortingFields = Arrays.asList(
new SortingField("userName", SortingField.ORDER_ASC),
new SortingField("u.id", SortingField.ORDER_DESC),
new SortingField("name`", SortingField.ORDER_ASC));
// 调用
MyBatisUtils.addOrder(query, sortingFields);
// 断言
assertEquals(" ORDER BY user_name ASC, u.id DESC", query.getSqlSegment());
}
@Test
public void testAddOrder_lambdaQueryWrapper_invalidSortingFields() {
// 准备参数
LambdaQueryWrapper<Object> query = new LambdaQueryWrapper<>();
List<SortingField> sortingFields = Arrays.asList(
new SortingField("name desc", SortingField.ORDER_ASC),
new SortingField("name;drop", SortingField.ORDER_DESC));
// 调用
MyBatisUtils.addOrder(query, sortingFields);
// 断言
assertEquals("", query.getSqlSegment());
}
@Test
public void testOrderDirection() {
assertTrue(MyBatisUtils.isAscOrder(SortingField.ORDER_ASC));
assertFalse(MyBatisUtils.isAscOrder(SortingField.ORDER_DESC));
assertEquals("ASC", MyBatisUtils.getOrderDirection(SortingField.ORDER_ASC));
assertEquals("DESC", MyBatisUtils.getOrderDirection(SortingField.ORDER_DESC));
assertEquals("DESC", MyBatisUtils.getOrderDirection(null));
}
@Test
public void testFindInSet() {
assertEquals("FIND_IN_SET({0}, websites) <> 0",
MyBatisUtils.findInSet(DbType.MYSQL, "websites", 0));
assertEquals("POSITION(',' || CAST({0} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.H2, "websites", 0));
assertEquals("INSTR(',' || t.websites || ',', ',' || {0} || ',') > 0",
MyBatisUtils.findInSet(DbType.ORACLE, "t.websites", 0));
assertEquals("POSITION(',' || CAST({1} AS VARCHAR) || ',' IN ',' || websites || ',') > 0",
MyBatisUtils.findInSet(DbType.POSTGRE_SQL, "websites", 1));
assertEquals("CHARINDEX(',' + CAST({2} AS varchar(255)) + ',', ',' + websites + ',') > 0",
MyBatisUtils.findInSet(DbType.SQL_SERVER, "websites", 2));
}
@Test
public void testFindInSet_invalidColumnName() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites;drop table system_tenant", 0));
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "FIND_IN_SET(value, websites)", 0));
}
@Test
public void testFindInSet_invalidParamIndex() {
assertThrows(IllegalArgumentException.class,
() -> MyBatisUtils.findInSet(DbType.MYSQL, "websites", -1));
}
@Test
public void testFindInSet_applyBindsValue() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value = "test' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "to_mails", 0), value);
// 断言SQL 片段里只有 MyBatis Plus 参数占位,用户输入不会被直接拼接进去
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, to_mails) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value));
assertEquals(value, query.getParamNameValuePairs().get("MPGENVAL1"));
}
@Test
public void testFindInSet_applyBindsMultipleValues() {
// 准备参数
QueryWrapper<Object> query = new QueryWrapper<>();
String value1 = "1' OR 1 = 1";
String value2 = "2' OR 1 = 1";
// 调用
query.apply(MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 0)
+ " OR " + MyBatisUtils.findInSet(DbType.MYSQL, "tag_ids", 1), value1, value2);
// 断言:多个参数都由 MyBatis Plus 生成占位符,不拼接用户输入
assertEquals("(FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL1}, tag_ids) <> 0"
+ " OR FIND_IN_SET(#{ew.paramNameValuePairs.MPGENVAL2}, tag_ids) <> 0)",
query.getSqlSegment());
assertFalse(query.getSqlSegment().contains(value1));
assertFalse(query.getSqlSegment().contains(value2));
assertEquals(value1, query.getParamNameValuePairs().get("MPGENVAL1"));
assertEquals(value2, query.getParamNameValuePairs().get("MPGENVAL2"));
}
private void assertOrderItem(OrderItem orderItem, String column, boolean asc) {
assertEquals(column, orderItem.getColumn());
assertEquals(asc, orderItem.isAsc());
}
}

View File

@ -75,12 +75,8 @@ public class YudaoCacheAutoConfiguration {
RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory()); RedisConnectionFactory connectionFactory = Objects.requireNonNull(redisTemplate.getConnectionFactory());
RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory, RedisCacheWriter cacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory,
BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize())); BatchStrategies.scan(yudaoCacheProperties.getRedisScanBatchSize()));
// 创建 TimeoutRedisCacheManager 对象 // 创建 TenantRedisCacheManager 对象
TimeoutRedisCacheManager cacheManager = new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration); return new TimeoutRedisCacheManager(cacheWriter, redisCacheConfiguration);
// 开启事务感知:@Transactional 方法内的 @CacheEvict / @CachePut 自动延迟到 afterCommit
// 避免事务未提交就清缓存被并发读穿写脏值;无事务时立即生效,行为不变
cacheManager.setTransactionAware(true);
return cacheManager;
} }
} }

View File

@ -36,22 +36,11 @@
<groupId>io.github.openfeign</groupId> <groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId> <artifactId>feign-okhttp</artifactId>
</dependency> </dependency>
<!--
TODO 芋艿WxJava 4.8.x 的 AbstractWxMpConfigStorageConfiguration 仍引用了 HttpClient 4.x 的
org.apache.http.ssl.TrustStrategy 类。升级 Spring Cloud Alibaba 到 2025.0.0.0 后Nacos 不再
传递 HttpClient 4.xhttpcore导致 ClassNotFoundException。
临时解决:显式引入 httpclient 4.x。待 WxJava 修复后移除。
-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- 工具相关 --> <!-- 工具相关 -->
<dependency> <dependency>
<groupId>javax.validation</groupId> <groupId>jakarta.validation</groupId>
<artifactId>validation-api</artifactId> <artifactId>jakarta.validation-api</artifactId>
</dependency> </dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -9,7 +9,6 @@ import uk.co.jemos.podam.api.PodamFactory;
import uk.co.jemos.podam.api.PodamFactoryImpl; import uk.co.jemos.podam.api.PodamFactoryImpl;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.math.BigDecimal;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
@ -53,10 +52,6 @@ public class RandomUtils {
} }
return RandomUtil.randomInt(); return RandomUtil.randomInt();
}); });
// BigDecimal限制精度在 DECIMAL(10,2) 范围内,避免 H2 等数据库溢出
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(BigDecimal.class,
(dataProviderStrategy, attributeMetadata, map) ->
BigDecimal.valueOf(RandomUtil.randomInt(0, 10000000), 2));
// LocalDateTime // LocalDateTime
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class, PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
(dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime()); (dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());

View File

@ -69,7 +69,7 @@ public class ApiAccessLogFilter extends ApiRequestFilter {
LocalDateTime beginTime = LocalDateTime.now(); LocalDateTime beginTime = LocalDateTime.now();
// 提前获得参数,避免 XssFilter 过滤处理 // 提前获得参数,避免 XssFilter 过滤处理
Map<String, String> queryString = ServletUtils.getParamMap(request); Map<String, String> queryString = ServletUtils.getParamMap(request);
String requestBody = ServletUtils.getBody(request); String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
try { try {
// 继续过滤器 // 继续过滤器

View File

@ -44,7 +44,7 @@ public class ApiAccessLogInterceptor implements HandlerInterceptor {
// 打印 request 日志 // 打印 request 日志
if (!SpringUtils.isProd()) { if (!SpringUtils.isProd()) {
Map<String, String> queryString = ServletUtils.getParamMap(request); Map<String, String> queryString = ServletUtils.getParamMap(request);
String requestBody = ServletUtils.getBody(request); String requestBody = ServletUtils.isJsonRequest(request) ? ServletUtils.getBody(request) : null;
if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) { if (CollUtil.isEmpty(queryString) && StrUtil.isEmpty(requestBody)) {
log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI()); log.info("[preHandle][开始请求 URL({}) 无参数]", request.getRequestURI());
} else { } else {

View File

@ -37,14 +37,8 @@ public class BannerApplicationRunner implements ApplicationRunner {
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
// ERP 系统 // ERP 系统
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
// WMS 仓库管理系统
System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
// CRM 系统 // CRM 系统
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
// MES 系统
System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
// IM 即时通讯
System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]");
// 微信公众号 // 微信公众号
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
// 支付平台 // 支付平台

View File

@ -410,43 +410,25 @@ public class GlobalExceptionHandler {
return CommonResult.error(NOT_IMPLEMENTED.getCode(), return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); "[ERP 系统 yudao-module-erp - 表结构未导入][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
} }
// 6. WMS 仓库管理系统 // 6. CRM 系统
if (message.contains("wms_")) {
log.error("[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[WMS 仓库管理系统 yudao-module-wms - 表结构未导入][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
}
// 7. CRM 系统
if (message.contains("crm_")) { if (message.contains("crm_")) {
log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); log.error("[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(), return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); "[CRM 系统 yudao-module-crm - 表结构未导入][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
} }
// 8. MES 系统 // 7. 支付平台
if (message.contains("mes_")) {
log.error("[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[MES 系统 yudao-module-mes - 表结构未导入][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
}
// 9. IM 即时通讯
if (message.contains("im_")) {
log.error("[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[IM 即时通讯 yudao-module-im - 表结构未导入][参考 https://cloud.iocoder.cn/im/build/ 开启]");
}
// 10. 支付平台
if (message.contains("pay_")) { if (message.contains("pay_")) {
log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); log.error("[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(), return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]"); "[支付模块 yudao-module-pay - 表结构未导入][参考 https://cloud.iocoder.cn/pay/build/ 开启]");
} }
// 11. AI 大模型 // 8. AI 大模型
if (message.contains("ai_")) { if (message.contains("ai_")) {
log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); log.error("[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(), return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
} }
// 12. IoT 物联网 // 9. IoT 物联网
if (message.contains("iot_")) { if (message.contains("iot_")) {
log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]"); log.error("[IoT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(), return CommonResult.error(NOT_IMPLEMENTED.getCode(),

View File

@ -76,7 +76,7 @@ public class JsonWebSocketMessageHandler extends TextWebSocketHandler {
Long tenantId = WebSocketFrameworkUtils.getTenantId(session); Long tenantId = WebSocketFrameworkUtils.getTenantId(session);
TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj)); TenantUtils.execute(tenantId, () -> messageListener.onMessage(session, messageObj));
} catch (Throwable ex) { } catch (Throwable ex) {
log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload(), ex); log.error("[handleTextMessage][session({}) message({}) 处理异常]", session.getId(), message.getPayload());
} }
} }

View File

@ -27,10 +27,9 @@ public class LoginUserHandshakeInterceptor implements HandshakeInterceptor {
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) { WebSocketHandler wsHandler, Map<String, Object> attributes) {
LoginUser loginUser = SecurityFrameworkUtils.getLoginUser(); LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
if (loginUser == null) { if (loginUser != null) {
return false; WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
} }
WebSocketFrameworkUtils.setLoginUser(loginUser, attributes);
return true; return true;
} }

View File

@ -40,6 +40,11 @@
<artifactId>javax.servlet-api</artifactId> <artifactId>javax.servlet-api</artifactId>
</dependency> </dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <!-- 接口文档 --> <groupId>com.github.xiaoymin</groupId> <!-- 接口文档 -->
<artifactId>knife4j-gateway-spring-boot-starter</artifactId> <artifactId>knife4j-gateway-spring-boot-starter</artifactId>

View File

@ -39,14 +39,8 @@ public class BannerApplicationRunner implements ApplicationRunner {
System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]"); System.out.println("[商城系统 yudao-module-mall 教程][参考 https://cloud.iocoder.cn/mall/build/ 开启]");
// ERP 系统 // ERP 系统
System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]"); System.out.println("[ERP 系统 yudao-module-erp - 教程][参考 https://cloud.iocoder.cn/erp/build/ 开启]");
// WMS 仓库管理系统
System.out.println("[WMS 仓库管理系统 yudao-module-wms - 教程][参考 https://cloud.iocoder.cn/wms/build/ 开启]");
// CRM 系统 // CRM 系统
System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]"); System.out.println("[CRM 系统 yudao-module-crm - 教程][参考 https://cloud.iocoder.cn/crm/build/ 开启]");
// MES 系统
System.out.println("[MES 系统 yudao-module-mes - 教程][参考 https://cloud.iocoder.cn/mes/build/ 开启]");
// IM 即时通讯
System.out.println("[IM 即时通讯 yudao-module-im - 教程][参考 https://cloud.iocoder.cn/im/build/ 开启]");
// 微信公众号 // 微信公众号
System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]"); System.out.println("[微信公众号 yudao-module-mp 教程][参考 https://cloud.iocoder.cn/mp/build/ 开启]");
// 支付平台 // 支付平台

View File

@ -192,27 +192,6 @@ spring:
- Path=/admin-api/iot/** - Path=/admin-api/iot/**
filters: filters:
- RewritePath=/admin-api/iot/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs - RewritePath=/admin-api/iot/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## mes-server 服务
- id: mes-admin-api # 路由的编号
uri: grayLb://mes-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/mes/**
filters:
- RewritePath=/admin-api/mes/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## wms-server 服务
- id: wms-admin-api # 路由的编号
uri: grayLb://wms-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/wms/**
filters:
- RewritePath=/admin-api/wms/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
## im-server 服务
- id: im-admin-api # 路由的编号
uri: grayLb://im-server
predicates: # 断言,作为路由的匹配条件,对应 RouteDefinition 数组
- Path=/admin-api/im/**
filters:
- RewritePath=/admin-api/im/v3/api-docs, /v3/api-docs # 配置,保证转发到 /v3/api-docs
x-forwarded: x-forwarded:
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀 prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组 default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
@ -272,15 +251,6 @@ knife4j:
- name: iot-server - name: iot-server
service-name: iot-server service-name: iot-server
url: /admin-api/iot/v3/api-docs url: /admin-api/iot/v3/api-docs
- name: mes-server
service-name: mes-server
url: /admin-api/mes/v3/api-docs
- name: wms-server
service-name: wms-server
url: /admin-api/wms/v3/api-docs
- name: im-server
service-name: im-server
url: /admin-api/im/v3/api-docs
--- #################### 芋道相关配置 #################### --- #################### 芋道相关配置 ####################

View File

@ -19,9 +19,9 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno 国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description> </description>
<properties> <properties>
<spring-ai.version>1.1.5</spring-ai.version> <spring-ai.version>1.1.2</spring-ai.version>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba --> <!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
<alibaba-ai.version>1.1.2.2</alibaba-ai.version> <alibaba-ai.version>1.1.0.0-RC2</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version> <tinyflow.version>1.2.6</tinyflow.version>
</properties> </properties>

View File

@ -83,15 +83,6 @@ public class AiKnowledgeSegmentController {
return success(true); return success(true);
} }
@DeleteMapping("/delete")
@Operation(summary = "删除段落")
@Parameter(name = "id", description = "段落编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:knowledge:delete')")
public CommonResult<Boolean> deleteKnowledgeSegment(@RequestParam("id") Long id) {
segmentService.deleteKnowledgeSegment(id);
return success(true);
}
@GetMapping("/split") @GetMapping("/split")
@Operation(summary = "切片内容") @Operation(summary = "切片内容")
@Parameters({ @Parameters({

View File

@ -109,7 +109,6 @@ public class AiImageServiceImpl implements AiImageService {
} }
@Async @Async
@SuppressWarnings("ConstantValue")
public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) { public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) {
try { try {
// 1.1 构建请求 // 1.1 构建请求
@ -165,8 +164,8 @@ public class AiImageServiceImpl implements AiImageService {
.build(); .build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) { } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) {
return DashScopeImageOptions.builder() return DashScopeImageOptions.builder()
.model(model.getModel()).n(1) .withModel(model.getModel()).withN(1)
.height(draw.getHeight()).width(draw.getWidth()) .withHeight(draw.getHeight()).withWidth(draw.getWidth())
.build(); .build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) { } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
return QianFanImageOptions.builder() return QianFanImageOptions.builder()

View File

@ -98,13 +98,6 @@ public interface AiKnowledgeSegmentService {
*/ */
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
/**
*
*
* @param id
*/
void deleteKnowledgeSegment(Long id);
/** /**
* *
* *

View File

@ -141,19 +141,6 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
} }
} }
@Override
public void deleteKnowledgeSegment(Long id) {
// 1. 校验段落存在
AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(id);
// 2. 删除向量
VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId());
deleteVectorStore(vectorStore, segment);
// 3. 删除段落记录
segmentMapper.deleteById(id);
}
@Override @Override
public void deleteKnowledgeSegmentByDocumentId(Long documentId) { public void deleteKnowledgeSegmentByDocumentId(Long documentId) {
// 1. 查询需要删除的段落 // 1. 查询需要删除的段落

View File

@ -84,7 +84,7 @@ public class UserProfileQueryToolFunction
request.setId(loginUser.getId()); request.setId(loginUser.getId());
} }
return TenantUtils.execute(tenantId, () -> { return TenantUtils.execute(tenantId, () -> {
AdminUserRespDTO user = adminUserApi.getUser(request.getId()); AdminUserRespDTO user = adminUserApi.getUser(request.getId()).getCheckedData();
return BeanUtils.toBean(user, Response.class); return BeanUtils.toBean(user, Response.class);
}); });
} }

View File

@ -1,9 +1,7 @@
package cn.iocoder.yudao.module.ai.util; package cn.iocoder.yudao.module.ai.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
@ -35,28 +33,6 @@ public class AiUtils {
public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER"; public static final String TOOL_CONTEXT_LOGIN_USER = "LOGIN_USER";
public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID"; public static final String TOOL_CONTEXT_TENANT_ID = "TENANT_ID";
/**
*
*
* @see <a href="https://bailian.console.aliyun.com/cn-beijing/?tab=model#/model-market/all?providers=qwen&capabilities=VU">广</a>
* @see <a href="https://help.aliyun.com/zh/model-studio/error-code#error-url"> withMultiModel </a>
*/
public static final Set<String> TONG_YI_MULTI_MODELS = SetUtils.asSet(
// qwen3.5 / 3.6 系列(统一多模态主干)
"qwen3.6-plus", "qwen3.6-flash",
"qwen3.5-plus", "qwen3.5-flash",
// qwen-vl 视觉理解
"qwen3-vl-plus", "qwen3-vl-flash",
"qwen-vl-max", "qwen-vl-plus",
"qwen2.5-vl-72b-instruct", "qwen2.5-vl-32b-instruct",
"qwen2.5-vl-7b-instruct", "qwen2.5-vl-3b-instruct",
// qvq 视觉推理
"qvq-max", "qvq-plus",
// qwen-omni 全模态
"qwen3.5-omni-plus", "qwen3.5-omni-flash",
"qwen3-omni-flash", "qwen-omni-turbo"
);
public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) { public static ChatOptions buildChatOptions(AiPlatformEnum platform, String model, Double temperature, Integer maxTokens) {
return buildChatOptions(platform, model, temperature, maxTokens, null, null); return buildChatOptions(platform, model, temperature, maxTokens, null, null);
} }
@ -68,10 +44,9 @@ public class AiUtils {
// noinspection EnhancedSwitchMigration // noinspection EnhancedSwitchMigration
switch (platform) { switch (platform) {
case TONG_YI: case TONG_YI:
return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens) return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置 .withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型 .withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
case YI_YAN: case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case DEEP_SEEK: case DEEP_SEEK:
@ -150,13 +125,10 @@ public class AiUtils {
|| response.getResult().getOutput() == null) { || response.getResult().getOutput() == null) {
return null; return null;
} }
AssistantMessage output = response.getResult().getOutput(); if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
if (output instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) output).getReasoningContent();
} }
// 通义千问等通过 metadata 透传 reasoningContent return null;
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
} }
} }

View File

@ -103,7 +103,7 @@ xxl:
# Lock4j 配置项 # Lock4j 配置项
lock4j: lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 #################### --- #################### 监控相关配置 ####################

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项 # Lock4j 配置项
lock4j: lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 #################### --- #################### 监控相关配置 ####################

View File

@ -34,15 +34,15 @@ public class TongYiChatModelTests {
private final DashScopeChatModel chatModel = DashScopeChatModel.builder() private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder() .dashScopeApi(DashScopeApi.builder()
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥 .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build()) .build())
.defaultOptions(DashScopeChatOptions.builder() .defaultOptions(DashScopeChatOptions.builder()
.multiModel(true) // 注意:当使用 qwen3.6-plus 等多模态模型,需要设置为 true可见 https://help.aliyun.com/zh/model-studio/error-code#error-url 链接 // .withModel("qwen1.5-72b-chat") // 模型
.model("qwen3.6-plus") // 模型 .withModel("qwen3-235b-a22b-thinking-2507") // 模型
// .model("deepseek-r1") // 模型deepseek-r1 // .withModel("deepseek-r1") // 模型deepseek-r1
// .model("deepseek-v3") // 模型deepseek-v3 // .withModel("deepseek-v3") // 模型deepseek-v3
// .model("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
// .enableThinking(true) // .withEnableThinking(true)
.build()) .build())
.build(); .build();
@ -85,9 +85,9 @@ public class TongYiChatModelTests {
List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?")); messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DashScopeChatOptions options = DashScopeChatOptions.builder() DashScopeChatOptions options = DashScopeChatOptions.builder()
.model("qwen3.6-plus").multiModel(true) .withModel("qwen3-235b-a22b-thinking-2507")
// .withModel("qwen-max-2025-01-25") // .withModel("qwen-max-2025-01-25")
.enableThinking(true) // 必须设置,否则会报错 .withEnableThinking(true) // 必须设置,否则会报错
.build(); .build();
// 调用 // 调用
@ -112,8 +112,8 @@ public class TongYiChatModelTests {
Document document01 = new Document("abc"); Document document01 = new Document("abc");
Document document02 = new Document("sapring"); Document document02 = new Document("sapring");
RerankOptions options = DashScopeRerankOptions.builder() RerankOptions options = DashScopeRerankOptions.builder()
.topN(1) .withTopN(1)
.model("gte-rerank-v2") .withModel("gte-rerank-v2")
.build(); .build();
RerankRequest rerankRequest = new RerankRequest( RerankRequest rerankRequest = new RerankRequest(
query, query,

View File

@ -12,34 +12,23 @@ import org.springframework.ai.image.ImageResponse;
/** /**
* {@link DashScopeImageModel} * {@link DashScopeImageModel}
* *
* TODO @spring-ai-alibaba-dashscope1.1.2.2 {@code DashScopeImageApi#resolveImagePath} {@code wan2.7-image}
* {@code text2image/image-synthesis} + {@code prompt}
* {@code multimodal-generation/generation} + {@code messages}
* SDK
*
* SDK {@code wan2.6-image} {@code qwen-image}
* {@code DashScopeImageApi} {@code wan2.7*} {@code wan2.6-image} {@code image-generation/generation}
*
* @author fansili * @author fansili
*/ */
public class TongYiImagesModelTest { public class TongYiImagesModelTest {
private final DashScopeImageModel imageModel = DashScopeImageModel.builder() private final DashScopeImageModel imageModel = DashScopeImageModel.builder()
.dashScopeApi(DashScopeImageApi.builder() .dashScopeApi(DashScopeImageApi.builder()
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥 .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build()) .build())
.build(); .build();
// TODO @芋艿:
@Test @Test
@Disabled @Disabled
public void imageCallTest() { public void imageCallTest() {
// 准备参数 // 准备参数
ImageOptions options = DashScopeImageOptions.builder() ImageOptions options = DashScopeImageOptions.builder()
.model("wan2.7-image") .withModel("wanx-v1")
// .withSize("2k") .withHeight(256).withWidth(256)
.height(768).width(768)
.n(1)
.build(); .build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options); ImagePrompt prompt = new ImagePrompt("中国长城!", options);

View File

@ -1,38 +0,0 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* BPM
*
* @author Lesan
*/
@RequiredArgsConstructor
@Getter
public enum BpmConditionOpCodeEnum {
EQ("==", "等于", " var:getOrDefault(%s, null) == %s "),
NE("!=", "不等于", " var:getOrDefault(%s, null) != %s "),
GT(">", "大于", " var:getOrDefault(%s, null) > %s "),
GE(">=", "大于等于", " var:getOrDefault(%s, null) >= %s "),
LT("<", "小于", " var:getOrDefault(%s, null) < %s "),
LE("<=", "小于等于", " var:getOrDefault(%s, null) <= %s "),
CONTAINS("contain", "包含", " var:contains(%s, %s) "),
NOT_CONTAINS("!contain", "不包含", " !var:contains(%s, %s) ");
private final String code;
private final String des;
private final String symbol;
public static BpmConditionOpCodeEnum fromCode(String code) {
for (BpmConditionOpCodeEnum op : BpmConditionOpCodeEnum.values()) {
if (op.code.equalsIgnoreCase(code)) {
return op;
}
}
throw new IllegalArgumentException("未知操作符: " + code);
}
}

View File

@ -2,8 +2,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
import lombok.Data; import lombok.Data;
import java.util.List;
/** /**
* VO * VO
*/ */
@ -22,9 +20,5 @@ public class BpmFormFieldVO {
* *
*/ */
private String title; private String title;
/**
*
*/
private List<BpmFormFieldVO> children;
} }

View File

@ -143,10 +143,10 @@ public class BpmProcessInstanceController {
processInstance.getProcessDefinitionId()); processInstance.getProcessDefinitionId());
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo( BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(
processInstance.getProcessDefinitionId()); processInstance.getProcessDefinitionId());
AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())); AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId())).getCheckedData();
DeptRespDTO dept = null; DeptRespDTO dept = null;
if (startUser != null && startUser.getDeptId() != null) { if (startUser != null && startUser.getDeptId() != null) {
dept = deptApi.getDept(startUser.getDeptId()); dept = deptApi.getDept(startUser.getDeptId()).getCheckedData();
} }
return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance, return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstance(processInstance,
processDefinition, processDefinitionInfo, startUser, dept)); processDefinition, processDefinitionInfo, startUser, dept));
@ -211,8 +211,8 @@ public class BpmProcessInstanceController {
if (historicProcessInstance == null) { if (historicProcessInstance == null) {
throw exception(PROCESS_INSTANCE_NOT_EXISTS); throw exception(PROCESS_INSTANCE_NOT_EXISTS);
} }
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId())); AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(historicProcessInstance.getStartUserId())).getCheckedData();
DeptRespDTO dept = deptApi.getDept(startUser.getDeptId()); DeptRespDTO dept = deptApi.getDept(startUser.getDeptId()).getCheckedData();
List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId); List<HistoricTaskInstance> tasks = taskService.getFinishedTaskListByProcessInstanceIdWithoutCancel(processInstanceId);
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap( Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(
convertSet(tasks, item -> Long.valueOf(item.getAssignee()))); convertSet(tasks, item -> Long.valueOf(item.getAssignee())));

View File

@ -66,15 +66,15 @@ public class BpmProcessInstanceCopyController {
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap( Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId)); convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(), Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
copy -> Stream.of(copy.getStartUserId(), copy.getUserId()))); copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator()))));
Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId)); convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId));
return success(convertPage(pageResult, copy -> { return success(convertPage(pageResult, copy -> {
BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class); BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class);
MapUtils.findAndThen(userMap, copy.getUserId(), MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class))); user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(), MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
processInstance -> { processInstance -> {
copyVO.setSummary(FlowableUtils.getSummary( copyVO.setSummary(FlowableUtils.getSummary(

View File

@ -42,7 +42,7 @@ public class BpmOALeaveDO extends BaseDO {
/** /**
* *
*/ */
private Integer type; private String type;
/** /**
* *
*/ */

View File

@ -14,7 +14,6 @@ import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
import org.flowable.engine.impl.persistence.entity.ExecutionEntity; import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -54,7 +53,7 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class); Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
if (assigneeUserIds == null) { if (assigneeUserIds == null) {
assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution)); assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
if (CollUtil.isEmpty(assigneeUserIds)) { if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务

View File

@ -57,7 +57,7 @@ public class BpmTaskAssignLeaderExpression {
return emptySet(); return emptySet();
} }
} else { } else {
DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()); DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()).getCheckedData();
if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少 if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少
break; break;
} }
@ -68,11 +68,11 @@ public class BpmTaskAssignLeaderExpression {
} }
private DeptRespDTO getStartUserDept(Long startUserId) { private DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId); AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData();
if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则 if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则
return null; return null;
} }
return deptApi.getDept(startUser.getDeptId()); return deptApi.getDept(startUser.getDeptId()).getCheckedData();
} }
} }

View File

@ -40,7 +40,7 @@ public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmT
} }
DeptRespDTO currentDept = dept; DeptRespDTO currentDept = dept;
for (int i = 1; i < level; i++) { for (int i = 1; i < level; i++) {
DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId()); DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId()).getCheckedData();
if (parentDept == null) { // 找不到父级部门,到了最高级。返回最高级的部门负责人 if (parentDept == null) { // 找不到父级部门,到了最高级。返回最高级的部门负责人
break; break;
} }
@ -63,12 +63,12 @@ public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmT
} }
Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序 Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序
for (Long deptId : deptIds) { for (Long deptId : deptIds) {
DeptRespDTO dept = deptApi.getDept(deptId); DeptRespDTO dept = deptApi.getDept(deptId).getCheckedData();
for (int i = 0; i < level; i++) { for (int i = 0; i < level; i++) {
if (dept.getLeaderUserId() != null) { if (dept.getLeaderUserId() != null) {
deptLeaderIds.add(dept.getLeaderUserId()); deptLeaderIds.add(dept.getLeaderUserId());
} }
DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()); DeptRespDTO parentDept = deptApi.getDept(dept.getParentId()).getCheckedData();
if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了 if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了
break; break;
} }
@ -84,11 +84,11 @@ public abstract class AbstractBpmTaskCandidateDeptLeaderStrategy implements BpmT
* @param startUserId Id * @param startUserId Id
*/ */
protected DeptRespDTO getStartUserDept(Long startUserId) { protected DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId); AdminUserRespDTO startUser = adminUserApi.getUser(startUserId).getCheckedData();
if (startUser.getDeptId() == null) { // 找不到部门 if (startUser.getDeptId() == null) { // 找不到部门
return null; return null;
} }
return deptApi.getDept(startUser.getDeptId()); return deptApi.getDept(startUser.getDeptId()).getCheckedData();
} }
} }

View File

@ -30,7 +30,7 @@ public class BpmTaskCandidateDeptLeaderMultiStrategy extends AbstractBpmTaskCand
List<Long> deptIds = StrUtils.splitToLong(params[0], ","); List<Long> deptIds = StrUtils.splitToLong(params[0], ",");
int level = Integer.parseInt(params[1]); int level = Integer.parseInt(params[1]);
// 校验部门存在 // 校验部门存在
deptApi.validateDeptList(deptIds); deptApi.validateDeptList(deptIds).checkError();
Assert.isTrue(level > 0, "部门层级必须大于 0"); Assert.isTrue(level > 0, "部门层级必须大于 0");
} }

View File

@ -32,13 +32,13 @@ public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrat
@Override @Override
public void validateParam(String param) { public void validateParam(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
deptApi.validateDeptList(deptIds); deptApi.validateDeptList(deptIds).checkError();
} }
@Override @Override
public Set<Long> calculateUsers(String param) { public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<DeptRespDTO> depts = deptApi.getDeptList(deptIds); List<DeptRespDTO> depts = deptApi.getDeptList(deptIds).getCheckedData();
return convertSet(depts, DeptRespDTO::getLeaderUserId); return convertSet(depts, DeptRespDTO::getLeaderUserId);
} }

View File

@ -35,13 +35,13 @@ public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrat
@Override @Override
public void validateParam(String param) { public void validateParam(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
deptApi.validateDeptList(deptIds); deptApi.validateDeptList(deptIds).checkError();
} }
@Override @Override
public Set<Long> calculateUsers(String param) { public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param); Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds); List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds).getCheckedData();
return convertSet(users, AdminUserRespDTO::getId); return convertSet(users, AdminUserRespDTO::getId);
} }

View File

@ -41,7 +41,7 @@ public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
@Override @Override
public Set<Long> calculateUsers(String param) { public Set<Long> calculateUsers(String param) {
Set<Long> postIds = StrUtils.splitToLongSet(param); Set<Long> postIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds); List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds).getCheckedData();
return convertSet(users, AdminUserRespDTO::getId); return convertSet(users, AdminUserRespDTO::getId);
} }

View File

@ -28,7 +28,7 @@ public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
@Override @Override
public void validateParam(String param) { public void validateParam(String param) {
adminUserApi.validateUserList(StrUtils.splitToLongSet(param)); adminUserApi.validateUserList(StrUtils.splitToLongSet(param)).checkError();
} }
@Override @Override

View File

@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
/** /**
* variable * variable
* *
* @deprecated * ConditionNodeConvert buildConditionExpression
*
* @author jason * @author jason
*/ */
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
@Component @Component
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction { public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {

View File

@ -456,54 +456,6 @@ public class BpmnModelUtils {
return new ArrayList<>(); return new ArrayList<>();
} }
/**
* source UserTask 线
*
* 1. 线 source UserTask线
* 2. 线 source UserTask
* 3. StartEvent SubProcess
*
* @param source
* @return UserTask 线
*/
public static List<SequenceFlow> getElementIncomingUserTaskFlows(FlowElement source) {
List<SequenceFlow> result = new ArrayList<>();
collectElementIncomingUserTaskFlows(source, new HashSet<>(), new HashSet<>(), result);
return result;
}
private static void collectElementIncomingUserTaskFlows(FlowElement source, Set<String> visitedSequenceFlowIds,
Set<String> resultSequenceFlowIds, List<SequenceFlow> result) {
// 如果是开始节点或子流程,则停止该分支向上查找
if (source == null || source instanceof StartEvent || source instanceof SubProcess) {
return;
}
// 获取入口连线
List<SequenceFlow> incomingFlows = getElementIncomingFlows(source);
if (CollUtil.isEmpty(incomingFlows)) {
return;
}
// 循环找到目标元素
for (SequenceFlow incomingFlow : incomingFlows) {
// 如果发现连线重复,说明连线已经走过。跳过
if (incomingFlow == null || !visitedSequenceFlowIds.add(incomingFlow.getId())) {
continue;
}
// 如果 source 是 UserTask则添加到结果中
FlowElement sourceFlowElement = incomingFlow.getSourceFlowElement();
if (sourceFlowElement instanceof UserTask) {
if (resultSequenceFlowIds.add(incomingFlow.getId())) {
result.add(incomingFlow);
}
continue;
}
// 递归向上查找 UserTask
collectElementIncomingUserTaskFlows(sourceFlowElement, visitedSequenceFlowIds,
resultSequenceFlowIds, result);
}
}
/** /**
* 线 * 线
* *
@ -721,7 +673,7 @@ public class BpmnModelUtils {
// 这条线路存在目标节点,直接返回 true // 这条线路存在目标节点,直接返回 true
FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement(); FlowElement sourceFlowElement = sequenceFlow.getSourceFlowElement();
if (target.getId().equals(sourceFlowElement.getId())) { if (target.getId().equals(sourceFlowElement.getId())) {
return true; return true;
} }
// 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?) // 如果目标节点为并行网关,跳过这个循环 (TODO 疑问:这个判断作用是防止回退到并行网关分支上的节点吗?)
if (sourceFlowElement instanceof ParallelGateway) { if (sourceFlowElement instanceof ParallelGateway) {
@ -846,9 +798,9 @@ public class BpmnModelUtils {
// 情况StartEvent/EndEvent/UserTask/ServiceTask // 情况StartEvent/EndEvent/UserTask/ServiceTask
if (currentElement instanceof StartEvent if (currentElement instanceof StartEvent
|| currentElement instanceof EndEvent || currentElement instanceof EndEvent
|| currentElement instanceof UserTask || currentElement instanceof UserTask
|| currentElement instanceof ServiceTask) { || currentElement instanceof ServiceTask) {
// 添加节点 // 添加节点
FlowNode flowNode = (FlowNode) currentElement; FlowNode flowNode = (FlowNode) currentElement;
resultElements.add(flowNode); resultElements.add(flowNode);
@ -1030,8 +982,8 @@ public class BpmnModelUtils {
*/ */
private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) { private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& (evalConditionExpress(variables, flow.getConditionExpression()))); && (evalConditionExpress(variables, flow.getConditionExpression())));
if (matchSequenceFlow == null) { if (matchSequenceFlow == null) {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@ -12,7 +11,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormFi
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import com.fasterxml.jackson.databind.JsonNode;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.flowable.common.engine.api.delegate.Expression; import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.api.variable.VariableContainer; import org.flowable.common.engine.api.variable.VariableContainer;
@ -28,7 +26,6 @@ import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.TaskInfo; import org.flowable.task.api.TaskInfo;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@ -247,10 +244,12 @@ public class FlowableUtils {
} }
// 解析表单配置 // 解析表单配置
Map<String, BpmFormFieldVO> formFieldsMap = new LinkedHashMap<>(); Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
processDefinitionInfo.getFormFields().forEach(formFieldStr -> { processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
JsonNode formFieldNode = JsonUtils.parseObject(formFieldStr, JsonNode.class); BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
parseFormField(formFieldNode, formFieldsMap); if (formField != null) {
formFieldsMap.put(formField.getField(), formField);
}
}); });
// 情况一:当自定义了摘要 // 情况一:当自定义了摘要
@ -274,40 +273,6 @@ public class FlowableUtils {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
/**
*
*/
private static void parseFormField(JsonNode formFieldNode, Map<String, BpmFormFieldVO> formFieldsMap) {
if (formFieldNode == null || !formFieldNode.isObject()) {
return;
}
// 如果 children 里存在对象节点,说明是布局组件;字符串节点是分割线、标签、文字等展示组件内容,直接跳过。
JsonNode children = formFieldNode.get("children");
if (children != null && children.isArray() && children.size() > 0) {
boolean hasObjectChild = false;
for (JsonNode child : children) {
if (!child.isObject()) {
continue;
}
hasObjectChild = true;
parseFormField(child, formFieldsMap);
}
if (hasObjectChild) {
return;
}
}
// 真实字段才加入 map
BpmFormFieldVO formField = new BpmFormFieldVO()
.setType(JsonUtils.getText(formFieldNode, "type"))
.setField(JsonUtils.getText(formFieldNode, "field"))
.setTitle(JsonUtils.getText(formFieldNode, "title"));
if (StrUtil.isNotBlank(formField.getField())) {
formFieldsMap.put(formField.getField(), formField);
}
}
// ========== Task 相关的工具方法 ========== // ========== Task 相关的工具方法 ==========
/** /**

View File

@ -707,9 +707,10 @@ public class SimpleModelUtils {
List<String> list = convertList(item.getRules(), (rule) -> { List<String> list = convertList(item.getRules(), (rule) -> {
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
: "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号 : "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
return String.format(BpmConditionOpCodeEnum.fromCode(rule.getOpCode()).getSymbol(), return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
rule.getLeftSide(), // 左侧:读取变量 rule.getLeftSide(), // 左侧:读取变量
rightSide); // 右侧:取值变量 rule.getOpCode(), // 中间:操作符,比较
rule.getLeftSide(), rightSide); // 右侧转换变量VariableConvertByTypeExpressionFunction
}); });
// 构造条件组的表达式 // 构造条件组的表达式
Boolean and = item.getAnd(); Boolean and = item.getAnd();

View File

@ -101,7 +101,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
// 校验用户是否在允许发起的部门列表中 // 校验用户是否在允许发起的部门列表中
if (CollUtil.isNotEmpty(processDefinition.getStartDeptIds())) { if (CollUtil.isNotEmpty(processDefinition.getStartDeptIds())) {
AdminUserRespDTO user = adminUserApi.getUser(userId); AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData();
return user != null return user != null
&& user.getDeptId() != null && user.getDeptId() != null
&& processDefinition.getStartDeptIds().contains(user.getDeptId()); && processDefinition.getStartDeptIds().contains(user.getDeptId());

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.bpm.service.definition.dto;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import lombok.Data;
/**
* BPM MetaInfo Response DTO
* { Model#setMetaInfo(String)}
*
* {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO}
*
* @author
*/
@Data
public class BpmModelMetaInfoRespDTO {
/**
*
*/
private String icon;
/**
*
*/
private String description;
/**
*
*/
private Integer formType;
/**
*
* {@link BpmModelFormTypeEnum#NORMAL}
*/
private Long formId;
/**
* 使 Vue
* {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomCreatePath;
/**
* 使 Vue
* {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomViewPath;
}

View File

@ -872,7 +872,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) { if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) {
return definition.getName(); return definition.getName();
} }
AdminUserRespDTO user = adminUserApi.getUser(userId); AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData();
Map<String, Object> cloneVariables = new HashMap<>(variables); Map<String, Object> cloneVariables = new HashMap<>(variables);
cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname()); cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname());
cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now()); cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now());
@ -920,7 +920,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
} }
// 2. 取消流程 // 2. 取消流程
AdminUserRespDTO user = adminUserApi.getUser(userId); AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData();
updateProcessInstanceCancel(cancelReqVO.getId(), updateProcessInstanceCancel(cancelReqVO.getId(),
BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason())); BpmReasonEnum.CANCEL_PROCESS_INSTANCE_BY_ADMIN.format(user.getNickname(), cancelReqVO.getReason()));
} }

View File

@ -618,14 +618,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
runtimeService.setVariable(task.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskIdsByReturn); runtimeService.setVariable(task.getProcessInstanceId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskIdsByReturn);
} }
// 6. 清理退回设置的不自动通过的变量。仅在该标记存在时才删除,避免每次完成任务都产生无谓的 DB delete // 6. 调用 BPM complete 去完成任务
String returnFlagKey = String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey());
if (runtimeService.hasVariable(task.getProcessInstanceId(), returnFlagKey)) {
log.info("[approveTask][taskId({}) 清理退回标记变量({})]", task.getId(), returnFlagKey);
runtimeService.removeVariable(task.getProcessInstanceId(), returnFlagKey);
}
// 7. 调用 BPM complete 去完成任务
taskService.complete(task.getId(), variables, true); taskService.complete(task.getId(), variables, true);
// 【加签专属】处理加签任务 // 【加签专属】处理加签任务
@ -787,8 +780,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
*/ */
private void approveDelegateTask(BpmTaskApproveReqVO reqVO, Task task) { private void approveDelegateTask(BpmTaskApproveReqVO reqVO, Task task) {
// 1. 添加审批意见 // 1. 添加审批意见
AdminUserRespDTO currentUser = adminUserApi.getUser(WebFrameworkUtils.getLoginUserId()); AdminUserRespDTO currentUser = adminUserApi.getUser(WebFrameworkUtils.getLoginUserId()).getCheckedData();
AdminUserRespDTO ownerUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())); // 发起委托的用户 AdminUserRespDTO ownerUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())).getCheckedData(); // 发起委托的用户
Assert.notNull(ownerUser, "委派任务找不到原审批人,需要检查数据"); Assert.notNull(ownerUser, "委派任务找不到原审批人,需要检查数据");
taskService.addComment(reqVO.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_END.getType(), taskService.addComment(reqVO.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_END.getType(),
BpmCommentTypeEnum.DELEGATE_END.formatComment(currentUser.getNickname(), ownerUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.DELEGATE_END.formatComment(currentUser.getNickname(), ownerUser.getNickname(), reqVO.getReason()));
@ -921,7 +914,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* @param reqVO * @param reqVO
*/ */
public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) { public void returnTask(Long userId, BpmnModel bpmnModel, Task currentTask, FlowElement targetElement, BpmTaskReturnReqVO reqVO) {
// 1. 获得所有需要回撤的任务 taskDefinitionKey用于稍后的 moveExecutionsToSingleActivityId 回撤 // 1. 获得所有需要回撤的任务 taskDefinitionKey用于稍后的 moveActivityIdsToSingleActivityId 回撤
// 1.1 获取所有正常进行的任务节点 Key // 1.1 获取所有正常进行的任务节点 Key
List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list(); List<Task> taskList = taskService.createTaskQuery().processInstanceId(currentTask.getProcessInstanceId()).list();
List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey); List<String> runTaskKeyList = convertList(taskList, Task::getTaskDefinitionKey);
@ -929,16 +922,14 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 为什么不直接使用 runTaskKeyList 呢因为可能存在多个审批分支例如说A -> B -> C 和 D -> F而只要 C 撤回到 A需要排除掉 F // 为什么不直接使用 runTaskKeyList 呢因为可能存在多个审批分支例如说A -> B -> C 和 D -> F而只要 C 撤回到 A需要排除掉 F
List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null); List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId); List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
List<String> runExecutionIds = new ArrayList<>();
// 2. 给当前要被退回的 task 数组,设置退回意见 // 2. 给当前要被退回的 task 数组,设置退回意见
taskList.forEach(task -> { taskList.forEach(task -> {
// 需要排除掉,不需要设置退回意见的任务 // 需要排除掉,不需要设置退回意见的任务
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
return; return;
} }
if (task.getExecutionId() != null) {
runExecutionIds.add(task.getExecutionId());
}
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
// 2.1.1 添加评论 // 2.1.1 添加评论
@ -955,25 +946,18 @@ public class BpmTaskServiceImpl implements BpmTaskService {
Set<String> needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement); Set<String> needSimulateTaskDefinitionKeys = getNeedSimulateTaskDefinitionKeys(bpmnModel, currentTask, targetElement);
// 4. 执行驳回 // 4. 执行驳回
// 4.1 校验是否有可回撤的 execution避免 moveExecutionsToSingleActivityId 传入空集合时 Flowable 内部报错
if (CollUtil.isEmpty(runExecutionIds)) {
throw exception(TASK_RETURN_FAIL_SOURCE_TARGET_ERROR);
}
// 4.2 执行驳回
// ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因当多实例任务回退的时候有问题。 // ① 使用 moveExecutionsToSingleActivityId 替换 moveActivityIdsToSingleActivityId。原因当多实例任务回退的时候有问题。
// 相关 issue: https://github.com/flowable/flowable-engine/issues/3944 // 相关 issue: https://github.com/flowable/flowable-engine/issues/3944
// ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因flowable 7.2.0 版本修复了该问题。 // ② flowable 7.2.0 版本后,继续使用 moveActivityIdsToSingleActivityId 方法。原因flowable 7.2.0 版本修复了该问题。
// 相关 issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/1018 // 相关 issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/1018
// ③ moveActivityIdsToSingleActivityId 使用遇到问题, 相关 issue https://gitee.com/zhijiantianya/yudao-cloud/issues/IJM8MS
// 改成 moveExecutionsToSingleActivityId 好像并没有遇到 ② 提到的超时提醒失效的问题。暂时先改回 moveExecutionsToSingleActivityId
// 目前还有的相关问题 https://t.zsxq.com/z4d9i。 估计需要升级 flowable 8 版本试试
runtimeService.createChangeActivityStateBuilder() runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId()) .processInstanceId(currentTask.getProcessInstanceId())
.moveExecutionsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey()) .moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey())
// 设置需要预测的任务 ids 的流程变量,用于辅助预测 // 设置需要预测的任务 ids 的流程变量,用于辅助预测
.processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys) .processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
// 设置流程变量节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过 // 设置流程变量local节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
.processVariable(String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE) .localVariable(reqVO.getTargetTaskDefinitionKey(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE)
.changeState(); .changeState();
} }
@ -1015,13 +999,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
throw exception(TASK_DELEGATE_FAIL_USER_REPEAT); throw exception(TASK_DELEGATE_FAIL_USER_REPEAT);
} }
// 1.2 校验目标用户存在 // 1.2 校验目标用户存在
AdminUserRespDTO delegateUser = adminUserApi.getUser(reqVO.getDelegateUserId()); AdminUserRespDTO delegateUser = adminUserApi.getUser(reqVO.getDelegateUserId()).getCheckedData();
if (delegateUser == null) { if (delegateUser == null) {
throw exception(TASK_DELEGATE_FAIL_USER_NOT_EXISTS); throw exception(TASK_DELEGATE_FAIL_USER_NOT_EXISTS);
} }
// 2. 添加委托意见 // 2. 添加委托意见
AdminUserRespDTO currentUser = adminUserApi.getUser(userId); AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData();
taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_START.getType(), taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.DELEGATE_START.getType(),
BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason()));
@ -1046,13 +1030,13 @@ public class BpmTaskServiceImpl implements BpmTaskService {
throw exception(TASK_TRANSFER_FAIL_USER_REPEAT); throw exception(TASK_TRANSFER_FAIL_USER_REPEAT);
} }
// 1.2 校验目标用户存在 // 1.2 校验目标用户存在
AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId()); AdminUserRespDTO assigneeUser = adminUserApi.getUser(reqVO.getAssigneeUserId()).getCheckedData();
if (assigneeUser == null) { if (assigneeUser == null) {
throw exception(TASK_TRANSFER_FAIL_USER_NOT_EXISTS); throw exception(TASK_TRANSFER_FAIL_USER_NOT_EXISTS);
} }
// 2. 添加委托意见 // 2. 添加委托意见
AdminUserRespDTO currentUser = adminUserApi.getUser(userId); AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData();
taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.TRANSFER.getType(), taskService.addComment(taskId, task.getProcessInstanceId(), BpmCommentTypeEnum.TRANSFER.getType(),
BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason()));
@ -1111,7 +1095,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) { public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) {
// 1. 获取和校验任务 // 1. 获取和校验任务
TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO); TaskEntityImpl taskEntity = validateTaskCanCreateSign(userId, reqVO);
List<AdminUserRespDTO> userList = adminUserApi.getUserList(reqVO.getUserIds()); List<AdminUserRespDTO> userList = adminUserApi.getUserList(reqVO.getUserIds()).getCheckedData();
if (CollUtil.isEmpty(userList)) { if (CollUtil.isEmpty(userList)) {
throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST); throw exception(TASK_SIGN_CREATE_USER_NOT_EXIST);
} }
@ -1138,7 +1122,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
createSignTaskList(convertList(reqVO.getUserIds(), String::valueOf), taskEntity); createSignTaskList(convertList(reqVO.getUserIds(), String::valueOf), taskEntity);
// 4. 记录加签的评论到 task 任务 // 4. 记录加签的评论到 task 任务
AdminUserRespDTO currentUser = adminUserApi.getUser(userId); AdminUserRespDTO currentUser = adminUserApi.getUser(userId).getCheckedData();
String comment = StrUtil.format(BpmCommentTypeEnum.ADD_SIGN.getComment(), String comment = StrUtil.format(BpmCommentTypeEnum.ADD_SIGN.getComment(),
currentUser.getNickname(), BpmTaskSignTypeEnum.nameOfType(reqVO.getType()), currentUser.getNickname(), BpmTaskSignTypeEnum.nameOfType(reqVO.getType()),
String.join(",", convertList(userList, AdminUserRespDTO::getNickname)), reqVO.getReason()); String.join(",", convertList(userList, AdminUserRespDTO::getNickname)), reqVO.getReason());
@ -1170,7 +1154,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况,因为向后加签时,它暂时没 assignee 而是 owner
Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner()))); Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner())));
if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) { if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) {
List<AdminUserRespDTO> userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds())); List<AdminUserRespDTO> userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds())).getCheckedData();
throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname))); throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname)));
} }
return taskEntity; return taskEntity;
@ -1232,10 +1216,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 1.2 校验取消人存在 // 1.2 校验取消人存在
AdminUserRespDTO cancelUser = null; AdminUserRespDTO cancelUser = null;
if (StrUtil.isNotBlank(task.getAssignee())) { if (StrUtil.isNotBlank(task.getAssignee())) {
cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getAssignee())); cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getAssignee())).getCheckedData();
} }
if (cancelUser == null && StrUtil.isNotBlank(task.getOwner())) { if (cancelUser == null && StrUtil.isNotBlank(task.getOwner())) {
cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())); cancelUser = adminUserApi.getUser(NumberUtils.parseLong(task.getOwner())).getCheckedData();
} }
Assert.notNull(cancelUser, "任务中没有所有者和审批人,数据错误"); Assert.notNull(cancelUser, "任务中没有所有者和审批人,数据错误");
@ -1249,7 +1233,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
taskService.deleteTasks(convertList(childTaskList, Task::getId)); taskService.deleteTasks(convertList(childTaskList, Task::getId));
// 3. 记录日志到父任务中。先记录日志是因为,通过 handleParentTask 方法之后,任务可能被完成了,并且不存在了,会报异常,所以先记录 // 3. 记录日志到父任务中。先记录日志是因为,通过 handleParentTask 方法之后,任务可能被完成了,并且不存在了,会报异常,所以先记录
AdminUserRespDTO user = adminUserApi.getUser(userId); AdminUserRespDTO user = adminUserApi.getUser(userId).getCheckedData();
taskService.addComment(task.getParentTaskId(), task.getProcessInstanceId(), BpmCommentTypeEnum.SUB_SIGN.getType(), taskService.addComment(task.getParentTaskId(), task.getProcessInstanceId(), BpmCommentTypeEnum.SUB_SIGN.getType(),
StrUtil.format(BpmCommentTypeEnum.SUB_SIGN.getComment(), user.getNickname(), cancelUser.getNickname())); StrUtil.format(BpmCommentTypeEnum.SUB_SIGN.getComment(), user.getNickname(), cancelUser.getNickname()));
@ -1480,106 +1464,102 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return; return;
} }
// 需要基于 instance 设置租户编号,避免 Flowable 内部异步执行时【例如:超时自动通过】 丢失租户编号 // 自动去重,通过自动审批的方式
FlowableUtils.execute(processInstance.getTenantId(), () -> { BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
// 自动去重,通过自动审批的方式 if (processDefinitionInfo == null) {
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId()); log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
if (processDefinitionInfo == null) { return;
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId()); }
if (processDefinitionInfo.getAutoApprovalType() != null) {
HistoricTaskInstanceQuery sameAssigneeQuery = historyService.createHistoricTaskInstanceQuery()
.processInstanceId(task.getProcessInstanceId())
.taskAssignee(task.getAssignee()) // 相同审批人
.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus())
.finished();
if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
&& sameAssigneeQuery.count() > 0) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
return; return;
} }
if (processDefinitionInfo.getAutoApprovalType() != null) { if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) {
HistoricTaskInstanceQuery approvedTaskQuery = historyService.createHistoricTaskInstanceQuery() BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
.processInstanceId(task.getProcessInstanceId()) if (bpmnModel == null) {
.taskVariableValueEquals(BpmnVariableConstants.TASK_VARIABLE_STATUS, BpmTaskStatusEnum.APPROVE.getStatus()) log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId());
.finished();
if (BpmAutoApproveTypeEnum.APPROVE_ALL.getType().equals(processDefinitionInfo.getAutoApprovalType())
&& approvedTaskQuery.taskAssignee(task.getAssignee()).count() > 0) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmAutoApproveTypeEnum.APPROVE_ALL.getName()));
return; return;
} }
// 连续审批的节点自动通过 List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingFlows( // 获取所有上一个节点
if (BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getType().equals(processDefinitionInfo.getAutoApprovalType())) { BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); SequenceFlow::getSourceRef);
if (bpmnModel == null) { if (sameAssigneeQuery.taskDefinitionKeys(sourceTaskIds).count() > 0) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型({})]", task.getId(), task.getProcessDefinitionId()); getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
return; .setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName()));
} return;
List<String> sourceTaskIds = convertList(BpmnModelUtils.getElementIncomingUserTaskFlows( // 获取所有的上一个 UserTask 节点连线
BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey())),
SequenceFlow::getSourceRef);
approvedTaskQuery.taskDefinitionKeys(sourceTaskIds).orderByTaskCreateTime().desc(); // 设置 taskIds, 并按创建时间倒序排序
HistoricTaskInstance firstHisTask = CollUtil.getFirst(approvedTaskQuery.list());
if (firstHisTask != null && StrUtil.equals(firstHisTask.getAssignee(), task.getAssignee())) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmAutoApproveTypeEnum.APPROVE_SEQUENT.getName()));
return;
}
} }
} }
}
// 获取发起人节点 // 获取发起人节点
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
if (bpmnModel == null) { if (bpmnModel == null) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
return; return;
} }
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
// 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略 // 判断是否为退回或者驳回:如果是退回或者驳回不走这个策略(使用 local variable
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(), Boolean returnTaskFlag = runtimeService.getVariableLocal(task.getExecutionId(),
String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class); String.format(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID) if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|| BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
return; return;
} }
// 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理 // 当不为发起人节点时,审批人与提交人为同一人时,根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID) if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
&& StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) { && StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement); Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
// 情况一:自动跳过 // 情况一:自动跳过
if (ObjectUtils.equalsAny(assignStartUserHandlerType, if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) { BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
return;
}
// 情况二:转交给部门负责人审批
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()).getCheckedData() : null;
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
// 找不到部门负责人的情况下,自动审批通过
// noinspection DataFlowIssue
if (dept.getLeaderUserId() == null) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason())); .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
return; return;
} }
// 情况二:转交给部门负责人审批 // 找得到部门负责人的情况下,修改负责人
if (ObjectUtils.equalsAny(assignStartUserHandlerType, if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) { getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())); .setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId()); .setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()) : null; return;
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
// 找不到部门负责人的情况下,自动审批通过
// noinspection DataFlowIssue
if (dept.getLeaderUserId() == null) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
return;
}
// 找得到部门负责人的情况下,修改负责人
if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
.setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
return;
}
// 如果部门负责人是自己,还是自己审批吧~
} }
// 如果部门负责人是自己,还是自己审批吧~
} }
} }
}
// 发送消息 // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())); FlowableUtils.execute(processInstance.getTenantId(), () -> {
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId())).getCheckedData();
messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task)); messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
}); });
} }

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项 # Lock4j 配置项
lock4j: lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 #################### --- #################### 监控相关配置 ####################

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项 # Lock4j 配置项
lock4j: lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒 acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒 expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 #################### --- #################### 监控相关配置 ####################

View File

@ -224,7 +224,7 @@ public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap); when(adminUserApi.getUserMap(eq(asSet(1L, 2L)))).thenReturn(userMap);
// mock 方法empty // mock 方法empty
when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId), when(emptyStrategy.calculateUsersByActivity(same(bpmnModel), eq(activityId),
eq(param), same(startUserId), same(processDefinitionId), same(processVariables))) eq(param), same(startUserId), same(processDefinitionId), same(processVariables)))
.thenReturn(Sets.newSet(2L)); .thenReturn(Sets.newSet(2L));
// 调用 // 调用

View File

@ -14,6 +14,7 @@ import org.mockito.Mock;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet; import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomString;
@ -40,9 +41,9 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
DelegateExecution execution = mockDelegateExecution(1L); DelegateExecution execution = mockDelegateExecution(1L);
// mock 方法(startUser) // mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); when(adminUserApi.getUser(eq(1L))).thenReturn(success(startUser));
// mock 方法(getStartUserDept)没有部门 // mock 方法(getStartUserDept)没有部门
when(deptApi.getDept(eq(10L))).thenReturn(null); when(deptApi.getDept(eq(10L))).thenReturn(success(null));
// 调用 // 调用
Set<Long> result = expression.calculateUsers(execution, 1); Set<Long> result = expression.calculateUsers(execution, 1);
@ -56,12 +57,12 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
DelegateExecution execution = mockDelegateExecution(1L); DelegateExecution execution = mockDelegateExecution(1L);
// mock 方法(startUser) // mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); when(adminUserApi.getUser(eq(1L))).thenReturn(success(startUser));
DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L)
.setLeaderUserId(20L)); .setLeaderUserId(20L));
// mock 方法getDept // mock 方法getDept
when(deptApi.getDept(eq(10L))).thenReturn(startUserDept); when(deptApi.getDept(eq(10L))).thenReturn(success(startUserDept));
when(deptApi.getDept(eq(100L))).thenReturn(null); when(deptApi.getDept(eq(100L))).thenReturn(success(null));
// 调用 // 调用
Set<Long> result = expression.calculateUsers(execution, 2); Set<Long> result = expression.calculateUsers(execution, 2);
@ -75,14 +76,14 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
DelegateExecution execution = mockDelegateExecution(1L); DelegateExecution execution = mockDelegateExecution(1L);
// mock 方法(startUser) // mock 方法(startUser)
AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L)); AdminUserRespDTO startUser = randomPojo(AdminUserRespDTO.class, o -> o.setDeptId(10L));
when(adminUserApi.getUser(eq(1L))).thenReturn(startUser); when(adminUserApi.getUser(eq(1L))).thenReturn(success(startUser));
DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L) DeptRespDTO startUserDept = randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(100L)
.setLeaderUserId(20L)); .setLeaderUserId(20L));
when(deptApi.getDept(eq(10L))).thenReturn(startUserDept); when(deptApi.getDept(eq(10L))).thenReturn(success(startUserDept));
// mock 方法(父 dept // mock 方法(父 dept
DeptRespDTO parentDept = randomPojo(DeptRespDTO.class, o -> o.setId(100L).setParentId(1000L) DeptRespDTO parentDept = randomPojo(DeptRespDTO.class, o -> o.setId(100L).setParentId(1000L)
.setLeaderUserId(200L)); .setLeaderUserId(200L));
when(deptApi.getDept(eq(100L))).thenReturn(parentDept); when(deptApi.getDept(eq(100L))).thenReturn(success(parentDept));
// 调用 // 调用
Set<Long> result = expression.calculateUsers(execution, 2); Set<Long> result = expression.calculateUsers(execution, 2);

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept; package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.system.api.dept.DeptApi; import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO; import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
@ -11,6 +12,7 @@ import org.mockito.stubbing.Answer;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
@ -29,9 +31,9 @@ public class BpmTaskCandidateDeptLeaderMultiStrategyTest extends BaseMockitoUnit
// 准备参数 // 准备参数
String param = "10,20|2"; String param = "10,20|2";
// mock 方法 // mock 方法
when(deptApi.getDept(any())).thenAnswer((Answer<DeptRespDTO>) invocationOnMock -> { when(deptApi.getDept(any())).thenAnswer((Answer< CommonResult<DeptRespDTO>>) invocationOnMock -> {
Long deptId = invocationOnMock.getArgument(0); Long deptId = invocationOnMock.getArgument(0);
return randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)); return success(randomPojo(DeptRespDTO.class, o -> o.setId(deptId).setParentId(deptId * 100).setLeaderUserId(deptId + 1)));
}); });
// 调用 // 调用

View File

@ -11,6 +11,7 @@ import org.mockito.Mock;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -30,9 +31,9 @@ public class BpmTaskCandidateDeptLeaderStrategyTest extends BaseMockitoUnitTest
// 准备参数 // 准备参数
String param = "10,20"; String param = "10,20";
// mock 方法 // mock 方法
when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList( when(deptApi.getDeptList(eq(SetUtils.asSet(10L, 20L)))).thenReturn(success(asList(
randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)), randomPojo(DeptRespDTO.class, o -> o.setId(10L).setParentId(10L).setLeaderUserId(11L)),
randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L)))); randomPojo(DeptRespDTO.class, o -> o.setId(20L).setParentId(20L).setLeaderUserId(21L)))));
// 调用 // 调用
Set<Long> userIds = strategy.calculateUsers(param); Set<Long> userIds = strategy.calculateUsers(param);

View File

@ -12,6 +12,7 @@ import org.mockito.Mock;
import java.util.Set; import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo; import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.randomPojo;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@ -33,9 +34,9 @@ public class BpmTaskCandidateDeptMemberStrategyTest extends BaseMockitoUnitTest
// 准备参数 // 准备参数
String param = "10,20"; String param = "10,20";
// mock 方法 // mock 方法
when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(asList( when(adminUserApi.getUserListByDeptIds(eq(SetUtils.asSet(10L, 20L)))).thenReturn(success(asList(
randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)), randomPojo(AdminUserRespDTO.class, o -> o.setId(11L)),
randomPojo(AdminUserRespDTO.class, o -> o.setId(21L)))); randomPojo(AdminUserRespDTO.class, o -> o.setId(21L)))));
// 调用 // 调用
Set<Long> userIds = strategy.calculateUsers(param); Set<Long> userIds = strategy.calculateUsers(param);

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