Compare commits

..

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

2562 changed files with 18980 additions and 198756 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -31,8 +31,8 @@
| 【完整版】[yudao-cloud](https://gitee.com/zhijiantianya/yudao-cloud) | [`master`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master/) 分支 | [`master-jdk17`](https://gitee.com/zhijiantianya/yudao-cloud/tree/master-jdk17/) 分支 |
| 【精简版】[yudao-cloud-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、MES、AI 大模型、IoT 物联网 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP、MES、AI 大模型、IoT 物联网 等功能
* 【完整版】包括系统功能、基础设施、会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
* 【精简版】只包括系统功能、基础设施功能不包括会员中心、数据报表、工作流程、商城系统、微信公众号、CRM、ERP 等功能
可参考 [《迁移文档》](https://cloud.iocoder.cn/migrate-module/) ,只需要 5-10 分钟,即可将【完整版】按需迁移到【精简版】
@ -115,7 +115,7 @@
* 通用模块(必选):系统功能、基础设施
* 通用模块(可选):工作流程、支付系统、数据报表、会员中心
* 业务系统按需ERP 系统、CRM 系统、MES 系统、商城系统、微信公众号、AI 大模型、IoT 物联网
* 业务系统按需ERP 系统、CRM 系统、商城系统、微信公众号、AI 大模型
> 友情提示:本项目基于 RuoYi-Vue 修改,**重构优化**后端的代码,**美化**前端的界面。
>
@ -279,14 +279,6 @@
![功能图](/.image/common/crm-feature.png)
### MES 系统
演示地址:<https://cloud.iocoder.cn/mes-preview/>
![功能图](/.image/common/mes-feature.png)
![功能图](/.image/common/mes-preview.png)
### AI 大模型
演示地址:<https://cloud.iocoder.cn/ai-preview/>
@ -295,14 +287,6 @@
![功能图](/.image/common/ai-preview.gif)
### IoT 物联网
演示地址:<https://cloud.iocoder.cn/iot/build>
![功能图](/.image/common/iot-feature.png)
![预览图](/.image/common/iot-preview.png)
## 🐨 技术栈
### 微服务
@ -320,9 +304,7 @@
| `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 模块 |

View File

@ -24,10 +24,9 @@
<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>
@ -35,7 +34,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.04-jdk8-SNAPSHOT</revision>
<revision>2025.11-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

View File

@ -62,10 +62,6 @@ 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 部分
# 相关 issuehttps://t.zsxq.com/96IFc 、https://t.zsxq.com/rC3A3
@ -81,11 +77,7 @@ class Convertor(ABC):
self.src = src
self.db_type = db_type
self.content = load_and_clean(self.src)
# 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)
self.table_script_list = re.findall(r"CREATE TABLE [^;]*;", self.content)
@abstractmethod
def translate_type(self, type: str, size: Optional[Union[int, Tuple[int]]]) -> str:
@ -190,8 +182,7 @@ class Convertor(ABC):
head = head.strip().replace("`", "").lower()
tail = tail.strip().replace(r"\"", '"')
# tail = tail.replace("b'0'", "'0'").replace("b'1'", "'1'")
col_part = f" {head}" if head else ""
yield f"INSERT INTO {table_name.lower()}{col_part} VALUES {tail}"
yield f"INSERT INTO {table_name.lower()} {head} VALUES {tail}"
@staticmethod
def index(ddl: Dict) -> Generator:
@ -236,8 +227,7 @@ class Convertor(ABC):
yield field, comment_string
def table_comment(self, table_sql: str) -> str:
# 兼容 COMMENT='xxx' / COMMENT = 'xxx' 等等号两侧空格变体;允许 COMMENT 内含转义单引号
match = re.search(r"COMMENT\s*=\s*'((?:[^'\\]|\\.)*)'", table_sql)
match = re.search(r"COMMENT \='([^']+)';", table_sql)
return match.group(1) if match else None
def print(self):
@ -261,9 +251,7 @@ class Convertor(ABC):
error_scripts = []
for table_sql in self.table_script_list:
# 剥离 COMMENT 子句避免 DDLParser 无法处理中文等特殊字符
table_sql_for_parse = re.sub(r"\s+COMMENT\s+'[^']*'", "", table_sql)
ddl = DDLParser(table_sql_for_parse.replace("`", "")).run()
ddl = DDLParser(table_sql.replace("`", "")).run()
# 如果parse失败, 需要跟进
if len(ddl) == 0:
@ -278,23 +266,17 @@ 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"] = comments_dict.get(column["name"], "")
table_ddl["comment"] = self.table_comment(orig_table_sql) or ""
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]
# 为每个表生成个6个基本部分
create = self.gen_create(table_ddl)
has_id = any(col["name"].lower() == "id" for col in table_ddl["columns"])
pk = self.gen_pk(table_name) if has_id else ""
pk = self.gen_pk(table_name)
uk = self.gen_uk(table_ddl)
index = self.gen_index(table_ddl)
comment = self.gen_comment(table_ddl)
@ -338,31 +320,25 @@ class PostgreSQLConvertor(Convertor):
if type == "varchar":
return f"varchar({size})"
if type in ("int", "int unsigned", "int unsigned zerofill"):
if type in ("int", "int unsigned"):
return "int4"
if type in ("bigint", "bigint unsigned"):
return "int8"
if type in ("tinyint", "smallint", "tinyint unsigned"):
return "int2"
if type in ("datetime", "timestamp null"):
if type == "datetime":
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})" if size else "timestamp"
return f"timestamp({size})"
if type == "bit":
return "bool"
if type in ("tinyint", "smallint"):
return "int2"
if type in ("text", "longtext"):
return "text"
if type in ("blob", "mediumblob", "longblob"):
if type in ("blob", "mediumblob"):
return "bytea"
if type == "decimal":
return (
f"numeric({','.join(str(s) for s in size)})" if size and len(size) else "numeric"
f"numeric({','.join(str(s) for s in size)})" if len(size) else "numeric"
)
def gen_create(self, ddl: Dict) -> str:
@ -375,10 +351,6 @@ 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}"
@ -435,8 +407,6 @@ 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

View File

@ -14,32 +14,32 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties>
<revision>2026.04-jdk8-SNAPSHOT</revision>
<revision>2025.11-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 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 版本 -->
<spring.cloud.version>2021.0.9</spring.cloud.version>
<spring.cloud.alibaba.version>2021.0.6.2</spring.cloud.alibaba.version>
<!-- 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.28</druid.version>
<druid.version>1.2.27</druid.version>
<mybatis.version>3.5.19</mybatis.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>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>4.3.1</redisson.version>
<redisson.version>3.52.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.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>
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<taos.version>3.7.8</taos.version>
<!-- 消息队列 -->
<rocketmq-spring.version>2.3.5</rocketmq-spring.version>
<rocketmq-spring.version>2.3.4</rocketmq-spring.version>
<!-- RPC 相关 -->
<!-- Config 配置中心相关 -->
<!-- Job 定时任务相关 -->
@ -55,41 +55,35 @@
<jedis-mock.version>1.1.12</jedis-mock.version>
<mockito-inline.version>4.11.0</mockito-inline.version>
<!-- Bpm 工作流相关 -->
<flowable.version>6.8.1</flowable.version>
<flowable.version>6.8.0</flowable.version>
<!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.22.2</jsoup.version>
<lombok.version>1.18.46</lombok.version>
<jsoup.version>1.21.2</jsoup.version>
<lombok.version>1.18.42</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.44</hutool-5.version>
<hutool-5.version>5.8.41</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.6.0-jre</guava.version>
<guava.version>33.5.0-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.13.0</commons-net.version>
<commons-net.version>3.12.0</commons-net.version>
<commons-lang3.version>3.20.0</commons-lang3.version>
<jsch.version>2.28.2</jsch.version>
<jsch.version>2.27.6</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.12.Final</netty.version>
<netty.version>4.2.7.Final</netty.version>
<mqtt.version>1.2.5</mqtt.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.2Spring Boot 2.7 默认 5.1.5 不兼容 -->
<vertx.version>4.5.22</vertx.version>
<!-- 三方云服务相关 -->
<awssdk.version>2.44.0</awssdk.version>
<awssdk.version>2.39.2</awssdk.version>
<justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.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>
<jimureport.version>2.1.3</jimureport.version>
<jimubi.version>2.2.0</jimubi.version>
<weixin-java.version>4.7.8-20251117.120146</weixin-java.version>
<!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
</properties>
@ -310,7 +304,7 @@
<exclusion>
<groupId>org.redisson</groupId>
<!-- 使用 redisson-spring-data-27 替代,解决 Tuple NoClassDefFoundError 报错 -->
<artifactId>redisson-spring-data-40</artifactId> <!-- Redisson 4.x 默认依赖 spring-data-40排除后使用 spring-data-27 适配 Spring Boot 2.7 -->
<artifactId>redisson-spring-data-35</artifactId>
</exclusion>
</exclusions>
</dependency>
@ -373,6 +367,7 @@
<artifactId>yudao-spring-boot-starter-mq</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
@ -630,6 +625,62 @@
<version>${reflections.version}</version>
</dependency>
<!-- 三方云服务相关 -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
<artifactId>s3</artifactId>
<version>${awssdk.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${justauth-starter.version}</version>
</dependency>
<!-- 积木报表-->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<version>${jimureport.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId>
<version>${jimubi.version}</version>
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Vert.x -->
<dependency>
<groupId>io.vertx</groupId>
@ -654,123 +705,16 @@
<version>${mqtt.version}</version>
</dependency>
<!-- OkHttp -->
<!-- 专属于 JDK8 安全漏洞升级 -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>${logback.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>
<artifactId>s3</artifactId>
<version>${awssdk.version}</version>
</dependency>
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId> <!-- 社交登陆(例如说,个人微信、企业微信等等) -->
<version>${justauth.version}</version>
</dependency>
<dependency>
<groupId>com.xkcoding.justauth</groupId>
<artifactId>justauth-spring-boot-starter</artifactId>
<version>${justauth-starter.version}</version>
<exclusions>
<!-- 移除,避免和项目里的 hutool-all 冲突 -->
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-sdk-java</artifactId>
<version>${alipay-sdk-java.version}</version>
<exclusions>
<exclusion>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-mp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>wx-java-miniapp-spring-boot-starter</artifactId>
<version>${weixin-java.version}</version>
</dependency>
<!-- 积木报表-->
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimureport-spring-boot-starter</artifactId>
<version>${jimureport.version}</version>
</dependency>
<dependency>
<groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId>
<version>${jimubi.version}</version>
<exclusions>
<exclusion>
<groupId>com.github.jsqlparser</groupId>
<artifactId>jsqlparser</artifactId>
</exclusion>
<exclusion>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</exclusion>
</exclusions>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -27,10 +27,10 @@ public class PageParam implements Serializable {
@Min(value = 1, message = "页码最小值为 1")
private Integer pageNo = PAGE_NO;
@Schema(description = "每页条数,最大值为 200", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@Schema(description = "每页条数,最大值为 100", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@NotNull(message = "每页条数不能为空")
@Min(value = 1, message = "每页条数最小值为 1")
@Max(value = 200, message = "每页条数最大值为 200")
@Max(value = 100, message = "每页条数最大值为 100")
private Integer pageSize = PAGE_SIZE;
}

View File

@ -349,27 +349,4 @@ 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;
}
}

View File

@ -7,7 +7,6 @@ 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;
@ -66,47 +65,4 @@ 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;
}
}

View File

@ -335,27 +335,6 @@ public class LocalDateTimeUtils {
}
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getQuarterStart(LocalDate date) {
Month firstMonthOfQuarter = date.getMonth().firstMonthOfQuarter();
return LocalDate.of(date.getYear(), firstMonthOfQuarter, 1);
}
/**
*
*
* @param date
* @return
*/
public static LocalDate getWeekStart(LocalDate date) {
return date.with(DayOfWeek.MONDAY);
}
/**
* {@link LocalDateTime} Unix 1970-01-01T00:00:00Z
*

View File

@ -1,7 +1,9 @@
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;
@ -12,7 +14,6 @@ import org.springframework.web.util.UriComponentsBuilder;
import javax.servlet.http.HttpServletRequest;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
@ -36,38 +37,14 @@ public class HttpUtils {
return URLEncoder.encode(value, StandardCharsets.UTF_8.name());
}
/**
* URL query parameter
* + query parameter URL path
*
* @see #decodeUrlPath(String)
* @param value
* @return
*/
@SneakyThrows
public static String decodeUtf8(String value) {
return URLDecoder.decode(value, StandardCharsets.UTF_8.name());
}
/**
* URL
* {@link #decodeUtf8(String)} + +
* URL path
*
* @param path URL
* @return
*/
@SneakyThrows
public static String decodeUrlPath(String path) {
// 先将 + 替换为 %2B避免被 URLDecoder 解码为空格
String encoded = path.replace("+", "%2B");
return URLDecoder.decode(encoded, StandardCharsets.UTF_8.name());
}
@SuppressWarnings("unchecked")
public static String replaceUrlQuery(String url, String key, String value) {
UrlBuilder builder = UrlBuilder.of(url, Charset.defaultCharset());
// 先移除;再添加
builder.getQuery().remove(key);
// 先移除
TableMap<CharSequence, CharSequence> query = (TableMap<CharSequence, CharSequence>)
ReflectUtil.getFieldValue(builder.getQuery(), "query");
query.remove(key);
// 后添加
builder.addQuery(key, value);
return builder.build();
}

View File

@ -173,24 +173,6 @@ 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<>();
@ -247,53 +229,4 @@ public class JsonUtils {
return JSONUtil.isTypeJSONObject(str);
}
/**
* Object
* <p>
* jsonString parseObject
*
* @param obj MapPOJO
* @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));
}
}

View File

@ -1,85 +1,42 @@
package cn.iocoder.yudao.framework.common.util.json.databind;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.lang.reflect.Field;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* LocalDateTime
*
* @author
*/
@Slf4j
public class TimestampLocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
public static final TimestampLocalDateTimeSerializer INSTANCE = new TimestampLocalDateTimeSerializer();
private static final Map<Class<?>, Map<String, Field>> FIELD_CACHE = new ConcurrentHashMap<>();
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
// 情况一:有 JsonFormat 自定义注解则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
String fieldName = gen.getOutputContext().getCurrentName();
if (fieldName != null) {
Object currentValue = gen.getOutputContext().getCurrentValue();
if (currentValue != null) {
Class<?> clazz = currentValue.getClass();
Map<String, Field> fieldMap = FIELD_CACHE.computeIfAbsent(clazz, this::buildFieldMap);
Field field = fieldMap.get(fieldName);
// 进一步修复https://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1480
if (field != null && field.isAnnotationPresent(JsonFormat.class)) {
JsonFormat jsonFormat = field.getAnnotation(JsonFormat.class);
try {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(jsonFormat.pattern());
gen.writeString(formatter.format(value));
return;
} catch (Exception ex) {
log.warn("[serialize][({}#{}) 使用 JsonFormat pattern 失败,尝试使用默认的 Long 时间戳]",
clazz.getName(), fieldName, ex);
}
}
}
Class<?> clazz = gen.getOutputContext().getCurrentValue().getClass();
Field field = ReflectUtil.getField(clazz, fieldName);
// 情况一:有 JsonFormat 自定义注解则使用它。https://github.com/YunaiV/ruoyi-vue-pro/pull/1019
JsonFormat[] jsonFormats = field.getAnnotationsByType(JsonFormat.class);
if (jsonFormats.length > 0) {
String pattern = jsonFormats[0].pattern();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern);
gen.writeString(formatter.format(value));
return;
}
// 情况二:默认将 LocalDateTime 对象,转换为 Long 时间戳
gen.writeNumber(value.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli());
}
/**
*
*
* @param clazz
* @return
*/
private Map<String, Field> buildFieldMap(Class<?> clazz) {
Map<String, Field> fieldMap = new HashMap<>();
for (Field field : ReflectUtil.getFields(clazz)) {
String fieldName = field.getName();
JsonProperty jsonProperty = field.getAnnotation(JsonProperty.class);
if (jsonProperty != null) {
String value = jsonProperty.value();
if (StrUtil.isNotEmpty(value) && ObjUtil.notEqual("\u0000", value)) {
fieldName = value;
}
}
fieldMap.put(fieldName, field);
}
return fieldMap;
}
}

View File

@ -60,11 +60,6 @@ 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);
}

View File

@ -1,52 +0,0 @@
package cn.iocoder.yudao.framework.common.util.http;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
/**
* {@link HttpUtils}
*/
public class HttpUtilsTest {
@Test
public void testReplaceUrlQuery_replace() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1&b=2";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "3");
// 断言:被替换的 key 会移到末尾,原顺序的其它参数保留
assertEquals("https://www.iocoder.cn/path?b=2&a=3", result);
}
@Test
public void testReplaceUrlQuery_add() {
// 准备参数
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "b", "2");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1&b=2", result);
}
@Test
public void testReplaceUrlQuery_noQuery() {
// 准备参数:原 URL 没有 query
String url = "https://www.iocoder.cn/path";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "1");
// 断言
assertEquals("https://www.iocoder.cn/path?a=1", result);
}
@Test
public void testReplaceUrlQuery_emptyValue() {
// 准备参数value 为空字符串
String url = "https://www.iocoder.cn/path?a=1";
// 调用
String result = HttpUtils.replaceUrlQuery(url, "a", "");
// 断言:保留 keyvalue 为空
assertEquals("https://www.iocoder.cn/path?a=", result);
}
}

