Compare commits
115 Commits
v2025.12(j
...
master
| Author | SHA1 | Date |
|---|---|---|
|
|
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 |
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 163 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 64 KiB |
24
README.md
24
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-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 等功能
|
||||
* 【完整版】:包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
|
||||
* 【精简版】:只包括系统功能、基础设施功能,不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
|
||||
|
||||
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
|
||||
|
||||
|
|
@ -115,7 +115,7 @@
|
|||
|
||||
* 通用模块(必选):系统功能、基础设施
|
||||
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
|
||||
* 业务系统(按需):ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
|
||||
* 业务系统(按需):ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
|
||||
|
||||
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
|
||||
>
|
||||
|
|
@ -279,6 +279,14 @@
|
|||
|
||||

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

|
||||
|
||||

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

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

|
||||
|
||||

|
||||
|
||||
## 🐨 技术栈
|
||||
|
||||
### 微服务
|
||||
|
|
@ -304,7 +320,9 @@
|
|||
| `yudao-module-mall` | 商城系统的 Module 模块 |
|
||||
| `yudao-module-erp` | ERP 系统的 Module 模块 |
|
||||
| `yudao-module-crm` | CRM 系统的 Module 模块 |
|
||||
| `yudao-module-mes` | MES 系统的 Module 模块 |
|
||||
| `yudao-module-ai` | AI 大模型的 Module 模块 |
|
||||
| `yudao-module-iot` | IoT 物联网的 Module 模块 |
|
||||
| `yudao-module-mp` | 微信公众号的 Module 模块 |
|
||||
| `yudao-module-report` | 大屏报表 Module 模块 |
|
||||
|
||||
|
|
|
|||
5
pom.xml
5
pom.xml
|
|
@ -24,9 +24,10 @@
|
|||
<module>yudao-module-mall</module>
|
||||
<module>yudao-module-erp</module>
|
||||
<module>yudao-module-crm</module>
|
||||
<module>yudao-module-iot</module>
|
||||
<module>yudao-module-mes</module>
|
||||
<!-- 友情提示:基于 Spring AI 实现 LLM 大模型的接入,需要使用 JDK17 版本,详细可见 https://doc.iocoder.cn/ai/build/ -->
|
||||
<!-- <module>yudao-module-ai</module>-->
|
||||
<module>yudao-module-iot</module>
|
||||
</modules>
|
||||
|
||||
<name>${project.artifactId}</name>
|
||||
|
|
@ -34,7 +35,7 @@
|
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2025.12-jdk8-SNAPSHOT</revision>
|
||||
<revision>2026.04-jdk8-SNAPSHOT</revision>
|
||||
<!-- Maven 相关 -->
|
||||
<java.version>1.8</java.version>
|
||||
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
|
@ -62,6 +62,10 @@ def load_and_clean(sql_file: str) -> str:
|
|||
content = open(sql_file, encoding="utf-8").read()
|
||||
for replace_pair in REPLACE_PAIR_LIST:
|
||||
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`
|
||||
# 移除索引定义上的 USING BTREE COMMENT 部分
|
||||
# 相关 issue:https://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
|
||||
|
|
@ -77,7 +81,11 @@ class Convertor(ABC):
|
|||
self.src = src
|
||||
self.db_type = db_type
|
||||
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
|
||||
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()
|
||||
tail = tail.strip().replace(r"\"", '"')
|
||||
# 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
|
||||
def index(ddl: Dict) -> Generator:
|
||||
|
|
@ -227,7 +236,8 @@ class Convertor(ABC):
|
|||
yield field, comment_string
|
||||
|
||||
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
|
||||
|
||||
def print(self):
|
||||
|
|
@ -251,7 +261,9 @@ class Convertor(ABC):
|
|||
|
||||
error_scripts = []
|
||||
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失败, 需要跟进
|
||||
if len(ddl) == 0:
|
||||
|
|
@ -266,17 +278,23 @@ class Convertor(ABC):
|
|||
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"]:
|
||||
column["comment"] = bytes(column["comment"], "utf-8").decode(
|
||||
r"unicode_escape"
|
||||
)[1:-1]
|
||||
table_ddl["comment"] = bytes(table_ddl["comment"], "utf-8").decode(
|
||||
r"unicode_escape"
|
||||
)[1:-1]
|
||||
column["comment"] = comments_dict.get(column["name"], "")
|
||||
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
|
||||
|
||||
# 为每个表生成个6个基本部分
|
||||
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)
|
||||
index = self.gen_index(table_ddl)
|
||||
comment = self.gen_comment(table_ddl)
|
||||
|
|
@ -320,25 +338,31 @@ class PostgreSQLConvertor(Convertor):
|
|||
|
||||
if type == "varchar":
|
||||
return f"varchar({size})"
|
||||
if type in ("int", "int unsigned"):
|
||||
if type in ("int", "int unsigned", "int unsigned zerofill"):
|
||||
return "int4"
|
||||
if type in ("bigint", "bigint unsigned"):
|
||||
return "int8"
|
||||
if type == "datetime":
|
||||
if type in ("tinyint", "smallint", "tinyint unsigned"):
|
||||
return "int2"
|
||||
if type in ("datetime", "timestamp null"):
|
||||
return "timestamp"
|
||||
if type == "date":
|
||||
return "date"
|
||||
if type == "json":
|
||||
return "jsonb"
|
||||
if type == "double":
|
||||
return "double precision"
|
||||
if type == "timestamp":
|
||||
return f"timestamp({size})"
|
||||
return f"timestamp({size})" if size else "timestamp"
|
||||
if type == "bit":
|
||||
return "bool"
|
||||
if type in ("tinyint", "smallint"):
|
||||
return "int2"
|
||||
if type in ("text", "longtext"):
|
||||
return "text"
|
||||
if type in ("blob", "mediumblob"):
|
||||
if type in ("blob", "mediumblob", "longblob"):
|
||||
return "bytea"
|
||||
if type == "decimal":
|
||||
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:
|
||||
|
|
@ -351,6 +375,10 @@ class PostgreSQLConvertor(Convertor):
|
|||
|
||||
type = col["type"].lower()
|
||||
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"
|
||||
default = f"DEFAULT {col['default']}" if col["default"] is not None else ""
|
||||
return f"{name} {full_type} {nullable} {default}"
|
||||
|
|
@ -407,6 +435,8 @@ CREATE TABLE {table_name} (
|
|||
"""生成 insert 语句,以及根据最后的 insert id+1 生成 Sequence"""
|
||||
|
||||
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 脚本
|
||||
script = ""
|
||||
last_id = 0
|
||||
|
|
|
|||
|
|
@ -14,30 +14,30 @@
|
|||
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
|
||||
|
||||
<properties>
|
||||
<revision>2025.12-jdk8-SNAPSHOT</revision>
|
||||
<revision>2026.04-jdk8-SNAPSHOT</revision>
|
||||
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
|
||||
<!-- 统一依赖管理 -->
|
||||
<spring.framework.version>5.3.39</spring.framework.version>
|
||||
<spring.security.version>5.8.16</spring.security.version>
|
||||
<spring.boot.version>2.7.18</spring.boot.version>
|
||||
<spring.cloud.version>2021.0.9</spring.cloud.version>
|
||||
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.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 Boot 2.X 最多使用 2021.0.6.2 版本 -->
|
||||
<!-- Web 相关 -->
|
||||
<servlet.versoin>2.5</servlet.versoin>
|
||||
<springdoc.version>1.8.0</springdoc.version>
|
||||
<knife4j.version>4.5.0</knife4j.version>
|
||||
<!-- DB 相关 -->
|
||||
<druid.version>1.2.27</druid.version>
|
||||
<druid.version>1.2.28</druid.version>
|
||||
<mybatis.version>3.5.19</mybatis.version>
|
||||
<mybatis-plus.version>3.5.15</mybatis-plus.version>
|
||||
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
|
||||
<mybatis-plus.version>3.5.16</mybatis-plus.version>
|
||||
<mybatis-plus-join.version>1.5.7</mybatis-plus-join.version>
|
||||
<dynamic-datasource.version>4.5.0</dynamic-datasource.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>
|
||||
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
|
||||
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
|
||||
<taos.version>3.7.9</taos.version>
|
||||
<kingbase.jdbc.version>9.0.1.jre7</kingbase.jdbc.version>
|
||||
<opengauss.jdbc.version>7.0.0-RC3-og</opengauss.jdbc.version>
|
||||
<taos.version>3.8.3</taos.version>
|
||||
<!-- 消息队列 -->
|
||||
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
|
||||
<!-- RPC 相关 -->
|
||||
|
|
@ -55,36 +55,41 @@
|
|||
<jedis-mock.version>1.1.12</jedis-mock.version>
|
||||
<mockito-inline.version>4.11.0</mockito-inline.version>
|
||||
<!-- 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>
|
||||
<jsoup.version>1.21.2</jsoup.version>
|
||||
<lombok.version>1.18.42</lombok.version>
|
||||
<jsoup.version>1.22.2</jsoup.version>
|
||||
<lombok.version>1.18.46</lombok.version>
|
||||
<mapstruct.version>1.6.3</mapstruct.version>
|
||||
<hutool-5.version>5.8.42</hutool-5.version>
|
||||
<hutool-5.version>5.8.44</hutool-5.version>
|
||||
<fastexcel.version>1.3.0</fastexcel.version>
|
||||
<velocity.version>2.4</velocity.version> <!-- JDK8 不能从 2.4 升级到 2.4.1,会报包不存在!!!! -->
|
||||
<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>
|
||||
<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>
|
||||
<jsch.version>2.27.7</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 不支持 -->
|
||||
<ip2region.version>2.7.0</ip2region.version>
|
||||
<bizlog-sdk.version>3.0.6</bizlog-sdk.version>
|
||||
<reflections.version>0.10.2</reflections.version>
|
||||
<netty.version>4.2.9.Final</netty.version>
|
||||
<netty.version>4.2.12.Final</netty.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.40.15</awssdk.version>
|
||||
<awssdk.version>2.44.0</awssdk.version>
|
||||
<justauth.version>1.16.7</justauth.version>
|
||||
<justauth-starter.version>1.4.0</justauth-starter.version>
|
||||
<jimureport.version>2.1.3</jimureport.version>
|
||||
<jimubi.version>2.3.0</jimubi.version>
|
||||
<weixin-java.version>4.7.9-20251224.161447</weixin-java.version>
|
||||
<alipay-sdk-java.version>4.40.607.ALL</alipay-sdk-java.version>
|
||||
<jimureport.version>2.3.2</jimureport.version>
|
||||
<jimubi.version>2.3.2</jimubi.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 安全漏洞升级 -->
|
||||
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
|
||||
</properties>
|
||||
|
|
@ -305,7 +310,7 @@
|
|||
<exclusion>
|
||||
<groupId>org.redisson</groupId>
|
||||
<!-- 使用 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>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
|
@ -368,7 +373,6 @@
|
|||
<artifactId>yudao-spring-boot-starter-mq</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.rocketmq</groupId>
|
||||
<artifactId>rocketmq-spring-boot-starter</artifactId>
|
||||
|
|
@ -626,6 +630,74 @@
|
|||
<version>${reflections.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.x -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-core</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>${mqtt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- OkHttp -->
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>${okhttp.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>mockwebserver</artifactId>
|
||||
<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>
|
||||
|
|
@ -633,6 +705,24 @@
|
|||
<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>
|
||||
|
|
@ -661,17 +751,6 @@
|
|||
<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>
|
||||
|
|
@ -693,42 +772,6 @@
|
|||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- Vert.x -->
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-core</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-web</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vertx</groupId>
|
||||
<artifactId>vertx-mqtt</artifactId>
|
||||
<version>${vertx.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- MQTT -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.paho</groupId>
|
||||
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
|
||||
<version>${mqtt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 专属于 JDK8 安全漏洞升级 -->
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-core</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ch.qos.logback</groupId>
|
||||
<artifactId>logback-classic</artifactId>
|
||||
<version>${logback.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
|
|
|||
|
|
@ -349,4 +349,27 @@ public class CollectionUtils {
|
|||
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.Multimap;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
|
@ -65,4 +66,47 @@ public class MapUtils {
|
|||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -335,6 +335,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)以来的秒数。
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,9 +1,7 @@
|
|||
package cn.iocoder.yudao.framework.common.util.http;
|
||||
|
||||
import cn.hutool.core.codec.Base64;
|
||||
import cn.hutool.core.map.TableMap;
|
||||
import cn.hutool.core.net.url.UrlBuilder;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.http.HttpRequest;
|
||||
import cn.hutool.http.HttpResponse;
|
||||
|
|
@ -39,8 +37,10 @@ public class HttpUtils {
|
|||
}
|
||||
|
||||
/**
|
||||
* 解码 URL 参数
|
||||
* 解码 URL 参数(query parameter)
|
||||
* 注意:此方法会将 + 解码为空格,适用于 query parameter,不适用于 URL path
|
||||
*
|
||||
* @see #decodeUrlPath(String)
|
||||
* @param value 参数
|
||||
* @return 解码后的参数
|
||||
*/
|
||||
|
|
@ -49,14 +49,25 @@ public class HttpUtils {
|
|||
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
/**
|
||||
* 解码 URL 路径
|
||||
* 与 {@link #decodeUtf8(String)} 不同,此方法不会将 + 解码为空格,保持 + 为字面字符
|
||||
* 适用于 URL path 部分的解码
|
||||
*
|
||||
* @param path URL 路径
|
||||
* @return 解码后的路径
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String decodeUrlPath(String path) {
|
||||
// 先将 + 替换为 %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) {
|
||||
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
|
||||
// 先移除
|
||||
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
|
||||
ReflectUtil.getFieldValue(builder.getQuery(), "query");
|
||||
query.remove(key);
|
||||
// 后添加
|
||||
// 先移除;再添加
|
||||
builder.getQuery().remove(key);
|
||||
builder.addQuery(key, value);
|
||||
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) {
|
||||
if (StrUtil.isEmpty(text)) {
|
||||
return new ArrayList<>();
|
||||
|
|
@ -229,4 +247,53 @@ public class JsonUtils {
|
|||
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));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -60,6 +60,11 @@ public class ObjectUtils {
|
|||
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) {
|
||||
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 USER_COLUMN_NAME = "user_id";
|
||||
|
||||
static final Expression EXPRESSION_NULL = new NullValue();
|
||||
|
||||
private final PermissionCommonApi permissionApi;
|
||||
|
||||
/**
|
||||
|
|
@ -120,7 +118,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
|
|||
|
||||
// 情况二,即不能查看部门,又不能查看自己,则说明 100% 无权限
|
||||
if (CollUtil.isEmpty(deptDataPermission.getDeptIds())
|
||||
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
||||
&& Boolean.FALSE.equals(deptDataPermission.getSelf())) {
|
||||
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));
|
||||
// throw new NullPointerException(String.format("LoginUser(%d) Table(%s/%s) 构建的条件为空",
|
||||
// loginUser.getId(), tableName, tableAlias.getName()));
|
||||
return EXPRESSION_NULL;
|
||||
return new EqualsTo(null, null); // WHERE null = null,可以保证返回的数据为空
|
||||
}
|
||||
if (deptExpression == null) {
|
||||
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.util.ReflectUtil;
|
||||
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.util.collection.SetUtils;
|
||||
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||
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.Expression;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
|
@ -20,7 +20,6 @@ import org.mockito.MockedStatic;
|
|||
import java.util.Map;
|
||||
|
||||
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.randomString;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
|
@ -151,7 +150,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
|
|||
// 调用
|
||||
Expression expression = rule.getExpression(tableName, tableAlias);
|
||||
// 断言
|
||||
assertSame(EXPRESSION_NULL, expression);
|
||||
assertEquals("null = null", expression.toString());
|
||||
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.enums.AreaTypeEnum;
|
||||
import lombok.NonNull;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
|
@ -25,44 +26,46 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
|
|||
* @author 芋道源码
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class AreaUtils {
|
||||
|
||||
/**
|
||||
* 初始化 SEARCHER
|
||||
*/
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
private final static AreaUtils INSTANCE = new AreaUtils();
|
||||
|
||||
/**
|
||||
* Area 内存缓存,提升访问速度
|
||||
*/
|
||||
private static Map<Integer, Area> areas;
|
||||
|
||||
private AreaUtils() {
|
||||
long now = System.currentTimeMillis();
|
||||
areas = new HashMap<>();
|
||||
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0,
|
||||
null, new ArrayList<>()));
|
||||
// 从 csv 中加载数据
|
||||
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
|
||||
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);
|
||||
}
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
// 构建父子关系:因为 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);
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
private static void init() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
areas = new HashMap<>();
|
||||
areas.put(Area.ID_GLOBAL, new Area(Area.ID_GLOBAL, "全球", 0, null, new ArrayList<>()));
|
||||
// 从 csv 中加载数据
|
||||
List<CsvRow> rows = CsvUtil.getReader().read(ResourceUtil.getUtf8Reader("area.csv")).getRows();
|
||||
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.iocoder.yudao.framework.ip.core.Area;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.experimental.UtilityClass;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.lionsoul.ip2region.xdb.Searcher;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* IP 工具类
|
||||
*
|
||||
|
|
@ -16,30 +15,29 @@ import java.io.IOException;
|
|||
* @author wanglhup
|
||||
*/
|
||||
@Slf4j
|
||||
@UtilityClass
|
||||
public class IPUtils {
|
||||
|
||||
/**
|
||||
* 初始化 SEARCHER
|
||||
*/
|
||||
@SuppressWarnings("InstantiationOfUtilityClass")
|
||||
private final static IPUtils INSTANCE = new IPUtils();
|
||||
|
||||
/**
|
||||
* IP 查询器,启动加载到内存中
|
||||
*/
|
||||
private static Searcher SEARCHER;
|
||||
|
||||
static {
|
||||
init();
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有化构造
|
||||
* 初始化
|
||||
*/
|
||||
private IPUtils() {
|
||||
private static void init() {
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
|
||||
SEARCHER = Searcher.newWithBuffer(bytes);
|
||||
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
|
||||
} catch (IOException e) {
|
||||
log.error("启动加载 IPUtils 失败", e);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("IPUtils 初始化失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -5,7 +5,12 @@ import cn.iocoder.yudao.framework.ip.core.Area;
|
|||
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
|
||||
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.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* {@link AreaUtils} 的单元测试
|
||||
|
|
@ -31,6 +36,46 @@ public class AreaUtilsTest {
|
|||
assertEquals(AreaUtils.format(110105), "北京市 北京市 朝阳区");
|
||||
assertEquals(AreaUtils.format(1), "中国");
|
||||
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())));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import java.util.function.Consumer;
|
|||
* <p>
|
||||
* 1. 拼接条件的方法,增加 xxxIfPresent 方法,用于判断值不存在的时候,不要拼接到条件中。
|
||||
* 2. SFunction<S, ?> column + <S> 泛型:支持任意类字段(主表、子表、三表),推荐写法, 让编译器自动推断 S 类型
|
||||
*
|
||||
* @param <T> 数据类型
|
||||
*/
|
||||
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
||||
|
|
@ -122,6 +123,12 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
|
|||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <X> MPJLambdaWrapperX<T> orderByAsc(SFunction<X, ?> column) {
|
||||
super.orderByAsc(true, column);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public MPJLambdaWrapperX<T> last(String lastSql) {
|
||||
super.last(lastSql);
|
||||
|
|
|
|||
|
|
@ -36,6 +36,17 @@
|
|||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-okhttp</artifactId>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import uk.co.jemos.podam.api.PodamFactory;
|
|||
import uk.co.jemos.podam.api.PodamFactoryImpl;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Arrays;
|
||||
import java.util.Date;
|
||||
|
|
@ -52,6 +53,10 @@ public class RandomUtils {
|
|||
}
|
||||
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
|
||||
PODAM_FACTORY.getStrategy().addOrReplaceTypeManufacturer(LocalDateTime.class,
|
||||
(dataProviderStrategy, attributeMetadata, map) -> randomLocalDateTime());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -4,14 +4,14 @@ spring:
|
|||
cloud:
|
||||
nacos:
|
||||
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
|
||||
username: # Nacos 账号
|
||||
password: # Nacos 密码
|
||||
username: nacos
|
||||
password: nacos-admin
|
||||
discovery: # 【配置中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
config: # 【注册中心】配置项
|
||||
namespace: dev # 命名空间。这里使用 dev 开发环境
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
|
||||
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUPn
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -192,8 +192,17 @@ spring:
|
|||
- Path=/admin-api/iot/**
|
||||
filters:
|
||||
- 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
|
||||
x-forwarded:
|
||||
prefix-enabled: false # 避免 Swagger 重复带上额外的 /admin-api/system 前缀
|
||||
default-filters: # 全局过滤器,对应 GatewayFilterDefinition 数组
|
||||
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
|
||||
|
||||
server:
|
||||
port: 48080
|
||||
|
|
@ -249,6 +258,9 @@ knife4j:
|
|||
- name: iot-server
|
||||
service-name: iot-server
|
||||
url: /admin-api/iot/v3/api-docs
|
||||
- name: mes-server
|
||||
service-name: mes-server
|
||||
url: /admin-api/mes/v3/api-docs
|
||||
|
||||
--- #################### 芋道相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@
|
|||
国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno
|
||||
</description>
|
||||
<properties>
|
||||
<spring-ai.version>1.1.2</spring-ai.version>
|
||||
<spring-ai.version>1.1.5</spring-ai.version>
|
||||
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud.ai/spring-ai-alibaba -->
|
||||
<alibaba-ai.version>1.1.0.0-RC2</alibaba-ai.version>
|
||||
<alibaba-ai.version>1.1.2.2</alibaba-ai.version>
|
||||
<tinyflow.version>1.2.6</tinyflow.version>
|
||||
</properties>
|
||||
|
||||
|
|
@ -240,6 +240,12 @@
|
|||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-mcp-server-webmvc</artifactId>
|
||||
<version>${spring-ai.version}</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>io.swagger.core.v3</groupId>
|
||||
<artifactId>swagger-annotations-jakarta</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<!-- 客户端 -->
|
||||
|
|
|
|||
|
|
@ -83,6 +83,15 @@ public class AiKnowledgeSegmentController {
|
|||
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")
|
||||
@Operation(summary = "切片内容")
|
||||
@Parameters({
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ import java.util.List;
|
|||
* @since 2024/4/14 17:35
|
||||
*/
|
||||
@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
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import lombok.*;
|
|||
* @author 芋道源码
|
||||
*/
|
||||
@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
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
|
|
|
|||
|
|
@ -51,8 +51,7 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
|
|||
MPJLambdaWrapper<AiKnowledgeSegmentDO> wrapper = new MPJLambdaWrapperX<AiKnowledgeSegmentDO>()
|
||||
.selectAs(AiKnowledgeSegmentDO::getDocumentId, AiKnowledgeSegmentProcessRespVO::getDocumentId)
|
||||
.selectCount(AiKnowledgeSegmentDO::getId, "count")
|
||||
.select("COUNT(CASE WHEN vector_id > '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY
|
||||
+ "' THEN 1 ELSE NULL END) AS embeddingCount")
|
||||
.select("COUNT(CASE WHEN vector_id IS NOT NULL AND vector_id <> '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY + "' THEN 1 ELSE NULL END) AS embeddingCount")
|
||||
.in(AiKnowledgeSegmentDO::getDocumentId, documentIds)
|
||||
.groupBy(AiKnowledgeSegmentDO::getDocumentId);
|
||||
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 com.google.common.collect.Maps;
|
||||
import io.modelcontextprotocol.client.McpSyncClient;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.messages.MessageType;
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@ public class AiImageServiceImpl implements AiImageService {
|
|||
}
|
||||
|
||||
@Async
|
||||
@SuppressWarnings("ConstantValue")
|
||||
public void executeDrawImage(AiImageDO image, AiImageDrawReqVO reqVO, AiModelDO model) {
|
||||
try {
|
||||
// 1.1 构建请求
|
||||
|
|
@ -164,8 +165,8 @@ public class AiImageServiceImpl implements AiImageService {
|
|||
.build();
|
||||
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.TONG_YI.getPlatform())) {
|
||||
return DashScopeImageOptions.builder()
|
||||
.withModel(model.getModel()).withN(1)
|
||||
.withHeight(draw.getHeight()).withWidth(draw.getWidth())
|
||||
.model(model.getModel()).n(1)
|
||||
.height(draw.getHeight()).width(draw.getWidth())
|
||||
.build();
|
||||
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.YI_YAN.getPlatform())) {
|
||||
return QianFanImageOptions.builder()
|
||||
|
|
|
|||
|
|
@ -98,6 +98,13 @@ public interface AiKnowledgeSegmentService {
|
|||
*/
|
||||
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
|
||||
|
||||
/**
|
||||
* 删除知识库段落
|
||||
*
|
||||
* @param id 段落编号
|
||||
*/
|
||||
void deleteKnowledgeSegment(Long id);
|
||||
|
||||
/**
|
||||
* 重新索引知识库下的所有文档段落
|
||||
*
|
||||
|
|
|
|||
|
|
@ -141,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
|
||||
public void deleteKnowledgeSegmentByDocumentId(Long documentId) {
|
||||
// 1. 查询需要删除的段落
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
package cn.iocoder.yudao.module.ai.tool.method;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.tool.annotation.Tool;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
package cn.iocoder.yudao.module.ai.util;
|
||||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
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.tenant.core.context.TenantContextHolder;
|
||||
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_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) {
|
||||
return buildChatOptions(platform, model, temperature, maxTokens, null, null);
|
||||
}
|
||||
|
|
@ -44,9 +68,10 @@ public class AiUtils {
|
|||
// noinspection EnhancedSwitchMigration
|
||||
switch (platform) {
|
||||
case TONG_YI:
|
||||
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
|
||||
.withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
|
||||
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
|
||||
return DashScopeChatOptions.builder().model(model).temperature(temperature).maxToken(maxTokens)
|
||||
.enableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
|
||||
.multiModel(TONG_YI_MULTI_MODELS.contains(model)) // 是否多模态模型
|
||||
.toolCallbacks(toolCallbacks).toolContext(toolContext).build();
|
||||
case YI_YAN:
|
||||
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
|
||||
case DEEP_SEEK:
|
||||
|
|
@ -125,10 +150,13 @@ public class AiUtils {
|
|||
|| response.getResult().getOutput() == null) {
|
||||
return null;
|
||||
}
|
||||
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
|
||||
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
|
||||
AssistantMessage output = response.getResult().getOutput();
|
||||
// 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:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -34,15 +34,15 @@ public class TongYiChatModelTests {
|
|||
|
||||
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
|
||||
.dashScopeApi(DashScopeApi.builder()
|
||||
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
|
||||
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
|
||||
.build())
|
||||
.defaultOptions(DashScopeChatOptions.builder()
|
||||
// .withModel("qwen1.5-72b-chat") // 模型
|
||||
.withModel("qwen3-235b-a22b-thinking-2507") // 模型
|
||||
// .withModel("deepseek-r1") // 模型(deepseek-r1)
|
||||
// .withModel("deepseek-v3") // 模型(deepseek-v3)
|
||||
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
|
||||
// .withEnableThinking(true)
|
||||
.multiModel(true) // 注意:当使用 qwen3.6-plus 等多模态模型,需要设置为 true,可见 https://help.aliyun.com/zh/model-studio/error-code#error-url 链接
|
||||
.model("qwen3.6-plus") // 模型
|
||||
// .model("deepseek-r1") // 模型(deepseek-r1)
|
||||
// .model("deepseek-v3") // 模型(deepseek-v3)
|
||||
// .model("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b)
|
||||
// .enableThinking(true)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
|
|
@ -85,9 +85,9 @@ public class TongYiChatModelTests {
|
|||
List<Message> messages = new ArrayList<>();
|
||||
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
|
||||
DashScopeChatOptions options = DashScopeChatOptions.builder()
|
||||
.withModel("qwen3-235b-a22b-thinking-2507")
|
||||
.model("qwen3.6-plus").multiModel(true)
|
||||
// .withModel("qwen-max-2025-01-25")
|
||||
.withEnableThinking(true) // 必须设置,否则会报错
|
||||
.enableThinking(true) // 必须设置,否则会报错
|
||||
.build();
|
||||
|
||||
// 调用
|
||||
|
|
@ -112,8 +112,8 @@ public class TongYiChatModelTests {
|
|||
Document document01 = new Document("abc");
|
||||
Document document02 = new Document("sapring");
|
||||
RerankOptions options = DashScopeRerankOptions.builder()
|
||||
.withTopN(1)
|
||||
.withModel("gte-rerank-v2")
|
||||
.topN(1)
|
||||
.model("gte-rerank-v2")
|
||||
.build();
|
||||
RerankRequest rerankRequest = new RerankRequest(
|
||||
query,
|
||||
|
|
|
|||
|
|
@ -12,23 +12,34 @@ import org.springframework.ai.image.ImageResponse;
|
|||
/**
|
||||
* {@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
|
||||
*/
|
||||
public class TongYiImagesModelTest {
|
||||
|
||||
private final DashScopeImageModel imageModel = DashScopeImageModel.builder()
|
||||
.dashScopeApi(DashScopeImageApi.builder()
|
||||
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
|
||||
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
|
||||
.build())
|
||||
.build();
|
||||
|
||||
// TODO @芋艿:
|
||||
@Test
|
||||
@Disabled
|
||||
public void imageCallTest() {
|
||||
// 准备参数
|
||||
ImageOptions options = DashScopeImageOptions.builder()
|
||||
.withModel("wanx-v1")
|
||||
.withHeight(256).withWidth(256)
|
||||
.model("wan2.7-image")
|
||||
// .withSize("2k")
|
||||
.height(768).width(768)
|
||||
.n(1)
|
||||
.build();
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* 流程表单字段 VO
|
||||
*/
|
||||
|
|
@ -20,5 +22,9 @@ public class BpmFormFieldVO {
|
|||
* 字段标题
|
||||
*/
|
||||
private String title;
|
||||
/**
|
||||
* 子表单字段(处理布局组件)
|
||||
*/
|
||||
private List<BpmFormFieldVO> children;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,15 +66,15 @@ public class BpmProcessInstanceCopyController {
|
|||
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
|
||||
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
|
||||
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(
|
||||
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessDefinitionId));
|
||||
return success(convertPage(pageResult, copy -> {
|
||||
BpmProcessInstanceCopyRespVO copyVO = BeanUtils.toBean(copy, BpmProcessInstanceCopyRespVO.class);
|
||||
MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
|
||||
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
|
||||
MapUtils.findAndThen(userMap, copy.getStartUserId(),
|
||||
MapUtils.findAndThen(userMap, copy.getUserId(),
|
||||
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(),
|
||||
processInstance -> {
|
||||
copyVO.setSummary(FlowableUtils.getSummary(
|
||||
|
|
|
|||
|
|
@ -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.annotation.Bean;
|
||||
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 java.util.List;
|
||||
|
|
@ -30,12 +30,12 @@ public class BpmFlowableConfiguration {
|
|||
|
||||
/**
|
||||
* 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
|
||||
*
|
||||
* <p>
|
||||
* 如果不创建,会导致项目启动时,Flowable 报错的问题
|
||||
*/
|
||||
@Bean(name = "applicationTaskExecutor")
|
||||
@ConditionalOnMissingBean(name = "applicationTaskExecutor")
|
||||
public AsyncListenableTaskExecutor taskExecutor() {
|
||||
public AsyncTaskExecutor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(8);
|
||||
executor.setMaxPoolSize(8);
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import org.flowable.bpmn.model.UserTask;
|
|||
import org.flowable.engine.delegate.DelegateExecution;
|
||||
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
||||
import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
|
||||
import org.flowable.common.engine.api.delegate.Expression;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
|
@ -56,14 +57,7 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
|
|||
protected int resolveNrOfInstances(DelegateExecution execution) {
|
||||
// 情况一: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")
|
||||
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
|
||||
if (assigneeUserIds == null) {
|
||||
|
|
@ -94,4 +88,21 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
|
|||
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 lombok.Setter;
|
||||
import org.flowable.bpmn.model.*;
|
||||
import org.flowable.common.engine.api.delegate.Expression;
|
||||
import org.flowable.engine.delegate.DelegateExecution;
|
||||
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
|
||||
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
|
||||
import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
|
|
@ -47,19 +49,12 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
|||
protected int resolveNrOfInstances(DelegateExecution execution) {
|
||||
// 情况一: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 变量没有清理, 如果重新进入该任务不会重新分配审批人
|
||||
@SuppressWarnings("unchecked")
|
||||
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariableLocal(super.collectionVariable, Set.class);
|
||||
if (assigneeUserIds == null) {
|
||||
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
|
||||
assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution));
|
||||
if (CollUtil.isEmpty(assigneeUserIds)) {
|
||||
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
|
||||
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
|
||||
|
|
@ -97,4 +92,21 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
|
|||
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) {
|
||||
// 保持自定义变量名,忽略解析器写入的单元素变量名
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import org.springframework.stereotype.Component;
|
|||
/**
|
||||
* 根据流程变量 variable 的类型,转换参数的值
|
||||
*
|
||||
* 目前用于 ConditionNodeConvert 的 buildConditionExpression 方法中
|
||||
*
|
||||
* @deprecated 已无调用方
|
||||
* @author jason
|
||||
*/
|
||||
@Deprecated // TODO @芋艿:兼容老版本,预计 27 年删除;
|
||||
@Component
|
||||
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
|
|||
|
||||
import cn.hutool.core.map.MapUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.spring.SpringUtil;
|
||||
import cn.iocoder.yudao.framework.common.core.KeyValue;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
|
|
@ -247,9 +248,7 @@ public class FlowableUtils {
|
|||
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
|
||||
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
|
||||
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
|
||||
if (formField != null) {
|
||||
formFieldsMap.put(formField.getField(), formField);
|
||||
}
|
||||
parseFormField(formField, formFieldsMap);
|
||||
});
|
||||
|
||||
// 情况一:当自定义了摘要
|
||||
|
|
@ -273,6 +272,26 @@ public class FlowableUtils {
|
|||
.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 相关的工具方法 ==========
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -707,10 +707,9 @@ public class SimpleModelUtils {
|
|||
List<String> list = convertList(item.getRules(), (rule) -> {
|
||||
String rightSide = NumberUtil.isNumber(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.getOpCode(), // 中间:操作符,比较
|
||||
rule.getLeftSide(), rightSide); // 右侧:转换变量,VariableConvertByTypeExpressionFunction
|
||||
rightSide); // 右侧:取值变量
|
||||
});
|
||||
// 构造条件组的表达式
|
||||
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)) // 分配给自己
|
||||
.active()
|
||||
.includeProcessVariables()
|
||||
.taskTenantId(FlowableUtils.getTenantId())
|
||||
.orderByTaskCreateTime().desc(); // 创建时间倒序
|
||||
if (StrUtil.isNotBlank(pageVO.getName())) {
|
||||
taskQuery.taskNameLike("%" + pageVO.getName() + "%");
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ spring:
|
|||
primary: master
|
||||
datasource:
|
||||
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:postgresql://127.0.0.1:5432/ruoyi-vue-pro # PostgreSQL 连接的示例
|
||||
# url: jdbc:oracle:thin:@127.0.0.1:1521:xe # Oracle 连接的示例
|
||||
|
|
@ -72,7 +72,7 @@ spring:
|
|||
# password: SYSDBA # DM 连接的示例
|
||||
slave: # 模拟从库,可根据自己需要修改
|
||||
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
|
||||
password: 123456
|
||||
|
||||
|
|
@ -98,7 +98,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -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.module.crm.dal.dataobject.product.CrmProductCategoryDO;
|
||||
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.ExcelProperty;
|
||||
import com.fhs.core.trans.anno.Trans;
|
||||
|
|
@ -58,7 +59,7 @@ public class CrmProductRespVO implements VO {
|
|||
private String description;
|
||||
|
||||
@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")
|
||||
private Long ownerUserId;
|
||||
@Schema(description = "负责人的用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
|
||||
|
|
@ -66,7 +67,7 @@ public class CrmProductRespVO implements VO {
|
|||
private String ownerUserName;
|
||||
|
||||
@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")
|
||||
private String creator;
|
||||
@Schema(description = "创建人名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
|
||||
|
|
|
|||
|
|
@ -27,10 +27,12 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
|
|||
Integer type, Long userId, Long deptId) {
|
||||
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
|
||||
.eq(CrmCustomerLimitConfigDO::getType, type);
|
||||
query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
|
||||
if (deptId != null) {
|
||||
query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
|
||||
}
|
||||
query.and(w -> {
|
||||
w.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
|
||||
if (deptId != null) {
|
||||
w.or().apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
|
||||
}
|
||||
});
|
||||
return selectList(query);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ public interface ErpFinancePaymentItemMapper extends BaseMapperX<ErpFinancePayme
|
|||
default BigDecimal selectPaymentPriceSumByBizIdAndBizType(Long bizId, Integer bizType) {
|
||||
// SQL sum 查询
|
||||
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_type", bizType));
|
||||
// 获得数量
|
||||
if (CollUtil.isEmpty(result)) {
|
||||
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) {
|
||||
// SQL sum 查询
|
||||
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_type", bizType));
|
||||
// 获得数量
|
||||
if (CollUtil.isEmpty(result)) {
|
||||
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 查询
|
||||
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")
|
||||
.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 查询
|
||||
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")
|
||||
.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 查询
|
||||
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")
|
||||
.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 查询
|
||||
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")
|
||||
.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) {
|
||||
// SQL sum 查询
|
||||
List<Map<String, Object>> result = selectMaps(new QueryWrapper<ErpStockDO>()
|
||||
.select("SUM(count) AS sumCount")
|
||||
.select("SUM(count) AS sum_count")
|
||||
.eq("product_id", productId));
|
||||
// 获得数量
|
||||
if (CollUtil.isEmpty(result)) {
|
||||
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:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -28,6 +28,6 @@ public class FileCreateReqVO {
|
|||
private String type;
|
||||
|
||||
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer size;
|
||||
private Long size;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class FileRespVO {
|
|||
private String type;
|
||||
|
||||
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Integer size;
|
||||
private Long size;
|
||||
|
||||
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private LocalDateTime createTime;
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ public class ApiAccessLogRespVO {
|
|||
|
||||
@Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
@ExcelProperty(value = "操作分类", converter = DictConvert.class)
|
||||
@DictFormat(cn.iocoder.yudao.module.infra.enums.DictTypeConstants.OPERATE_TYPE)
|
||||
@DictFormat(DictTypeConstants.OPERATE_TYPE)
|
||||
private Integer operateType;
|
||||
|
||||
@Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
|
|
|
|||
|
|
@ -52,6 +52,6 @@ public class FileDO extends BaseDO {
|
|||
/**
|
||||
* 文件大小
|
||||
*/
|
||||
private Integer size;
|
||||
private Long size;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,4 +55,10 @@ public class CodegenProperties {
|
|||
@NotNull(message = "是否生成单元测试不能为空")
|
||||
private Boolean unitTestEnable;
|
||||
|
||||
/**
|
||||
* 是否生成 Excel 导入接口
|
||||
*/
|
||||
@NotNull(message = "是否生成 Excel 导入接口不能为空")
|
||||
private Boolean importEnable;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
AwsCredentialsProvider credentialsProvider = StaticCredentialsProvider.create(
|
||||
AwsBasicCredentials.create(config.getAccessKey(), config.getAccessSecret()));
|
||||
URI endpoint = URI.create(buildEndpoint());
|
||||
URI presignerEndpoint = URI.create(buildPresignerEndpoint());
|
||||
S3Configuration serviceConfiguration = S3Configuration.builder() // Path-style 访问
|
||||
.pathStyleAccessEnabled(Boolean.TRUE.equals(config.getEnablePathStyleAccess()))
|
||||
.chunkedEncodingEnabled(false) // 禁用分块编码,参见 https://t.zsxq.com/kBy57
|
||||
|
|
@ -66,7 +67,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
presigner = S3Presigner.builder()
|
||||
.credentialsProvider(credentialsProvider)
|
||||
.region(region)
|
||||
.endpointOverride(endpoint)
|
||||
.endpointOverride(presignerEndpoint)
|
||||
.serviceConfiguration(serviceConfiguration)
|
||||
.build();
|
||||
}
|
||||
|
|
@ -116,7 +117,7 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
public String presignGetUrl(String url, Integer expirationSeconds) {
|
||||
// 1. 将 url 转换为 path
|
||||
String path = StrUtil.removePrefix(url, config.getDomain() + "/");
|
||||
path = HttpUtils.decodeUtf8(HttpUtils.removeUrlQuery(path));
|
||||
path = HttpUtils.decodeUrlPath(HttpUtils.removeUrlQuery(path));
|
||||
|
||||
// 2.1 情况一:公开访问:无需签名
|
||||
// 考虑到老版本的兼容,所以必须是 config.getEnablePublicAccess() 为 false 时,才进行签名
|
||||
|
|
@ -161,6 +162,23 @@ public class S3FileClient extends AbstractFileClient<S3FileClientConfig> {
|
|||
return StrUtil.format("https://{}", config.getEndpoint());
|
||||
}
|
||||
|
||||
/**
|
||||
* presigner 节点地址
|
||||
*
|
||||
* @return 节点地址
|
||||
*/
|
||||
private String buildPresignerEndpoint() {
|
||||
// 补全 domain
|
||||
if (StrUtil.isEmpty(config.getDomain())) {
|
||||
config.setDomain(buildDomain());
|
||||
}
|
||||
|
||||
if (Boolean.TRUE.equals(config.getEnablePathStyleAccess())) {
|
||||
return StrUtil.removeSuffix(config.getDomain(), StrUtil.format("/{}", config.getBucket()));
|
||||
}
|
||||
return StrUtil.replace(config.getDomain(), StrUtil.format("://{}.", config.getBucket()), "://");
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析 AWS 区域
|
||||
* 优先级:配置的 region > 从 endpoint 解析的 region > 默认值 us-east-1
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import org.springframework.context.annotation.Configuration;
|
|||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.Customizer;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
|
|
@ -40,6 +41,9 @@ public class AdminServerConfiguration {
|
|||
@Value("${spring.boot.admin.client.password:admin}")
|
||||
private String password;
|
||||
|
||||
@Value("${spring.boot.admin.frame-ancestors:'self'}")
|
||||
private String frameAncestors;
|
||||
|
||||
/**
|
||||
* Spring Boot Admin 专用的 InMemoryUserDetailsManager
|
||||
* 使用内存存储,与系统用户隔离
|
||||
|
|
@ -100,6 +104,16 @@ public class AdminServerConfiguration {
|
|||
adminSeverContextPath + "/instances", // Admin Client 注册端点忽略 CSRF
|
||||
adminSeverContextPath + "/actuator/**" // Actuator 端点忽略 CSRF
|
||||
)
|
||||
)
|
||||
.headers(headers -> headers
|
||||
// 特殊:Spring Boot Admin 前端基于 Vue,需 unsafe-inline / unsafe-eval 支持内联脚本与表达式
|
||||
.contentSecurityPolicy(csp -> csp.policyDirectives(
|
||||
"default-src 'self'; "
|
||||
+ "script-src 'self' 'unsafe-inline' 'unsafe-eval'; "
|
||||
+ "style-src 'self' 'unsafe-inline'; "
|
||||
+ "frame-ancestors " + frameAncestors))
|
||||
.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin) // 显式设置 X-Frame-Options 为 SAMEORIGIN
|
||||
.cacheControl(HeadersConfigurer.CacheControlConfig::disable) // 禁用缓存,避免旧配置生效
|
||||
);
|
||||
return httpSecurity.build();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.infra.enums.codegen.CodegenColumnListConditionEnu
|
|||
import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableField;
|
||||
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.Sets;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
|
|
@ -117,8 +118,8 @@ public class CodegenBuilder {
|
|||
table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase());
|
||||
// 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名
|
||||
table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false))));
|
||||
// 去除结尾的表,作为类描述
|
||||
table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表"));
|
||||
// 去除结尾的表,作为类描述;注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
|
||||
table.setClassComment(StrUtil.removeSuffixIgnoreCase(sanitizeComment(table.getTableComment()), "表"));
|
||||
table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +129,7 @@ public class CodegenBuilder {
|
|||
for (CodegenColumnDO column : columns) {
|
||||
column.setTableId(tableId);
|
||||
column.setOrdinalPosition(index++);
|
||||
column.setColumnComment(sanitizeComment(column.getColumnComment()));
|
||||
// 特殊处理:Byte => Integer
|
||||
if (Byte.class.getSimpleName().equals(column.getJavaType())) {
|
||||
column.setJavaType(Integer.class.getSimpleName());
|
||||
|
|
@ -217,4 +219,18 @@ public class CodegenBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将注释中的英文引号替换为中文引号,避免破坏生成代码中的字符串字面量
|
||||
*
|
||||
* @param comment 原始注释
|
||||
* @return 清理后的注释
|
||||
*/
|
||||
@VisibleForTesting
|
||||
String sanitizeComment(String comment) {
|
||||
if (StrUtil.isEmpty(comment)) {
|
||||
return comment;
|
||||
}
|
||||
return comment.replace('"', '“').replace('\'', '‘');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,8 @@ public class CodegenEngine {
|
|||
.put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
|
||||
.put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
|
||||
.put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO"))
|
||||
.put(javaTemplatePath("controller/vo/importExcelVO"), javaModuleImplVOFilePath("ImportExcelVO"))
|
||||
.put(javaTemplatePath("controller/vo/importRespVO"), javaModuleImplVOFilePath("ImportRespVO"))
|
||||
.put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
|
||||
.put(javaTemplatePath("dal/do"),
|
||||
javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
|
||||
|
|
@ -126,6 +128,8 @@ public class CodegenEngine {
|
|||
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/form.vue"),
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/import.vue"),
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}ImportForm.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_ELEMENT_PLUS.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -164,6 +168,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -181,6 +187,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -200,6 +208,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -217,6 +227,8 @@ public class CodegenEngine {
|
|||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/import.vue"),
|
||||
vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/import-form.vue"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"),
|
||||
vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
|
||||
.put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑
|
||||
|
|
@ -284,6 +296,7 @@ public class CodegenEngine {
|
|||
globalBindingMap.put("jakartaPackage", jakartaEnable ? "jakarta" : "javax");
|
||||
globalBindingMap.put("voType", codegenProperties.getVoType());
|
||||
globalBindingMap.put("deleteBatchEnable", codegenProperties.getDeleteBatchEnable());
|
||||
globalBindingMap.put("importEnable", codegenProperties.getImportEnable());
|
||||
// 全局 Java Bean
|
||||
globalBindingMap.put("CommonResultClassName", CommonResult.class.getName());
|
||||
globalBindingMap.put("PageResultClassName", PageResult.class.getName());
|
||||
|
|
@ -343,6 +356,11 @@ public class CodegenEngine {
|
|||
if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
|
||||
return;
|
||||
}
|
||||
} else if (isImportTemplate(vmPath)) {
|
||||
// 关闭 import 时,跳过 ImportExcelVO / ImportRespVO 的生成
|
||||
if (!Boolean.TRUE.equals(codegenProperties.getImportEnable())) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
// 2.3 默认生成
|
||||
generateCode(result, vmPath, filePath, bindingMap);
|
||||
|
|
@ -676,4 +694,9 @@ public class CodegenEngine {
|
|||
return path.contains("listReqVO");
|
||||
}
|
||||
|
||||
private static boolean isImportTemplate(String path) {
|
||||
return path.contains("importExcelVO") || path.contains("importRespVO")
|
||||
|| path.contains("views/import.vue");
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,9 +59,9 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
|
|||
strategyConfig.addInclude(name);
|
||||
} else {
|
||||
// 移除工作流和定时任务前缀的表名
|
||||
strategyConfig.addExclude("ACT_[\\S\\s]+|QRTZ_[\\S\\s]+|FLW_[\\S\\s]+");
|
||||
strategyConfig.addExclude("ACT_[\\S\\s]+|QRTZ_[\\S\\s]+|FLW_[\\S\\s]+|act_[\\S\\s]+|qrtz_[\\S\\s]+|flw_[\\S\\s]+");
|
||||
// 移除 ORACLE 相关的系统表
|
||||
strategyConfig.addExclude("IMPDP_[\\S\\s]+|ALL_[\\S\\s]+|HS_[\\S\\\\s]+");
|
||||
strategyConfig.addExclude("IMPDP_[\\S\\s]+|ALL_[\\S\\s]+|HS_[\\S\\s]+|impdp_[\\S\\s]+|all_[\\S\\s]+|hs_[\\S\\s]+");
|
||||
strategyConfig.addExclude("[\\S\\s]+\\$[\\S\\s]+|[\\S\\s]+\\$"); // 表里不能有 $,一般有都是系统的表
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.infra.service.file;
|
|||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
|
||||
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
|
||||
|
|
@ -93,7 +94,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.updateById(updateObj);
|
||||
|
||||
// 清空缓存
|
||||
clearCache(config.getId(), null);
|
||||
clearCache(config.getId(), config.getMaster());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -132,7 +133,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.deleteById(id);
|
||||
|
||||
// 清空缓存
|
||||
clearCache(id, null);
|
||||
clearCache(id, config.getMaster());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
@ -149,7 +150,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
fileConfigMapper.deleteByIds(ids);
|
||||
|
||||
// 清空缓存
|
||||
ids.forEach(id -> clearCache(id, null));
|
||||
ids.forEach(id -> clearCache(id, false));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,7 +192,7 @@ public class FileConfigServiceImpl implements FileConfigService {
|
|||
validateFileConfigExists(id);
|
||||
// 上传文件
|
||||
byte[] content = ResourceUtil.readBytes("file/erweima.jpg");
|
||||
return getFileClient(id).upload(content, IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
|
||||
return getFileClient(id).upload(content, "public" + StrUtil.SLASH + IdUtil.fastSimpleUUID() + ".jpg", "image/jpeg");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.service.file;
|
|||
import cn.hutool.core.date.LocalDateTimeUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.RandomUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.crypto.digest.DigestUtil;
|
||||
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||
|
|
@ -41,12 +42,19 @@ public class FileServiceImpl implements FileService {
|
|||
*/
|
||||
static boolean PATH_PREFIX_DATE_ENABLE = true;
|
||||
/**
|
||||
* 上传文件的后缀,是否包含时间戳
|
||||
* 上传文件的后缀,是否启用
|
||||
*
|
||||
* 目的:保证文件的唯一性,避免覆盖
|
||||
* 算法:当前时间戳(毫秒)+ 5 位随机数;目的是保证文件的唯一性,避免覆盖
|
||||
* 定制:可按需调整成 UUID、或者其他方式
|
||||
*/
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = true;
|
||||
static boolean PATH_SUFFIX_TIMESTAMP_ENABLE = false;
|
||||
/**
|
||||
* 后缀是否作为上级目录
|
||||
*
|
||||
* true:{@code yyyyMMdd/<后缀>/原文件名.ext};保留原文件名
|
||||
* false:{@code yyyyMMdd/原文件名_<后缀>.ext};后缀拼到文件名
|
||||
*/
|
||||
static boolean PATH_SUFFIX_AS_DIRECTORY = true;
|
||||
|
||||
@Resource
|
||||
private FileConfigService fileConfigService;
|
||||
|
|
@ -88,7 +96,7 @@ public class FileServiceImpl implements FileService {
|
|||
// 3. 保存到数据库
|
||||
fileMapper.insert(new FileDO().setConfigId(client.getId())
|
||||
.setName(name).setPath(path).setUrl(url)
|
||||
.setType(type).setSize(content.length));
|
||||
.setType(type).setSize((long) content.length));
|
||||
return url;
|
||||
}
|
||||
|
||||
|
|
@ -101,16 +109,21 @@ public class FileServiceImpl implements FileService {
|
|||
}
|
||||
String suffix = null;
|
||||
if (PATH_SUFFIX_TIMESTAMP_ENABLE) {
|
||||
suffix = String.valueOf(System.currentTimeMillis());
|
||||
// 5 位随机数,避免同一毫秒内的重复
|
||||
suffix = String.valueOf(System.currentTimeMillis()) + RandomUtil.randomInt(10000, 100000);
|
||||
}
|
||||
|
||||
// 2.1 先拼接 suffix 后缀
|
||||
if (StrUtil.isNotEmpty(suffix)) {
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
if (PATH_SUFFIX_AS_DIRECTORY) {
|
||||
name = suffix + StrUtil.SLASH + name;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
String ext = FileUtil.extName(name);
|
||||
if (StrUtil.isNotEmpty(ext)) {
|
||||
name = FileUtil.mainName(name) + StrUtil.C_UNDERLINE + suffix + StrUtil.DOT + ext;
|
||||
} else {
|
||||
name = name + StrUtil.C_UNDERLINE + suffix;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 2.2 再拼接 prefix 前缀
|
||||
|
|
|
|||
|
|
@ -102,7 +102,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,7 @@ xxl:
|
|||
# Lock4j 配置项
|
||||
lock4j:
|
||||
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
|
||||
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
|
||||
|
||||
--- #################### 监控相关配置 ####################
|
||||
|
||||
|
|
@ -137,6 +137,8 @@ spring:
|
|||
password: admin
|
||||
# Spring Boot Admin Server 服务端的相关配置
|
||||
context-path: /admin # 配置 Spring
|
||||
# 允许嵌入 iframe 的域名(支持通配符),实际部署时,可以改为 "'self' [你的公网域名]"
|
||||
frame-ancestors: "'self' localhost localhost:48082 127.0.0.1 127.0.0.1:48082"
|
||||
|
||||
# 日志文件配置
|
||||
logging:
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ yudao:
|
|||
vo-type: 10 # VO 的类型,参见 CodegenVOTypeEnum 枚举类
|
||||
delete-batch-enable: true # 是否生成批量删除接口
|
||||
unit-test-enable: false # 是否生成单元测试
|
||||
import-enable: false # 是否生成 Excel 导入接口
|
||||
tenant: # 多租户相关配置项
|
||||
enable: true
|
||||
ignore-urls:
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName};
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
#if ($importEnable)
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
#end
|
||||
import ${jakartaPackage}.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
#if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end
|
||||
|
|
@ -159,6 +162,29 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
|
|||
BeanUtils.toBean(list, ${table.className}RespVO.class));
|
||||
}
|
||||
#end
|
||||
#if ($importEnable)
|
||||
|
||||
@GetMapping("/get-import-template")
|
||||
@Operation(summary = "获得导入${table.classComment}模板")
|
||||
#if ($sceneEnum.scene == 1)
|
||||
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
|
||||
#end
|
||||
public void importTemplate(HttpServletResponse response) throws IOException {
|
||||
ExcelUtils.write(response, "${table.classComment}导入模板.xls", "数据",
|
||||
${sceneEnum.prefixClass}${table.className}ImportExcelVO.class, Collections.emptyList());
|
||||
}
|
||||
|
||||
@PostMapping("/import")
|
||||
@Operation(summary = "导入${table.classComment}")
|
||||
@Parameter(name = "file", description = "Excel 文件", required = true)
|
||||
#if ($sceneEnum.scene == 1)
|
||||
@PreAuthorize("@ss.hasPermission('${permissionPrefix}:import')")
|
||||
#end
|
||||
public CommonResult<${sceneEnum.prefixClass}${table.className}ImportRespVO> importExcel(@RequestParam("file") MultipartFile file) throws Exception {
|
||||
List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> list = ExcelUtils.read(file, ${sceneEnum.prefixClass}${table.className}ImportExcelVO.class);
|
||||
return success(${classNameVar}Service.import${simpleClassName}List(list));
|
||||
}
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
|
|
@ -268,4 +294,4 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
|
|||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
#foreach ($column in $columns)
|
||||
#if (${column.createOperation} && "$!column.dictType" != "")
|
||||
import ${DictFormatClassName};
|
||||
import ${DictConvertClassName};
|
||||
#break
|
||||
#end
|
||||
#end
|
||||
|
||||
/**
|
||||
* ${table.classComment} Excel 导入 VO
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class ${sceneEnum.prefixClass}${table.className}ImportExcelVO {
|
||||
|
||||
## 逐个处理字段
|
||||
#foreach ($column in $columns)
|
||||
#if (${column.createOperation})
|
||||
#if ("$!column.dictType" != "")
|
||||
@ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
|
||||
@DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
|
||||
#else
|
||||
@ExcelProperty("${column.columnComment}")
|
||||
#end
|
||||
private ${column.javaType} ${column.javaField};
|
||||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Schema(description = "${sceneEnum.name} - ${table.classComment}导入 Response VO")
|
||||
@Data
|
||||
@Builder
|
||||
public class ${sceneEnum.prefixClass}${table.className}ImportRespVO {
|
||||
|
||||
@Schema(description = "创建成功的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
|
||||
private Integer successCount;
|
||||
|
||||
@Schema(description = "导入失败的数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||
private Integer failureCount;
|
||||
|
||||
@Schema(description = "导入失败的数据集合,key 为行号,value 为失败原因", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||
private Map<Integer, String> failureRows;
|
||||
|
||||
}
|
||||
|
|
@ -25,6 +25,16 @@ public interface ${table.className}Service {
|
|||
* @return 编号
|
||||
*/
|
||||
${primaryColumn.javaType} create${simpleClassName}(@Valid ${saveReqVOClass} ${saveReqVOVar});
|
||||
#if ($importEnable)
|
||||
|
||||
/**
|
||||
* 导入${table.classComment}
|
||||
*
|
||||
* @param importList 导入信息
|
||||
* @return 导入结果
|
||||
*/
|
||||
${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList);
|
||||
#end
|
||||
|
||||
/**
|
||||
* 更新${table.classComment}
|
||||
|
|
@ -162,4 +172,4 @@ public interface ${table.className}Service {
|
|||
|
||||
#end
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ import org.springframework.validation.annotation.Validated;
|
|||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.*;
|
||||
#if ($importEnable)
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
#end
|
||||
import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
|
||||
import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
|
||||
## 特殊:主子表专属逻辑
|
||||
|
|
@ -91,6 +94,32 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
// 返回
|
||||
return ${classNameVar}.getId();
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class) // 添加事务,异常则回滚所有导入
|
||||
public ${sceneEnum.prefixClass}${table.className}ImportRespVO import${simpleClassName}List(List<${sceneEnum.prefixClass}${table.className}ImportExcelVO> importList) {
|
||||
if (CollUtil.isEmpty(importList)) {
|
||||
return ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
|
||||
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
|
||||
}
|
||||
// 遍历,逐个创建
|
||||
${sceneEnum.prefixClass}${table.className}ImportRespVO respVO = ${sceneEnum.prefixClass}${table.className}ImportRespVO.builder()
|
||||
.successCount(0).failureCount(0).failureRows(new LinkedHashMap<>()).build();
|
||||
AtomicInteger index = new AtomicInteger(1);
|
||||
importList.forEach(importItem -> {
|
||||
int currentIndex = index.getAndIncrement();
|
||||
try {
|
||||
create${simpleClassName}(BeanUtils.toBean(importItem, ${saveReqVOClass}.class));
|
||||
respVO.setSuccessCount(respVO.getSuccessCount() + 1);
|
||||
} catch (Exception ex) {
|
||||
respVO.getFailureRows().put(currentIndex, ex.getMessage());
|
||||
}
|
||||
});
|
||||
respVO.setFailureCount(respVO.getFailureRows().size());
|
||||
return respVO;
|
||||
}
|
||||
#end
|
||||
|
||||
@Override
|
||||
## 特殊:主子表专属逻辑(非 ERP 模式)
|
||||
|
|
@ -359,6 +388,9 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
#else
|
||||
#if ( $subTable.subJoinMany)
|
||||
private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
|
||||
if (CollUtil.isEmpty(list)) {
|
||||
return;
|
||||
}
|
||||
list.forEach(o -> o.set${SubJoinColumnName}(${subJoinColumn.javaField}).clean());
|
||||
${subClassNameVars.get($index)}Mapper.insertBatch(list);
|
||||
}
|
||||
|
|
@ -416,4 +448,4 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
|
|||
#end
|
||||
|
||||
#end
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
## 通用变量定义
|
||||
#if ($importEnable)
|
||||
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出', '导入'])
|
||||
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export', 'import'])
|
||||
#else
|
||||
#set ($functionNames = ['查询', '创建', '更新', '删除', '导出'])
|
||||
#set ($functionOps = ['query', 'create', 'update', 'delete', 'export'])
|
||||
#end
|
||||
##
|
||||
## 宏定义:生成按钮 SQL(通用部分)
|
||||
#macro(insertButtonSql $parentIdVar)
|
||||
|
|
|
|||
|
|
@ -73,6 +73,26 @@ export function export${simpleClassName}Excel(params) {
|
|||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
#if ($importEnable)
|
||||
|
||||
// 下载${table.classComment}导入模板
|
||||
export function import${simpleClassName}Template() {
|
||||
return request({
|
||||
url: '${baseURL}/get-import-template',
|
||||
method: 'get',
|
||||
responseType: 'blob'
|
||||
})
|
||||
}
|
||||
|
||||
// 导入${table.classComment}
|
||||
export function import${simpleClassName}(data) {
|
||||
return request({
|
||||
url: '${baseURL}/import',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
#end
|
||||
## 特殊:主子表专属逻辑
|
||||
#foreach ($subTable in $subTables)
|
||||
#set ($index = $foreach.count - 1)
|
||||
|
|
@ -157,4 +177,4 @@ export function export${simpleClassName}Excel(params) {
|
|||
})
|
||||
}
|
||||
#end
|
||||
#end
|
||||
#end
|
||||
|
|
|
|||
|
|
@ -49,6 +49,16 @@
|
|||
<el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
|
||||
v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
|
||||
</el-col>
|
||||
#if ($importEnable)
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-upload2" size="mini" @click="handleImport"
|
||||
:loading="importLoading" v-hasPermi="['${permissionPrefix}:import']">导入</el-button>
|
||||
</el-col>
|
||||
<el-col :span="1.5">
|
||||
<el-button type="info" plain icon="el-icon-document" size="mini" @click="handleImportTemplate"
|
||||
v-hasPermi="['${permissionPrefix}:import']">导入模板</el-button>
|
||||
</el-col>
|
||||
#end
|
||||
<el-col :span="1.5">
|
||||
<el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
|
||||
v-hasPermi="['${permissionPrefix}:export']">导出</el-button>
|
||||
|
|
@ -78,6 +88,9 @@
|
|||
#end
|
||||
<right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
|
||||
</el-row>
|
||||
#if ($importEnable)
|
||||
<input ref="importFileRef" type="file" style="display: none" accept=".xls,.xlsx" @change="handleImportFileChange" />
|
||||
#end
|
||||
|
||||
## 特殊:主子表专属逻辑
|
||||
#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
|
||||
|
|
@ -244,6 +257,10 @@ export default {
|
|||
loading: true,
|
||||
// 导出遮罩层
|
||||
exportLoading: false,
|
||||
#if ($importEnable)
|
||||
// 导入遮罩层
|
||||
importLoading: false,
|
||||
#end
|
||||
// 显示搜索条件
|
||||
showSearch: true,
|
||||
## 特殊:树表专属逻辑(树不需要分页接口)
|
||||
|
|
@ -322,6 +339,44 @@ export default {
|
|||
openForm(id) {
|
||||
this.#[[$]]#refs["formRef"].open(id);
|
||||
},
|
||||
#if ($importEnable)
|
||||
/** 导入按钮操作 */
|
||||
handleImport() {
|
||||
this.$refs.importFileRef && this.$refs.importFileRef.click();
|
||||
},
|
||||
/** 导入模板下载 */
|
||||
async handleImportTemplate() {
|
||||
const data = await ${simpleClassName}Api.import${simpleClassName}Template();
|
||||
this.#[[$]]#download.excel(data, '${table.classComment}导入模板.xls');
|
||||
},
|
||||
/** 导入文件变更 */
|
||||
async handleImportFileChange(event) {
|
||||
const target = event.target;
|
||||
const file = target.files && target.files[0];
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
this.importLoading = true;
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await ${simpleClassName}Api.import${simpleClassName}(formData);
|
||||
const data = res.data || res;
|
||||
let text = '导入成功数量:' + (data.successCount || 0) + ';导入失败数量:' + (data.failureCount || 0) + ';';
|
||||
if (data.failureRows) {
|
||||
Object.keys(data.failureRows).forEach((rowNo) => {
|
||||
text += '< 第' + rowNo + '行: ' + data.failureRows[rowNo] + ' >';
|
||||
});
|
||||
}
|
||||
await this.$alert(text, '${table.classComment}导入结果', { dangerouslyUseHTMLString: true });
|
||||
await this.getList();
|
||||
} catch {
|
||||
} finally {
|
||||
target.value = '';
|
||||
this.importLoading = false;
|
||||
}
|
||||
},
|
||||
#end
|
||||
/** 删除按钮操作 */
|
||||
async handleDelete(row) {
|
||||
const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue