Compare commits
163 Commits
v2025.11(j
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
d67f91a63e | |
|
|
f6b769fc2d | |
|
|
36fbb9a68b | |
|
|
25a823fe82 | |
|
|
3314376e59 | |
|
|
050edb2db7 | |
|
|
5088b8c2e2 | |
|
|
8637b2a28f | |
|
|
1702fc1acb | |
|
|
d5ab0b06a7 | |
|
|
5cf473d48e | |
|
|
9d1dd25bc7 | |
|
|
63cae8bc31 | |
|
|
568b0c29c0 | |
|
|
3f07aa3cd2 | |
|
|
42133c7141 | |
|
|
ff2dd155a6 | |
|
|
a53558cf4b | |
|
|
52e883d5be | |
|
|
32c353c53d | |
|
|
de4abc2f5f | |
|
|
9db40c8b80 | |
|
|
e657805544 | |
|
|
3e8eca7b8d | |
|
|
4b8346ec80 | |
|
|
e3e1b2b3d5 | |
|
|
3ba4104542 | |
|
|
23c934f727 | |
|
|
61bfcdfa00 | |
|
|
915885c825 | |
|
|
c8b85ad8a7 | |
|
|
63e6880a10 | |
|
|
4dabfea1df | |
|
|
11a6a049fd | |
|
|
d545eb5631 | |
|
|
1f7f85bddb | |
|
|
8f3b6cb0aa | |
|
|
6780ed6879 | |
|
|
d07426e43f | |
|
|
996ac02c0b | |
|
|
e3a34d9067 | |
|
|
116b6766b3 | |
|
|
6b91b4169d | |
|
|
8c7087ca2a | |
|
|
3e5e60ce96 | |
|
|
f57f0c551c | |
|
|
e1b3589bff | |
|
|
d66d1fcac0 | |
|
|
5fe868e096 | |
|
|
c3125dbc92 | |
|
|
adbcc60225 | |
|
|
2fe63be6c9 | |
|
|
d947d0463a | |
|
|
05375287bc | |
|
|
f5f53d59ca | |
|
|
838e2923bb | |
|
|
83ea45911b | |
|
|
81009a7082 | |
|
|
11ff5b4a7c | |
|
|
b794031b71 | |
|
|
199799b0c9 | |
|
|
86087983a7 | |
|
|
2eb326d62d | |
|
|
1e14c5c38f | |
|
|
d9b57e6897 | |
|
|
dc0ca32697 | |
|
|
79f233149c | |
|
|
b38cbe9c7f | |
|
|
9d2392a81a | |
|
|
2b9a03bd93 | |
|
|
16e095c343 | |
|
|
804d3eaaeb | |
|
|
8937853307 | |
|
|
9ffec01fa0 | |
|
|
3f599a623a | |
|
|
bb3f1954ee | |
|
|
34d74b378e | |
|
|
53d3146ad9 | |
|
|
a61ecaa019 | |
|
|
f969670fd3 | |
|
|
1fca0acc92 | |
|
|
6ca2c97849 | |
|
|
71393eed21 | |
|
|
92eda45afd | |
|
|
2d4251eda7 | |
|
|
06586b85f6 | |
|
|
7dc6b24e66 | |
|
|
4052c5c4d0 | |
|
|
a3db49babf | |
|
|
33ff11edcf | |
|
|
b8a213849c | |
|
|
5a0d95e493 | |
|
|
622db6dc73 | |
|
|
456c96df16 | |
|
|
560636ed50 | |
|
|
8fd3173670 | |
|
|
c57d3a65f9 | |
|
|
2a48bcbee9 | |
|
|
fa72dc4e59 | |
|
|
ad68973d19 | |
|
|
eee662adaf | |
|
|
35ccc5eaa4 | |
|
|
44bfa54d32 | |
|
|
4bd1ac8424 | |
|
|
13b37b4d2f | |
|
|
d1fa308961 | |
|
|
9ef140f07f | |
|
|
99ffe0fd41 | |
|
|
e10be5160b | |
|
|
17a1af1069 | |
|
|
304b2f102a | |
|
|
2e317b165b | |
|
|
61b6b6c7bd | |
|
|
bcf00e3332 | |
|
|
c95268dfba | |
|
|
09d58706cc | |
|
|
c6f8680da3 | |
|
|
f6eff05053 | |
|
|
efae658cc5 | |
|
|
d5ed2f4728 | |
|
|
9defcf736c | |
|
|
b292bc2c19 | |
|
|
72c0c013a0 | |
|
|
6b519a7654 | |
|
|
29e4976da3 | |
|
|
d61554c9bb | |
|
|
6e9933cf0f | |
|
|
edb74f64d9 | |
|
|
51da2a2f36 | |
|
|
fead40e564 | |
|
|
716e2a63c1 | |
|
|
7739c2a35d | |
|
|
0f27c0aa72 | |
|
|
cad6da432b | |
|
|
f922d3fd55 | |
|
|
3b4d1ef4d8 | |
|
|
291d705307 | |
|
|
0869ad0513 | |
|
|
e60f5496fa | |
|
|
be65f9cf97 | |
|
|
bc4d8f85ad | |
|
|
c93f2ac2a4 | |
|
|
15a2f2611f | |
|
|
845315ff8a | |
|
|
acbbdf2237 | |
|
|
742a6f3caf | |
|
|
45b06e285a | |
|
|
382e9e8cdd | |
|
|
8e07be59d5 | |
|
|
201711cec1 | |
|
|
677110ff91 | |
|
|
bec1b083ec | |
|
|
7015d221b1 | |
|
|
e63b792741 | |
|
|
e455572adb | |
|
|
13a1993dd2 | |
|
|
4017f71d10 | |
|
|
ec8577bdd9 | |
|
|
84834c7a65 | |
|
|
667cf9d1c9 | |
|
|
b5a7350fe9 | |
|
|
af5bb360bf | |
|
|
f4ba70ec5a |
|
After Width: | Height: | Size: 58 KiB |
|
After Width: | Height: | Size: 163 KiB |
|
After Width: | Height: | Size: 79 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 41 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 54 KiB |
33
README.md
|
|
@ -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 等功能
|
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、AI 大模型、IoT 物联网 等功能
|
||||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
|
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、WMS、MES、AI 大模型、IoT 物联网 等功能
|
||||||
|
|
||||||
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
||||||
|
|
||||||
|
|
@ -115,7 +115,7 @@
|
||||||
|
|
||||||
* 通用模块(必选):系统功能、基础设施
|
* 通用模块(必选):系统功能、基础设施
|
||||||
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||||
* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
|
* 业务系统(按需):Mall 电子商城、OA 办公自动化、ERP 企业资源计划系统、WMS 仓库管理系统、CRM 客户关系管理、CMS 内容管理系统、MES 执行制造系统、AI 大模型平台、IoT 物联网系统、IM 即时通讯系统、Mobile 手机移动端、Report 数据大屏
|
||||||
|
|
||||||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||||
>
|
>
|
||||||
|
|
@ -273,12 +273,28 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### WMS 系统
|
||||||
|
|
||||||
|
演示地址:<https://cloud.iocoder.cn/wms-preview/>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### CRM 系统
|
### CRM 系统
|
||||||
|
|
||||||
演示地址:<https://cloud.iocoder.cn/crm-preview/>
|
演示地址:<https://cloud.iocoder.cn/crm-preview/>
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### MES 系统
|
||||||
|
|
||||||
|
演示地址:<https://cloud.iocoder.cn/mes-preview/>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
### AI 大模型
|
### AI 大模型
|
||||||
|
|
||||||
演示地址:<https://cloud.iocoder.cn/ai-preview/>
|
演示地址:<https://cloud.iocoder.cn/ai-preview/>
|
||||||
|
|
@ -287,6 +303,14 @@
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
### IoT 物联网
|
||||||
|
|
||||||
|
演示地址:<https://cloud.iocoder.cn/iot/build>
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## 🐨 技术栈
|
## 🐨 技术栈
|
||||||
|
|
||||||
### 微服务
|
### 微服务
|
||||||
|
|
@ -304,7 +328,10 @@
|
||||||
| `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-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 模块 |
|
||||||
|
|
||||||
|
|
|
||||||
6
pom.xml
|
|
@ -24,9 +24,11 @@
|
||||||
<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>
|
||||||
<!-- 友情提示:基于 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>
|
||||||
|
|
@ -34,7 +36,7 @@
|
||||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>2025.11-jdk8-SNAPSHOT</revision>
|
<revision>2026.04-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>
|
||||||
|
|
|
||||||
|
|
@ -62,6 +62,10 @@ 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 部分
|
||||||
# 相关 issue:https://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
|
# 相关 issue:https://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
|
||||||
|
|
@ -77,7 +81,11 @@ class Convertor(ABC):
|
||||||
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)
|
||||||
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
|
# original_content 保留原始 COMMENT 信息,用于注释提取
|
||||||
|
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,7 +190,8 @@ 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'")
|
||||||
yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
|
col_part = f" {head}" if head else ""
|
||||||
|
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def index(ddl: Dict) -> Generator:
|
def index(ddl: Dict) -> Generator:
|
||||||
|
|
@ -227,7 +236,8 @@ 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:
|
||||||
match = re.search(r"COMMENT \='([^']+)';", table_sql)
|
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号
|
||||||
|
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):
|
||||||
|
|
@ -251,7 +261,9 @@ class Convertor(ABC):
|
||||||
|
|
||||||
error_scripts = []
|
error_scripts = []
|
||||||
for table_sql in self.table_script_list:
|
for table_sql in self.table_script_list:
|
||||||
ddl = DDLParser(table_sql.replace("`", "")).run()
|
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符
|
||||||
|
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:
|
||||||
|
|
@ -266,17 +278,23 @@ 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"] = bytes(column["comment"], "utf-8").decode(
|
column["comment"] = comments_dict.get(column["name"], "")
|
||||||
r"unicode_escape"
|
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
|
||||||
)[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)
|
||||||
pk = self.gen_pk(table_name)
|
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"])
|
||||||
|
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)
|
||||||
|
|
@ -320,25 +338,31 @@ class PostgreSQLConvertor(Convertor):
|
||||||
|
|
||||||
if type == "varchar":
|
if type == "varchar":
|
||||||
return f"varchar({size})"
|
return f"varchar({size})"
|
||||||
if type in ("int", "int unsigned"):
|
if type in ("int", "int unsigned", "int unsigned zerofill"):
|
||||||
return "int4"
|
return "int4"
|
||||||
if type in ("bigint", "bigint unsigned"):
|
if type in ("bigint", "bigint unsigned"):
|
||||||
return "int8"
|
return "int8"
|
||||||
if type == "datetime":
|
if type in ("tinyint", "smallint", "tinyint unsigned"):
|
||||||
|
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})"
|
return f"timestamp({size})" if size else "timestamp"
|
||||||
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"):
|
if type in ("blob", "mediumblob", "longblob"):
|
||||||
return "bytea"
|
return "bytea"
|
||||||
if type == "decimal":
|
if type == "decimal":
|
||||||
return (
|
return (
|
||||||
f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
|
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric"
|
||||||
)
|
)
|
||||||
|
|
||||||
def gen_create(self, ddl: Dict) -> str:
|
def gen_create(self, ddl: Dict) -> str:
|
||||||
|
|
@ -351,6 +375,10 @@ 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"{name} {full_type} {nullable} {default}"
|
return f"{name} {full_type} {nullable} {default}"
|
||||||
|
|
@ -407,6 +435,8 @@ 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))
|
||||||
|
# 转换 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
|
||||||
|
|
|
||||||
|
|
@ -14,32 +14,32 @@
|
||||||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<revision>2025.11-jdk8-SNAPSHOT</revision>
|
<revision>2026.04-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.cloud.version>2021.0.9</spring.cloud.version> <!-- Spring Boot 2.X 最多使用 2021.0.9 版本 -->
|
||||||
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version>
|
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version> <!-- Spring Boot 2.X 最多使用 2021.0.6.2 版本 -->
|
||||||
<!-- 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.27</druid.version>
|
<druid.version>1.2.28</druid.version>
|
||||||
<mybatis.version>3.5.19</mybatis.version>
|
<mybatis.version>3.5.19</mybatis.version>
|
||||||
<mybatis-plus.version>3.5.14</mybatis-plus.version>
|
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
||||||
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
|
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
|
||||||
<dynamic-datasource.version>4.3.1</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>3.52.0</redisson.version>
|
<redisson.version>4.3.1</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>8.6.0</kingbase.jdbc.version>
|
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
|
||||||
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
|
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
|
||||||
<taos.version>3.7.8</taos.version>
|
<taos.version>3.8.3</taos.version>
|
||||||
<!-- 消息队列 -->
|
<!-- 消息队列 -->
|
||||||
<rocketmq-spring.version>2.3.4</rocketmq-spring.version>
|
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
|
||||||
<!-- RPC 相关 -->
|
<!-- RPC 相关 -->
|
||||||
<!-- Config 配置中心相关 -->
|
<!-- Config 配置中心相关 -->
|
||||||
<!-- Job 定时任务相关 -->
|
<!-- Job 定时任务相关 -->
|
||||||
|
|
@ -55,35 +55,41 @@
|
||||||
<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.0</flowable.version>
|
<flowable.version>6.8.1</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.21.2</jsoup.version>
|
<jsoup.version>1.22.2</jsoup.version>
|
||||||
<lombok.version>1.18.42</lombok.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.41</hutool-5.version>
|
<hutool-5.version>5.8.44</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.5.0-jre</guava.version>
|
<guava.version>33.6.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.12.0</commons-net.version>
|
<commons-net.version>3.13.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.27.6</jsch.version>
|
<jsch.version>2.28.2</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.7.Final</netty.version>
|
<netty.version>4.2.12.Final</netty.version>
|
||||||
<mqtt.version>1.2.5</mqtt.version>
|
<mqtt.version>1.2.5</mqtt.version>
|
||||||
<vertx.version>4.5.22</vertx.version>
|
<vertx.version>4.5.26</vertx.version>
|
||||||
|
<okhttp.version>4.12.0</okhttp.version>
|
||||||
|
<californium.version>3.14.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.2,Spring Boot 2.7 默认 5.1.5 不兼容 -->
|
||||||
<!-- 三方云服务相关 -->
|
<!-- 三方云服务相关 -->
|
||||||
<awssdk.version>2.39.2</awssdk.version>
|
<awssdk.version>2.44.0</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.1.3</jimureport.version>
|
<jimureport.version>2.3.2</jimureport.version>
|
||||||
<jimubi.version>2.2.0</jimubi.version>
|
<jimubi.version>2.3.2</jimubi.version>
|
||||||
<weixin-java.version>4.7.8-20251117.120146</weixin-java.version>
|
<weixin-java.version>4.8.2-20260501.180637</weixin-java.version>
|
||||||
|
<alipay-sdk-java.version>4.40.771.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>
|
||||||
|
|
@ -304,7 +310,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-35</artifactId>
|
<artifactId>redisson-spring-data-40</artifactId> <!-- Redisson 4.x 默认依赖 spring-data-40,排除后使用 spring-data-27 适配 Spring Boot 2.7 -->
|
||||||
</exclusion>
|
</exclusion>
|
||||||
</exclusions>
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
@ -367,7 +373,6 @@
|
||||||
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.apache.rocketmq</groupId>
|
<groupId>org.apache.rocketmq</groupId>
|
||||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||||
|
|
@ -625,62 +630,6 @@
|
||||||
<version>${reflections.version}</version>
|
<version>${reflections.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 三方云服务相关 -->
|
|
||||||
<dependency>
|
|
||||||
<groupId>software.amazon.awssdk</groupId>
|
|
||||||
<artifactId>s3</artifactId>
|
|
||||||
<version>${awssdk.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.binarywang</groupId>
|
|
||||||
<artifactId>weixin-java-pay</artifactId>
|
|
||||||
<version>${weixin-java.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.binarywang</groupId>
|
|
||||||
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
|
||||||
<version>${weixin-java.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.github.binarywang</groupId>
|
|
||||||
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
|
||||||
<version>${weixin-java.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<dependency>
|
|
||||||
<groupId>me.zhyd.oauth</groupId>
|
|
||||||
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
|
|
||||||
<version>${justauth.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>com.xkcoding.justauth</groupId>
|
|
||||||
<artifactId>justauth-spring-boot-starter</artifactId>
|
|
||||||
<version>${justauth-starter.version}</version>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- 积木报表-->
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.jimureport</groupId>
|
|
||||||
<artifactId>jimureport-spring-boot-starter</artifactId>
|
|
||||||
<version>${jimureport.version}</version>
|
|
||||||
</dependency>
|
|
||||||
<dependency>
|
|
||||||
<groupId>org.jeecgframework.jimureport</groupId>
|
|
||||||
<artifactId>jimubi-spring-boot-starter</artifactId>
|
|
||||||
<version>${jimubi.version}</version>
|
|
||||||
<exclusions>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>com.github.jsqlparser</groupId>
|
|
||||||
<artifactId>jsqlparser</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
<exclusion>
|
|
||||||
<groupId>cn.hutool</groupId>
|
|
||||||
<artifactId>hutool-core</artifactId>
|
|
||||||
</exclusion>
|
|
||||||
</exclusions>
|
|
||||||
</dependency>
|
|
||||||
|
|
||||||
<!-- Vert.x -->
|
<!-- Vert.x -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.vertx</groupId>
|
<groupId>io.vertx</groupId>
|
||||||
|
|
@ -705,16 +654,123 @@
|
||||||
<version>${mqtt.version}</version>
|
<version>${mqtt.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- 专属于 JDK8 安全漏洞升级 -->
|
<!-- OkHttp -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
<artifactId>logback-core</artifactId>
|
<artifactId>okhttp</artifactId>
|
||||||
<version>${logback.version}</version>
|
<version>${okhttp.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ch.qos.logback</groupId>
|
<groupId>com.squareup.okhttp3</groupId>
|
||||||
<artifactId>logback-classic</artifactId>
|
<artifactId>mockwebserver</artifactId>
|
||||||
<version>${logback.version}</version>
|
<version>${okhttp.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- CoAP - Eclipse Californium -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.eclipse.californium</groupId>
|
||||||
|
<artifactId>californium-core</artifactId>
|
||||||
|
<version>${californium.version}</version>
|
||||||
|
</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>
|
||||||
|
<groupId>software.amazon.awssdk</groupId>
|
||||||
|
<artifactId>s3</artifactId>
|
||||||
|
<version>${awssdk.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>me.zhyd.oauth</groupId>
|
||||||
|
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
|
||||||
|
<version>${justauth.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.xkcoding.justauth</groupId>
|
||||||
|
<artifactId>justauth-spring-boot-starter</artifactId>
|
||||||
|
<version>${justauth-starter.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<!-- 移除,避免和项目里的 hutool-all 冲突 -->
|
||||||
|
<exclusion>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alipay.sdk</groupId>
|
||||||
|
<artifactId>alipay-sdk-java</artifactId>
|
||||||
|
<version>${alipay-sdk-java.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15on</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.binarywang</groupId>
|
||||||
|
<artifactId>weixin-java-pay</artifactId>
|
||||||
|
<version>${weixin-java.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.binarywang</groupId>
|
||||||
|
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
|
||||||
|
<version>${weixin-java.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.binarywang</groupId>
|
||||||
|
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
|
||||||
|
<version>${weixin-java.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 积木报表-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.jimureport</groupId>
|
||||||
|
<artifactId>jimureport-spring-boot-starter</artifactId>
|
||||||
|
<version>${jimureport.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jeecgframework.jimureport</groupId>
|
||||||
|
<artifactId>jimubi-spring-boot-starter</artifactId>
|
||||||
|
<version>${jimubi.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>com.github.jsqlparser</groupId>
|
||||||
|
<artifactId>jsqlparser</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>cn.hutool</groupId>
|
||||||
|
<artifactId>hutool-core</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,10 @@ public class PageParam implements Serializable {
|
||||||
@Min(value = 1, message = "页码最小值为 1")
|
@Min(value = 1, message = "页码最小值为 1")
|
||||||
private Integer pageNo = PAGE_NO;
|
private Integer pageNo = PAGE_NO;
|
||||||
|
|
||||||
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
@Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||||
@NotNull(message = "每页条数不能为空")
|
@NotNull(message = "每页条数不能为空")
|
||||||
@Min(value = 1, message = "每页条数最小值为 1")
|
@Min(value = 1, message = "每页条数最小值为 1")
|
||||||
@Max(value = 100, message = "每页条数最大值为 100")
|
@Max(value = 200, message = "每页条数最大值为 200")
|
||||||
private Integer pageSize = PAGE_SIZE;
|
private Integer pageSize = PAGE_SIZE;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -349,4 +349,27 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -7,6 +7,7 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -65,4 +66,47 @@ public class MapUtils {
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Map 中获取 BigDecimal 值
|
||||||
|
*
|
||||||
|
* @param map Map 数据源
|
||||||
|
* @param key 键名
|
||||||
|
* @return BigDecimal 值,解析失败或值为 null 时返回 null
|
||||||
|
*/
|
||||||
|
public static BigDecimal getBigDecimal(Map<String, ?> map, String key) {
|
||||||
|
return getBigDecimal(map, key, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Map 中获取 BigDecimal 值
|
||||||
|
*
|
||||||
|
* @param map Map 数据源
|
||||||
|
* @param key 键名
|
||||||
|
* @param defaultValue 默认值
|
||||||
|
* @return BigDecimal 值,解析失败或值为 null 时返回默认值
|
||||||
|
*/
|
||||||
|
public static BigDecimal getBigDecimal(Map<String, ?> map, String key, BigDecimal defaultValue) {
|
||||||
|
if (map == null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
Object value = map.get(key);
|
||||||
|
if (value == null) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
if (value instanceof BigDecimal) {
|
||||||
|
return (BigDecimal) value;
|
||||||
|
}
|
||||||
|
if (value instanceof Number) {
|
||||||
|
return BigDecimal.valueOf(((Number) value).doubleValue());
|
||||||
|
}
|
||||||
|
if (value instanceof String) {
|
||||||
|
try {
|
||||||
|
return new BigDecimal((String) value);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,21 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 格式化时间范围
|
* 格式化时间范围
|
||||||
*
|
*
|
||||||
|
|
@ -335,6 +350,27 @@ 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)以来的秒数。
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
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;
|
||||||
|
|
@ -14,6 +12,7 @@ import org.springframework.web.util.UriComponentsBuilder;
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.net.URLDecoder;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
@ -37,14 +36,38 @@ public class HttpUtils {
|
||||||
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
|
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
/**
|
||||||
|
* 解码 URL 参数(query parameter)
|
||||||
|
* 注意:此方法会将 + 解码为空格,适用于 query parameter,不适用于 URL path
|
||||||
|
*
|
||||||
|
* @see #decodeUrlPath(String)
|
||||||
|
* @param value 参数
|
||||||
|
* @return 解码后的参数
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static String decodeUtf8(String value) {
|
||||||
|
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解码 URL 路径
|
||||||
|
* 与 {@link #decodeUtf8(String)} 不同,此方法不会将 + 解码为空格,保持 + 为字面字符
|
||||||
|
* 适用于 URL path 部分的解码
|
||||||
|
*
|
||||||
|
* @param path URL 路径
|
||||||
|
* @return 解码后的路径
|
||||||
|
*/
|
||||||
|
@SneakyThrows
|
||||||
|
public static String decodeUrlPath(String path) {
|
||||||
|
// 先将 + 替换为 %2B,避免被 URLDecoder 解码为空格
|
||||||
|
String encoded = path.replace("+", "%2B");
|
||||||
|
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
|
||||||
|
}
|
||||||
|
|
||||||
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());
|
||||||
// 先移除
|
// 先移除;再添加
|
||||||
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
|
builder.getQuery().remove(key);
|
||||||
ReflectUtil.getFieldValue(builder.getQuery(), "query");
|
|
||||||
query.remove(key);
|
|
||||||
// 后添加
|
|
||||||
builder.addQuery(key, value);
|
builder.addQuery(key, value);
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -173,6 +173,24 @@ public class JsonUtils {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 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<>();
|
||||||
|
|
@ -229,4 +247,53 @@ public class JsonUtils {
|
||||||
return JSONUtil.isTypeJSONObject(str);
|
return JSONUtil.isTypeJSONObject(str);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Object 转换为目标类型
|
||||||
|
* <p>
|
||||||
|
* 避免先转 jsonString 再 parseObject 的性能损耗
|
||||||
|
*
|
||||||
|
* @param obj 源对象(可以是 Map、POJO 等)
|
||||||
|
* @param clazz 目标类型
|
||||||
|
* @return 转换后的对象
|
||||||
|
*/
|
||||||
|
public static <T> T convertObject(Object obj, Class<T> clazz) {
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (clazz.isInstance(obj)) {
|
||||||
|
return clazz.cast(obj);
|
||||||
|
}
|
||||||
|
return objectMapper.convertValue(obj, clazz);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Object 转换为目标类型(支持泛型)
|
||||||
|
*
|
||||||
|
* @param obj 源对象
|
||||||
|
* @param typeReference 目标类型引用
|
||||||
|
* @return 转换后的对象
|
||||||
|
*/
|
||||||
|
public static <T> T convertObject(Object obj, TypeReference<T> typeReference) {
|
||||||
|
if (obj == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return objectMapper.convertValue(obj, typeReference);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Object 转换为 List 类型
|
||||||
|
* <p>
|
||||||
|
* 避免先转 jsonString 再 parseArray 的性能损耗
|
||||||
|
*
|
||||||
|
* @param obj 源对象(可以是 List、数组等)
|
||||||
|
* @param clazz 目标元素类型
|
||||||
|
* @return 转换后的 List
|
||||||
|
*/
|
||||||
|
public static <T> List<T> convertList(Object obj, Class<T> clazz) {
|
||||||
|
if (obj == null) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
return objectMapper.convertValue(obj, objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,85 @@
|
||||||
package cn.iocoder.yudao.framework.common.util.json.databind;
|
package cn.iocoder.yudao.framework.common.util.json.databind;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.ObjUtil;
|
||||||
import cn.hutool.core.util.ReflectUtil;
|
import cn.hutool.core.util.ReflectUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.core.JsonGenerator;
|
import com.fasterxml.jackson.core.JsonGenerator;
|
||||||
import com.fasterxml.jackson.databind.JsonSerializer;
|
import com.fasterxml.jackson.databind.JsonSerializer;
|
||||||
import com.fasterxml.jackson.databind.SerializerProvider;
|
import com.fasterxml.jackson.databind.SerializerProvider;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.ZoneId;
|
import java.time.ZoneId;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 基于时间戳的 LocalDateTime 序列化器
|
* 基于时间戳的 LocalDateTime 序列化器
|
||||||
*
|
*
|
||||||
* @author 老五
|
* @author 老五
|
||||||
*/
|
*/
|
||||||
|
@Slf4j
|
||||||
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
|
||||||
|
|
||||||
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
|
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
|
||||||
|
|
||||||
|
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||||
String fieldName = gen.getOutputContext().getCurrentName();
|
|
||||||
Class<?> clazz = gen.getOutputContext().getCurrentValue().getClass();
|
|
||||||
Field field = ReflectUtil.getField(clazz, fieldName);
|
|
||||||
// 情况一:有 JsonFormat 自定义注解,则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
|
// 情况一:有 JsonFormat 自定义注解,则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
|
||||||
JsonFormat[] jsonFormats = field.getAnnotationsByType(JsonFormat.class);
|
String fieldName = gen.getOutputContext().getCurrentName();
|
||||||
if (jsonFormats.length > 0) {
|
if (fieldName != null) {
|
||||||
String pattern = jsonFormats[0].pattern();
|
Object currentValue = gen.getOutputContext().getCurrentValue();
|
||||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
|
if (currentValue != null) {
|
||||||
gen.writeString(formatter.format(value));
|
Class<?> clazz = currentValue.getClass();
|
||||||
return;
|
Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
|
||||||
|
Field field = fieldMap.get(fieldName);
|
||||||
|
// 进一步修复:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480
|
||||||
|
if (field != null && field.isAnnotationPresent(JsonFormat.class)) {
|
||||||
|
JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class);
|
||||||
|
try {
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern());
|
||||||
|
gen.writeString(formatter.format(value));
|
||||||
|
return;
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]",
|
||||||
|
clazz.getName(), fieldName, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳
|
// 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳
|
||||||
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
|
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建字段映射(缓存)
|
||||||
|
*
|
||||||
|
* @param clazz 类
|
||||||
|
* @return 字段映射
|
||||||
|
*/
|
||||||
|
private Map<String, Field> buildFieldMap(Class<?> clazz) {
|
||||||
|
Map<String, Field> fieldMap = new HashMap<>();
|
||||||
|
for (Field field : ReflectUtil.getFields(clazz)) {
|
||||||
|
String fieldName = field.getName();
|
||||||
|
JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
|
||||||
|
if (jsonProperty != null) {
|
||||||
|
String value = jsonProperty.value();
|
||||||
|
if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) {
|
||||||
|
fieldName = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fieldMap.put(fieldName, field);
|
||||||
|
}
|
||||||
|
return fieldMap;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,6 +60,11 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
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 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", "");
|
||||||
|
// 断言:保留 key,value 为空
|
||||||
|
assertEquals("https://www.iocoder.cn/path?a=", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -57,8 +57,6 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||||
private static final String DEPT_COLUMN_NAME = "dept_id";
|
private static final String DEPT_COLUMN_NAME = "dept_id";
|
||||||
private static final String USER_COLUMN_NAME = "user_id";
|
private static final String USER_COLUMN_NAME = "user_id";
|
||||||
|
|
||||||
static final Expression EXPRESSION_NULL = new NullValue();
|
|
||||||
|
|
||||||
private final PermissionCommonApi permissionApi;
|
private final PermissionCommonApi permissionApi;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -120,7 +118,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||||
|
|
||||||
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||||
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
||||||
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
||||||
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -133,7 +131,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
||||||
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
|
JsonUtils.toJsonString(loginUser), tableName, tableAlias, JsonUtils.toJsonString(deptDataPermission));
|
||||||
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
||||||
// loginUser.getId(), tableName, tableAlias.getName()));
|
// loginUser.getId(), tableName, tableAlias.getName()));
|
||||||
return EXPRESSION_NULL;
|
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
||||||
}
|
}
|
||||||
if (deptExpression == null) {
|
if (deptExpression == null) {
|
||||||
return userExpression;
|
return userExpression;
|
||||||
|
|
|
||||||
|
|
@ -3,12 +3,12 @@ package cn.iocoder.yudao.framework.datapermission.core.rule.dept;
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.util.ReflectUtil;
|
import cn.hutool.core.util.ReflectUtil;
|
||||||
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
|
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
|
||||||
|
import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
||||||
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||||
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
|
||||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
|
||||||
import cn.iocoder.yudao.framework.common.biz.system.permission.dto.DeptDataPermissionRespDTO;
|
|
||||||
import net.sf.jsqlparser.expression.Alias;
|
import net.sf.jsqlparser.expression.Alias;
|
||||||
import net.sf.jsqlparser.expression.Expression;
|
import net.sf.jsqlparser.expression.Expression;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
|
@ -20,7 +20,6 @@ import org.mockito.MockedStatic;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
import static cn.iocoder.yudao.framework.datapermission.core.rule.dept.DeptDataPermissionRule.EXPRESSION_NULL;
|
|
||||||
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;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
@ -151,7 +150,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
||||||
// 调用
|
// 调用
|
||||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||||
// 断言
|
// 断言
|
||||||
assertSame(EXPRESSION_NULL, expression);
|
assertEquals("null = null", expression.toString());
|
||||||
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ 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;
|
||||||
|
|
@ -25,44 +26,46 @@ 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;
|
||||||
|
|
||||||
private AreaUtils() {
|
static {
|
||||||
long now = System.currentTimeMillis();
|
init();
|
||||||
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();
|
|
||||||
rows.remove(0); // 删除 header
|
|
||||||
for (CsvRow row : rows) {
|
|
||||||
// 创建 Area 对象
|
|
||||||
Area area = new Area(Integer.valueOf(row.get(0)), row.get(1), Integer.valueOf(row.get(2)),
|
|
||||||
null, new ArrayList<>());
|
|
||||||
// 添加到 areas 中
|
|
||||||
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))); // 父
|
private static void init() {
|
||||||
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
|
try {
|
||||||
area.setParent(parent);
|
long now = System.currentTimeMillis();
|
||||||
parent.getChildren().add(area);
|
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();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,10 @@ 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 工具类
|
||||||
*
|
*
|
||||||
|
|
@ -16,30 +15,29 @@ import java.io.IOException;
|
||||||
* @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 IPUtils() {
|
private static void init() {
|
||||||
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 (IOException e) {
|
} catch (Exception e) {
|
||||||
log.error("启动加载 IPUtils 失败", e);
|
throw new RuntimeException("IPUtils 初始化失败", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,12 @@ 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} 的单元测试
|
||||||
|
|
@ -31,6 +36,46 @@ 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())));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package cn.iocoder.yudao.framework.tenant.core.redis;
|
package cn.iocoder.yudao.framework.tenant.core.redis;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
|
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
|
||||||
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
|
@ -21,6 +22,8 @@ import java.util.Set;
|
||||||
@Slf4j
|
@Slf4j
|
||||||
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
|
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
|
||||||
|
|
||||||
|
private static final String SPLIT = "#";
|
||||||
|
|
||||||
private final Set<String> ignoreCaches;
|
private final Set<String> ignoreCaches;
|
||||||
|
|
||||||
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
|
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
|
||||||
|
|
@ -32,10 +35,11 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Cache getCache(String name) {
|
public Cache getCache(String name) {
|
||||||
|
String[] names = StrUtil.splitToArray(name, SPLIT);
|
||||||
// 如果开启多租户,则 name 拼接租户后缀
|
// 如果开启多租户,则 name 拼接租户后缀
|
||||||
if (!TenantContextHolder.isIgnore()
|
if (!TenantContextHolder.isIgnore()
|
||||||
&& TenantContextHolder.getTenantId() != null
|
&& TenantContextHolder.getTenantId() != null
|
||||||
&& !CollUtil.contains(ignoreCaches, name)) {
|
&& !CollUtil.contains(ignoreCaches, names[0])) {
|
||||||
name = name + ":" + TenantContextHolder.getTenantId();
|
name = name + ":" + TenantContextHolder.getTenantId();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,31 @@ 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 条记录
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import java.util.function.Consumer;
|
||||||
* <p>
|
* <p>
|
||||||
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
||||||
* 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
|
* 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
|
||||||
|
*
|
||||||
* @param <T> 数据类型
|
* @param <T> 数据类型
|
||||||
*/
|
*/
|
||||||
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||||
|
|
@ -122,6 +123,12 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <X> MPJLambdaWrapperX<T> orderByAsc(SFunction<X, ?> column) {
|
||||||
|
super.orderByAsc(true, column);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MPJLambdaWrapperX<T> last(String lastSql) {
|
public MPJLambdaWrapperX<T> last(String lastSql) {
|
||||||
super.last(lastSql);
|
super.last(lastSql);
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,17 @@
|
||||||
<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.x(httpcore),导致 ClassNotFoundException。
|
||||||
|
临时解决:显式引入 httpclient 4.x。待 WxJava 修复后移除。
|
||||||
|
-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.httpcomponents</groupId>
|
||||||
|
<artifactId>httpclient</artifactId>
|
||||||
|
<version>4.5.14</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 工具相关 -->
|
<!-- 工具相关 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ 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;
|
||||||
|
|
@ -52,6 +53,10 @@ 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());
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,14 @@ 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/ 开启]");
|
||||||
// 支付平台
|
// 支付平台
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,9 @@ import javax.servlet.Filter;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Predicate;
|
import java.util.function.Predicate;
|
||||||
|
|
||||||
@AutoConfiguration
|
@AutoConfiguration(beforeName = {
|
||||||
|
"com.fhs.trans.config.TransServiceConfig" // cloud 独有:避免一键改包后,RestTemplate 初始化的冲突。可见 https://t.zsxq.com/T4yj7 帖子
|
||||||
|
})
|
||||||
@EnableConfigurationProperties(WebProperties.class)
|
@EnableConfigurationProperties(WebProperties.class)
|
||||||
public class YudaoWebAutoConfiguration {
|
public class YudaoWebAutoConfiguration {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -410,25 +410,43 @@ 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. CRM 系统
|
// 6. WMS 仓库管理系统
|
||||||
|
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/ 开启]");
|
||||||
}
|
}
|
||||||
// 7. 支付平台
|
// 8. MES 系统
|
||||||
|
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/ 开启]");
|
||||||
}
|
}
|
||||||
// 8. AI 大模型
|
// 11. 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/ 开启]");
|
||||||
}
|
}
|
||||||
// 9. IoT 物联网
|
// 12. 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(),
|
||||||
|
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
package cn.iocoder.yudao.gateway.filter.cors;
|
|
||||||
|
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
|
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter;
|
|
||||||
import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
|
|
||||||
import org.springframework.core.Ordered;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
|
||||||
import org.springframework.stereotype.Component;
|
|
||||||
import org.springframework.web.server.ServerWebExchange;
|
|
||||||
import reactor.core.publisher.Mono;
|
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解决 Spring Cloud Gateway 2.x 跨域时,出现重复 Origin 的 BUG
|
|
||||||
*
|
|
||||||
* 参考文档:<a href="https://blog.csdn.net/zimou5581/article/details/90043178" />
|
|
||||||
*
|
|
||||||
* @author 芋道源码
|
|
||||||
*/
|
|
||||||
@Component
|
|
||||||
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getOrder() {
|
|
||||||
// 指定此过滤器位于 NettyWriteResponseFilter 之后
|
|
||||||
// 即待处理完响应体后接着处理响应头
|
|
||||||
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
|
|
||||||
return chain.filter(exchange).then(Mono.defer(() -> {
|
|
||||||
// https://gitee.com/zhijiantianya/yudao-cloud/pulls/177/
|
|
||||||
List<String> keysToModify = exchange.getResponse().getHeaders().entrySet().stream()
|
|
||||||
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
|
|
||||||
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|
|
||||||
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)))
|
|
||||||
.map(Map.Entry::getKey)
|
|
||||||
.collect(Collectors.toList());
|
|
||||||
keysToModify.forEach(key->{
|
|
||||||
List<String> values = exchange.getResponse().getHeaders().get(key);
|
|
||||||
if (values != null && !values.isEmpty()) {
|
|
||||||
exchange.getResponse().getHeaders().put(key, Collections.singletonList(values.get(0)));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return chain.filter(exchange);
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -39,8 +39,14 @@ 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/ 开启]");
|
||||||
// 支付平台
|
// 支付平台
|
||||||
|
|
|
||||||
|
|
@ -12,3 +12,24 @@ spring:
|
||||||
config: # 【注册中心】配置项
|
config: # 【注册中心】配置项
|
||||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||||
|
|
||||||
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
# Actuator 监控端点的配置项
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
|
||||||
|
exposure:
|
||||||
|
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
|
||||||
|
|
||||||
|
# Spring Boot Admin 配置项
|
||||||
|
spring:
|
||||||
|
boot:
|
||||||
|
admin:
|
||||||
|
# Spring Boot Admin Client 客户端的相关配置
|
||||||
|
client:
|
||||||
|
instance:
|
||||||
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
@ -4,16 +4,38 @@ spring:
|
||||||
cloud:
|
cloud:
|
||||||
nacos:
|
nacos:
|
||||||
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
||||||
username: # Nacos 账号
|
username: nacos
|
||||||
password: # Nacos 密码
|
password: nacos-admin
|
||||||
discovery: # 【配置中心】配置项
|
discovery: # 【配置中心】配置项
|
||||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||||
config: # 【注册中心】配置项
|
config: # 【注册中心】配置项
|
||||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUPn
|
||||||
|
|
||||||
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
# Actuator 监控端点的配置项
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
|
||||||
|
exposure:
|
||||||
|
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
|
||||||
|
|
||||||
|
# Spring Boot Admin 配置项
|
||||||
|
spring:
|
||||||
|
boot:
|
||||||
|
admin:
|
||||||
|
# Spring Boot Admin Client 客户端的相关配置
|
||||||
|
client:
|
||||||
|
instance:
|
||||||
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
# 日志文件配置
|
# 日志文件配置
|
||||||
logging:
|
logging:
|
||||||
level:
|
level:
|
||||||
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
|
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿:先禁用,Spring Boot 3.X 存在部分错误的 WARN 提示
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -192,8 +192,24 @@ 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
|
||||||
x-forwarded:
|
x-forwarded:
|
||||||
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
||||||
|
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
|
||||||
|
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 48080
|
port: 48080
|
||||||
|
|
@ -249,6 +265,12 @@ 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
|
||||||
|
|
||||||
--- #################### 芋道相关配置 ####################
|
--- #################### 芋道相关配置 ####################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package cn.iocoder.yudao.module.ai.enums;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AI 知识库文档切片策略枚举
|
||||||
|
*
|
||||||
|
* @author runzhen
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum AiDocumentSplitStrategyEnum {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动识别文档类型并选择最佳切片策略
|
||||||
|
*/
|
||||||
|
AUTO("auto", "自动识别"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基于 Token 数量机械切分(默认策略)
|
||||||
|
*/
|
||||||
|
TOKEN("token", "Token 切分"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按段落切分(以双换行符为分隔)
|
||||||
|
*/
|
||||||
|
PARAGRAPH("paragraph", "段落切分"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown QA 格式专用切片器
|
||||||
|
* 识别二级标题作为问题,保持问答对完整性
|
||||||
|
* 长答案智能切分但保留问题作为上下文
|
||||||
|
*/
|
||||||
|
MARKDOWN_QA("markdown_qa", "Markdown QA 切分"),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语义化切分,保留句子完整性
|
||||||
|
* 在段落和句子边界处切分,避免截断
|
||||||
|
*/
|
||||||
|
SEMANTIC("semantic", "语义切分");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 策略代码
|
||||||
|
*/
|
||||||
|
private final String code;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 策略名称
|
||||||
|
*/
|
||||||
|
private final String name;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -19,8 +19,9 @@
|
||||||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||||
</description>
|
</description>
|
||||||
<properties>
|
<properties>
|
||||||
<spring-ai.version>1.1.0</spring-ai.version>
|
<spring-ai.version>1.1.5</spring-ai.version>
|
||||||
<alibaba-ai.version>1.1.0.0-M5</alibaba-ai.version>
|
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
|
||||||
|
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
|
||||||
<tinyflow.version>1.2.6</tinyflow.version>
|
<tinyflow.version>1.2.6</tinyflow.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
|
|
@ -239,6 +240,12 @@
|
||||||
<groupId>org.springframework.ai</groupId>
|
<groupId>org.springframework.ai</groupId>
|
||||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||||
<version>${spring-ai.version}</version>
|
<version>${spring-ai.version}</version>
|
||||||
|
<exclusions>
|
||||||
|
<exclusion>
|
||||||
|
<groupId>io.swagger.core.v3</groupId>
|
||||||
|
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||||
|
</exclusion>
|
||||||
|
</exclusions>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<!-- 客户端 -->
|
<!-- 客户端 -->
|
||||||
|
|
@ -262,6 +269,11 @@
|
||||||
<groupId>com.agentsflex</groupId>
|
<groupId>com.agentsflex</groupId>
|
||||||
<artifactId>agents-flex-store-elasticsearch</artifactId>
|
<artifactId>agents-flex-store-elasticsearch</artifactId>
|
||||||
</exclusion>
|
</exclusion>
|
||||||
|
<exclusion>
|
||||||
|
<!-- 解决 https://t.zsxq.com/pCBZC 问题 -->
|
||||||
|
<groupId>com.agentsflex</groupId>
|
||||||
|
<artifactId>agents-flex-search-engine-es</artifactId>
|
||||||
|
</exclusion>
|
||||||
<exclusion>
|
<exclusion>
|
||||||
<!-- TODO @芋艿:暂时移除 groovy,和 iot 冲突 -->
|
<!-- TODO @芋艿:暂时移除 groovy,和 iot 冲突 -->
|
||||||
<groupId>org.codehaus.groovy</groupId>
|
<groupId>org.codehaus.groovy</groupId>
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,15 @@ 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({
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ import lombok.Data;
|
||||||
public class AiKnowledgeSegmentPageReqVO extends PageParam {
|
public class AiKnowledgeSegmentPageReqVO extends PageParam {
|
||||||
|
|
||||||
@Schema(description = "文档编号", example = "1")
|
@Schema(description = "文档编号", example = "1")
|
||||||
private Integer documentId;
|
private Long documentId;
|
||||||
|
|
||||||
@Schema(description = "分段内容关键字", example = "Java 开发")
|
@Schema(description = "分段内容关键字", example = "Java 开发")
|
||||||
private String content;
|
private String content;
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import java.util.List;
|
||||||
* @since 2024/4/14 17:35
|
* @since 2024/4/14 17:35
|
||||||
*/
|
*/
|
||||||
@TableName(value = "ai_chat_message", autoResultMap = true)
|
@TableName(value = "ai_chat_message", autoResultMap = true)
|
||||||
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
@KeySequence("ai_chat_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ import lombok.*;
|
||||||
* @author 芋道源码
|
* @author 芋道源码
|
||||||
*/
|
*/
|
||||||
@TableName("ai_api_key")
|
@TableName("ai_api_key")
|
||||||
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
@KeySequence("ai_api_key_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||||
@Data
|
@Data
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
|
|
|
||||||
|
|
@ -51,8 +51,7 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
|
||||||
MPJLambdaWrapper<AiKnowledgeSegmentDO> wrapper = new MPJLambdaWrapperX<AiKnowledgeSegmentDO>()
|
MPJLambdaWrapper<AiKnowledgeSegmentDO> wrapper = new MPJLambdaWrapperX<AiKnowledgeSegmentDO>()
|
||||||
.selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId)
|
.selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId)
|
||||||
.selectCount(AiKnowledgeSegmentDO::getId, "count")
|
.selectCount(AiKnowledgeSegmentDO::getId, "count")
|
||||||
.select("COUNT(CASE WHEN vector_id > '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY
|
.select("COUNT(CASE WHEN vector_id IS NOT NULL AND vector_id <> '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY + "' THEN 1 ELSE NULL END) AS embeddingCount")
|
||||||
+ "' THEN 1 ELSE NULL END) AS embeddingCount")
|
|
||||||
.in(AiKnowledgeSegmentDO::getDocumentId, documentIds)
|
.in(AiKnowledgeSegmentDO::getDocumentId, documentIds)
|
||||||
.groupBy(AiKnowledgeSegmentDO::getDocumentId);
|
.groupBy(AiKnowledgeSegmentDO::getDocumentId);
|
||||||
return selectJoinList(AiKnowledgeSegmentProcessRespVO.class, wrapper);
|
return selectJoinList(AiKnowledgeSegmentProcessRespVO.class, wrapper);
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,6 @@ import cn.iocoder.yudao.module.ai.util.AiUtils;
|
||||||
import cn.iocoder.yudao.module.ai.util.FileTypeUtils;
|
import cn.iocoder.yudao.module.ai.util.FileTypeUtils;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
import io.modelcontextprotocol.client.McpSyncClient;
|
import io.modelcontextprotocol.client.McpSyncClient;
|
||||||
import jakarta.annotation.Resource;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.chat.messages.Message;
|
import org.springframework.ai.chat.messages.Message;
|
||||||
import org.springframework.ai.chat.messages.MessageType;
|
import org.springframework.ai.chat.messages.MessageType;
|
||||||
|
|
@ -59,6 +58,8 @@ import reactor.core.publisher.Flux;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
|
@ -231,20 +232,24 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||||
// 4.3 流式返回
|
// 4.3 流式返回
|
||||||
StringBuffer contentBuffer = new StringBuffer();
|
StringBuffer contentBuffer = new StringBuffer();
|
||||||
StringBuffer reasoningContentBuffer = new StringBuffer();
|
StringBuffer reasoningContentBuffer = new StringBuffer();
|
||||||
|
|
||||||
|
// 防止执行多次知识库和联网搜索
|
||||||
|
AtomicBoolean firstExecuteFlag = new AtomicBoolean(true);
|
||||||
|
AtomicReference<List<AiChatMessageRespVO.KnowledgeSegment>> cacheSegments = new AtomicReference<>();
|
||||||
|
AtomicReference<List<AiWebSearchResponse.WebPage>> cacheWebSearchPages = new AtomicReference<>();
|
||||||
return streamResponse.map(chunk -> {
|
return streamResponse.map(chunk -> {
|
||||||
// 仅首次:返回知识库、联网搜索
|
// 仅首次:返回知识库、联网搜索
|
||||||
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
|
|
||||||
List<AiWebSearchResponse.WebPage> webSearchPages = null;
|
|
||||||
if (StrUtil.isEmpty(contentBuffer)) {
|
if (StrUtil.isEmpty(contentBuffer)) {
|
||||||
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
|
if (firstExecuteFlag.compareAndSet(true, false)) { // CAS 操作,确保仅执行一次
|
||||||
knowledgeDocumentService.getKnowledgeDocumentMap(
|
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> knowledgeDocumentService.getKnowledgeDocumentMap(
|
||||||
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
|
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
|
||||||
segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
|
cacheSegments.set(BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
|
||||||
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
|
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
|
||||||
segment.setDocumentName(document != null ? document.getName() : null);
|
segment.setDocumentName(document != null ? document.getName() : null);
|
||||||
});
|
}));
|
||||||
if (webSearchResponse != null) {
|
if (webSearchResponse != null) {
|
||||||
webSearchPages = webSearchResponse.getLists();
|
cacheWebSearchPages.set(webSearchResponse.getLists());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 响应结果
|
// 响应结果
|
||||||
|
|
@ -261,7 +266,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
|
||||||
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
|
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
|
||||||
.setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
|
.setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
|
||||||
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
|
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
|
||||||
.setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
|
.setSegments(cacheSegments.get()).setWebSearchPages(cacheWebSearchPages.get()))); // 知识库 + 联网搜索
|
||||||
}).doOnComplete(() -> {
|
}).doOnComplete(() -> {
|
||||||
// 忽略租户,因为 Flux 异步无法透传租户
|
// 忽略租户,因为 Flux 异步无法透传租户
|
||||||
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
|
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,7 @@ 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 构建请求
|
||||||
|
|
@ -164,8 +165,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()
|
||||||
.withModel(model.getModel()).withN(1)
|
.model(model.getModel()).n(1)
|
||||||
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
|
.height(draw.getHeight()).width(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()
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,13 @@ public interface AiKnowledgeSegmentService {
|
||||||
*/
|
*/
|
||||||
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
|
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除知识库段落
|
||||||
|
*
|
||||||
|
* @param id 段落编号
|
||||||
|
*/
|
||||||
|
void deleteKnowledgeSegment(Long id);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 重新索引知识库下的所有文档段落
|
* 重新索引知识库下的所有文档段落
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.hutool.core.collection.ListUtil;
|
import cn.hutool.core.collection.ListUtil;
|
||||||
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.enums.CommonStatusEnum;
|
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
|
||||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
|
@ -15,8 +16,11 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
|
||||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
|
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
|
||||||
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
|
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
|
||||||
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
|
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
|
||||||
|
import cn.iocoder.yudao.module.ai.enums.AiDocumentSplitStrategyEnum;
|
||||||
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
|
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
|
||||||
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
|
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
|
||||||
|
import cn.iocoder.yudao.module.ai.service.knowledge.splitter.MarkdownQaSplitter;
|
||||||
|
import cn.iocoder.yudao.module.ai.service.knowledge.splitter.SemanticTextSplitter;
|
||||||
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
|
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
|
||||||
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
|
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
|
||||||
import com.alibaba.cloud.ai.model.RerankModel;
|
import com.alibaba.cloud.ai.model.RerankModel;
|
||||||
|
|
@ -39,8 +43,7 @@ import java.util.*;
|
||||||
|
|
||||||
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
|
||||||
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
|
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
|
||||||
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
|
|
||||||
import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
|
import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -95,8 +98,9 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||||
AiKnowledgeDO knowledgeDO = knowledgeService.validateKnowledgeExists(documentDO.getKnowledgeId());
|
AiKnowledgeDO knowledgeDO = knowledgeService.validateKnowledgeExists(documentDO.getKnowledgeId());
|
||||||
VectorStore vectorStore = getVectorStoreById(knowledgeDO);
|
VectorStore vectorStore = getVectorStoreById(knowledgeDO);
|
||||||
|
|
||||||
// 2. 文档切片
|
// 2. 文档切片(使用自动检测策略)
|
||||||
List<Document> documentSegments = splitContentByToken(content, documentDO.getSegmentMaxTokens());
|
List<Document> documentSegments = splitContentByStrategy(content, documentDO.getSegmentMaxTokens(),
|
||||||
|
AiDocumentSplitStrategyEnum.AUTO, documentDO.getUrl());
|
||||||
|
|
||||||
// 3.1 存储切片
|
// 3.1 存储切片
|
||||||
List<AiKnowledgeSegmentDO> segmentDOs = convertList(documentSegments, segment -> {
|
List<AiKnowledgeSegmentDO> segmentDOs = convertList(documentSegments, segment -> {
|
||||||
|
|
@ -137,6 +141,19 @@ 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. 查询需要删除的段落
|
||||||
|
|
@ -295,8 +312,10 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||||
// 1. 读取 URL 内容
|
// 1. 读取 URL 内容
|
||||||
String content = knowledgeDocumentService.readUrl(url);
|
String content = knowledgeDocumentService.readUrl(url);
|
||||||
|
|
||||||
// 2. 文档切片
|
// 2.1 自动检测文档类型并选择策略
|
||||||
List<Document> documentSegments = splitContentByToken(content, segmentMaxTokens);
|
AiDocumentSplitStrategyEnum strategy = detectDocumentStrategy(content, url);
|
||||||
|
// 2.2 文档切片
|
||||||
|
List<Document> documentSegments = splitContentByStrategy(content, segmentMaxTokens, strategy, url);
|
||||||
|
|
||||||
// 3. 转换为段落对象
|
// 3. 转换为段落对象
|
||||||
return convertList(documentSegments, segment -> {
|
return convertList(documentSegments, segment -> {
|
||||||
|
|
@ -333,11 +352,103 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
|
||||||
return getVectorStoreById(knowledge);
|
return getVectorStoreById(knowledge);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static List<Document> splitContentByToken(String content, Integer segmentMaxTokens) {
|
/**
|
||||||
TextSplitter textSplitter = buildTokenTextSplitter(segmentMaxTokens);
|
* 根据策略切分内容
|
||||||
|
*
|
||||||
|
* @param content 文档内容
|
||||||
|
* @param segmentMaxTokens 分段的最大 Token 数
|
||||||
|
* @param strategy 切片策略
|
||||||
|
* @param url 文档 URL(用于自动检测文件类型)
|
||||||
|
* @return 切片后的文档列表
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("EnhancedSwitchMigration")
|
||||||
|
private List<Document> splitContentByStrategy(String content, Integer segmentMaxTokens,
|
||||||
|
AiDocumentSplitStrategyEnum strategy, String url) {
|
||||||
|
// 自动检测策略
|
||||||
|
if (strategy == AiDocumentSplitStrategyEnum.AUTO) {
|
||||||
|
strategy = detectDocumentStrategy(content, url);
|
||||||
|
log.info("[splitContentByStrategy][自动检测到文档策略: {}]", strategy.getName());
|
||||||
|
}
|
||||||
|
// 根据策略切分
|
||||||
|
TextSplitter textSplitter;
|
||||||
|
switch (strategy) {
|
||||||
|
case MARKDOWN_QA:
|
||||||
|
textSplitter = new MarkdownQaSplitter(segmentMaxTokens);
|
||||||
|
break;
|
||||||
|
case SEMANTIC:
|
||||||
|
textSplitter = new SemanticTextSplitter(segmentMaxTokens);
|
||||||
|
break;
|
||||||
|
case PARAGRAPH:
|
||||||
|
textSplitter = new SemanticTextSplitter(segmentMaxTokens, 0); // 段落切分,无重叠
|
||||||
|
break;
|
||||||
|
case TOKEN:
|
||||||
|
default:
|
||||||
|
textSplitter = buildTokenTextSplitter(segmentMaxTokens);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 执行切分
|
||||||
return textSplitter.apply(Collections.singletonList(new Document(content)));
|
return textSplitter.apply(Collections.singletonList(new Document(content)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自动检测文档类型并选择切片策略
|
||||||
|
*
|
||||||
|
* @param content 文档内容
|
||||||
|
* @param url 文档 URL
|
||||||
|
* @return 推荐的切片策略
|
||||||
|
*/
|
||||||
|
private AiDocumentSplitStrategyEnum detectDocumentStrategy(String content, String url) {
|
||||||
|
if (StrUtil.isEmpty(content)) {
|
||||||
|
return AiDocumentSplitStrategyEnum.TOKEN;
|
||||||
|
}
|
||||||
|
// 1. 检测 Markdown QA 格式
|
||||||
|
if (isMarkdownQaFormat(content, url)) {
|
||||||
|
return AiDocumentSplitStrategyEnum.MARKDOWN_QA;
|
||||||
|
}
|
||||||
|
// 2. 检测普通 Markdown 文档
|
||||||
|
if (isMarkdownDocument(url)) {
|
||||||
|
return AiDocumentSplitStrategyEnum.SEMANTIC;
|
||||||
|
}
|
||||||
|
// 3. 默认使用语义切分(比 Token 切分更智能)
|
||||||
|
return AiDocumentSplitStrategyEnum.SEMANTIC;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为 Markdown QA 格式
|
||||||
|
* 特征:包含多个二级标题(## )且标题后紧跟答案内容
|
||||||
|
*/
|
||||||
|
private boolean isMarkdownQaFormat(String content, String url) {
|
||||||
|
// 文件扩展名判断
|
||||||
|
if (StrUtil.isNotEmpty(url) && !url.toLowerCase().endsWith(".md")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 统计二级标题数量
|
||||||
|
long h2Count = content.lines()
|
||||||
|
.filter(line -> line.trim().startsWith("## "))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// 要求一:至少包含 2 个二级标题才认为是 QA 格式
|
||||||
|
if (h2Count < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 要求二:检查标题占比(QA 文档标题行数相对较多),如果二级标题占比超过 10%,认为是 QA 格式
|
||||||
|
long totalLines = content.lines().count();
|
||||||
|
double h2Ratio = (double) h2Count / totalLines;
|
||||||
|
return h2Ratio > 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检测是否为 Markdown 文档
|
||||||
|
*/
|
||||||
|
private boolean isMarkdownDocument(String url) {
|
||||||
|
return StrUtil.endWithAnyIgnoreCase(url, ".md", ".markdown");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建基于 Token 的文本切片器(原有逻辑保留)
|
||||||
|
*/
|
||||||
private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) {
|
private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) {
|
||||||
return TokenTextSplitter.builder()
|
return TokenTextSplitter.builder()
|
||||||
.withChunkSize(segmentMaxTokens)
|
.withChunkSize(segmentMaxTokens)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,342 @@
|
||||||
|
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
|
||||||
|
|
||||||
|
import cn.hutool.core.collection.CollUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.transformer.splitter.TextSplitter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Markdown QA 格式专用切片器
|
||||||
|
*
|
||||||
|
* <p>功能特点:
|
||||||
|
* <ul>
|
||||||
|
* <li>识别二级标题(## )作为问题标记</li>
|
||||||
|
* <li>短 QA 对保持完整(不超过 Token 限制)</li>
|
||||||
|
* <li>长答案智能切分,每个片段保留完整问题作为上下文</li>
|
||||||
|
* <li>支持自定义 Token 估算器</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author runzhen
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@SuppressWarnings("SizeReplaceableByIsEmpty")
|
||||||
|
public class MarkdownQaSplitter extends TextSplitter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 二级标题正则:匹配 "## " 开头的行
|
||||||
|
*/
|
||||||
|
private static final Pattern H2_PATTERN = Pattern.compile("^##\\s+(.+)$", Pattern.MULTILINE);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 段落分隔符:双换行
|
||||||
|
*/
|
||||||
|
private static final String PARAGRAPH_SEPARATOR = "\n\n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 句子分隔符
|
||||||
|
*/
|
||||||
|
private static final Pattern SENTENCE_PATTERN = Pattern.compile("[。!?.!?]\\s*");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分段的最大 Token 数
|
||||||
|
*/
|
||||||
|
private final int chunkSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 估算器(简单实现:中文按字符数,英文按单词数的 1.3 倍)
|
||||||
|
*/
|
||||||
|
private final TokenEstimator tokenEstimator;
|
||||||
|
|
||||||
|
public MarkdownQaSplitter(int chunkSize) {
|
||||||
|
this.chunkSize = chunkSize;
|
||||||
|
this.tokenEstimator = new SimpleTokenEstimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> splitText(String text) {
|
||||||
|
if (StrUtil.isEmpty(text)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 QA 对
|
||||||
|
List<QaPair> qaPairs = parseQaPairs(text);
|
||||||
|
if (CollUtil.isEmpty(qaPairs)) {
|
||||||
|
// 如果没有识别到 QA 格式,按段落切分
|
||||||
|
return fallbackSplit(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理每个 QA 对
|
||||||
|
List<String> result = new ArrayList<>();
|
||||||
|
for (QaPair qaPair : qaPairs) {
|
||||||
|
result.addAll(splitQaPair(qaPair));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Markdown QA 对
|
||||||
|
*
|
||||||
|
* @param content 文本内容
|
||||||
|
* @return QA 对列表
|
||||||
|
*/
|
||||||
|
private List<QaPair> parseQaPairs(String content) {
|
||||||
|
// 找到所有二级标题位置
|
||||||
|
List<QaPair> qaPairs = new ArrayList<>();
|
||||||
|
List<Integer> headingPositions = new ArrayList<>();
|
||||||
|
List<String> questions = new ArrayList<>();
|
||||||
|
Matcher matcher = H2_PATTERN.matcher(content);
|
||||||
|
while (matcher.find()) {
|
||||||
|
headingPositions.add(matcher.start());
|
||||||
|
questions.add(matcher.group(1).trim());
|
||||||
|
}
|
||||||
|
if (CollUtil.isEmpty(headingPositions)) {
|
||||||
|
return qaPairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取每个 QA 对
|
||||||
|
for (int i = 0; i < headingPositions.size(); i++) {
|
||||||
|
int start = headingPositions.get(i);
|
||||||
|
int end = (i + 1 < headingPositions.size())
|
||||||
|
? headingPositions.get(i + 1)
|
||||||
|
: content.length();
|
||||||
|
String qaText = content.substring(start, end).trim();
|
||||||
|
String question = questions.get(i);
|
||||||
|
// 提取答案部分(去掉问题标题)
|
||||||
|
String answer = qaText.substring(qaText.indexOf('\n') + 1).trim();
|
||||||
|
qaPairs.add(new QaPair(question, answer, qaText));
|
||||||
|
}
|
||||||
|
return qaPairs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切分单个 QA 对
|
||||||
|
*
|
||||||
|
* @param qaPair QA 对
|
||||||
|
* @return 切分后的文本片段列表
|
||||||
|
*/
|
||||||
|
private List<String> splitQaPair(QaPair qaPair) {
|
||||||
|
// 如果整个 QA 对不超过限制,保持完整
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
String fullQa = qaPair.fullText;
|
||||||
|
int qaTokens = tokenEstimator.estimate(fullQa);
|
||||||
|
if (qaTokens <= chunkSize) {
|
||||||
|
chunks.add(fullQa);
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 长答案需要切分
|
||||||
|
log.debug("QA 对超过 Token 限制 ({} > {}),开始智能切分: {}", qaTokens, chunkSize, qaPair.question);
|
||||||
|
List<String> answerChunks = splitLongAnswer(qaPair.answer, qaPair.question);
|
||||||
|
for (String answerChunk : answerChunks) {
|
||||||
|
// 每个片段都包含完整问题
|
||||||
|
String chunkText = "## " + qaPair.question + "\n" + answerChunk;
|
||||||
|
chunks.add(chunkText);
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切分长答案
|
||||||
|
*
|
||||||
|
* @param answer 答案文本
|
||||||
|
* @param question 问题文本
|
||||||
|
* @return 切分后的答案片段列表
|
||||||
|
*/
|
||||||
|
private List<String> splitLongAnswer(String answer, String question) {
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
// 预留问题的 Token 空间
|
||||||
|
String questionHeader = "## " + question + "\n";
|
||||||
|
int questionTokens = tokenEstimator.estimate(questionHeader);
|
||||||
|
int availableTokens = chunkSize - questionTokens - 10; // 预留 10 个 Token 的缓冲
|
||||||
|
|
||||||
|
// 先按段落切分
|
||||||
|
String[] paragraphs = answer.split(PARAGRAPH_SEPARATOR);
|
||||||
|
StringBuilder currentChunk = new StringBuilder();
|
||||||
|
int currentTokens = 0;
|
||||||
|
for (String paragraph : paragraphs) {
|
||||||
|
if (StrUtil.isEmpty(paragraph)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int paragraphTokens = tokenEstimator.estimate(paragraph);
|
||||||
|
// 如果单个段落就超过限制,需要按句子切分
|
||||||
|
if (paragraphTokens > availableTokens) {
|
||||||
|
// 先保存当前块
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
currentChunk = new StringBuilder();
|
||||||
|
currentTokens = 0;
|
||||||
|
}
|
||||||
|
// 按句子切分长段落
|
||||||
|
chunks.addAll(splitLongParagraph(paragraph, availableTokens));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 如果加上这个段落会超过限制
|
||||||
|
if (currentTokens + paragraphTokens > availableTokens && currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
currentChunk = new StringBuilder();
|
||||||
|
currentTokens = 0;
|
||||||
|
}
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
currentChunk.append("\n\n");
|
||||||
|
}
|
||||||
|
// 添加段落
|
||||||
|
currentChunk.append(paragraph);
|
||||||
|
currentTokens += paragraphTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加最后一块
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
}
|
||||||
|
return CollUtil.isEmpty(chunks) ? Collections.singletonList(answer) : chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切分长段落(按句子)
|
||||||
|
*
|
||||||
|
* @param paragraph 段落文本
|
||||||
|
* @param availableTokens 可用的 Token 数
|
||||||
|
* @return 切分后的文本片段列表
|
||||||
|
*/
|
||||||
|
private List<String> splitLongParagraph(String paragraph, int availableTokens) {
|
||||||
|
// 按句子切分
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
String[] sentences = SENTENCE_PATTERN.split(paragraph);
|
||||||
|
|
||||||
|
// 按句子累积切分
|
||||||
|
StringBuilder currentChunk = new StringBuilder();
|
||||||
|
int currentTokens = 0;
|
||||||
|
for (String sentence : sentences) {
|
||||||
|
if (StrUtil.isEmpty(sentence)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int sentenceTokens = tokenEstimator.estimate(sentence);
|
||||||
|
// 如果单个句子就超过限制,强制切分
|
||||||
|
if (sentenceTokens > availableTokens) {
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
currentChunk = new StringBuilder();
|
||||||
|
currentTokens = 0;
|
||||||
|
}
|
||||||
|
chunks.add(sentence.trim());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 如果加上这个句子会超过限制
|
||||||
|
if (currentTokens + sentenceTokens > availableTokens && currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
currentChunk = new StringBuilder();
|
||||||
|
currentTokens = 0;
|
||||||
|
}
|
||||||
|
// 添加句子
|
||||||
|
currentChunk.append(sentence);
|
||||||
|
currentTokens += sentenceTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加最后一块
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
}
|
||||||
|
return chunks.isEmpty() ? Collections.singletonList(paragraph) : chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 降级切分策略(当未识别到 QA 格式时)
|
||||||
|
*
|
||||||
|
* @param content 文本内容
|
||||||
|
* @return 切分后的文本片段列表
|
||||||
|
*/
|
||||||
|
private List<String> fallbackSplit(String content) {
|
||||||
|
// 按段落切分
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
String[] paragraphs = content.split(PARAGRAPH_SEPARATOR);
|
||||||
|
|
||||||
|
// 按段落累积切分
|
||||||
|
StringBuilder currentChunk = new StringBuilder();
|
||||||
|
int currentTokens = 0;
|
||||||
|
for (String paragraph : paragraphs) {
|
||||||
|
if (StrUtil.isEmpty(paragraph)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int paragraphTokens = tokenEstimator.estimate(paragraph);
|
||||||
|
// 如果加上这个段落会超过限制
|
||||||
|
if (currentTokens + paragraphTokens > chunkSize && currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
currentChunk = new StringBuilder();
|
||||||
|
currentTokens = 0;
|
||||||
|
}
|
||||||
|
// 添加段落
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
currentChunk.append("\n\n");
|
||||||
|
}
|
||||||
|
currentChunk.append(paragraph);
|
||||||
|
currentTokens += paragraphTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加最后一块
|
||||||
|
if (currentChunk.length() > 0) {
|
||||||
|
chunks.add(currentChunk.toString().trim());
|
||||||
|
}
|
||||||
|
return chunks.isEmpty() ? Collections.singletonList(content) : chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* QA 对数据结构
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
private static class QaPair {
|
||||||
|
|
||||||
|
String question;
|
||||||
|
String answer;
|
||||||
|
String fullText;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 估算器接口
|
||||||
|
*/
|
||||||
|
public interface TokenEstimator {
|
||||||
|
|
||||||
|
int estimate(String text);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的 Token 估算器实现
|
||||||
|
* 中文:1 字符 ≈ 1 Token
|
||||||
|
* 英文:1 单词 ≈ 1.3 Token
|
||||||
|
*/
|
||||||
|
private static class SimpleTokenEstimator implements TokenEstimator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int estimate(String text) {
|
||||||
|
if (StrUtil.isEmpty(text)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chineseChars = 0;
|
||||||
|
int englishWords = 0;
|
||||||
|
// 简单统计中英文
|
||||||
|
for (char c : text.toCharArray()) {
|
||||||
|
if (c >= 0x4E00 && c <= 0x9FA5) {
|
||||||
|
chineseChars++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 英文单词估算
|
||||||
|
String[] words = text.split("\\s+");
|
||||||
|
for (String word : words) {
|
||||||
|
if (word.matches(".*[a-zA-Z].*")) {
|
||||||
|
englishWords++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chineseChars + (int) (englishWords * 1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,301 @@
|
||||||
|
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.ai.transformer.splitter.TextSplitter;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语义化文本切片器
|
||||||
|
*
|
||||||
|
* <p>功能特点:
|
||||||
|
* <ul>
|
||||||
|
* <li>优先在段落边界(双换行)处切分</li>
|
||||||
|
* <li>其次在句子边界(句号、问号、感叹号)处切分</li>
|
||||||
|
* <li>避免在句子中间截断,保持语义完整性</li>
|
||||||
|
* <li>支持中英文标点符号识别</li>
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* @author runzhen
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
public class SemanticTextSplitter extends TextSplitter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分段的最大 Token 数
|
||||||
|
*/
|
||||||
|
private final int chunkSize;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 段落重叠大小(用于保持上下文连贯性)
|
||||||
|
*/
|
||||||
|
private final int chunkOverlap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 段落分隔符(按优先级排序)
|
||||||
|
*/
|
||||||
|
private static final List<String> PARAGRAPH_SEPARATORS = Arrays.asList(
|
||||||
|
"\n\n\n", // 三个换行
|
||||||
|
"\n\n", // 双换行
|
||||||
|
"\n" // 单换行
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 句子结束标记(中英文标点)
|
||||||
|
*/
|
||||||
|
private static final Pattern SENTENCE_END_PATTERN = Pattern.compile(
|
||||||
|
"[。!?.!?]+[\\s\"'))】\\]]*"
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token 估算器
|
||||||
|
*/
|
||||||
|
private final MarkdownQaSplitter.TokenEstimator tokenEstimator;
|
||||||
|
|
||||||
|
public SemanticTextSplitter(int chunkSize, int chunkOverlap) {
|
||||||
|
this.chunkSize = chunkSize;
|
||||||
|
this.chunkOverlap = Math.min(chunkOverlap, chunkSize / 2); // 重叠不超过一半
|
||||||
|
this.tokenEstimator = new SimpleTokenEstimator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SemanticTextSplitter(int chunkSize) {
|
||||||
|
this(chunkSize, 50); // 默认重叠 50 个 Token
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected List<String> splitText(String text) {
|
||||||
|
if (StrUtil.isEmpty(text)) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return splitTextRecursive(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 切分文本(递归策略)
|
||||||
|
*
|
||||||
|
* @param text 待切分文本
|
||||||
|
* @return 切分后的文本块列表
|
||||||
|
*/
|
||||||
|
private List<String> splitTextRecursive(String text) {
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
|
||||||
|
// 如果文本不超过限制,直接返回
|
||||||
|
int textTokens = tokenEstimator.estimate(text);
|
||||||
|
if (textTokens <= chunkSize) {
|
||||||
|
chunks.add(text.trim());
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试按不同分隔符切分
|
||||||
|
List<String> splits = null;
|
||||||
|
String usedSeparator = null;
|
||||||
|
for (String separator : PARAGRAPH_SEPARATORS) {
|
||||||
|
if (text.contains(separator)) {
|
||||||
|
splits = Arrays.asList(text.split(Pattern.quote(separator)));
|
||||||
|
usedSeparator = separator;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有找到段落分隔符,按句子切分
|
||||||
|
if (splits == null || splits.size() == 1) {
|
||||||
|
splits = splitBySentences(text);
|
||||||
|
usedSeparator = ""; // 句子切分不需要分隔符
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合并小片段
|
||||||
|
chunks = mergeSplits(splits, usedSeparator);
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按句子切分
|
||||||
|
*
|
||||||
|
* @param text 待切分文本
|
||||||
|
* @return 句子列表
|
||||||
|
*/
|
||||||
|
private List<String> splitBySentences(String text) {
|
||||||
|
// 使用正则表达式匹配句子结束位置
|
||||||
|
List<String> sentences = new ArrayList<>();
|
||||||
|
int lastEnd = 0;
|
||||||
|
Matcher matcher = SENTENCE_END_PATTERN.matcher(text);
|
||||||
|
while (matcher.find()) {
|
||||||
|
String sentence = text.substring(lastEnd, matcher.end()).trim();
|
||||||
|
if (StrUtil.isNotEmpty(sentence)) {
|
||||||
|
sentences.add(sentence);
|
||||||
|
}
|
||||||
|
lastEnd = matcher.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加剩余部分
|
||||||
|
if (lastEnd < text.length()) {
|
||||||
|
String remaining = text.substring(lastEnd).trim();
|
||||||
|
if (StrUtil.isNotEmpty(remaining)) {
|
||||||
|
sentences.add(remaining);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sentences.isEmpty() ? Collections.singletonList(text) : sentences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 合并切分后的小片段
|
||||||
|
*
|
||||||
|
* @param splits 切分后的片段列表
|
||||||
|
* @param separator 片段间的分隔符
|
||||||
|
* @return 合并后的文本块列表
|
||||||
|
*/
|
||||||
|
private List<String> mergeSplits(List<String> splits, String separator) {
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
List<String> currentChunks = new ArrayList<>();
|
||||||
|
int currentLength = 0;
|
||||||
|
|
||||||
|
for (String split : splits) {
|
||||||
|
if (StrUtil.isEmpty(split)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int splitTokens = tokenEstimator.estimate(split);
|
||||||
|
// 如果单个片段就超过限制,进一步递归切分
|
||||||
|
if (splitTokens > chunkSize) {
|
||||||
|
// 先保存当前累积的块
|
||||||
|
if (!currentChunks.isEmpty()) {
|
||||||
|
String chunkText = String.join(separator, currentChunks);
|
||||||
|
chunks.add(chunkText.trim());
|
||||||
|
currentChunks.clear();
|
||||||
|
currentLength = 0;
|
||||||
|
}
|
||||||
|
// 递归切分大片段
|
||||||
|
if (!separator.isEmpty()) {
|
||||||
|
// 如果是段落分隔符,尝试按句子切分
|
||||||
|
chunks.addAll(splitTextRecursive(split));
|
||||||
|
} else {
|
||||||
|
// 如果已经是句子级别,强制按字符切分
|
||||||
|
chunks.addAll(forceSplitLongText(split));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// 计算加上分隔符的 Token 数
|
||||||
|
int separatorTokens = StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator);
|
||||||
|
// 如果加上这个片段会超过限制
|
||||||
|
if (!currentChunks.isEmpty() && currentLength + splitTokens + separatorTokens > chunkSize) {
|
||||||
|
// 保存当前块
|
||||||
|
String chunkText = String.join(separator, currentChunks);
|
||||||
|
chunks.add(chunkText.trim());
|
||||||
|
|
||||||
|
// 处理重叠:保留最后几个片段
|
||||||
|
currentChunks = getOverlappingChunks(currentChunks, separator);
|
||||||
|
currentLength = estimateTokens(currentChunks, separator);
|
||||||
|
}
|
||||||
|
// 添加当前片段
|
||||||
|
currentChunks.add(split);
|
||||||
|
currentLength += splitTokens + separatorTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加最后一块
|
||||||
|
if (!currentChunks.isEmpty()) {
|
||||||
|
String chunkText = String.join(separator, currentChunks);
|
||||||
|
chunks.add(chunkText.trim());
|
||||||
|
}
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取重叠的片段(用于保持上下文)
|
||||||
|
*
|
||||||
|
* @param chunks 当前片段列表
|
||||||
|
* @param separator 片段间的分隔符
|
||||||
|
* @return 重叠的片段列表
|
||||||
|
*/
|
||||||
|
private List<String> getOverlappingChunks(List<String> chunks, String separator) {
|
||||||
|
if (chunkOverlap == 0 || chunks.isEmpty()) {
|
||||||
|
return new ArrayList<>();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从后往前取片段,直到达到重叠大小
|
||||||
|
List<String> overlapping = new ArrayList<>();
|
||||||
|
int tokens = 0;
|
||||||
|
for (int i = chunks.size() - 1; i >= 0; i--) {
|
||||||
|
String chunk = chunks.get(i);
|
||||||
|
int chunkTokens = tokenEstimator.estimate(chunk);
|
||||||
|
if (tokens + chunkTokens > chunkOverlap) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// 添加到重叠列表前端
|
||||||
|
overlapping.add(0, chunk);
|
||||||
|
tokens += chunkTokens + (StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator));
|
||||||
|
}
|
||||||
|
return overlapping;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 估算片段列表的总 Token 数
|
||||||
|
*
|
||||||
|
* @param chunks 片段列表
|
||||||
|
* @param separator 片段间的分隔符
|
||||||
|
* @return 总 Token 数
|
||||||
|
*/
|
||||||
|
private int estimateTokens(List<String> chunks, String separator) {
|
||||||
|
int total = 0;
|
||||||
|
for (int i = 0; i < chunks.size(); i++) {
|
||||||
|
total += tokenEstimator.estimate(chunks.get(i));
|
||||||
|
if (i < chunks.size() - 1 && StrUtil.isNotEmpty(separator)) {
|
||||||
|
total += tokenEstimator.estimate(separator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 强制切分长文本(当语义切分失败时)
|
||||||
|
*
|
||||||
|
* @param text 待切分文本
|
||||||
|
* @return 切分后的文本块列表
|
||||||
|
*/
|
||||||
|
private List<String> forceSplitLongText(String text) {
|
||||||
|
List<String> chunks = new ArrayList<>();
|
||||||
|
int charsPerChunk = (int) (chunkSize * 0.8); // 保守估计
|
||||||
|
for (int i = 0; i < text.length(); i += charsPerChunk) {
|
||||||
|
int end = Math.min(i + charsPerChunk, text.length());
|
||||||
|
String chunk = text.substring(i, end);
|
||||||
|
chunks.add(chunk.trim());
|
||||||
|
}
|
||||||
|
log.warn("文本过长,已强制按字符切分,可能影响语义完整性");
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 简单的 Token 估算器实现
|
||||||
|
*/
|
||||||
|
private static class SimpleTokenEstimator implements MarkdownQaSplitter.TokenEstimator {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int estimate(String text) {
|
||||||
|
if (StrUtil.isEmpty(text)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int chineseChars = 0;
|
||||||
|
int englishWords = 0;
|
||||||
|
// 简单统计中英文
|
||||||
|
for (char c : text.toCharArray()) {
|
||||||
|
if (c >= 0x4E00 && c <= 0x9FA5) {
|
||||||
|
chineseChars++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 英文单词估算
|
||||||
|
String[] words = text.split("\\s+");
|
||||||
|
for (String word : words) {
|
||||||
|
if (word.matches(".*[a-zA-Z].*")) {
|
||||||
|
englishWords++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chineseChars + (int) (englishWords * 1.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
package cn.iocoder.yudao.module.ai.tool.method;
|
package cn.iocoder.yudao.module.ai.tool.method;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.ai.tool.annotation.Tool;
|
import org.springframework.ai.tool.annotation.Tool;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import javax.annotation.PostConstruct;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
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;
|
||||||
|
|
@ -33,6 +35,28 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
@ -44,9 +68,10 @@ public class AiUtils {
|
||||||
// noinspection EnhancedSwitchMigration
|
// noinspection EnhancedSwitchMigration
|
||||||
switch (platform) {
|
switch (platform) {
|
||||||
case TONG_YI:
|
case TONG_YI:
|
||||||
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
|
return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens)
|
||||||
.withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
|
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
|
||||||
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
|
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
|
||||||
|
.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:
|
||||||
|
|
@ -125,10 +150,13 @@ public class AiUtils {
|
||||||
|| response.getResult().getOutput() == null) {
|
|| response.getResult().getOutput() == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
|
AssistantMessage output = response.getResult().getOutput();
|
||||||
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
|
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent
|
||||||
|
if (output instanceof DeepSeekAssistantMessage) {
|
||||||
|
return ((DeepSeekAssistantMessage) output).getReasoningContent();
|
||||||
}
|
}
|
||||||
return null;
|
// 通义千问等通过 metadata 透传 reasoningContent
|
||||||
|
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -123,6 +123,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
# Spring Boot Admin Server 服务端的相关配置
|
# Spring Boot Admin Server 服务端的相关配置
|
||||||
context-path: /admin # 配置 Spring
|
context-path: /admin # 配置 Spring
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -118,6 +118,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
# 日志文件配置
|
# 日志文件配置
|
||||||
logging:
|
logging:
|
||||||
|
|
|
||||||
|
|
@ -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-47aa124781be4bfb95244cc62f63f7d0")
|
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
|
||||||
.build())
|
.build())
|
||||||
.defaultOptions(DashScopeChatOptions.builder()
|
.defaultOptions(DashScopeChatOptions.builder()
|
||||||
// .withModel("qwen1.5-72b-chat") // 模型
|
.multiModel(true) // 注意:当使用 qwen3.6-plus 等多模态模型,需要设置为 true,可见 https://help.aliyun.com/zh/model-studio/error-code#error-url 链接
|
||||||
.withModel("qwen3-235b-a22b-thinking-2507") // 模型
|
.model("qwen3.6-plus") // 模型
|
||||||
// .withModel("deepseek-r1") // 模型(deepseek-r1)
|
// .model("deepseek-r1") // 模型(deepseek-r1)
|
||||||
// .withModel("deepseek-v3") // 模型(deepseek-v3)
|
// .model("deepseek-v3") // 模型(deepseek-v3)
|
||||||
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
|
// .model("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
|
||||||
// .withEnableThinking(true)
|
// .enableThinking(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()
|
||||||
.withModel("qwen3-235b-a22b-thinking-2507")
|
.model("qwen3.6-plus").multiModel(true)
|
||||||
// .withModel("qwen-max-2025-01-25")
|
// .withModel("qwen-max-2025-01-25")
|
||||||
.withEnableThinking(true) // 必须设置,否则会报错
|
.enableThinking(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()
|
||||||
.withTopN(1)
|
.topN(1)
|
||||||
.withModel("gte-rerank-v2")
|
.model("gte-rerank-v2")
|
||||||
.build();
|
.build();
|
||||||
RerankRequest rerankRequest = new RerankRequest(
|
RerankRequest rerankRequest = new RerankRequest(
|
||||||
query,
|
query,
|
||||||
|
|
|
||||||
|
|
@ -12,23 +12,34 @@ import org.springframework.ai.image.ImageResponse;
|
||||||
/**
|
/**
|
||||||
* {@link DashScopeImageModel} 集成测试类
|
* {@link DashScopeImageModel} 集成测试类
|
||||||
*
|
*
|
||||||
|
* TODO @芋艿:注:spring-ai-alibaba-dashscope(1.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-47aa124781be4bfb95244cc62f63f7d0")
|
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
|
||||||
.build())
|
.build())
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
|
// TODO @芋艿:
|
||||||
@Test
|
@Test
|
||||||
@Disabled
|
@Disabled
|
||||||
public void imageCallTest() {
|
public void imageCallTest() {
|
||||||
// 准备参数
|
// 准备参数
|
||||||
ImageOptions options = DashScopeImageOptions.builder()
|
ImageOptions options = DashScopeImageOptions.builder()
|
||||||
.withModel("wanx-v1")
|
.model("wan2.7-image")
|
||||||
.withHeight(256).withWidth(256)
|
// .withSize("2k")
|
||||||
|
.height(768).width(768)
|
||||||
|
.n(1)
|
||||||
.build();
|
.build();
|
||||||
ImagePrompt prompt = new ImagePrompt("中国长城!", options);
|
ImagePrompt prompt = new ImagePrompt("中国长城!", options);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form;
|
||||||
|
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 流程表单字段 VO
|
* 流程表单字段 VO
|
||||||
*/
|
*/
|
||||||
|
|
@ -20,5 +22,9 @@ public class BpmFormFieldVO {
|
||||||
* 字段标题
|
* 字段标题
|
||||||
*/
|
*/
|
||||||
private String title;
|
private String title;
|
||||||
|
/**
|
||||||
|
* 子表单字段(处理布局组件)
|
||||||
|
*/
|
||||||
|
private List<BpmFormFieldVO> children;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(), Long.parseLong(copy.getCreator()))));
|
copy -> Stream.of(copy.getStartUserId(), copy.getUserId())));
|
||||||
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, Long.valueOf(copy.getCreator()),
|
MapUtils.findAndThen(userMap, copy.getUserId(),
|
||||||
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
|
||||||
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
|
||||||
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||||
|
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
||||||
|
user -> copyVO.setStartUser(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(
|
||||||
|
|
|
||||||
|
|
@ -100,7 +100,7 @@ public class BpmTaskController {
|
||||||
|
|
||||||
@GetMapping("manager-page")
|
@GetMapping("manager-page")
|
||||||
@Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单")
|
@Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单")
|
||||||
@PreAuthorize("@ss.hasPermission('bpm:task:mananger-query')")
|
@PreAuthorize("@ss.hasPermission('bpm:task:manager-query')")
|
||||||
public CommonResult<PageResult<BpmTaskRespVO>> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) {
|
public CommonResult<PageResult<BpmTaskRespVO>> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) {
|
||||||
PageResult<HistoricTaskInstance> pageResult = taskService.getTaskPage(getLoginUserId(), pageVO);
|
PageResult<HistoricTaskInstance> pageResult = taskService.getTaskPage(getLoginUserId(), pageVO);
|
||||||
if (CollUtil.isEmpty(pageResult.getList())) {
|
if (CollUtil.isEmpty(pageResult.getList())) {
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ public class BpmOALeaveDO extends BaseDO {
|
||||||
/**
|
/**
|
||||||
* 请假类型
|
* 请假类型
|
||||||
*/
|
*/
|
||||||
private String type;
|
private Integer type;
|
||||||
/**
|
/**
|
||||||
* 原因
|
* 原因
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
|
||||||
import org.springframework.context.ApplicationEventPublisher;
|
import org.springframework.context.ApplicationEventPublisher;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.core.task.AsyncListenableTaskExecutor;
|
import org.springframework.core.task.AsyncTaskExecutor;
|
||||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
@ -30,12 +30,12 @@ public class BpmFlowableConfiguration {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
|
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
|
||||||
*
|
* <p>
|
||||||
* 如果不创建,会导致项目启动时,Flowable 报错的问题
|
* 如果不创建,会导致项目启动时,Flowable 报错的问题
|
||||||
*/
|
*/
|
||||||
@Bean(name = "applicationTaskExecutor")
|
@Bean(name = "applicationTaskExecutor")
|
||||||
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
|
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
|
||||||
public AsyncListenableTaskExecutor taskExecutor() {
|
public AsyncTaskExecutor taskExecutor() {
|
||||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||||
executor.setCorePoolSize(8);
|
executor.setCorePoolSize(8);
|
||||||
executor.setMaxPoolSize(8);
|
executor.setMaxPoolSize(8);
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import org.flowable.bpmn.model.UserTask;
|
||||||
import org.flowable.engine.delegate.DelegateExecution;
|
import org.flowable.engine.delegate.DelegateExecution;
|
||||||
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
||||||
import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
|
import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
|
||||||
|
import org.flowable.common.engine.api.delegate.Expression;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
@ -34,6 +35,12 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
|
||||||
public BpmParallelMultiInstanceBehavior(Activity activity,
|
public BpmParallelMultiInstanceBehavior(Activity activity,
|
||||||
AbstractBpmnActivityBehavior innerActivityBehavior) {
|
AbstractBpmnActivityBehavior innerActivityBehavior) {
|
||||||
super(activity, innerActivityBehavior);
|
super(activity, innerActivityBehavior);
|
||||||
|
// 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
|
||||||
|
// 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
|
||||||
|
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
|
||||||
|
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
|
||||||
|
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
|
||||||
|
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(activity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -50,14 +57,7 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
|
||||||
protected int resolveNrOfInstances(DelegateExecution execution) {
|
protected int resolveNrOfInstances(DelegateExecution execution) {
|
||||||
// 情况一:UserTask 节点
|
// 情况一:UserTask 节点
|
||||||
if (execution.getCurrentFlowElement() instanceof UserTask) {
|
if (execution.getCurrentFlowElement() instanceof UserTask) {
|
||||||
// 第一步,设置 collectionVariable 和 CollectionVariable
|
// 获取任务的所有处理人
|
||||||
// 从 execution.getVariable() 读取所有任务处理人的 key
|
|
||||||
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
|
|
||||||
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
|
|
||||||
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
|
|
||||||
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
|
|
||||||
|
|
||||||
// 第二步,获取任务的所有处理人
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
|
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
|
||||||
if (assigneeUserIds == null) {
|
if (assigneeUserIds == null) {
|
||||||
|
|
@ -88,4 +88,21 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
|
||||||
return super.resolveNrOfInstances(execution);
|
return super.resolveNrOfInstances(execution);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 屏蔽解析器覆写 ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCollectionExpression(Expression collectionExpression) {
|
||||||
|
// 保持自定义变量名,忽略解析器写入的 collection 表达式
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCollectionVariable(String collectionVariable) {
|
||||||
|
// 保持自定义变量名,忽略解析器写入的 collection 变量名
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCollectionElementVariable(String collectionElementVariable) {
|
||||||
|
// 保持自定义变量名,忽略解析器写入的单元素变量名
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,11 +8,13 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
import org.flowable.bpmn.model.*;
|
import org.flowable.bpmn.model.*;
|
||||||
|
import org.flowable.common.engine.api.delegate.Expression;
|
||||||
import org.flowable.engine.delegate.DelegateExecution;
|
import org.flowable.engine.delegate.DelegateExecution;
|
||||||
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
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;
|
||||||
|
|
||||||
|
|
@ -30,6 +32,12 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
||||||
|
|
||||||
public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) {
|
public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) {
|
||||||
super(activity, innerActivityBehavior);
|
super(activity, innerActivityBehavior);
|
||||||
|
// 关联 Pull Request:https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
|
||||||
|
// 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
|
||||||
|
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
|
||||||
|
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
|
||||||
|
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
|
||||||
|
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(activity.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -41,19 +49,12 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
||||||
protected int resolveNrOfInstances(DelegateExecution execution) {
|
protected int resolveNrOfInstances(DelegateExecution execution) {
|
||||||
// 情况一:UserTask 节点
|
// 情况一:UserTask 节点
|
||||||
if (execution.getCurrentFlowElement() instanceof UserTask) {
|
if (execution.getCurrentFlowElement() instanceof UserTask) {
|
||||||
// 第一步,设置 collectionVariable 和 CollectionVariable
|
// 获取任务的所有处理人
|
||||||
// 从 execution.getVariable() 读取所有任务处理人的 key
|
|
||||||
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
|
|
||||||
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
|
|
||||||
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
|
|
||||||
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
|
|
||||||
|
|
||||||
// 第二步,获取任务的所有处理人
|
|
||||||
// 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人
|
// 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人
|
||||||
@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 = taskCandidateInvoker.calculateUsersByTask(execution);
|
assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution));
|
||||||
if (CollUtil.isEmpty(assigneeUserIds)) {
|
if (CollUtil.isEmpty(assigneeUserIds)) {
|
||||||
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
|
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
|
||||||
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
|
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
|
||||||
|
|
@ -88,11 +89,24 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
||||||
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
|
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC239F
|
|
||||||
super.collectionExpression = null;
|
|
||||||
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
|
|
||||||
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
|
|
||||||
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
|
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 屏蔽解析器覆写 ==========
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCollectionExpression(Expression collectionExpression) {
|
||||||
|
// 保持自定义变量名,忽略解析器写入的 collection 表达式
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCollectionVariable(String collectionVariable) {
|
||||||
|
// 保持自定义变量名,忽略解析器写入的 collection 变量名
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setCollectionElementVariable(String collectionElementVariable) {
|
||||||
|
// 保持自定义变量名,忽略解析器写入的单元素变量名
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -125,6 +125,7 @@ public class BpmTaskCandidateInvoker {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人
|
||||||
public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
|
public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
|
||||||
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
|
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
|
||||||
// 如果是 CallActivity 子流程,不进行计算候选人
|
// 如果是 CallActivity 子流程,不进行计算候选人
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
|
||||||
/**
|
/**
|
||||||
* 根据流程变量 variable 的类型,转换参数的值
|
* 根据流程变量 variable 的类型,转换参数的值
|
||||||
*
|
*
|
||||||
* 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中
|
* @deprecated 已无调用方
|
||||||
*
|
|
||||||
* @author jason
|
* @author jason
|
||||||
*/
|
*/
|
||||||
|
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
|
||||||
@Component
|
@Component
|
||||||
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
|
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
|
||||||
|
|
||||||
import cn.hutool.core.collection.CollUtil;
|
import cn.hutool.core.collection.CollUtil;
|
||||||
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
|
||||||
|
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
|
||||||
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
|
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
|
||||||
import org.flowable.bpmn.model.FlowElement;
|
import org.flowable.bpmn.model.FlowElement;
|
||||||
import org.flowable.engine.delegate.DelegateExecution;
|
import org.flowable.engine.delegate.DelegateExecution;
|
||||||
|
|
@ -40,8 +41,9 @@ public class BpmCopyTaskDelegate implements JavaDelegate {
|
||||||
}
|
}
|
||||||
// 2. 执行抄送
|
// 2. 执行抄送
|
||||||
FlowElement currentFlowElement = execution.getCurrentFlowElement();
|
FlowElement currentFlowElement = execution.getCurrentFlowElement();
|
||||||
processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(),
|
FlowableUtils.execute(execution.getTenantId(), () ->
|
||||||
currentFlowElement.getId(), currentFlowElement.getName(), null);
|
processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(),
|
||||||
|
currentFlowElement.getId(), currentFlowElement.getName(), null));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ 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;
|
||||||
|
|
@ -247,9 +248,7 @@ public class FlowableUtils {
|
||||||
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
|
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
|
||||||
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
||||||
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
|
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
|
||||||
if (formField != null) {
|
parseFormField(formField, formFieldsMap);
|
||||||
formFieldsMap.put(formField.getField(), formField);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 情况一:当自定义了摘要
|
// 情况一:当自定义了摘要
|
||||||
|
|
@ -273,6 +272,26 @@ public class FlowableUtils {
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归解析表单字段
|
||||||
|
*/
|
||||||
|
private static void parseFormField(BpmFormFieldVO formField, Map<String, BpmFormFieldVO> formFieldsMap) {
|
||||||
|
if (formField == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 如果存在 children -> 说明是布局组件
|
||||||
|
if (formField.getChildren() != null && !formField.getChildren().isEmpty()) {
|
||||||
|
for (BpmFormFieldVO child : formField.getChildren()) {
|
||||||
|
parseFormField(child, formFieldsMap);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 真实字段才加入 map
|
||||||
|
if (StrUtil.isNotBlank(formField.getField())) {
|
||||||
|
formFieldsMap.put(formField.getField(), formField);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ========== Task 相关的工具方法 ==========
|
// ========== Task 相关的工具方法 ==========
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -707,10 +707,9 @@ 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(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
|
return String.format(BpmConditionOpCodeEnum.fromCode(rule.getOpCode()).getSymbol(),
|
||||||
rule.getLeftSide(), // 左侧:读取变量
|
rule.getLeftSide(), // 左侧:读取变量
|
||||||
rule.getOpCode(), // 中间:操作符,比较
|
rightSide); // 右侧:取值变量
|
||||||
rule.getLeftSide(), rightSide); // 右侧:转换变量,VariableConvertByTypeExpressionFunction
|
|
||||||
});
|
});
|
||||||
// 构造条件组的表达式
|
// 构造条件组的表达式
|
||||||
Boolean and = item.getAnd();
|
Boolean and = item.getAnd();
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -116,6 +116,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
.taskAssignee(String.valueOf(userId)) // 分配给自己
|
.taskAssignee(String.valueOf(userId)) // 分配给自己
|
||||||
.active()
|
.active()
|
||||||
.includeProcessVariables()
|
.includeProcessVariables()
|
||||||
|
.taskTenantId(FlowableUtils.getTenantId())
|
||||||
.orderByTaskCreateTime().desc(); // 创建时间倒序
|
.orderByTaskCreateTime().desc(); // 创建时间倒序
|
||||||
if (StrUtil.isNotBlank(pageVO.getName())) {
|
if (StrUtil.isNotBlank(pageVO.getName())) {
|
||||||
taskQuery.taskNameLike("%" + pageVO.getName() + "%");
|
taskQuery.taskNameLike("%" + pageVO.getName() + "%");
|
||||||
|
|
@ -922,16 +923,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
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 标记
|
||||||
|
|
@ -955,7 +952,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
// 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018
|
// 相关 issue:https://github.com/YunaiV/ruoyi-vue-pro/issues/1018
|
||||||
runtimeService.createChangeActivityStateBuilder()
|
runtimeService.createChangeActivityStateBuilder()
|
||||||
.processInstanceId(currentTask.getProcessInstanceId())
|
.processInstanceId(currentTask.getProcessInstanceId())
|
||||||
.moveActivityIdsToSingleActivityId(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)
|
||||||
// 设置流程变量(local)节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
|
// 设置流程变量(local)节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
|
||||||
|
|
@ -1467,7 +1464,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动去重,通过自动审批的方式 TODO @芋艿 驳回的情况得考虑一下;@lesan:驳回后,又自动审批么?
|
// 自动去重,通过自动审批的方式
|
||||||
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
|
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
|
||||||
if (processDefinitionInfo == null) {
|
if (processDefinitionInfo == null) {
|
||||||
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
|
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -107,6 +107,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
--- #################### 芋道相关配置 ####################
|
--- #################### 芋道相关配置 ####################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ spring:
|
||||||
primary: master
|
primary: master
|
||||||
datasource:
|
datasource:
|
||||||
master:
|
master:
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true # MySQL Connector/J 8.X 连接的示例
|
||||||
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
# url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=true&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true # MySQL Connector/J 5.X 连接的示例
|
||||||
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
# url: jdbc:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||||
|
|
@ -72,7 +72,7 @@ spring:
|
||||||
# password: SYSDBA # DM 连接的示例
|
# password: SYSDBA # DM 连接的示例
|
||||||
slave: # 模拟从库,可根据自己需要修改
|
slave: # 模拟从库,可根据自己需要修改
|
||||||
lazy: true # 开启懒加载,保证启动速度
|
lazy: true # 开启懒加载,保证启动速度
|
||||||
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
|
||||||
username: root
|
username: root
|
||||||
password: 123456
|
password: 123456
|
||||||
|
|
||||||
|
|
@ -98,7 +98,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -118,6 +118,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
# 日志文件配置
|
# 日志文件配置
|
||||||
logging:
|
logging:
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
|
||||||
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
|
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
|
||||||
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
|
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
|
||||||
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
|
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||||
import cn.idev.excel.annotation.ExcelProperty;
|
import cn.idev.excel.annotation.ExcelProperty;
|
||||||
import com.fhs.core.trans.anno.Trans;
|
import com.fhs.core.trans.anno.Trans;
|
||||||
|
|
@ -58,7 +59,7 @@ public class CrmProductRespVO implements VO {
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
@Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31926")
|
@Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31926")
|
||||||
@Trans(type = TransType.SIMPLE, targetClassName = "cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO",
|
@Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX,
|
||||||
fields = "nickname", ref = "ownerUserName")
|
fields = "nickname", ref = "ownerUserName")
|
||||||
private Long ownerUserId;
|
private Long ownerUserId;
|
||||||
@Schema(description = "负责人的用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
|
@Schema(description = "负责人的用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
|
||||||
|
|
@ -66,7 +67,7 @@ public class CrmProductRespVO implements VO {
|
||||||
private String ownerUserName;
|
private String ownerUserName;
|
||||||
|
|
||||||
@Schema(description = "创建人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
@Schema(description = "创建人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
@Trans(type = TransType.SIMPLE, targetClassName = "cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO",
|
@Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX,
|
||||||
fields = "nickname", ref = "creatorName")
|
fields = "nickname", ref = "creatorName")
|
||||||
private String creator;
|
private String creator;
|
||||||
@Schema(description = "创建人名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
|
@Schema(description = "创建人名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
|
||||||
|
|
|
||||||
|
|
@ -27,10 +27,12 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
|
||||||
Integer type, Long userId, Long deptId) {
|
Integer type, Long userId, Long deptId) {
|
||||||
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
|
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
|
||||||
.eq(CrmCustomerLimitConfigDO::getType, type);
|
.eq(CrmCustomerLimitConfigDO::getType, type);
|
||||||
query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
|
query.and(w -> {
|
||||||
if (deptId != null) {
|
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
|
||||||
query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
|
if (deptId != null) {
|
||||||
}
|
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
|
||||||
|
}
|
||||||
|
});
|
||||||
return selectList(query);
|
return selectList(query);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListen
|
||||||
import cn.iocoder.yudao.module.crm.enums.ApiConstants;
|
import cn.iocoder.yudao.module.crm.enums.ApiConstants;
|
||||||
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
|
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
|
||||||
import cn.iocoder.yudao.module.crm.service.contract.CrmContractServiceImpl;
|
import cn.iocoder.yudao.module.crm.service.contract.CrmContractServiceImpl;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
@ -20,7 +19,6 @@ import javax.annotation.Resource;
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@Validated
|
@Validated
|
||||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
|
||||||
public class CrmContractStatusListener extends BpmProcessInstanceStatusEventListener {
|
public class CrmContractStatusListener extends BpmProcessInstanceStatusEventListener {
|
||||||
|
|
||||||
private static final String PREFIX = ApiConstants.PREFIX + "/contract";
|
private static final String PREFIX = ApiConstants.PREFIX + "/contract";
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListen
|
||||||
import cn.iocoder.yudao.module.crm.enums.ApiConstants;
|
import cn.iocoder.yudao.module.crm.enums.ApiConstants;
|
||||||
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
|
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
|
||||||
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableServiceImpl;
|
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableServiceImpl;
|
||||||
import org.springframework.cloud.openfeign.FeignClient;
|
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
|
@ -20,7 +19,6 @@ import javax.annotation.Resource;
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@Validated
|
@Validated
|
||||||
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿:fallbackFactory =
|
|
||||||
public class CrmReceivableStatusListener extends BpmProcessInstanceStatusEventListener {
|
public class CrmReceivableStatusListener extends BpmProcessInstanceStatusEventListener {
|
||||||
|
|
||||||
private static final String PREFIX = ApiConstants.PREFIX + "/receivable";
|
private static final String PREFIX = ApiConstants.PREFIX + "/receivable";
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -107,6 +107,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
--- #################### 芋道相关配置 ####################
|
--- #################### 芋道相关配置 ####################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -116,6 +116,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
# 日志文件配置
|
# 日志文件配置
|
||||||
logging:
|
logging:
|
||||||
|
|
|
||||||
|
|
@ -31,14 +31,14 @@ public interface ErpFinancePaymentItemMapper extends BaseMapperX<ErpFinancePayme
|
||||||
default BigDecimal selectPaymentPriceSumByBizIdAndBizType(Long bizId, Integer bizType) {
|
default BigDecimal selectPaymentPriceSumByBizIdAndBizType(Long bizId, Integer bizType) {
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpFinancePaymentItemDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpFinancePaymentItemDO>()
|
||||||
.select("SUM(payment_price) AS paymentPriceSum")
|
.select("SUM(payment_price) AS payment_price_sum")
|
||||||
.eq("biz_id", bizId)
|
.eq("biz_id", bizId)
|
||||||
.eq("biz_type", bizType));
|
.eq("biz_type", bizType));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
if (CollUtil.isEmpty(result)) {
|
if (CollUtil.isEmpty(result)) {
|
||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "paymentPriceSum", 0D));
|
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "payment_price_sum", 0D));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -31,14 +31,14 @@ public interface ErpFinanceReceiptItemMapper extends BaseMapperX<ErpFinanceRecei
|
||||||
default BigDecimal selectReceiptPriceSumByBizIdAndBizType(Long bizId, Integer bizType) {
|
default BigDecimal selectReceiptPriceSumByBizIdAndBizType(Long bizId, Integer bizType) {
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpFinanceReceiptItemDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpFinanceReceiptItemDO>()
|
||||||
.select("SUM(receipt_price) AS receiptPriceSum")
|
.select("SUM(receipt_price) AS receipt_price_sum")
|
||||||
.eq("biz_id", bizId)
|
.eq("biz_id", bizId)
|
||||||
.eq("biz_type", bizType));
|
.eq("biz_type", bizType));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
if (CollUtil.isEmpty(result)) {
|
if (CollUtil.isEmpty(result)) {
|
||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "receiptPriceSum", 0D));
|
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "receipt_price_sum", 0D));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +46,11 @@ public interface ErpPurchaseInItemMapper extends BaseMapperX<ErpPurchaseInItemDO
|
||||||
}
|
}
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpPurchaseInItemDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpPurchaseInItemDO>()
|
||||||
.select("order_item_id, SUM(count) AS sumCount")
|
.select("order_item_id, SUM(count) AS sum_count")
|
||||||
.groupBy("order_item_id")
|
.groupBy("order_item_id")
|
||||||
.in("in_id", inIds));
|
.in("in_id", inIds));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
|
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +46,11 @@ public interface ErpPurchaseReturnItemMapper extends BaseMapperX<ErpPurchaseRetu
|
||||||
}
|
}
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpPurchaseReturnItemDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpPurchaseReturnItemDO>()
|
||||||
.select("order_item_id, SUM(count) AS sumCount")
|
.select("order_item_id, SUM(count) AS sum_count")
|
||||||
.groupBy("order_item_id")
|
.groupBy("order_item_id")
|
||||||
.in("return_id", returnIds));
|
.in("return_id", returnIds));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
|
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +46,11 @@ public interface ErpSaleOutItemMapper extends BaseMapperX<ErpSaleOutItemDO> {
|
||||||
}
|
}
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpSaleOutItemDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpSaleOutItemDO>()
|
||||||
.select("order_item_id, SUM(count) AS sumCount")
|
.select("order_item_id, SUM(count) AS sum_count")
|
||||||
.groupBy("order_item_id")
|
.groupBy("order_item_id")
|
||||||
.in("out_id", outIds));
|
.in("out_id", outIds));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
|
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -46,11 +46,11 @@ public interface ErpSaleReturnItemMapper extends BaseMapperX<ErpSaleReturnItemDO
|
||||||
}
|
}
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpSaleReturnItemDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpSaleReturnItemDO>()
|
||||||
.select("order_item_id, SUM(count) AS sumCount")
|
.select("order_item_id, SUM(count) AS sum_count")
|
||||||
.groupBy("order_item_id")
|
.groupBy("order_item_id")
|
||||||
.in("return_id", returnIds));
|
.in("return_id", returnIds));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
|
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -52,13 +52,13 @@ public interface ErpStockMapper extends BaseMapperX<ErpStockDO> {
|
||||||
default BigDecimal selectSumByProductId(Long productId) {
|
default BigDecimal selectSumByProductId(Long productId) {
|
||||||
// SQL sum 查询
|
// SQL sum 查询
|
||||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpStockDO>()
|
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpStockDO>()
|
||||||
.select("SUM(count) AS sumCount")
|
.select("SUM(count) AS sum_count")
|
||||||
.eq("product_id", productId));
|
.eq("product_id", productId));
|
||||||
// 获得数量
|
// 获得数量
|
||||||
if (CollUtil.isEmpty(result)) {
|
if (CollUtil.isEmpty(result)) {
|
||||||
return BigDecimal.ZERO;
|
return BigDecimal.ZERO;
|
||||||
}
|
}
|
||||||
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "sumCount", 0D));
|
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "sum_count", 0D));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -87,7 +87,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -107,6 +107,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
--- #################### 芋道相关配置 ####################
|
--- #################### 芋道相关配置 ####################
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ xxl:
|
||||||
# Lock4j 配置项
|
# Lock4j 配置项
|
||||||
lock4j:
|
lock4j:
|
||||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||||
|
|
||||||
--- #################### 监控相关配置 ####################
|
--- #################### 监控相关配置 ####################
|
||||||
|
|
||||||
|
|
@ -116,6 +116,8 @@ spring:
|
||||||
client:
|
client:
|
||||||
instance:
|
instance:
|
||||||
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
|
||||||
# 日志文件配置
|
# 日志文件配置
|
||||||
logging:
|
logging:
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ public enum CodegenFrontTypeEnum {
|
||||||
|
|
||||||
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
|
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
|
||||||
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
|
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
|
||||||
|
|
||||||
|
VUE3_ADMIN_UNIAPP_WOT(60), // Vue3 Admin + Uniapp + WOT 标准模版
|
||||||
;
|
;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.infra.service.file.FileService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.Parameters;
|
import io.swagger.v3.oas.annotations.Parameters;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.annotation.security.PermitAll;
|
import javax.annotation.security.PermitAll;
|
||||||
|
|
@ -44,6 +45,8 @@ public class FileController {
|
||||||
|
|
||||||
@PostMapping("/upload")
|
@PostMapping("/upload")
|
||||||
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
|
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
|
||||||
|
@Parameter(name = "file", description = "文件附件", required = true,
|
||||||
|
schema = @Schema(type = "string", format = "binary"))
|
||||||
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
|
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
|
||||||
MultipartFile file = uploadReqVO.getFile();
|
MultipartFile file = uploadReqVO.getFile();
|
||||||
byte[] content = IoUtil.readBytes(file.getInputStream());
|
byte[] content = IoUtil.readBytes(file.getInputStream());
|
||||||
|
|
@ -69,6 +72,14 @@ public class FileController {
|
||||||
return success(fileService.createFile(createReqVO));
|
return success(fileService.createFile(createReqVO));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "获得文件")
|
||||||
|
@Parameter(name = "id", description = "编号", required = true)
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:file:query')")
|
||||||
|
public CommonResult<FileRespVO> getFile(@RequestParam("id") Long id) {
|
||||||
|
return success(BeanUtils.toBean(fileService.getFile(id), FileRespVO.class));
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/delete")
|
@DeleteMapping("/delete")
|
||||||
@Operation(summary = "删除文件")
|
@Operation(summary = "删除文件")
|
||||||
@Parameter(name = "id", description = "编号", required = true)
|
@Parameter(name = "id", description = "编号", required = true)
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,6 @@ public class FileCreateReqVO {
|
||||||
private String type;
|
private String type;
|
||||||
|
|
||||||
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
|
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
private Integer size;
|
private Long size;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||