View File

@ -57,6 +57,8 @@ 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;
/**
@ -118,7 +120,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可以保证返回的数据为空
}
@ -131,7 +133,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 new EqualsTo(null, null); // WHERE null = null可以保证返回的数据为空
return EXPRESSION_NULL;
}
if (deptExpression == null) {
return userExpression;

View File

@ -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,6 +20,7 @@ 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.*;
@ -150,7 +151,7 @@ class DeptDataPermissionRuleTest extends BaseMockitoUnitTest {
// 调用
Expression expression = rule.getExpression(tableName, tableAlias);
// 断言
assertEquals("null = null", expression.toString());
assertSame(EXPRESSION_NULL, expression);
assertSame(deptDataPermission, loginUser.getContext(DeptDataPermissionRule.CONTEXT_KEY, DeptDataPermissionRespDTO.class));
}
}

View File

@ -8,7 +8,6 @@ import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import lombok.NonNull;
import lombok.experimental.UtilityClass;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
@ -26,46 +25,44 @@ 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;
static {
init();
}
/**
*
*/
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);
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);
}
// 构建父子关系:因为 Area 中没有 parentId 字段,所以需要重复读取
for (CsvRow row : rows) {
Area area = areas.get(Integer.valueOf(row.get(0))); // 自己
Area parent = areas.get(Integer.valueOf(row.get(3))); // 父
Assert.isTrue(area != parent, "{}:父子节点相同", area.getName());
area.setParent(parent);
parent.getChildren().add(area);
}
log.info("启动加载 AreaUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
}
/**

View File

@ -3,10 +3,11 @@ package cn.iocoder.yudao.framework.ip.core.utils;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.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
*
@ -15,29 +16,30 @@ import org.lionsoul.ip2region.xdb.Searcher;
* @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 static void init() {
private IPUtils() {
try {
long now = System.currentTimeMillis();
byte[] bytes = ResourceUtil.readBytes("ip2region.xdb");
SEARCHER = Searcher.newWithBuffer(bytes);
log.info("启动加载 IPUtils 成功,耗时 ({}) 毫秒", System.currentTimeMillis() - now);
} catch (Exception e) {
throw new RuntimeException("IPUtils 初始化失败", e);
} catch (IOException e) {
log.error("启动加载 IPUtils 失败", e);
}
}

View File

@ -5,12 +5,7 @@ import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.enums.AreaTypeEnum;
import 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}
@ -36,46 +31,6 @@ 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())));
}
}

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.framework.tenant.core.redis;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.redis.core.TimeoutRedisCacheManager;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import lombok.extern.slf4j.Slf4j;
@ -22,8 +21,6 @@ import java.util.Set;
@Slf4j
public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
private static final String SPLIT = "#";
private final Set<String> ignoreCaches;
public TenantRedisCacheManager(RedisCacheWriter cacheWriter,
@ -35,11 +32,10 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
@Override
public Cache getCache(String name) {
String[] names = StrUtil.splitToArray(name, SPLIT);
// 如果开启多租户,则 name 拼接租户后缀
if (!TenantContextHolder.isIgnore()
&& TenantContextHolder.getTenantId() != null
&& !CollUtil.contains(ignoreCaches, names[0])) {
&& !CollUtil.contains(ignoreCaches, name)) {
name = name + ":" + TenantContextHolder.getTenantId();
}
@ -47,4 +43,4 @@ public class TenantRedisCacheManager extends TimeoutRedisCacheManager {
return super.getCache(name);
}
}
}

View File

@ -15,7 +15,6 @@ import java.util.function.Consumer;
* <p>
* 1. xxxIfPresent
* 2. SFunction<S, ?> column + <S> , S
*
* @param <T>
*/
public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
@ -123,12 +122,6 @@ 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);

View File

@ -36,17 +36,6 @@
<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.xhttpcore导致 ClassNotFoundException。
临时解决:显式引入 httpclient 4.x。待 WxJava 修复后移除。
-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.14</version>
</dependency>
<!-- 工具相关 -->
<dependency>

View File

@ -9,7 +9,6 @@ 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;
@ -53,10 +52,6 @@ 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());

View File

@ -34,9 +34,7 @@ import javax.servlet.Filter;
import java.util.Map;
import java.util.function.Predicate;
@AutoConfiguration(beforeName = {
"com.fhs.trans.config.TransServiceConfig" // cloud 独有避免一键改包后RestTemplate 初始化的冲突。可见 https://t.zsxq.com/T4yj7 帖子
})
@AutoConfiguration
@EnableConfigurationProperties(WebProperties.class)
public class YudaoWebAutoConfiguration {

View File

@ -0,0 +1,54 @@
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);
}));
}
}

View File

@ -11,25 +11,4 @@ spring:
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
--- #################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# Spring Boot Admin 配置项
spring:
boot:
admin:
# Spring Boot Admin Client 客户端的相关配置
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP

View File

@ -4,38 +4,16 @@ spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
username: nacos
password: nacos-admin
username: # Nacos 账号
password: # Nacos 密码
discovery: # 【配置中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
config: # 【注册中心】配置项
namespace: dev # 命名空间。这里使用 dev 开发环境
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUPn
--- #################### 监控相关配置 ####################
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# Spring Boot Admin 配置项
spring:
boot:
admin:
# Spring Boot Admin Client 客户端的相关配置
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
group: DEFAULT_GROUP # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
# 日志文件配置
logging:
level:
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR # TODO 芋艿先禁用Spring Boot 3.X 存在部分错误的 WARN 提示

View File

@ -192,17 +192,8 @@ 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
@ -258,9 +249,6 @@ 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
--- #################### 芋道相关配置 ####################

View File

@ -1,53 +0,0 @@
package cn.iocoder.yudao.module.ai.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* AI
*
* @author runzhen
*/
@AllArgsConstructor
@Getter
public enum AiDocumentSplitStrategyEnum {
/**
*
*/
AUTO("auto", "自动识别"),
/**
* Token
*/
TOKEN("token", "Token 切分"),
/**
*
*/
PARAGRAPH("paragraph", "段落切分"),
/**
* Markdown QA
*
*
*/
MARKDOWN_QA("markdown_qa", "Markdown QA 切分"),
/**
*
*
*/
SEMANTIC("semantic", "语义切分");
/**
*
*/
private final String code;
/**
*
*/
private final String name;
}

View File

@ -19,9 +19,8 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description>
<properties>
<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.2.2</alibaba-ai.version>
<spring-ai.version>1.1.0</spring-ai.version>
<alibaba-ai.version>1.1.0.0-M5</alibaba-ai.version>
<tinyflow.version>1.2.6</tinyflow.version>
</properties>
@ -240,12 +239,6 @@
<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>
<!-- 客户端 -->
@ -269,11 +262,6 @@
<groupId>com.agentsflex</groupId>
<artifactId>agents-flex-store-elasticsearch</artifactId>
</exclusion>
<exclusion>
<!-- 解决 https://t.zsxq.com/pCBZC 问题 -->
<groupId>com.agentsflex</groupId>
<artifactId>agents-flex-search-engine-es</artifactId>
</exclusion>
<exclusion>
<!-- TODO @芋艿:暂时移除 groovy和 iot 冲突 -->
<groupId>org.codehaus.groovy</groupId>

View File

@ -83,15 +83,6 @@ 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({

View File

@ -11,7 +11,7 @@ import lombok.Data;
public class AiKnowledgeSegmentPageReqVO extends PageParam {
@Schema(description = "文档编号", example = "1")
private Long documentId;
private Integer documentId;
@Schema(description = "分段内容关键字", example = "Java 开发")
private String content;

View File

@ -27,7 +27,7 @@ import java.util.List;
* @since 2024/4/14 17:35
*/
@TableName(value = "ai_chat_message", autoResultMap = true)
@KeySequence("ai_chat_message_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@Builder
@NoArgsConstructor

View File

@ -14,7 +14,7 @@ import lombok.*;
* @author
*/
@TableName("ai_api_key")
@KeySequence("ai_api_key_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@KeySequence("ai_chat_conversation_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@Builder
@NoArgsConstructor

View File

@ -51,7 +51,8 @@ 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 IS NOT NULL AND vector_id <> '" + AiKnowledgeSegmentDO.VECTOR_ID_EMPTY + "' THEN 1 ELSE NULL END) AS embeddingCount")
.select("COUNT(CASE WHEN 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);

View File

@ -37,6 +37,7 @@ 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;
@ -58,8 +59,6 @@ import reactor.core.publisher.Flux;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -232,24 +231,20 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 4.3 流式返回
StringBuffer contentBuffer = new StringBuffer();
StringBuffer reasoningContentBuffer = new StringBuffer();
// 防止执行多次知识库和联网搜索
AtomicBoolean firstExecuteFlag = new AtomicBoolean(true);
AtomicReference<List<AiChatMessageRespVO.KnowledgeSegment>> cacheSegments = new AtomicReference<>();
AtomicReference<List<AiWebSearchResponse.WebPage>> cacheWebSearchPages = new AtomicReference<>();
return streamResponse.map(chunk -> {
// 仅首次:返回知识库、联网搜索
List<AiChatMessageRespVO.KnowledgeSegment> segments = null;
List<AiWebSearchResponse.WebPage> webSearchPages = null;
if (StrUtil.isEmpty(contentBuffer)) {
if (firstExecuteFlag.compareAndSet(true, false)) { // CAS 操作,确保仅执行一次
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() -> knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
cacheSegments.set(BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null);
}));
if (webSearchResponse != null) {
cacheWebSearchPages.set(webSearchResponse.getLists());
}
Map<Long, AiKnowledgeDocumentDO> documentMap = TenantUtils.executeIgnore(() ->
knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId)));
segments = BeanUtils.toBean(knowledgeSegments, AiChatMessageRespVO.KnowledgeSegment.class, segment -> {
AiKnowledgeDocumentDO document = documentMap.get(segment.getDocumentId());
segment.setDocumentName(document != null ? document.getName() : null);
});
if (webSearchResponse != null) {
webSearchPages = webSearchResponse.getLists();
}
}
// 响应结果
@ -266,7 +261,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
.setContent(StrUtil.nullToDefault(newContent, "")) // 避免 null 的 情况
.setReasoningContent(StrUtil.nullToDefault(newReasoningContent, "")) // 避免 null 的 情况
.setSegments(cacheSegments.get()).setWebSearchPages(cacheWebSearchPages.get()))); // 知识库 + 联网搜索
.setSegments(segments).setWebSearchPages(webSearchPages))); // 知识库 + 联网搜索
}).doOnComplete(() -> {
// 忽略租户,因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> chatMessageMapper.updateById(

View File

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

View File

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

View File

@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@ -16,11 +15,8 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.enums.AiDocumentSplitStrategyEnum;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchReqBO;
import cn.iocoder.yudao.module.ai.service.knowledge.bo.AiKnowledgeSegmentSearchRespBO;
import cn.iocoder.yudao.module.ai.service.knowledge.splitter.MarkdownQaSplitter;
import cn.iocoder.yudao.module.ai.service.knowledge.splitter.SemanticTextSplitter;
import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import com.alibaba.cloud.ai.dashscope.rerank.DashScopeRerankOptions;
import com.alibaba.cloud.ai.model.RerankModel;
@ -43,7 +39,8 @@ import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_CONTENT_TOO_LONG;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
import static org.springframework.ai.vectorstore.SearchRequest.SIMILARITY_THRESHOLD_ACCEPT_ALL;
/**
@ -98,9 +95,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
AiKnowledgeDO knowledgeDO = knowledgeService.validateKnowledgeExists(documentDO.getKnowledgeId());
VectorStore vectorStore = getVectorStoreById(knowledgeDO);
// 2. 文档切片(使用自动检测策略)
List<Document> documentSegments = splitContentByStrategy(content, documentDO.getSegmentMaxTokens(),
AiDocumentSplitStrategyEnum.AUTO, documentDO.getUrl());
// 2. 文档切片
List<Document> documentSegments = splitContentByToken(content, documentDO.getSegmentMaxTokens());
// 3.1 存储切片
List<AiKnowledgeSegmentDO> segmentDOs = convertList(documentSegments, segment -> {
@ -141,19 +137,6 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
}
}
@Override
public void deleteKnowledgeSegment(Long id) {
// 1. 校验段落存在
AiKnowledgeSegmentDO segment = validateKnowledgeSegmentExists(id);
// 2. 删除向量
VectorStore vectorStore = getVectorStoreById(segment.getKnowledgeId());
deleteVectorStore(vectorStore, segment);
// 3. 删除段落记录
segmentMapper.deleteById(id);
}
@Override
public void deleteKnowledgeSegmentByDocumentId(Long documentId) {
// 1. 查询需要删除的段落
@ -312,10 +295,8 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
// 1. 读取 URL 内容
String content = knowledgeDocumentService.readUrl(url);
// 2.1 自动检测文档类型并选择策略
AiDocumentSplitStrategyEnum strategy = detectDocumentStrategy(content, url);
// 2.2 文档切片
List<Document> documentSegments = splitContentByStrategy(content, segmentMaxTokens, strategy, url);
// 2. 文档切片
List<Document> documentSegments = splitContentByToken(content, segmentMaxTokens);
// 3. 转换为段落对象
return convertList(documentSegments, segment -> {
@ -352,103 +333,11 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
return getVectorStoreById(knowledge);
}
/**
*
*
* @param content
* @param segmentMaxTokens Token
* @param strategy
* @param url URL
* @return
*/
@SuppressWarnings("EnhancedSwitchMigration")
private List<Document> splitContentByStrategy(String content, Integer segmentMaxTokens,
AiDocumentSplitStrategyEnum strategy, String url) {
// 自动检测策略
if (strategy == AiDocumentSplitStrategyEnum.AUTO) {
strategy = detectDocumentStrategy(content, url);
log.info("[splitContentByStrategy][自动检测到文档策略: {}]", strategy.getName());
}
// 根据策略切分
TextSplitter textSplitter;
switch (strategy) {
case MARKDOWN_QA:
textSplitter = new MarkdownQaSplitter(segmentMaxTokens);
break;
case SEMANTIC:
textSplitter = new SemanticTextSplitter(segmentMaxTokens);
break;
case PARAGRAPH:
textSplitter = new SemanticTextSplitter(segmentMaxTokens, 0); // 段落切分,无重叠
break;
case TOKEN:
default:
textSplitter = buildTokenTextSplitter(segmentMaxTokens);
break;
}
// 执行切分
private static List<Document> splitContentByToken(String content, Integer segmentMaxTokens) {
TextSplitter textSplitter = buildTokenTextSplitter(segmentMaxTokens);
return textSplitter.apply(Collections.singletonList(new Document(content)));
}
/**
*
*
* @param content
* @param url URL
* @return
*/
private AiDocumentSplitStrategyEnum detectDocumentStrategy(String content, String url) {
if (StrUtil.isEmpty(content)) {
return AiDocumentSplitStrategyEnum.TOKEN;
}
// 1. 检测 Markdown QA 格式
if (isMarkdownQaFormat(content, url)) {
return AiDocumentSplitStrategyEnum.MARKDOWN_QA;
}
// 2. 检测普通 Markdown 文档
if (isMarkdownDocument(url)) {
return AiDocumentSplitStrategyEnum.SEMANTIC;
}
// 3. 默认使用语义切分(比 Token 切分更智能)
return AiDocumentSplitStrategyEnum.SEMANTIC;
}
/**
* Markdown QA
* ##
*/
private boolean isMarkdownQaFormat(String content, String url) {
// 文件扩展名判断
if (StrUtil.isNotEmpty(url) && !url.toLowerCase().endsWith(".md")) {
return false;
}
// 统计二级标题数量
long h2Count = content.lines()
.filter(line -> line.trim().startsWith("## "))
.count();
// 要求一:至少包含 2 个二级标题才认为是 QA 格式
if (h2Count < 2) {
return false;
}
// 要求二检查标题占比QA 文档标题行数相对较多),如果二级标题占比超过 10%,认为是 QA 格式
long totalLines = content.lines().count();
double h2Ratio = (double) h2Count / totalLines;
return h2Ratio > 0.1;
}
/**
* Markdown
*/
private boolean isMarkdownDocument(String url) {
return StrUtil.endWithAnyIgnoreCase(url, ".md", ".markdown");
}
/**
* Token
*/
private static TextSplitter buildTokenTextSplitter(Integer segmentMaxTokens) {
return TokenTextSplitter.builder()
.withChunkSize(segmentMaxTokens)

View File

@ -1,342 +0,0 @@
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Markdown QA
*
* <p>
* <ul>
* <li>## </li>
* <li> QA Token </li>
* <li></li>
* <li> Token </li>
* </ul>
*
* @author runzhen
*/
@Slf4j
@SuppressWarnings("SizeReplaceableByIsEmpty")
public class MarkdownQaSplitter extends TextSplitter {
/**
* "## "
*/
private static final Pattern H2_PATTERN = Pattern.compile("^##\\s+(.+)$", Pattern.MULTILINE);
/**
*
*/
private static final String PARAGRAPH_SEPARATOR = "\n\n";
/**
*
*/
private static final Pattern SENTENCE_PATTERN = Pattern.compile("[。!?.!?]\\s*");
/**
* Token
*/
private final int chunkSize;
/**
* Token 1.3
*/
private final TokenEstimator tokenEstimator;
public MarkdownQaSplitter(int chunkSize) {
this.chunkSize = chunkSize;
this.tokenEstimator = new SimpleTokenEstimator();
}
@Override
protected List<String> splitText(String text) {
if (StrUtil.isEmpty(text)) {
return Collections.emptyList();
}
// 解析 QA 对
List<QaPair> qaPairs = parseQaPairs(text);
if (CollUtil.isEmpty(qaPairs)) {
// 如果没有识别到 QA 格式,按段落切分
return fallbackSplit(text);
}
// 处理每个 QA 对
List<String> result = new ArrayList<>();
for (QaPair qaPair : qaPairs) {
result.addAll(splitQaPair(qaPair));
}
return result;
}
/**
* Markdown QA
*
* @param content
* @return QA
*/
private List<QaPair> parseQaPairs(String content) {
// 找到所有二级标题位置
List<QaPair> qaPairs = new ArrayList<>();
List<Integer> headingPositions = new ArrayList<>();
List<String> questions = new ArrayList<>();
Matcher matcher = H2_PATTERN.matcher(content);
while (matcher.find()) {
headingPositions.add(matcher.start());
questions.add(matcher.group(1).trim());
}
if (CollUtil.isEmpty(headingPositions)) {
return qaPairs;
}
// 提取每个 QA 对
for (int i = 0; i < headingPositions.size(); i++) {
int start = headingPositions.get(i);
int end = (i + 1 < headingPositions.size())
? headingPositions.get(i + 1)
: content.length();
String qaText = content.substring(start, end).trim();
String question = questions.get(i);
// 提取答案部分(去掉问题标题)
String answer = qaText.substring(qaText.indexOf('\n') + 1).trim();
qaPairs.add(new QaPair(question, answer, qaText));
}
return qaPairs;
}
/**
* QA
*
* @param qaPair QA
* @return
*/
private List<String> splitQaPair(QaPair qaPair) {
// 如果整个 QA 对不超过限制,保持完整
List<String> chunks = new ArrayList<>();
String fullQa = qaPair.fullText;
int qaTokens = tokenEstimator.estimate(fullQa);
if (qaTokens <= chunkSize) {
chunks.add(fullQa);
return chunks;
}
// 长答案需要切分
log.debug("QA 对超过 Token 限制 ({} > {}),开始智能切分: {}", qaTokens, chunkSize, qaPair.question);
List<String> answerChunks = splitLongAnswer(qaPair.answer, qaPair.question);
for (String answerChunk : answerChunks) {
// 每个片段都包含完整问题
String chunkText = "## " + qaPair.question + "\n" + answerChunk;
chunks.add(chunkText);
}
return chunks;
}
/**
*
*
* @param answer
* @param question
* @return
*/
private List<String> splitLongAnswer(String answer, String question) {
List<String> chunks = new ArrayList<>();
// 预留问题的 Token 空间
String questionHeader = "## " + question + "\n";
int questionTokens = tokenEstimator.estimate(questionHeader);
int availableTokens = chunkSize - questionTokens - 10; // 预留 10 个 Token 的缓冲
// 先按段落切分
String[] paragraphs = answer.split(PARAGRAPH_SEPARATOR);
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String paragraph : paragraphs) {
if (StrUtil.isEmpty(paragraph)) {
continue;
}
int paragraphTokens = tokenEstimator.estimate(paragraph);
// 如果单个段落就超过限制,需要按句子切分
if (paragraphTokens > availableTokens) {
// 先保存当前块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 按句子切分长段落
chunks.addAll(splitLongParagraph(paragraph, availableTokens));
continue;
}
// 如果加上这个段落会超过限制
if (currentTokens + paragraphTokens > availableTokens && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
if (currentChunk.length() > 0) {
currentChunk.append("\n\n");
}
// 添加段落
currentChunk.append(paragraph);
currentTokens += paragraphTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return CollUtil.isEmpty(chunks) ? Collections.singletonList(answer) : chunks;
}
/**
*
*
* @param paragraph
* @param availableTokens Token
* @return
*/
private List<String> splitLongParagraph(String paragraph, int availableTokens) {
// 按句子切分
List<String> chunks = new ArrayList<>();
String[] sentences = SENTENCE_PATTERN.split(paragraph);
// 按句子累积切分
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String sentence : sentences) {
if (StrUtil.isEmpty(sentence)) {
continue;
}
int sentenceTokens = tokenEstimator.estimate(sentence);
// 如果单个句子就超过限制,强制切分
if (sentenceTokens > availableTokens) {
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
chunks.add(sentence.trim());
continue;
}
// 如果加上这个句子会超过限制
if (currentTokens + sentenceTokens > availableTokens && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 添加句子
currentChunk.append(sentence);
currentTokens += sentenceTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks.isEmpty() ? Collections.singletonList(paragraph) : chunks;
}
/**
* QA
*
* @param content
* @return
*/
private List<String> fallbackSplit(String content) {
// 按段落切分
List<String> chunks = new ArrayList<>();
String[] paragraphs = content.split(PARAGRAPH_SEPARATOR);
// 按段落累积切分
StringBuilder currentChunk = new StringBuilder();
int currentTokens = 0;
for (String paragraph : paragraphs) {
if (StrUtil.isEmpty(paragraph)) {
continue;
}
int paragraphTokens = tokenEstimator.estimate(paragraph);
// 如果加上这个段落会超过限制
if (currentTokens + paragraphTokens > chunkSize && currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
currentChunk = new StringBuilder();
currentTokens = 0;
}
// 添加段落
if (currentChunk.length() > 0) {
currentChunk.append("\n\n");
}
currentChunk.append(paragraph);
currentTokens += paragraphTokens;
}
// 添加最后一块
if (currentChunk.length() > 0) {
chunks.add(currentChunk.toString().trim());
}
return chunks.isEmpty() ? Collections.singletonList(content) : chunks;
}
/**
* QA
*/
@AllArgsConstructor
private static class QaPair {
String question;
String answer;
String fullText;
}
/**
* Token
*/
public interface TokenEstimator {
int estimate(String text);
}
/**
* Token
* 1 1 Token
* 1 1.3 Token
*/
private static class SimpleTokenEstimator implements TokenEstimator {
@Override
public int estimate(String text) {
if (StrUtil.isEmpty(text)) {
return 0;
}
int chineseChars = 0;
int englishWords = 0;
// 简单统计中英文
for (char c : text.toCharArray()) {
if (c >= 0x4E00 && c <= 0x9FA5) {
chineseChars++;
}
}
// 英文单词估算
String[] words = text.split("\\s+");
for (String word : words) {
if (word.matches(".*[a-zA-Z].*")) {
englishWords++;
}
}
return chineseChars + (int) (englishWords * 1.3);
}
}
}

View File

@ -1,301 +0,0 @@
package cn.iocoder.yudao.module.ai.service.knowledge.splitter;
import cn.hutool.core.util.StrUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.transformer.splitter.TextSplitter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
*
*
* <p>
* <ul>
* <li></li>
* <li></li>
* <li></li>
* <li></li>
* </ul>
*
* @author runzhen
*/
@Slf4j
public class SemanticTextSplitter extends TextSplitter {
/**
* Token
*/
private final int chunkSize;
/**
*
*/
private final int chunkOverlap;
/**
*
*/
private static final List<String> PARAGRAPH_SEPARATORS = Arrays.asList(
"\n\n\n", // 三个换行
"\n\n", // 双换行
"\n" // 单换行
);
/**
*
*/
private static final Pattern SENTENCE_END_PATTERN = Pattern.compile(
"[。!?.!?]+[\\s\"')】\\]]*"
);
/**
* Token
*/
private final MarkdownQaSplitter.TokenEstimator tokenEstimator;
public SemanticTextSplitter(int chunkSize, int chunkOverlap) {
this.chunkSize = chunkSize;
this.chunkOverlap = Math.min(chunkOverlap, chunkSize / 2); // 重叠不超过一半
this.tokenEstimator = new SimpleTokenEstimator();
}
public SemanticTextSplitter(int chunkSize) {
this(chunkSize, 50); // 默认重叠 50 个 Token
}
@Override
protected List<String> splitText(String text) {
if (StrUtil.isEmpty(text)) {
return Collections.emptyList();
}
return splitTextRecursive(text);
}
/**
*
*
* @param text
* @return
*/
private List<String> splitTextRecursive(String text) {
List<String> chunks = new ArrayList<>();
// 如果文本不超过限制,直接返回
int textTokens = tokenEstimator.estimate(text);
if (textTokens <= chunkSize) {
chunks.add(text.trim());
return chunks;
}
// 尝试按不同分隔符切分
List<String> splits = null;
String usedSeparator = null;
for (String separator : PARAGRAPH_SEPARATORS) {
if (text.contains(separator)) {
splits = Arrays.asList(text.split(Pattern.quote(separator)));
usedSeparator = separator;
break;
}
}
// 如果没有找到段落分隔符,按句子切分
if (splits == null || splits.size() == 1) {
splits = splitBySentences(text);
usedSeparator = ""; // 句子切分不需要分隔符
}
// 合并小片段
chunks = mergeSplits(splits, usedSeparator);
return chunks;
}
/**
*
*
* @param text
* @return
*/
private List<String> splitBySentences(String text) {
// 使用正则表达式匹配句子结束位置
List<String> sentences = new ArrayList<>();
int lastEnd = 0;
Matcher matcher = SENTENCE_END_PATTERN.matcher(text);
while (matcher.find()) {
String sentence = text.substring(lastEnd, matcher.end()).trim();
if (StrUtil.isNotEmpty(sentence)) {
sentences.add(sentence);
}
lastEnd = matcher.end();
}
// 添加剩余部分
if (lastEnd < text.length()) {
String remaining = text.substring(lastEnd).trim();
if (StrUtil.isNotEmpty(remaining)) {
sentences.add(remaining);
}
}
return sentences.isEmpty() ? Collections.singletonList(text) : sentences;
}
/**
*
*
* @param splits
* @param separator
* @return
*/
private List<String> mergeSplits(List<String> splits, String separator) {
List<String> chunks = new ArrayList<>();
List<String> currentChunks = new ArrayList<>();
int currentLength = 0;
for (String split : splits) {
if (StrUtil.isEmpty(split)) {
continue;
}
int splitTokens = tokenEstimator.estimate(split);
// 如果单个片段就超过限制,进一步递归切分
if (splitTokens > chunkSize) {
// 先保存当前累积的块
if (!currentChunks.isEmpty()) {
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
currentChunks.clear();
currentLength = 0;
}
// 递归切分大片段
if (!separator.isEmpty()) {
// 如果是段落分隔符,尝试按句子切分
chunks.addAll(splitTextRecursive(split));
} else {
// 如果已经是句子级别,强制按字符切分
chunks.addAll(forceSplitLongText(split));
}
continue;
}
// 计算加上分隔符的 Token 数
int separatorTokens = StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator);
// 如果加上这个片段会超过限制
if (!currentChunks.isEmpty() && currentLength + splitTokens + separatorTokens > chunkSize) {
// 保存当前块
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
// 处理重叠:保留最后几个片段
currentChunks = getOverlappingChunks(currentChunks, separator);
currentLength = estimateTokens(currentChunks, separator);
}
// 添加当前片段
currentChunks.add(split);
currentLength += splitTokens + separatorTokens;
}
// 添加最后一块
if (!currentChunks.isEmpty()) {
String chunkText = String.join(separator, currentChunks);
chunks.add(chunkText.trim());
}
return chunks;
}
/**
*
*
* @param chunks
* @param separator
* @return
*/
private List<String> getOverlappingChunks(List<String> chunks, String separator) {
if (chunkOverlap == 0 || chunks.isEmpty()) {
return new ArrayList<>();
}
// 从后往前取片段,直到达到重叠大小
List<String> overlapping = new ArrayList<>();
int tokens = 0;
for (int i = chunks.size() - 1; i >= 0; i--) {
String chunk = chunks.get(i);
int chunkTokens = tokenEstimator.estimate(chunk);
if (tokens + chunkTokens > chunkOverlap) {
break;
}
// 添加到重叠列表前端
overlapping.add(0, chunk);
tokens += chunkTokens + (StrUtil.isEmpty(separator) ? 0 : tokenEstimator.estimate(separator));
}
return overlapping;
}
/**
* Token
*
* @param chunks
* @param separator
* @return Token
*/
private int estimateTokens(List<String> chunks, String separator) {
int total = 0;
for (int i = 0; i < chunks.size(); i++) {
total += tokenEstimator.estimate(chunks.get(i));
if (i < chunks.size() - 1 && StrUtil.isNotEmpty(separator)) {
total += tokenEstimator.estimate(separator);
}
}
return total;
}
/**
*
*
* @param text
* @return
*/
private List<String> forceSplitLongText(String text) {
List<String> chunks = new ArrayList<>();
int charsPerChunk = (int) (chunkSize * 0.8); // 保守估计
for (int i = 0; i < text.length(); i += charsPerChunk) {
int end = Math.min(i + charsPerChunk, text.length());
String chunk = text.substring(i, end);
chunks.add(chunk.trim());
}
log.warn("文本过长,已强制按字符切分,可能影响语义完整性");
return chunks;
}
/**
* Token
*/
private static class SimpleTokenEstimator implements MarkdownQaSplitter.TokenEstimator {
@Override
public int estimate(String text) {
if (StrUtil.isEmpty(text)) {
return 0;
}
int chineseChars = 0;
int englishWords = 0;
// 简单统计中英文
for (char c : text.toCharArray()) {
if (c >= 0x4E00 && c <= 0x9FA5) {
chineseChars++;
}
}
// 英文单词估算
String[] words = text.split("\\s+");
for (String word : words) {
if (word.matches(".*[a-zA-Z].*")) {
englishWords++;
}
}
return chineseChars + (int) (englishWords * 1.3);
}
}
}

View File

@ -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;

View File

@ -1,9 +1,7 @@
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;
@ -35,28 +33,6 @@ 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);
}
@ -68,10 +44,9 @@ public class AiUtils {
// noinspection EnhancedSwitchMigration
switch (platform) {
case TONG_YI:
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();
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withEnableThinking(true) // TODO 芋艿:默认都开启 thinking 模式,后续可以让用户配置
.withToolCallbacks(toolCallbacks).withToolContext(toolContext).build();
case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case DEEP_SEEK:
@ -150,13 +125,10 @@ public class AiUtils {
|| response.getResult().getOutput() == null) {
return null;
}
AssistantMessage output = response.getResult().getOutput();
// DeepSeek 通过专属 AssistantMessage 暴露 reasoningContent
if (output instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) output).getReasoningContent();
if (response.getResult().getOutput() instanceof DeepSeekAssistantMessage) {
return ((DeepSeekAssistantMessage) (response.getResult().getOutput())).getReasoningContent();
}
// 通义千问等通过 metadata 透传 reasoningContent
return MapUtil.getStr(output.getMetadata(), "reasoningContent");
return null;
}
}

View File

@ -103,7 +103,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -123,8 +123,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# Spring Boot Admin Server 服务端的相关配置
context-path: /admin # 配置 Spring

View File

@ -98,7 +98,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -118,8 +118,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# 日志文件配置
logging:

View File

@ -34,15 +34,15 @@ public class TongYiChatModelTests {
private final DashScopeChatModel chatModel = DashScopeChatModel.builder()
.dashScopeApi(DashScopeApi.builder()
.apiKey("sk-cd9f39e99ea54840bd1888282325f55a") // https://bailian.console.aliyun.com/cn-beijing/?tab=model#/api-key 获取密钥
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0")
.build())
.defaultOptions(DashScopeChatOptions.builder()
.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)
// .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)
.build())
.build();
@ -85,9 +85,9 @@ public class TongYiChatModelTests {
List<Message> messages = new ArrayList<>();
messages.add(new UserMessage("详细分析下,如何设计一个电商系统?"));
DashScopeChatOptions options = DashScopeChatOptions.builder()
.model("qwen3.6-plus").multiModel(true)
.withModel("qwen3-235b-a22b-thinking-2507")
// .withModel("qwen-max-2025-01-25")
.enableThinking(true) // 必须设置,否则会报错
.withEnableThinking(true) // 必须设置,否则会报错
.build();
// 调用
@ -112,8 +112,8 @@ public class TongYiChatModelTests {
Document document01 = new Document("abc");
Document document02 = new Document("sapring");
RerankOptions options = DashScopeRerankOptions.builder()
.topN(1)
.model("gte-rerank-v2")
.withTopN(1)
.withModel("gte-rerank-v2")
.build();
RerankRequest rerankRequest = new RerankRequest(
query,

View File

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

View File

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

View File

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

View File

@ -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(), copy.getUserId())));
copy -> Stream.of(copy.getStartUserId(), Long.parseLong(copy.getCreator()))));
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, copy.getUserId(),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
MapUtils.findAndThen(userMap, Long.valueOf(copy.getCreator()),
user -> copyVO.setStartUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(userMap, copy.getStartUserId(),
user -> copyVO.setCreateUser(BeanUtils.toBean(user, UserSimpleBaseVO.class)));
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
processInstance -> {
copyVO.setSummary(FlowableUtils.getSummary(

View File

@ -100,7 +100,7 @@ public class BpmTaskController {
@GetMapping("manager-page")
@Operation(summary = "获取全部任务的分页", description = "用于【流程任务】菜单")
@PreAuthorize("@ss.hasPermission('bpm:task:manager-query')")
@PreAuthorize("@ss.hasPermission('bpm:task:mananger-query')")
public CommonResult<PageResult<BpmTaskRespVO>> getTaskManagerPage(@Valid BpmTaskPageReqVO pageVO) {
PageResult<HistoricTaskInstance> pageResult = taskService.getTaskPage(getLoginUserId(), pageVO);
if (CollUtil.isEmpty(pageResult.getList())) {

View File

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

View File

@ -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.AsyncTaskExecutor;
import org.springframework.core.task.AsyncListenableTaskExecutor;
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 AsyncTaskExecutor taskExecutor() {
public AsyncListenableTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(8);
executor.setMaxPoolSize(8);

View File

@ -14,7 +14,6 @@ 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;
@ -35,12 +34,6 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
public BpmParallelMultiInstanceBehavior(Activity activity,
AbstractBpmnActivityBehavior innerActivityBehavior) {
super(activity, innerActivityBehavior);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
// 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(activity.getId());
}
/**
@ -57,7 +50,14 @@ 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) {
@ -88,21 +88,4 @@ 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) {
// 保持自定义变量名,忽略解析器写入的单元素变量名
}
}

View File

@ -8,13 +8,11 @@ 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;
@ -32,12 +30,6 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
public BpmSequentialMultiInstanceBehavior(Activity activity, AbstractBpmnActivityBehavior innerActivityBehavior) {
super(activity, innerActivityBehavior);
// 关联 Pull Requesthttps://gitee.com/zhijiantianya/ruoyi-vue-pro/pulls/1483
// 在解析/构造阶段基于 activityId 初始化与 activity 绑定且不变的字段,避免在运行期修改 Behavior 实例状态
super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(activity.getId());
// 从 execution.getVariable() 读取当前所有任务处理的人的 key
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(activity.getId());
}
/**
@ -49,12 +41,19 @@ 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 = new LinkedHashSet<>(taskCandidateInvoker.calculateUsersByTask(execution));
assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过!
// 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
@ -89,24 +88,11 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
return;
}
// 参见 https://gitee.com/zhijiantianya/yudao-cloud/issues/IC239F
super.collectionExpression = null;
super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId());
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
super.executeOriginalBehavior(execution, multiInstanceRootExecution, loopCounter);
}
// ========== 屏蔽解析器覆写 ==========
@Override
public void setCollectionExpression(Expression collectionExpression) {
// 保持自定义变量名,忽略解析器写入的 collection 表达式
}
@Override
public void setCollectionVariable(String collectionVariable) {
// 保持自定义变量名,忽略解析器写入的 collection 变量名
}
@Override
public void setCollectionElementVariable(String collectionElementVariable) {
// 保持自定义变量名,忽略解析器写入的单元素变量名
}
}

View File

@ -125,7 +125,6 @@ public class BpmTaskCandidateInvoker {
});
}
@DataPermission(enable = false) // 忽略数据权限,避免因为过滤,导致找不到候选人
public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
// 如果是 CallActivity 子流程,不进行计算候选人

View File

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

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.delegate.DelegateExecution;
@ -41,9 +40,8 @@ public class BpmCopyTaskDelegate implements JavaDelegate {
}
// 2. 执行抄送
FlowElement currentFlowElement = execution.getCurrentFlowElement();
FlowableUtils.execute(execution.getTenantId(), () ->
processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(),
currentFlowElement.getId(), currentFlowElement.getName(), null));
processInstanceCopyService.createProcessInstanceCopy(userIds, null, execution.getProcessInstanceId(),
currentFlowElement.getId(), currentFlowElement.getName(), null);
}
}

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.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;
@ -248,7 +247,9 @@ public class FlowableUtils {
Map<String, BpmFormFieldVO> formFieldsMap = new HashMap<>();
processDefinitionInfo.getFormFields().forEach(formFieldStr -> {
BpmFormFieldVO formField = JsonUtils.parseObject(formFieldStr, BpmFormFieldVO.class);
parseFormField(formField, formFieldsMap);
if (formField != null) {
formFieldsMap.put(formField.getField(), formField);
}
});
// 情况一:当自定义了摘要
@ -272,26 +273,6 @@ 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 相关的工具方法 ==========
/**

View File

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

View File

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

View File

@ -116,7 +116,6 @@ 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() + "%");
@ -923,12 +922,16 @@ public class BpmTaskServiceImpl implements BpmTaskService {
List<UserTask> returnUserTaskList = BpmnModelUtils.iteratorFindChildUserTasks(targetElement, runTaskKeyList, null, null);
List<String> returnTaskKeyList = convertList(returnUserTaskList, UserTask::getId);
List<String> runExecutionIds = new ArrayList<>();
// 2. 给当前要被退回的 task 数组,设置退回意见
taskList.forEach(task -> {
// 需要排除掉,不需要设置退回意见的任务
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
return;
}
if (task.getExecutionId() != null) {
runExecutionIds.add(task.getExecutionId());
}
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
@ -952,7 +955,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 相关 issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/1018
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
.moveActivityIdsToSingleActivityId(returnTaskKeyList, reqVO.getTargetTaskDefinitionKey())
.moveActivityIdsToSingleActivityId(runExecutionIds, reqVO.getTargetTaskDefinitionKey())
// 设置需要预测的任务 ids 的流程变量,用于辅助预测
.processVariable(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_NEED_SIMULATE_TASK_IDS, needSimulateTaskDefinitionKeys)
// 设置流程变量local节点退回标记, 用于退回到节点,不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略,导致自动通过
@ -1464,7 +1467,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return;
}
// 自动去重,通过自动审批的方式
// 自动去重,通过自动审批的方式 TODO @芋艿 驳回的情况得考虑一下;@lesan驳回后又自动审批么
BpmProcessDefinitionInfoDO processDefinitionInfo = bpmProcessDefinitionService.getProcessDefinitionInfo(task.getProcessDefinitionId());
if (processDefinitionInfo == null) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程定义({})]", task.getId(), task.getProcessDefinitionId());

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -107,8 +107,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
--- #################### 芋道相关配置 ####################

View File

@ -58,7 +58,7 @@ spring:
primary: master
datasource:
master:
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=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-jdk8-new?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true&rewriteBatchedStatements=true
url: jdbc:mysql://127.0.0.1:3306/ruoyi-vue-pro?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 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -118,8 +118,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# 日志文件配置
logging:

View File

@ -4,7 +4,6 @@ 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;
@ -59,7 +58,7 @@ public class CrmProductRespVO implements VO {
private String description;
@Schema(description = "负责人的用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "31926")
@Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX,
@Trans(type = TransType.SIMPLE, targetClassName = "cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO",
fields = "nickname", ref = "ownerUserName")
private Long ownerUserId;
@Schema(description = "负责人的用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")
@ -67,7 +66,7 @@ public class CrmProductRespVO implements VO {
private String ownerUserName;
@Schema(description = "创建人编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX,
@Trans(type = TransType.SIMPLE, targetClassName = "cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO",
fields = "nickname", ref = "creatorName")
private String creator;
@Schema(description = "创建人名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道源码")

View File

@ -27,12 +27,10 @@ public interface CrmCustomerLimitConfigMapper extends BaseMapperX<CrmCustomerLim
Integer type, Long userId, Long deptId) {
LambdaQueryWrapperX<CrmCustomerLimitConfigDO> query = new LambdaQueryWrapperX<CrmCustomerLimitConfigDO>()
.eq(CrmCustomerLimitConfigDO::getType, type);
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);
}
});
query.apply("FIND_IN_SET({0}, user_ids) > 0", userId);
if (deptId != null) {
query.apply("FIND_IN_SET({0}, dept_ids) > 0", deptId);
}
return selectList(query);
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListen
import cn.iocoder.yudao.module.crm.enums.ApiConstants;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractService;
import cn.iocoder.yudao.module.crm.service.contract.CrmContractServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -19,6 +20,7 @@ import javax.annotation.Resource;
*/
@RestController
@Validated
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿fallbackFactory =
public class CrmContractStatusListener extends BpmProcessInstanceStatusEventListener {
private static final String PREFIX = ApiConstants.PREFIX + "/contract";

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEventListen
import cn.iocoder.yudao.module.crm.enums.ApiConstants;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableService;
import cn.iocoder.yudao.module.crm.service.receivable.CrmReceivableServiceImpl;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@ -19,6 +20,7 @@ import javax.annotation.Resource;
*/
@RestController
@Validated
@FeignClient(name = ApiConstants.NAME) // TODO 芋艿fallbackFactory =
public class CrmReceivableStatusListener extends BpmProcessInstanceStatusEventListener {
private static final String PREFIX = ApiConstants.PREFIX + "/receivable";

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -107,8 +107,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
--- #################### 芋道相关配置 ####################

View File

@ -96,7 +96,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -116,8 +116,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# 日志文件配置
logging:

View File

@ -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 payment_price_sum")
.select("SUM(payment_price) AS paymentPriceSum")
.eq("biz_id", bizId)
.eq("biz_type", bizType));
// 获得数量
if (CollUtil.isEmpty(result)) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "payment_price_sum", 0D));
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "paymentPriceSum", 0D));
}
}

View File

@ -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 receipt_price_sum")
.select("SUM(receipt_price) AS receiptPriceSum")
.eq("biz_id", bizId)
.eq("biz_type", bizType));
// 获得数量
if (CollUtil.isEmpty(result)) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "receipt_price_sum", 0D));
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "receiptPriceSum", 0D));
}
}

View File

@ -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 sum_count")
.select("order_item_id, SUM(count) AS sumCount")
.groupBy("order_item_id")
.in("in_id", inIds));
// 获得数量
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
}
}

View File

@ -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 sum_count")
.select("order_item_id, SUM(count) AS sumCount")
.groupBy("order_item_id")
.in("return_id", returnIds));
// 获得数量
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
}
}

View File

@ -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 sum_count")
.select("order_item_id, SUM(count) AS sumCount")
.groupBy("order_item_id")
.in("out_id", outIds));
// 获得数量
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
}
}

View File

@ -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 sum_count")
.select("order_item_id, SUM(count) AS sumCount")
.groupBy("order_item_id")
.in("return_id", returnIds));
// 获得数量
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sum_count"));
return convertMap(result, obj -> (Long) obj.get("order_item_id"), obj -> (BigDecimal) obj.get("sumCount"));
}
}

View File

@ -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 sum_count")
.select("SUM(count) AS sumCount")
.eq("product_id", productId));
// 获得数量
if (CollUtil.isEmpty(result)) {
return BigDecimal.ZERO;
}
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "sum_count", 0D));
return BigDecimal.valueOf(MapUtil.getDouble(result.get(0), "sumCount", 0D));
}
}

View File

@ -87,7 +87,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -107,8 +107,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
--- #################### 芋道相关配置 ####################

View File

@ -96,7 +96,7 @@ xxl:
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
--- #################### 监控相关配置 ####################
@ -116,8 +116,6 @@ spring:
client:
instance:
service-host-type: IP # 注册实例时,优先使用 IP [IP, HOST_NAME, CANONICAL_HOST_NAME]
username: admin
password: admin
# 日志文件配置
logging:

View File

@ -23,8 +23,6 @@ public enum CodegenFrontTypeEnum {
VUE3_VBEN5_EP_SCHEMA(50), // Vue3 VBEN5 + EP + schema 模版
VUE3_VBEN5_EP_GENERAL(51), // Vue3 VBEN5 + EP 标准模版
VUE3_ADMIN_UNIAPP_WOT(60), // Vue3 Admin + Uniapp + WOT 标准模版
;
/**

View File

@ -13,7 +13,6 @@ import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import javax.annotation.Resource;
import javax.annotation.security.PermitAll;
@ -45,8 +44,6 @@ public class FileController {
@PostMapping("/upload")
@Operation(summary = "上传文件", description = "模式一:后端上传文件")
@Parameter(name = "file", description = "文件附件", required = true,
schema = @Schema(type = "string", format = "binary"))
public CommonResult<String> uploadFile(@Valid FileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();
byte[] content = IoUtil.readBytes(file.getInputStream());
@ -72,14 +69,6 @@ public class FileController {
return success(fileService.createFile(createReqVO));
}
@GetMapping("/get")
@Operation(summary = "获得文件")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('infra:file:query')")
public CommonResult<FileRespVO> getFile(@RequestParam("id") Long id) {
return success(BeanUtils.toBean(fileService.getFile(id), FileRespVO.class));
}
@DeleteMapping("/delete")
@Operation(summary = "删除文件")
@Parameter(name = "id", description = "编号", required = true)

View File

@ -28,6 +28,6 @@ public class FileCreateReqVO {
private String type;
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
private Long size;
private Integer size;
}

View File

@ -28,7 +28,7 @@ public class FileRespVO {
private String type;
@Schema(description = "文件大小", example = "2048", requiredMode = Schema.RequiredMode.REQUIRED)
private Long size;
private Integer size;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;

View File

@ -23,14 +23,7 @@ public class FileUploadReqVO {
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return isDirectoryValid(directory);
}
public static boolean isDirectoryValid(String directory) {
// 1. 不能包含 .. 防止目录穿越
// 2. 不能以 / 或 \ 开头,防止上传到根目录
return !StrUtil.contains(directory, "..")
&& !StrUtil.startWithAny(directory, "/", "\\");
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
}

View File

@ -11,13 +11,11 @@ import cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog.Api
import cn.iocoder.yudao.module.infra.dal.dataobject.logger.ApiAccessLogDO;
import cn.iocoder.yudao.module.infra.service.logger.ApiAccessLogService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@ -38,15 +36,6 @@ public class ApiAccessLogController {
@Resource
private ApiAccessLogService apiAccessLogService;
@GetMapping("/get")
@Operation(summary = "获得 API 访问日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:api-access-log:query')")
public CommonResult<ApiAccessLogRespVO> getApiAccessLog(@RequestParam("id") Long id) {
ApiAccessLogDO apiAccessLog = apiAccessLogService.getApiAccessLog(id);
return success(BeanUtils.toBean(apiAccessLog, ApiAccessLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得API 访问日志分页")
@PreAuthorize("@ss.hasPermission('infra:api-access-log:query')")

View File

@ -50,15 +50,6 @@ public class ApiErrorLogController {
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得 API 错误日志")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('infra:api-error-log:query')")
public CommonResult<ApiErrorLogRespVO> getApiErrorLog(@RequestParam("id") Long id) {
ApiErrorLogDO apiErrorLog = apiErrorLogService.getApiErrorLog(id);
return success(BeanUtils.toBean(apiErrorLog, ApiErrorLogRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得 API 错误日志分页")
@PreAuthorize("@ss.hasPermission('infra:api-error-log:query')")
@ -72,7 +63,7 @@ public class ApiErrorLogController {
@PreAuthorize("@ss.hasPermission('infra:api-error-log:export')")
@ApiAccessLog(operateType = EXPORT)
public void exportApiErrorLogExcel(@Valid ApiErrorLogPageReqVO exportReqVO,
HttpServletResponse response) throws IOException {
HttpServletResponse response) throws IOException {
exportReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<ApiErrorLogDO> list = apiErrorLogService.getApiErrorLogPage(exportReqVO).getList();
// 导出 Excel

View File

@ -70,7 +70,7 @@ public class ApiAccessLogRespVO {
@Schema(description = "操作分类", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@ExcelProperty(value = "操作分类", converter = DictConvert.class)
@DictFormat(DictTypeConstants.OPERATE_TYPE)
@DictFormat(cn.iocoder.yudao.module.infra.enums.DictTypeConstants.OPERATE_TYPE)
private Integer operateType;
@Schema(description = "开始请求时间", requiredMode = Schema.RequiredMode.REQUIRED)

View File

@ -9,7 +9,6 @@ import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
@ -34,8 +33,6 @@ public class AppFileController {
@PostMapping("/upload")
@Operation(summary = "上传文件")
@Parameter(name = "file", description = "文件附件", required = true,
schema = @Schema(type = "string", format = "binary"))
@PermitAll
public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile();

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