Merge branch 'master-jdk17' of https://gitee.com/zhijiantianya/ruoyi-vue-pro into feature/bpm

# Conflicts:
#	yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java
This commit is contained in:
YunaiV 2024-10-03 11:28:22 +08:00
commit 9334edc9ea
320 changed files with 10940 additions and 5311 deletions

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="https://img.shields.io/badge/Spring%20Boot-3.3.1-blue.svg" alt="Downloads"> <img src="https://img.shields.io/badge/Spring%20Boot-3.3.4-blue.svg" alt="Downloads">
<img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads"> <img src="https://img.shields.io/badge/Vue-3.2-blue.svg" alt="Downloads">
<img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" /> <img src="https://img.shields.io/github/license/YunaiV/ruoyi-vue-pro" alt="Downloads" />
</p> </p>
@ -281,7 +281,7 @@
| 框架 | 说明 | 版本 | 学习指南 | | 框架 | 说明 | 版本 | 学习指南 |
|---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------| |---------------------------------------------------------------------------------------------|------------------|----------------|----------------------------------------------------------------|
| [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.3.1 | [文档](https://github.com/YunaiV/SpringBoot-Labs) | | [Spring Boot](https://spring.io/projects/spring-boot) | 应用开发框架 | 3.3.4 | [文档](https://github.com/YunaiV/SpringBoot-Labs) |
| [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | | | [MySQL](https://www.mysql.com/cn/) | 数据库服务器 | 5.7 / 8.0+ | |
| [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) | | [Druid](https://github.com/alibaba/druid) | JDBC 连接池、监控组件 | 1.2.23 | [文档](http://www.iocoder.cn/Spring-Boot/datasource-pool/?yudao) |
| [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) | | [MyBatis Plus](https://mp.baomidou.com/) | MyBatis 增强工具包 | 3.5.7 | [文档](http://www.iocoder.cn/Spring-Boot/MyBatis/?yudao) |
@ -297,7 +297,7 @@
| [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 9.0.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) | | [SkyWalking](https://skywalking.apache.org/) | 分布式应用追踪系统 | 9.0.0 | [文档](http://www.iocoder.cn/Spring-Boot/SkyWalking/?yudao) |
| [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 3.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) | | [Spring Boot Admin](https://github.com/codecentric/spring-boot-admin) | Spring Boot 监控平台 | 3.3.2 | [文档](http://www.iocoder.cn/Spring-Boot/Admin/?yudao) |
| [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.17.1 | | | [Jackson](https://github.com/FasterXML/jackson) | JSON 工具库 | 2.17.1 | |
| [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.5.5.Final | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) | | [MapStruct](https://mapstruct.org/) | Java Bean 转换 | 1.6.2 | [文档](http://www.iocoder.cn/Spring-Boot/MapStruct/?yudao) |
| [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.34 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) | | [Lombok](https://projectlombok.org/) | 消除冗长的 Java 代码 | 1.18.34 | [文档](http://www.iocoder.cn/Spring-Boot/Lombok/?yudao) |
| [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.10.1 | - | | [JUnit](https://junit.org/junit5/) | Java 单元测试框架 | 5.10.1 | - |
| [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 5.7.0 | - | | [Mockito](https://github.com/mockito/mockito) | Java Mock 框架 | 5.7.0 | - |

View File

@ -24,6 +24,7 @@
<!-- <module>yudao-module-crm</module>--> <!-- <module>yudao-module-crm</module>-->
<!-- <module>yudao-module-erp</module>--> <!-- <module>yudao-module-erp</module>-->
<!-- <module>yudao-module-ai</module>--> <!-- <module>yudao-module-ai</module>-->
<!-- <module>yudao-module-iot</module>-->
</modules> </modules>
<name>${project.artifactId}</name> <name>${project.artifactId}</name>
@ -31,7 +32,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>2.2.0-snapshot</revision> <revision>2.2.0-SNAPSHOT</revision>
<!-- Maven 相关 --> <!-- Maven 相关 -->
<java.version>17</java.version> <java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
@ -41,8 +42,8 @@
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 看看咋放到 bom 里 --> <!-- 看看咋放到 bom 里 -->
<lombok.version>1.18.34</lombok.version> <lombok.version>1.18.34</lombok.version>
<spring.boot.version>3.3.1</spring.boot.version> <spring.boot.version>3.3.4</spring.boot.version>
<mapstruct.version>1.5.5.Final</mapstruct.version> <mapstruct.version>1.6.2</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@ -113,7 +114,7 @@
<artifactId>flatten-maven-plugin</artifactId> <artifactId>flatten-maven-plugin</artifactId>
<version>${flatten-maven-plugin.version}</version> <version>${flatten-maven-plugin.version}</version>
<configuration> <configuration>
<flattenMode>resolveCiFriendliesOnly</flattenMode> <flattenMode>oss</flattenMode>
<updatePomFile>true</updatePomFile> <updatePomFile>true</updatePomFile>
</configuration> </configuration>
<executions> <executions>

179
sql/dm/quartz.sql Normal file
View File

@ -0,0 +1,179 @@
--
-- A hint submitted by a user: Oracle DB MUST be created as "shared" and the
-- job_queue_processes parameter must be greater than 2
-- However, these settings are pretty much standard after any
-- Oracle install, so most users need not worry about this.
--
-- Many other users (including the primary author of Quartz) have had success
-- running in dedicated mode, so only consider the above as a hint ;-)
--
drop table if exists qrtz_calendars;
drop table if exists qrtz_fired_triggers;
drop table if exists qrtz_blob_triggers;
drop table if exists qrtz_cron_triggers;
drop table if exists qrtz_simple_triggers;
drop table if exists qrtz_simprop_triggers;
drop table if exists qrtz_triggers;
drop table if exists qrtz_job_details;
drop table if exists qrtz_paused_trigger_grps;
drop table if exists qrtz_locks;
drop table if exists qrtz_scheduler_state;
CREATE TABLE qrtz_job_details
(
SCHED_NAME VARCHAR2(120) NOT NULL,
JOB_NAME VARCHAR2(200) NOT NULL,
JOB_GROUP VARCHAR2(200) NOT NULL,
DESCRIPTION VARCHAR2(250) NULL,
JOB_CLASS_NAME VARCHAR2(250) NOT NULL,
IS_DURABLE VARCHAR2(1) NOT NULL,
IS_NONCONCURRENT VARCHAR2(1) NOT NULL,
IS_UPDATE_DATA VARCHAR2(1) NOT NULL,
REQUESTS_RECOVERY VARCHAR2(1) NOT NULL,
JOB_DATA BLOB NULL,
CONSTRAINT QRTZ_JOB_DETAILS_PK PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE qrtz_triggers
(
SCHED_NAME VARCHAR2(120) NOT NULL,
TRIGGER_NAME VARCHAR2(200) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
JOB_NAME VARCHAR2(200) NOT NULL,
JOB_GROUP VARCHAR2(200) NOT NULL,
DESCRIPTION VARCHAR2(250) NULL,
NEXT_FIRE_TIME NUMBER(19) NULL,
PREV_FIRE_TIME NUMBER(19) NULL,
PRIORITY NUMBER(13) NULL,
TRIGGER_STATE VARCHAR2(16) NOT NULL,
TRIGGER_TYPE VARCHAR2(8) NOT NULL,
START_TIME NUMBER(19) NOT NULL,
END_TIME NUMBER(19) NULL,
CALENDAR_NAME VARCHAR2(200) NULL,
MISFIRE_INSTR NUMBER(2) NULL,
JOB_DATA BLOB NULL,
CONSTRAINT QRTZ_TRIGGERS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
CONSTRAINT QRTZ_TRIGGER_TO_JOBS_FK FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)
REFERENCES QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)
);
CREATE TABLE qrtz_simple_triggers
(
SCHED_NAME VARCHAR2(120) NOT NULL,
TRIGGER_NAME VARCHAR2(200) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
REPEAT_COUNT NUMBER(7) NOT NULL,
REPEAT_INTERVAL NUMBER(12) NOT NULL,
TIMES_TRIGGERED NUMBER(10) NOT NULL,
CONSTRAINT QRTZ_SIMPLE_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
CONSTRAINT QRTZ_SIMPLE_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_cron_triggers
(
SCHED_NAME VARCHAR2(120) NOT NULL,
TRIGGER_NAME VARCHAR2(200) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
CRON_EXPRESSION VARCHAR2(120) NOT NULL,
TIME_ZONE_ID VARCHAR2(80),
CONSTRAINT QRTZ_CRON_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
CONSTRAINT QRTZ_CRON_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_simprop_triggers
(
SCHED_NAME VARCHAR2(120) NOT NULL,
TRIGGER_NAME VARCHAR2(200) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
STR_PROP_1 VARCHAR2(512) NULL,
STR_PROP_2 VARCHAR2(512) NULL,
STR_PROP_3 VARCHAR2(512) NULL,
INT_PROP_1 NUMBER(10) NULL,
INT_PROP_2 NUMBER(10) NULL,
LONG_PROP_1 NUMBER(19) NULL,
LONG_PROP_2 NUMBER(19) NULL,
DEC_PROP_1 NUMERIC(13,4) NULL,
DEC_PROP_2 NUMERIC(13,4) NULL,
BOOL_PROP_1 VARCHAR2(1) NULL,
BOOL_PROP_2 VARCHAR2(1) NULL,
TIME_ZONE_ID VARCHAR2(80) NULL,
CONSTRAINT QRTZ_SIMPROP_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
CONSTRAINT QRTZ_SIMPROP_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_blob_triggers
(
SCHED_NAME VARCHAR2(120) NOT NULL,
TRIGGER_NAME VARCHAR2(200) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
BLOB_DATA BLOB NULL,
CONSTRAINT QRTZ_BLOB_TRIG_PK PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP),
CONSTRAINT QRTZ_BLOB_TRIG_TO_TRIG_FK FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
REFERENCES QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_calendars
(
SCHED_NAME VARCHAR2(120) NOT NULL,
CALENDAR_NAME VARCHAR2(200) NOT NULL,
CALENDAR BLOB NOT NULL,
CONSTRAINT QRTZ_CALENDARS_PK PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)
);
CREATE TABLE qrtz_paused_trigger_grps
(
SCHED_NAME VARCHAR2(120) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
CONSTRAINT QRTZ_PAUSED_TRIG_GRPS_PK PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)
);
CREATE TABLE qrtz_fired_triggers
(
SCHED_NAME VARCHAR2(120) NOT NULL,
ENTRY_ID VARCHAR2(140) NOT NULL,
TRIGGER_NAME VARCHAR2(200) NOT NULL,
TRIGGER_GROUP VARCHAR2(200) NOT NULL,
INSTANCE_NAME VARCHAR2(200) NOT NULL,
FIRED_TIME NUMBER(19) NOT NULL,
SCHED_TIME NUMBER(19) NOT NULL,
PRIORITY NUMBER(13) NOT NULL,
STATE VARCHAR2(16) NOT NULL,
JOB_NAME VARCHAR2(200) NULL,
JOB_GROUP VARCHAR2(200) NULL,
IS_NONCONCURRENT VARCHAR2(1) NULL,
REQUESTS_RECOVERY VARCHAR2(1) NULL,
CONSTRAINT QRTZ_FIRED_TRIGGER_PK PRIMARY KEY (SCHED_NAME,ENTRY_ID)
);
CREATE TABLE qrtz_scheduler_state
(
SCHED_NAME VARCHAR2(120) NOT NULL,
INSTANCE_NAME VARCHAR2(200) NOT NULL,
LAST_CHECKIN_TIME NUMBER(19) NOT NULL,
CHECKIN_INTERVAL NUMBER(13) NOT NULL,
CONSTRAINT QRTZ_SCHEDULER_STATE_PK PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)
);
CREATE TABLE qrtz_locks
(
SCHED_NAME VARCHAR2(120) NOT NULL,
LOCK_NAME VARCHAR2(40) NOT NULL,
CONSTRAINT QRTZ_LOCKS_PK PRIMARY KEY (SCHED_NAME,LOCK_NAME)
);
create index idx_qrtz_j_req_recovery on qrtz_job_details(SCHED_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_j_grp on qrtz_job_details(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_j on qrtz_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_t_jg on qrtz_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_t_c on qrtz_triggers(SCHED_NAME,CALENDAR_NAME);
create index idx_qrtz_t_g on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP);
create index idx_qrtz_t_state on qrtz_triggers(SCHED_NAME,TRIGGER_STATE);
create index idx_qrtz_t_n_state on qrtz_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_n_g_state on qrtz_triggers(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(SCHED_NAME,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st on qrtz_triggers(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME);
create index idx_qrtz_t_nft_st_misfire on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE);
create index idx_qrtz_t_nft_st_misfire_grp on qrtz_triggers(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE);
create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME);
create index idx_qrtz_ft_inst_job_req_rcvry on qrtz_fired_triggers(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY);
create index idx_qrtz_ft_j_g on qrtz_fired_triggers(SCHED_NAME,JOB_NAME,JOB_GROUP);
create index idx_qrtz_ft_jg on qrtz_fired_triggers(SCHED_NAME,JOB_GROUP);
create index idx_qrtz_ft_t_g on qrtz_fired_triggers(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP);
create index idx_qrtz_ft_tg on qrtz_fired_triggers(SCHED_NAME,TRIGGER_GROUP);

File diff suppressed because it is too large Load Diff

170
sql/kingbase/quartz.sql Normal file
View File

@ -0,0 +1,170 @@
set client_min_messages = WARNING;
DROP TABLE IF EXISTS qrtz_fired_triggers;
DROP TABLE IF EXISTS qrtz_paused_trigger_grps;
DROP TABLE IF EXISTS qrtz_scheduler_state;
DROP TABLE IF EXISTS qrtz_locks;
DROP TABLE IF EXISTS qrtz_simprop_triggers;
DROP TABLE IF EXISTS qrtz_simple_triggers;
DROP TABLE IF EXISTS qrtz_cron_triggers;
DROP TABLE IF EXISTS qrtz_blob_triggers;
DROP TABLE IF EXISTS qrtz_triggers;
DROP TABLE IF EXISTS qrtz_job_details;
DROP TABLE IF EXISTS qrtz_calendars;
set client_min_messages = NOTICE;
CREATE TABLE qrtz_job_details
(
sched_name TEXT NOT NULL,
job_name TEXT NOT NULL,
job_group TEXT NOT NULL,
description TEXT NULL,
job_class_name TEXT NOT NULL,
is_durable BOOL NOT NULL,
is_nonconcurrent BOOL NOT NULL,
is_update_data BOOL NOT NULL,
requests_recovery BOOL NOT NULL,
job_data BYTEA NULL,
PRIMARY KEY (sched_name,job_name,job_group)
);
CREATE TABLE qrtz_triggers
(
sched_name TEXT NOT NULL,
trigger_name TEXT NOT NULL,
trigger_group TEXT NOT NULL,
job_name TEXT NOT NULL,
job_group TEXT NOT NULL,
description TEXT NULL,
next_fire_time BIGINT NULL,
prev_fire_time BIGINT NULL,
priority INTEGER NULL,
trigger_state TEXT NOT NULL,
trigger_type TEXT NOT NULL,
start_time BIGINT NOT NULL,
end_time BIGINT NULL,
calendar_name TEXT NULL,
misfire_instr SMALLINT NULL,
job_data BYTEA NULL,
PRIMARY KEY (sched_name,trigger_name,trigger_group),
FOREIGN KEY (sched_name,job_name,job_group)
REFERENCES qrtz_job_details(sched_name,job_name,job_group)
);
CREATE TABLE qrtz_simple_triggers
(
sched_name TEXT NOT NULL,
trigger_name TEXT NOT NULL,
trigger_group TEXT NOT NULL,
repeat_count BIGINT NOT NULL,
repeat_interval BIGINT NOT NULL,
times_triggered BIGINT NOT NULL,
PRIMARY KEY (sched_name,trigger_name,trigger_group),
FOREIGN KEY (sched_name,trigger_name,trigger_group)
REFERENCES qrtz_triggers(sched_name,trigger_name,trigger_group) ON DELETE CASCADE
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
sched_name TEXT NOT NULL,
trigger_name TEXT NOT NULL ,
trigger_group TEXT NOT NULL ,
str_prop_1 TEXT NULL,
str_prop_2 TEXT NULL,
str_prop_3 TEXT NULL,
int_prop_1 INTEGER NULL,
int_prop_2 INTEGER NULL,
long_prop_1 BIGINT NULL,
long_prop_2 BIGINT NULL,
dec_prop_1 NUMERIC NULL,
dec_prop_2 NUMERIC NULL,
bool_prop_1 BOOL NULL,
bool_prop_2 BOOL NULL,
time_zone_id TEXT NULL,
PRIMARY KEY (sched_name,trigger_name,trigger_group),
FOREIGN KEY (sched_name,trigger_name,trigger_group)
REFERENCES qrtz_triggers(sched_name,trigger_name,trigger_group) ON DELETE CASCADE
);
CREATE TABLE qrtz_cron_triggers
(
sched_name TEXT NOT NULL,
trigger_name TEXT NOT NULL,
trigger_group TEXT NOT NULL,
cron_expression TEXT NOT NULL,
time_zone_id TEXT,
PRIMARY KEY (sched_name,trigger_name,trigger_group),
FOREIGN KEY (sched_name,trigger_name,trigger_group)
REFERENCES qrtz_triggers(sched_name,trigger_name,trigger_group) ON DELETE CASCADE
);
CREATE TABLE qrtz_blob_triggers
(
sched_name TEXT NOT NULL,
trigger_name TEXT NOT NULL,
trigger_group TEXT NOT NULL,
blob_data BYTEA NULL,
PRIMARY KEY (sched_name,trigger_name,trigger_group),
FOREIGN KEY (sched_name,trigger_name,trigger_group)
REFERENCES qrtz_triggers(sched_name,trigger_name,trigger_group) ON DELETE CASCADE
);
CREATE TABLE qrtz_calendars
(
sched_name TEXT NOT NULL,
calendar_name TEXT NOT NULL,
calendar BYTEA NOT NULL,
PRIMARY KEY (sched_name,calendar_name)
);
CREATE TABLE qrtz_paused_trigger_grps
(
sched_name TEXT NOT NULL,
trigger_group TEXT NOT NULL,
PRIMARY KEY (sched_name,trigger_group)
);
CREATE TABLE qrtz_fired_triggers
(
sched_name TEXT NOT NULL,
entry_id TEXT NOT NULL,
trigger_name TEXT NOT NULL,
trigger_group TEXT NOT NULL,
instance_name TEXT NOT NULL,
fired_time BIGINT NOT NULL,
sched_time BIGINT NOT NULL,
priority INTEGER NOT NULL,
state TEXT NOT NULL,
job_name TEXT NULL,
job_group TEXT NULL,
is_nonconcurrent BOOL NOT NULL,
requests_recovery BOOL NULL,
PRIMARY KEY (sched_name,entry_id)
);
CREATE TABLE qrtz_scheduler_state
(
sched_name TEXT NOT NULL,
instance_name TEXT NOT NULL,
last_checkin_time BIGINT NOT NULL,
checkin_interval BIGINT NOT NULL,
PRIMARY KEY (sched_name,instance_name)
);
CREATE TABLE qrtz_locks
(
sched_name TEXT NOT NULL,
lock_name TEXT NOT NULL,
PRIMARY KEY (sched_name,lock_name)
);
create index idx_qrtz_j_req_recovery on qrtz_job_details(requests_recovery);
create index idx_qrtz_t_next_fire_time on qrtz_triggers(next_fire_time);
create index idx_qrtz_t_state on qrtz_triggers(trigger_state);
create index idx_qrtz_t_nft_st on qrtz_triggers(next_fire_time,trigger_state);
create index idx_qrtz_ft_trig_name on qrtz_fired_triggers(trigger_name);
create index idx_qrtz_ft_trig_group on qrtz_fired_triggers(trigger_group);
create index idx_qrtz_ft_trig_nm_gp on qrtz_fired_triggers(sched_name,trigger_name,trigger_group);
create index idx_qrtz_ft_trig_inst_name on qrtz_fired_triggers(instance_name);
create index idx_qrtz_ft_job_name on qrtz_fired_triggers(job_name);
create index idx_qrtz_ft_job_group on qrtz_fired_triggers(job_group);
create index idx_qrtz_ft_job_req_recovery on qrtz_fired_triggers(requests_recovery);

253
sql/opengauss/quartz.sql Normal file
View File

@ -0,0 +1,253 @@
-- ----------------------------
-- qrtz_blob_triggers
-- ----------------------------
CREATE TABLE qrtz_blob_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
blob_data bytea NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- qrtz_calendars
-- ----------------------------
CREATE TABLE qrtz_calendars
(
sched_name varchar(120) NOT NULL,
calendar_name varchar(190) NOT NULL,
calendar bytea NOT NULL,
PRIMARY KEY (sched_name, calendar_name)
);
-- ----------------------------
-- qrtz_cron_triggers
-- ----------------------------
CREATE TABLE qrtz_cron_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
cron_expression varchar(120) NOT NULL,
time_zone_id varchar(80) NULL DEFAULT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_fired_triggers
-- ----------------------------
CREATE TABLE qrtz_fired_triggers
(
sched_name varchar(120) NOT NULL,
entry_id varchar(95) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
instance_name varchar(190) NOT NULL,
fired_time int8 NOT NULL,
sched_time int8 NOT NULL,
priority int4 NOT NULL,
state varchar(16) NOT NULL,
job_name varchar(190) NULL DEFAULT NULL,
job_group varchar(190) NULL DEFAULT NULL,
is_nonconcurrent varchar(1) NULL DEFAULT NULL,
requests_recovery varchar(1) NULL DEFAULT NULL,
PRIMARY KEY (sched_name, entry_id)
);
CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name);
CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery);
CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group);
CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group);
-- ----------------------------
-- qrtz_job_details
-- ----------------------------
CREATE TABLE qrtz_job_details
(
sched_name varchar(120) NOT NULL,
job_name varchar(190) NOT NULL,
job_group varchar(190) NOT NULL,
description varchar(250) NULL DEFAULT NULL,
job_class_name varchar(250) NOT NULL,
is_durable varchar(1) NOT NULL,
is_nonconcurrent varchar(1) NOT NULL,
is_update_data varchar(1) NOT NULL,
requests_recovery varchar(1) NOT NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, job_name, job_group)
);
CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery);
CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_locks
-- ----------------------------
CREATE TABLE qrtz_locks
(
sched_name varchar(120) NOT NULL,
lock_name varchar(40) NOT NULL,
PRIMARY KEY (sched_name, lock_name)
);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_paused_trigger_grps
-- ----------------------------
CREATE TABLE qrtz_paused_trigger_grps
(
sched_name varchar(120) NOT NULL,
trigger_group varchar(190) NOT NULL,
PRIMARY KEY (sched_name, trigger_group)
);
-- ----------------------------
-- qrtz_scheduler_state
-- ----------------------------
CREATE TABLE qrtz_scheduler_state
(
sched_name varchar(120) NOT NULL,
instance_name varchar(190) NOT NULL,
last_checkin_time int8 NOT NULL,
checkin_interval int8 NOT NULL,
PRIMARY KEY (sched_name, instance_name)
);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_simple_triggers
-- ----------------------------
CREATE TABLE qrtz_simple_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
repeat_count int8 NOT NULL,
repeat_interval int8 NOT NULL,
times_triggered int8 NOT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
-- ----------------------------
-- qrtz_simprop_triggers
-- ----------------------------
CREATE TABLE qrtz_simprop_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
str_prop_1 varchar(512) NULL DEFAULT NULL,
str_prop_2 varchar(512) NULL DEFAULT NULL,
str_prop_3 varchar(512) NULL DEFAULT NULL,
int_prop_1 int4 NULL DEFAULT NULL,
int_prop_2 int4 NULL DEFAULT NULL,
long_prop_1 int8 NULL DEFAULT NULL,
long_prop_2 int8 NULL DEFAULT NULL,
dec_prop_1 numeric(13, 4) NULL DEFAULT NULL,
dec_prop_2 numeric(13, 4) NULL DEFAULT NULL,
bool_prop_1 varchar(1) NULL DEFAULT NULL,
bool_prop_2 varchar(1) NULL DEFAULT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
-- ----------------------------
-- qrtz_triggers
-- ----------------------------
CREATE TABLE qrtz_triggers
(
sched_name varchar(120) NOT NULL,
trigger_name varchar(190) NOT NULL,
trigger_group varchar(190) NOT NULL,
job_name varchar(190) NOT NULL,
job_group varchar(190) NOT NULL,
description varchar(250) NULL DEFAULT NULL,
next_fire_time int8 NULL DEFAULT NULL,
prev_fire_time int8 NULL DEFAULT NULL,
priority int4 NULL DEFAULT NULL,
trigger_state varchar(16) NOT NULL,
trigger_type varchar(8) NOT NULL,
start_time int8 NOT NULL,
end_time int8 NULL DEFAULT NULL,
calendar_name varchar(190) NULL DEFAULT NULL,
misfire_instr int2 NULL DEFAULT NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
);
CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_t_jg ON qrtz_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name);
CREATE INDEX idx_qrtz_t_g ON qrtz_triggers (sched_name, trigger_group);
CREATE INDEX idx_qrtz_t_state ON qrtz_triggers (sched_name, trigger_state);
CREATE INDEX idx_qrtz_t_n_state ON qrtz_triggers (sched_name, trigger_name, trigger_group, trigger_state);
CREATE INDEX idx_qrtz_t_n_g_state ON qrtz_triggers (sched_name, trigger_group, trigger_state);
CREATE INDEX idx_qrtz_t_next_fire_time ON qrtz_triggers (sched_name, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time);
CREATE INDEX idx_qrtz_t_nft_st_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_state);
CREATE INDEX idx_qrtz_t_nft_st_misfire_grp ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_group,
trigger_state);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- FK: qrtz_blob_triggers
-- ----------------------------
ALTER TABLE qrtz_blob_triggers
ADD CONSTRAINT qrtz_blob_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
trigger_name,
trigger_group);
-- ----------------------------
-- FK: qrtz_cron_triggers
-- ----------------------------
ALTER TABLE qrtz_cron_triggers
ADD CONSTRAINT qrtz_cron_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- FK: qrtz_simple_triggers
-- ----------------------------
ALTER TABLE qrtz_simple_triggers
ADD CONSTRAINT qrtz_simple_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
trigger_name,
trigger_group);
-- ----------------------------
-- FK: qrtz_simprop_triggers
-- ----------------------------
ALTER TABLE qrtz_simprop_triggers
ADD CONSTRAINT qrtz_simprop_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- FK: qrtz_triggers
-- ----------------------------
ALTER TABLE qrtz_triggers
ADD CONSTRAINT qrtz_triggers_ibfk_1 FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group);

View File

@ -19,10 +19,14 @@ docker compose up -d mysql
#### 1.2 Oracle #### 1.2 Oracle
```Bash ```Bash
## x86 版本
docker compose up -d oracle docker compose up -d oracle
## MacBook Apple Silicon
docker compose up -d oracle_m1
``` ```
暂不支持 MacBook Apple Silicon因为 Oracle 官方没有提供 Apple Silicon 版本的 Docker 镜像。 > 注意:如果使用 MacBook Apple Silicon 版本,它的 ORACLE_SID 不是 XE而是 FREE
### 1.3 PostgreSQL ### 1.3 PostgreSQL
@ -38,16 +42,14 @@ docker compose up -d sqlserver
docker compose exec sqlserver bash /tmp/create_schema.sh docker compose exec sqlserver bash /tmp/create_schema.sh
``` ```
暂不支持 MacBook Apple Silicon因为 SQL Server 官方没有提供 Apple Silicon 版本的 Docker 镜像。
### 1.5 DM 达梦 ### 1.5 DM 达梦
① 下载达梦 Docker 镜像:https://download.dameng.com/eco/dm8/dm8_20230808_rev197096_x86_rh6_64_single.tar ① 下载达梦 Docker 镜像:<https://eco.dameng.com/download/> 地址点击“Docker 镜像”选项,进行下载。
② 加载镜像文件,在镜像 tar 文件所在目录运行: ② 加载镜像文件,在镜像 tar 文件所在目录运行:
```Bash ```Bash
docker load -i dm8_20230808_rev197096_x86_rh6_64_single.tar docker load -i dm8_20240715_x86_rh6_rq_single.tar
``` ```
③ 在项目 `sql/tools` 目录下运行: ③ 在项目 `sql/tools` 目录下运行:
@ -59,22 +61,17 @@ docker compose exec dm8 bash -c '/opt/dmdbms/bin/disql SYSDBA/SYSDBA001 \`/tmp/s
exit exit
``` ```
**注意**: `sql/dm/ruoyi-vue-pro-dm8.sql` 文件编码必须为 `GBK` 或者 `GBK` 超集,否则会出现中文乱码。
暂不支持 MacBook Apple Silicon因为 达梦 官方没有提供 Apple Silicon 版本的 Docker 镜像。
### 1.6 KingbaseES 人大金仓 ### 1.6 KingbaseES 人大金仓
① 下载人大金仓 Docker 镜像: ① 下载人大金仓 Docker 镜像:
> x86_64 版本: https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/x86_64/kdb_x86_64_V009R001C001B0025.tar * [x86_64 版本](https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/x86_64/kdb_x86_64_V009R001C001B0025.tar) 【Windows 选择这个】
* [aarch64 版本](https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/aarch64/kdb_aarch64_V009R001C001B0025.tar) 【MacBook Apple Silicon 选择这个】
> aarch64 版本https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/aarch64/kdb_aarch64_V009R001C001B0025.tar
② 加载镜像文件,在镜像 tar 文件所在目录运行: ② 加载镜像文件,在镜像 tar 文件所在目录运行:
```Bash ```Bash
docker load -i x86_64/kdb_x86_64_V009R001C001B0025.tar docker load -i kdb_x86_64_V009R001C001B0025.tar
``` ```
③ 在项目 `sql/tools` 目录下运行: ③ 在项目 `sql/tools` 目录下运行:
@ -106,9 +103,11 @@ docker volume rm ruoyi-vue-pro_postgres
## 2. MySQL 转换其它数据库 ## 2. MySQL 转换其它数据库
项目提供了 `sql/tools/convertor.py` 脚本,支持将 MySQL 转换为 Oracle、PostgreSQL、SQL Server、达梦、人大金仓、OpenGauss 等数据库的脚本。
### 2.1 实现原理 ### 2.1 实现原理
通过读取 MySQL 的 `sql/mysql/ruoyi-vue-pro.sql` 数据库文件,转换成 Oracle、PostgreSQL、SQL Server、达梦、人大金仓 等数据库的脚本。 通过读取 MySQL 的 `sql/mysql/ruoyi-vue-pro.sql` 数据库文件,转换成对应的数据库脚本。
### 2.2 使用方法 ### 2.2 使用方法
@ -119,7 +118,7 @@ pip install simple-ddl-parser
# pip3 install simple-ddl-parser # pip3 install simple-ddl-parser
``` ```
② 执行如下命令打印生成 postgres 的脚本内容,其他可选参数有:`oracle`、`sqlserver`、`dm8`、`kingbase` `sql/tools/` 目录下,执行如下命令打印生成 postgres 的脚本内容,其他可选参数有:`oracle`、`sqlserver`、`dm8`、`kingbase`、`opengauss`
```Bash ```Bash
python3 convertor.py postgres python3 convertor.py postgres

View File

@ -58,6 +58,20 @@ services:
- ./oracle/1_create_user.sql:/docker-entrypoint-initdb.d/1_create_user.sql:ro - ./oracle/1_create_user.sql:/docker-entrypoint-initdb.d/1_create_user.sql:ro
- ./oracle/2_create_schema.sh:/docker-entrypoint-initdb.d/2_create_schema.sh:ro - ./oracle/2_create_schema.sh:/docker-entrypoint-initdb.d/2_create_schema.sh:ro
oracle_m1:
image: einslib/oracle-19c:19.3.0-ee-slim-faststart
restart: unless-stopped
environment:
## 登录信息 SID: FREE user: system password: oracle
ORACLE_PASSWORD: oracle
ports:
- "1521:1521"
volumes:
- ../oracle/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
# 创建app用户: ROOT/123456@//localhost/XEPDB1
- ./oracle/1_create_user.sql:/docker-entrypoint-initdb.d/1_create_user.sql:ro
- ./oracle/2_create_schema.sh:/docker-entrypoint-initdb.d/2_create_schema.sh:ro
sqlserver: sqlserver:
image: mcr.microsoft.com/mssql/server:2017-latest image: mcr.microsoft.com/mssql/server:2017-latest
restart: unless-stopped restart: unless-stopped
@ -73,11 +87,9 @@ services:
# docker compose exec sqlserver bash /tmp/create_schema.sh # docker compose exec sqlserver bash /tmp/create_schema.sh
- ./sqlserver/create_schema.sh:/tmp/create_schema.sh:ro - ./sqlserver/create_schema.sh:/tmp/create_schema.sh:ro
dm8: dm8:
# wget https://download.dameng.com/eco/dm8/dm8_20230808_rev197096_x86_rh6_64_single.tar # docker load -i dm8_20240715_x86_rh6_rq_single.tar
# docker load -i dm8_20230808_rev197096_x86_rh6_64_single.tar image: dm8_single:dm8_20240715_rev232765_x86_rh6_64
image: dm8_single:dm8_20230808_rev197096_x86_rh6_64
restart: unless-stopped restart: unless-stopped
environment: environment:
PAGE_SIZE: 16 PAGE_SIZE: 16
@ -93,13 +105,10 @@ services:
volumes: volumes:
- dm8:/opt/dmdbms/data - dm8:/opt/dmdbms/data
- ../dm/ruoyi-vue-pro-dm8.sql:/tmp/schema.sql:ro - ../dm/ruoyi-vue-pro-dm8.sql:/tmp/schema.sql:ro
# docker compose exec dm8 bash -c '/opt/dmdbms/bin/disql SYSDBA/SYSDBA001 \`/tmp/schema.sql'
kingbase: kingbase:
# x86_64: https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/x86_64/kdb_x86_64_V009R001C001B0025.tar
# aarch64: https://kingbase.oss-cn-beijing.aliyuncs.com/KESV8R3/V009R001C001B0025-安装包-docker/aarch64/kdb_aarch64_V009R001C001B0025.tar
# docker load -i kdb_x86_64_V009R001C001B0025.tar
image: kingbase_v009r001c001b0025_single_x86:v1 image: kingbase_v009r001c001b0025_single_x86:v1
# image: kingbase_v009r001c001b0025_single_arm:v1
restart: unless-stopped restart: unless-stopped
environment: environment:
DB_USER: root DB_USER: root
@ -109,7 +118,6 @@ services:
volumes: volumes:
- kingbase:/home/kingbase/userdata - kingbase:/home/kingbase/userdata
- ../kingbase/ruoyi-vue-pro.sql:/tmp/schema.sql:ro - ../kingbase/ruoyi-vue-pro.sql:/tmp/schema.sql:ro
# docker compose exec kingbase bash -c 'ksql -U $DB_USER -d test -f /tmp/schema.sql'
opengauss: opengauss:
image: opengauss/opengauss:5.0.0 image: opengauss/opengauss:5.0.0

View File

@ -14,36 +14,35 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>2.2.0-snapshot</revision> <revision>2.2.0-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.boot.version>3.3.1</spring.boot.version> <spring.boot.version>3.3.4</spring.boot.version>
<!-- Web 相关 --> <!-- Web 相关 -->
<springdoc.version>2.3.0</springdoc.version> <springdoc.version>2.3.0</springdoc.version>
<knife4j.version>4.5.0</knife4j.version> <knife4j.version>4.5.0</knife4j.version>
<!-- DB 相关 --> <!-- DB 相关 -->
<druid.version>1.2.23</druid.version> <druid.version>1.2.23</druid.version>
<mybatis.version>3.5.16</mybatis.version> <mybatis.version>3.5.16</mybatis.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version> <mybatis-plus.version>3.5.8</mybatis-plus.version>
<mybatis-plus-generator.version>3.5.7</mybatis-plus-generator.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version> <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<mybatis-plus-join.version>1.4.13</mybatis-plus-join.version> <mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
<easy-trans.version>3.0.5</easy-trans.version> <easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>3.32.0</redisson.version> <redisson.version>3.36.0</redisson.version>
<dm8.jdbc.version>8.1.3.62</dm8.jdbc.version> <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
<kingbase.jdbc.version>8.6.0</kingbase.jdbc.version> <kingbase.jdbc.version>8.6.0</kingbase.jdbc.version>
<opengauss.jdbc.version>5.0.2</opengauss.jdbc.version> <opengauss.jdbc.version>5.1.0</opengauss.jdbc.version>
<!-- 消息队列 --> <!-- 消息队列 -->
<rocketmq-spring.version>2.3.0</rocketmq-spring.version> <rocketmq-spring.version>2.3.1</rocketmq-spring.version>
<!-- 服务保障相关 --> <!-- 服务保障相关 -->
<lock4j.version>2.2.7</lock4j.version> <lock4j.version>2.2.7</lock4j.version>
<!-- 监控相关 --> <!-- 监控相关 -->
<skywalking.version>9.0.0</skywalking.version> <skywalking.version>9.0.0</skywalking.version>
<spring-boot-admin.version>3.3.2</spring-boot-admin.version> <spring-boot-admin.version>3.3.3</spring-boot-admin.version>
<opentracing.version>0.33.0</opentracing.version> <opentracing.version>0.33.0</opentracing.version>
<!-- Test 测试相关 --> <!-- Test 测试相关 -->
<podam.version>8.0.2.RELEASE</podam.version> <podam.version>8.0.2.RELEASE</podam.version>
<jedis-mock.version>1.1.2</jedis-mock.version> <jedis-mock.version>1.1.4</jedis-mock.version>
<mockito-inline.version>5.2.0</mockito-inline.version> <mockito-inline.version>5.2.0</mockito-inline.version>
<!-- Bpm 工作流相关 --> <!-- Bpm 工作流相关 -->
<flowable.version>7.0.1</flowable.version> <flowable.version>7.0.1</flowable.version>
@ -51,33 +50,42 @@
<captcha-plus.version>2.0.3</captcha-plus.version> <captcha-plus.version>2.0.3</captcha-plus.version>
<jsoup.version>1.18.1</jsoup.version> <jsoup.version>1.18.1</jsoup.version>
<lombok.version>1.18.34</lombok.version> <lombok.version>1.18.34</lombok.version>
<mapstruct.version>1.5.5.Final</mapstruct.version> <mapstruct.version>1.6.2</mapstruct.version>
<hutool-5.version>5.8.29</hutool-5.version> <hutool-5.version>5.8.32</hutool-5.version>
<hutool-6.version>6.0.0-M14</hutool-6.version> <hutool-6.version>6.0.0-M16</hutool-6.version>
<easyexcel.verion>3.3.4</easyexcel.verion> <easyexcel.verion>4.0.3</easyexcel.verion>
<velocity.version>2.3</velocity.version> <velocity.version>2.4</velocity.version>
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<guava.version>33.2.1-jre</guava.version> <guava.version>33.3.1-jre</guava.version>
<transmittable-thread-local.version>2.14.5</transmittable-thread-local.version> <transmittable-thread-local.version>2.14.5</transmittable-thread-local.version>
<commons-net.version>3.11.1</commons-net.version> <commons-net.version>3.11.1</commons-net.version>
<jsch.version>0.1.55</jsch.version> <jsch.version>0.1.55</jsch.version>
<tika-core.version>2.9.2</tika-core.version> <tika-core.version>2.9.2</tika-core.version>
<ip2region.version>2.7.0</ip2region.version> <ip2region.version>2.7.0</ip2region.version>
<bizlog-sdk.version>3.0.6</bizlog-sdk.version> <bizlog-sdk.version>3.0.6</bizlog-sdk.version>
<netty.version>4.1.113.Final</netty.version>
<mqtt.version>1.2.5</mqtt.version>
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<okio.version>3.5.0</okio.version> <okio.version>3.5.0</okio.version>
<okhttp3.version>4.11.0</okhttp3.version> <okhttp3.version>4.11.0</okhttp3.version>
<commons-io.version>2.15.1</commons-io.version> <commons-io.version>2.17.0</commons-io.version>
<commons-compress.version>1.27.1</commons-compress.version>
<minio.version>8.5.7</minio.version> <minio.version>8.5.7</minio.version>
<justauth.version>2.0.5</justauth.version> <justauth.version>2.0.5</justauth.version>
<jimureport.version>1.7.8</jimureport.version> <jimureport.version>1.8.1</jimureport.version>
<xercesImpl.version>2.12.2</xercesImpl.version> <weixin-java.version>4.6.5.B</weixin-java.version>
<weixin-java.version>4.6.0</weixin-java.version>
</properties> </properties>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-bom</artifactId>
<version>${netty.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId> <artifactId>spring-boot-dependencies</artifactId>
@ -177,7 +185,7 @@
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 --> <artifactId>mybatis-plus-generator</artifactId> <!-- 代码生成器,使用它解析表结构 -->
<version>${mybatis-plus-generator.version}</version> <version>${mybatis-plus.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.baomidou</groupId> <groupId>com.baomidou</groupId>
@ -467,6 +475,11 @@
<artifactId>commons-io</artifactId> <artifactId>commons-io</artifactId>
<version>${commons-io.version}</version> <version>${commons-io.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency>
<dependency> <dependency>
<groupId>org.apache.tika</groupId> <groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 --> <artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
@ -584,12 +597,13 @@
</exclusion> </exclusion>
</exclusions> </exclusions>
</dependency> </dependency>
<dependency>
<groupId>xerces</groupId>
<artifactId>xercesImpl</artifactId>
<version>${xercesImpl.version}</version>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>${mqtt.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@ -601,7 +615,7 @@
<artifactId>flatten-maven-plugin</artifactId> <artifactId>flatten-maven-plugin</artifactId>
<version>${flatten-maven-plugin.version}</version> <version>${flatten-maven-plugin.version}</version>
<configuration> <configuration>
<flattenMode>resolveCiFriendliesOnly</flattenMode> <flattenMode>bom</flattenMode>
<updatePomFile>true</updatePomFile> <updatePomFile>true</updatePomFile>
</configuration> </configuration>
<executions> <executions>

View File

@ -290,7 +290,15 @@ public class CollectionUtils {
return valueFunc.apply(t); return valueFunc.apply(t);
} }
public static <T, V extends Comparable<? super V>> V getSumValue(List<T> from, Function<T, V> valueFunc, public static <T, V extends Comparable<? super V>> T getMinObject(List<T> from, Function<T, V> valueFunc) {
if (CollUtil.isEmpty(from)) {
return null;
}
assert from.size() > 0; // 断言避免告警
return from.stream().min(Comparator.comparing(valueFunc)).get();
}
public static <T, V extends Comparable<? super V>> V getSumValue(Collection<T> from, Function<T, V> valueFunc,
BinaryOperator<V> accumulator) { BinaryOperator<V> accumulator) {
return getSumValue(from, valueFunc, accumulator, null); return getSumValue(from, valueFunc, accumulator, null);
} }

View File

@ -21,6 +21,7 @@ import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
@ -141,7 +142,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
return deptExpression; return deptExpression;
} }
// 目前如果有指定部门 + 可查看自己采用 OR 条件WHERE (dept_id IN ? OR user_id = ?) // 目前如果有指定部门 + 可查看自己采用 OR 条件WHERE (dept_id IN ? OR user_id = ?)
return new Parenthesis(new OrExpression(deptExpression, userExpression)); return new ParenthesedExpressionList(new OrExpression(deptExpression, userExpression));
} }
private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) { private Expression buildDeptExpression(String tableName, Alias tableAlias, Set<Long> deptIds) {
@ -157,7 +158,7 @@ public class DeptDataPermissionRule implements DataPermissionRule {
// 拼接条件 // 拼接条件
return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName), return new InExpression(MyBatisUtils.buildColumn(tableName, tableAlias, columnName),
// Parenthesis 的目的是提供 (1,2,3) () 左右括号 // Parenthesis 的目的是提供 (1,2,3) () 左右括号
new Parenthesis(new ExpressionList<LongValue>(CollectionUtils.convertList(deptIds, LongValue::new)))); new ParenthesedExpressionList(new ExpressionList<LongValue>(CollectionUtils.convertList(deptIds, LongValue::new))));
} }
private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) { private Expression buildUserExpression(String tableName, Alias tableAlias, Boolean self, Long userId) {

View File

@ -8,10 +8,10 @@ import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionIntercepto
import net.sf.jsqlparser.expression.Alias; import net.sf.jsqlparser.expression.Alias;
import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.LongValue;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.EqualsTo;
import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.ExpressionList;
import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.InExpression;
import net.sf.jsqlparser.expression.operators.relational.ParenthesedExpressionList;
import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Column;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -81,7 +81,7 @@ public class DataPermissionRuleHandlerTest extends BaseMockitoUnitTest {
Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN); Column column = MyBatisUtils.buildColumn(tableName, tableAlias, COLUMN);
ExpressionList<LongValue> values = new ExpressionList<>(new LongValue(10L), ExpressionList<LongValue> values = new ExpressionList<>(new LongValue(10L),
new LongValue(20L)); new LongValue(20L));
return new InExpression(column, new Parenthesis((values))); return new InExpression(column, new ParenthesedExpressionList((values)));
} }
}; };

View File

@ -58,6 +58,11 @@
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId> <!-- 解决 https://github.com/alibaba/easyexcel/issues/3954 问题 -->
</dependency>
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId> <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.framework.mybatis.config;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.IdType;
@ -42,9 +41,6 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
// TODO 芋艿暂时没有找到特别合适的地方先放在这里 // TODO 芋艿暂时没有找到特别合适的地方先放在这里
setJobStoreDriverIfPresent(environment, dbType); setJobStoreDriverIfPresent(environment, dbType);
// 初始化 SQL 静态变量
SqlConstants.init(dbType);
// 如果非 NONE则不进行处理 // 如果非 NONE则不进行处理
IdType idType = getIdType(environment); IdType idType = getIdType(environment);
if (idType != IdType.NONE) { if (idType != IdType.NONE) {
@ -55,7 +51,7 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
setIdType(environment, IdType.INPUT); setIdType(environment, IdType.INPUT);
return; return;
} }
// 情况二自增 ID适合 MySQL 等直接自增的数据库 // 情况二自增 ID适合 MySQLDM 达梦等直接自增的数据库
setIdType(environment, IdType.AUTO); setIdType(environment, IdType.AUTO);
} }
@ -86,6 +82,10 @@ public class IdTypeEnvironmentPostProcessor implements EnvironmentPostProcessor
case SQL_SERVER2005: case SQL_SERVER2005:
driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate"; driverClass = "org.quartz.impl.jdbcjobstore.MSSQLDelegate";
break; break;
case DM:
case KINGBASE_ES:
driverClass = "org.quartz.impl.jdbcjobstore.StdJDBCDelegate";
break;
} }
// 设置 driverClass 变量 // 设置 driverClass 变量
if (StrUtil.isNotEmpty(driverClass)) { if (StrUtil.isNotEmpty(driverClass)) {

View File

@ -18,6 +18,13 @@ import java.util.stream.Collectors;
@AllArgsConstructor @AllArgsConstructor
public enum DbTypeEnum { public enum DbTypeEnum {
/**
* H2
*
* 注意H2 不支持 find_in_set 函数
*/
H2(DbType.H2, "H2", ""),
/** /**
* MySQL * MySQL
*/ */
@ -39,6 +46,10 @@ public enum DbTypeEnum {
* SQL Server * SQL Server
*/ */
SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"), SQL_SERVER(DbType.SQL_SERVER, "Microsoft SQL Server", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/**
* SQL Server 2005
*/
SQL_SERVER2005(DbType.SQL_SERVER2005, "Microsoft SQL Server 2005", "CHARINDEX(',' + #{value} + ',', ',' + #{column} + ',') <> 0"),
/** /**
* 达梦 * 达梦

View File

@ -1,21 +0,0 @@
package cn.iocoder.yudao.framework.mybatis.core.enums;
import com.baomidou.mybatisplus.annotation.DbType;
/**
* SQL相关常量类
*
* @author 芋道源码
*/
public class SqlConstants {
/**
* 数据库的类型
*/
public static DbType DB_TYPE;
public static void init(DbType dbType) {
DB_TYPE = dbType;
}
}

View File

@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.pojo.SortablePageParam; import cn.iocoder.yudao.framework.common.pojo.SortablePageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.common.pojo.SortingField;
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants; import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils; import cn.iocoder.yudao.framework.mybatis.core.util.MyBatisUtils;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.conditions.Wrapper;
@ -22,7 +22,6 @@ import org.apache.ibatis.annotations.Param;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects;
/** /**
* MyBatis Plus BaseMapper 的基础上拓展提供更多的能力 * MyBatis Plus BaseMapper 的基础上拓展提供更多的能力
@ -135,11 +134,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return selectList(new LambdaQueryWrapper<T>().in(field, values)); return selectList(new LambdaQueryWrapper<T>().in(field, values));
} }
@Deprecated
default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) {
return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value));
}
default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) { default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2)); return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
} }
@ -151,7 +145,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
*/ */
default Boolean insertBatch(Collection<T> entities) { default Boolean insertBatch(Collection<T> entities) {
// 特殊SQL Server 批量插入后获取 id 会报错因此通过循环处理 // 特殊SQL Server 批量插入后获取 id 会报错因此通过循环处理
if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) { DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert); entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities); return CollUtil.isNotEmpty(entities);
} }
@ -166,7 +161,8 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
*/ */
default Boolean insertBatch(Collection<T> entities, int size) { default Boolean insertBatch(Collection<T> entities, int size) {
// 特殊SQL Server 批量插入后获取 id 会报错因此通过循环处理 // 特殊SQL Server 批量插入后获取 id 会报错因此通过循环处理
if (Objects.equals(SqlConstants.DB_TYPE, DbType.SQL_SERVER)) { DbType dbType = JdbcUtils.getDbType();
if (JdbcUtils.isSQLServer(dbType)) {
entities.forEach(this::insert); entities.forEach(this::insert);
return CollUtil.isNotEmpty(entities); return CollUtil.isNotEmpty(entities);
} }
@ -185,10 +181,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
return Db.updateBatchById(entities, size); return Db.updateBatchById(entities, size);
} }
default Boolean insertOrUpdateBatch(Collection<T> collection) {
return Db.saveOrUpdateBatch(collection);
}
default int delete(String field, String value) { default int delete(String field, String value) {
return delete(new QueryWrapper<T>().eq(field, value)); return delete(new QueryWrapper<T>().eq(field, value));
} }

View File

@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.query; package cn.iocoder.yudao.framework.mybatis.core.query;
import cn.hutool.core.lang.Assert; import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.framework.mybatis.core.enums.SqlConstants; import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.ArrayUtils; import com.baomidou.mybatisplus.core.toolkit.ArrayUtils;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@ -147,8 +147,8 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
* @return this * @return this
*/ */
public QueryWrapperX<T> limitN(int n) { public QueryWrapperX<T> limitN(int n) {
Assert.notNull(SqlConstants.DB_TYPE, "获取不到数据库的类型"); DbType dbType = JdbcUtils.getDbType();
switch (SqlConstants.DB_TYPE) { switch (dbType) {
case ORACLE: case ORACLE:
case ORACLE_12C: case ORACLE_12C:
super.le("ROWNUM", n); super.le("ROWNUM", n);
@ -157,7 +157,7 @@ public class QueryWrapperX<T> extends QueryWrapper<T> {
case SQL_SERVER2005: case SQL_SERVER2005:
super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条所以只好使用 * 查询剩余字段 super.select("TOP " + n + " *"); // 由于 SQL Server 是通过 SELECT TOP 1 实现限制一条所以只好使用 * 查询剩余字段
break; break;
default: default: // MySQLPostgreSQLDM 达梦KingbaseES 大金都是采用 LIMIT 实现
super.last("LIMIT " + n); super.last("LIMIT " + n);
} }
return this; return this;

View File

@ -1,9 +1,11 @@
package cn.iocoder.yudao.framework.mybatis.core.util; package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
import com.baomidou.dynamic.datasource.DynamicRoutingDataSource; import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.annotation.DbType;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import javax.sql.DataSource; import javax.sql.DataSource;
import java.sql.Connection; import java.sql.Connection;
@ -49,8 +51,13 @@ public class JdbcUtils {
* @return DB 类型 * @return DB 类型
*/ */
public static DbType getDbType() { public static DbType getDbType() {
DataSource dataSource;
try {
DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class); DynamicRoutingDataSource dynamicRoutingDataSource = SpringUtils.getBean(DynamicRoutingDataSource.class);
DataSource dataSource = dynamicRoutingDataSource.determineDataSource(); dataSource = dynamicRoutingDataSource.determineDataSource();
} catch (NoSuchBeanDefinitionException e) {
dataSource = SpringUtils.getBean(DataSource.class);
}
try (Connection conn = dataSource.getConnection()) { try (Connection conn = dataSource.getConnection()) {
return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName()); return DbTypeEnum.find(conn.getMetaData().getDatabaseProductName());
} catch (SQLException e) { } catch (SQLException e) {
@ -58,4 +65,25 @@ public class JdbcUtils {
} }
} }
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param url JDBC 连接
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(String url) {
DbType dbType = getDbType(url);
return isSQLServer(dbType);
}
/**
* 判断 JDBC 连接是否为 SQLServer 数据库
*
* @param dbType DB 类型
* @return 是否为 SQLServer 数据库
*/
public static boolean isSQLServer(DbType dbType) {
return ObjectUtils.equalsAny(dbType, DbType.SQL_SERVER, DbType.SQL_SERVER2005);
}
} }

View File

@ -96,7 +96,6 @@ public class MyBatisUtils {
* @return sql * @return sql
*/ */
public static String findInSet(String column, Object value) { public static String findInSet(String column, Object value) {
// 这里不用SqlConstants.DB_TYPE因为它是使用 primary 数据源的 url 推断出来的类型
DbType dbType = JdbcUtils.getDbType(); DbType dbType = JdbcUtils.getDbType();
return DbTypeEnum.getFindInSetTemplate(dbType) return DbTypeEnum.getFindInSetTemplate(dbType)
.replace("#{column}", column) .replace("#{column}", column)

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.framework.security.config; package cn.iocoder.yudao.framework.security.config;
import cn.iocoder.yudao.framework.security.core.aop.PreAuthenticatedAspect;
import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy; import cn.iocoder.yudao.framework.security.core.context.TransmittableThreadLocalSecurityContextHolderStrategy;
import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter; import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter;
import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
@ -38,14 +37,6 @@ public class YudaoSecurityAutoConfiguration {
@Resource @Resource
private SecurityProperties securityProperties; private SecurityProperties securityProperties;
/**
* 处理用户未登录拦截的切面的 Bean
*/
@Bean
public PreAuthenticatedAspect preAuthenticatedAspect() {
return new PreAuthenticatedAspect();
}
/** /**
* 认证失败处理类 Bean * 认证失败处理类 Bean
*/ */

View File

@ -129,17 +129,15 @@ public class YudaoWebSecurityConfigurerAdapter {
.authorizeHttpRequests(c -> c .authorizeHttpRequests(c -> c
// 1.1 静态资源可匿名访问 // 1.1 静态资源可匿名访问
.requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll() .requestMatchers(HttpMethod.GET, "/*.html", "/*.html", "/*.css", "/*.js").permitAll()
// 1.1 设置 @PermitAll 无需认证 // 1.2 设置 @PermitAll 无需认证
.requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.GET, permitAllUrls.get(HttpMethod.GET).toArray(new String[0])).permitAll()
.requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.POST, permitAllUrls.get(HttpMethod.POST).toArray(new String[0])).permitAll()
.requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.PUT, permitAllUrls.get(HttpMethod.PUT).toArray(new String[0])).permitAll()
.requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.DELETE, permitAllUrls.get(HttpMethod.DELETE).toArray(new String[0])).permitAll()
.requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.HEAD, permitAllUrls.get(HttpMethod.HEAD).toArray(new String[0])).permitAll()
.requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll() .requestMatchers(HttpMethod.PATCH, permitAllUrls.get(HttpMethod.PATCH).toArray(new String[0])).permitAll()
// 1.2 基于 yudao.security.permit-all-urls 无需认证 // 1.3 基于 yudao.security.permit-all-urls 无需认证
.requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll() .requestMatchers(securityProperties.getPermitAllUrls().toArray(new String[0])).permitAll()
// 1.3 设置 App API 无需认证
.requestMatchers(buildAppApi("/**")).permitAll()
) )
// 每个项目的自定义规则 // 每个项目的自定义规则
.authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c))) .authorizeHttpRequests(c -> authorizeRequestsCustomizers.forEach(customizer -> customizer.customize(c)))

View File

@ -1,17 +0,0 @@
package cn.iocoder.yudao.framework.security.core.annotations;
import java.lang.annotation.*;
/**
* 声明用户需要登录
*
* 为什么不使用 {@link org.springframework.security.access.prepost.PreAuthorize} 注解原因是不通过时抛出的是认证不通过而不是未登录
*
* @author 芋道源码
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface PreAuthenticated {
}

View File

@ -1,25 +0,0 @@
package cn.iocoder.yudao.framework.security.core.aop;
import cn.iocoder.yudao.framework.security.core.annotations.PreAuthenticated;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import static cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants.UNAUTHORIZED;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@Aspect
@Slf4j
public class PreAuthenticatedAspect {
@Around("@annotation(preAuthenticated)")
public Object around(ProceedingJoinPoint joinPoint, PreAuthenticated preAuthenticated) throws Throwable {
if (SecurityFrameworkUtils.getLoginUser() == null) {
throw exception(UNAUTHORIZED);
}
return joinPoint.proceed();
}
}

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.test.core.ut; package cn.iocoder.yudao.framework.test.core.ut;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
@ -44,6 +45,9 @@ public class BaseDbAndRedisUnitTest {
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedisAutoConfiguration.class, // Spring Redis 自动配置类 RedisAutoConfiguration.class, // Spring Redis 自动配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类 RedissonAutoConfiguration.class, // Redisson 自动配置类
// 其它配置类
SpringUtil.class
}) })
public static class Application { public static class Application {
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.test.core.ut; package cn.iocoder.yudao.framework.test.core.ut;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration; import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration; import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration; import cn.iocoder.yudao.framework.test.config.SqlInitializationTestConfiguration;
@ -36,6 +37,9 @@ public class BaseDbUnitTest {
YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类 YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类 MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类 MybatisPlusJoinAutoConfiguration.class, // MyBatis 的Join配置类
// 其它配置类
SpringUtil.class
}) })
public static class Application { public static class Application {
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.test.core.ut; package cn.iocoder.yudao.framework.test.core.ut;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration; import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration; import cn.iocoder.yudao.framework.test.config.RedisTestConfiguration;
import org.redisson.spring.starter.RedissonAutoConfiguration; import org.redisson.spring.starter.RedissonAutoConfiguration;
@ -25,6 +26,9 @@ public class BaseRedisUnitTest {
RedisAutoConfiguration.class, // Spring Redis 自动配置类 RedisAutoConfiguration.class, // Spring Redis 自动配置类
YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类 YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
RedissonAutoConfiguration.class, // Redisson 自动配置类 RedissonAutoConfiguration.class, // Redisson 自动配置类
// 其它配置类
SpringUtil.class
}) })
public static class Application { public static class Application {
} }

View File

@ -134,6 +134,11 @@ public class RandomUtils {
@SafeVarargs @SafeVarargs
public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) { public static <T> List<T> randomPojoList(Class<T> clazz, Consumer<T>... consumers) {
int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH); int size = RandomUtil.randomInt(1, RANDOM_COLLECTION_LENGTH);
return randomPojoList(clazz, size, consumers);
}
@SafeVarargs
public static <T> List<T> randomPojoList(Class<T> clazz, int size, Consumer<T>... consumers) {
return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers)) return Stream.iterate(0, i -> i).limit(size).map(o -> randomPojo(clazz, consumers))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }

View File

@ -62,6 +62,10 @@ public class BannerApplicationRunner implements ApplicationRunner {
if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) { if (isNotPresent("cn.iocoder.yudao.module.ai.framework.web.config.AiWebConfiguration")) {
System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]"); System.out.println("[AI 大模型 yudao-module-ai - 已禁用][参考 https://doc.iocoder.cn/ai/build/ 开启]");
} }
// IOT 物联网
if (isNotPresent("cn.iocoder.yudao.module.iot.framework.web.config.IotWebConfiguration")) {
System.out.println("[IOT 物联网 yudao-module-iot - 已禁用][参考 https://doc.iocoder.cn/iot/build/ 开启]");
}
}); });
} }

View File

@ -378,6 +378,12 @@ public class GlobalExceptionHandler {
return CommonResult.error(NOT_IMPLEMENTED.getCode(), return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]"); "[AI 大模型 yudao-module-ai - 表结构未导入][参考 https://cloud.iocoder.cn/ai/build/ 开启]");
} }
// 9. IOT 物联网
if (message.contains("iot_")) {
log.error("[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
return CommonResult.error(NOT_IMPLEMENTED.getCode(),
"[IOT 物联网 yudao-module-iot - 表结构未导入][参考 https://doc.iocoder.cn/iot/build/ 开启]");
}
return null; return null;
} }

View File

@ -85,7 +85,7 @@ public class YudaoWebSocketAutoConfiguration {
// ==================== Sender 相关 ==================== // ==================== Sender 相关 ====================
@Configuration @Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true) @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local")
public class LocalWebSocketMessageSenderConfiguration { public class LocalWebSocketMessageSenderConfiguration {
@Bean @Bean
@ -96,7 +96,7 @@ public class YudaoWebSocketAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis", matchIfMissing = true) @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "redis")
public class RedisWebSocketMessageSenderConfiguration { public class RedisWebSocketMessageSenderConfiguration {
@Bean @Bean
@ -114,7 +114,7 @@ public class YudaoWebSocketAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq", matchIfMissing = true) @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rocketmq")
public class RocketMQWebSocketMessageSenderConfiguration { public class RocketMQWebSocketMessageSenderConfiguration {
@Bean @Bean
@ -133,7 +133,7 @@ public class YudaoWebSocketAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq", matchIfMissing = true) @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "rabbitmq")
public class RabbitMQWebSocketMessageSenderConfiguration { public class RabbitMQWebSocketMessageSenderConfiguration {
@Bean @Bean
@ -162,7 +162,7 @@ public class YudaoWebSocketAutoConfiguration {
} }
@Configuration @Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka", matchIfMissing = true) @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "kafka")
public class KafkaWebSocketMessageSenderConfiguration { public class KafkaWebSocketMessageSenderConfiguration {
@Bean @Bean

View File

@ -34,6 +34,11 @@ public enum AiChatRoleEnum {
### 支付宝 ### 支付宝
### 微信 ### 微信
除此之外不要任何解释性语句 除此之外不要任何解释性语句
"""),
AI_KNOWLEDGE_ROLE("知识库助手", """
给你提供一些数据参考{info},请回答我的问题
请你跟进数据参考与工具返回结果回复用户的请求
"""); """);
/** /**

View File

@ -10,4 +10,7 @@ public class AiChatConversationCreateMyReqVO {
@Schema(description = "聊天角色编号", example = "666") @Schema(description = "聊天角色编号", example = "666")
private Long roleId; private Long roleId;
@Schema(description = "知识库编号", example = "1204")
private Long knowledgeId;
} }

View File

@ -21,6 +21,9 @@ public class AiChatConversationUpdateMyReqVO {
@Schema(description = "模型编号", example = "1") @Schema(description = "模型编号", example = "1")
private Long modelId; private Long modelId;
@Schema(description = "知识库编号", example = "1")
private Long knowledgeId;
@Schema(description = "角色设定", example = "一个快乐的程序员") @Schema(description = "角色设定", example = "一个快乐的程序员")
private String systemMessage; private String systemMessage;

View File

@ -1,12 +1,12 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge; package cn.iocoder.yudao.module.ai.controller.admin.knowledge;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService; import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
@ -28,24 +28,23 @@ public class AiKnowledgeController {
@Resource @Resource
private AiKnowledgeService knowledgeService; private AiKnowledgeService knowledgeService;
@GetMapping("/my-page") @GetMapping("/page")
@Operation(summary = "获取【我的】知识库分页") @Operation(summary = "获取知识库分页")
public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePageMy(@Validated PageParam pageReqVO) { public CommonResult<PageResult<AiKnowledgeRespVO>> getKnowledgePage(@Valid AiKnowledgePageReqVO pageReqVO) {
PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePageMy(getLoginUserId(), pageReqVO); PageResult<AiKnowledgeDO> pageResult = knowledgeService.getKnowledgePage(getLoginUserId(), pageReqVO);
return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class)); return success(BeanUtils.toBean(pageResult, AiKnowledgeRespVO.class));
} }
@PostMapping("/create-my") @PostMapping("/create")
@Operation(summary = "创建【我的】知识库") @Operation(summary = "创建知识库")
public CommonResult<Long> createKnowledgeMy(@RequestBody @Valid AiKnowledgeCreateMyReqVO createReqVO) { public CommonResult<Long> createKnowledge(@RequestBody @Valid AiKnowledgeCreateReqVO createReqVO) {
return success(knowledgeService.createKnowledgeMy(createReqVO, getLoginUserId())); return success(knowledgeService.createKnowledge(createReqVO, getLoginUserId()));
} }
@PutMapping("/update-my") @PutMapping("/update")
@Operation(summary = "更新【我的】知识库") @Operation(summary = "更新知识库")
public CommonResult<Boolean> updateKnowledgeMy(@RequestBody @Valid AiKnowledgeUpdateMyReqVO updateReqVO) { public CommonResult<Boolean> updateKnowledge(@RequestBody @Valid AiKnowledgeUpdateReqVO updateReqVO) {
knowledgeService.updateKnowledgeMy(updateReqVO, getLoginUserId()); knowledgeService.updateKnowledge(updateReqVO, getLoginUserId());
return success(true); return success(true);
} }
} }

View File

@ -36,7 +36,7 @@ public class AiKnowledgeDocumentController {
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获取文档分页") @Operation(summary = "获取文档分页")
public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPageMy(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) { public CommonResult<PageResult<AiKnowledgeDocumentRespVO>> getKnowledgeDocumentPage(@Valid AiKnowledgeDocumentPageReqVO pageReqVO) {
PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO); PageResult<AiKnowledgeDocumentDO> pageResult = documentService.getKnowledgeDocumentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class)); return success(BeanUtils.toBean(pageResult, AiKnowledgeDocumentRespVO.class));
} }

View File

@ -29,7 +29,7 @@ public class AiKnowledgeSegmentController {
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获取段落分页") @Operation(summary = "获取段落分页")
public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPageMy(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) { public CommonResult<PageResult<AiKnowledgeSegmentRespVO>> getKnowledgeSegmentPage(@Valid AiKnowledgeSegmentPageReqVO pageReqVO) {
PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO); PageResult<AiKnowledgeSegmentDO> pageResult = segmentService.getKnowledgeSegmentPage(pageReqVO);
return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class)); return success(BeanUtils.toBean(pageResult, AiKnowledgeSegmentRespVO.class));
} }

View File

@ -7,9 +7,9 @@ import lombok.Data;
import java.util.List; import java.util.List;
@Schema(description = "管理后台 - AI 知识库创建【我的】 Request VO") @Schema(description = "管理后台 - AI 知识库创建 Request VO")
@Data @Data
public class AiKnowledgeCreateMyReqVO { public class AiKnowledgeCreateReqVO {
@Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南") @Schema(description = "知识库名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "ruoyi-vue-pro 用户指南")
@NotBlank(message = "知识库名称不能为空") @NotBlank(message = "知识库名称不能为空")
@ -18,11 +18,19 @@ public class AiKnowledgeCreateMyReqVO {
@Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档") @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "存储 ruoyi-vue-pro 操作文档")
private String description; private String description;
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1,2,3]")
private List<Long> visibilityPermissions; private List<Long> visibilityPermissions;
@Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "嵌入模型不能为空") @NotNull(message = "嵌入模型不能为空")
private Long modelId; private Long modelId;
@Schema(description = "相似性阈值", requiredMode = Schema.RequiredMode.REQUIRED, example = "0.5")
@NotNull(message = "相似性阈值不能为空")
private Double similarityThreshold;
@Schema(description = "topK", requiredMode = Schema.RequiredMode.REQUIRED, example = "3")
@NotNull(message = "topK 不能为空")
private Integer topK;
} }

View File

@ -23,4 +23,24 @@ public class AiKnowledgeDocumentCreateReqVO {
@URL(message = "文档 URL 格式不正确") @URL(message = "文档 URL 格式不正确")
private String url; private String url;
@Schema(description = "每个段落的目标 token 数", requiredMode = Schema.RequiredMode.REQUIRED, example = "800")
@NotNull(message = "每个段落的目标 token 数不能为空")
private Integer defaultSegmentTokens;
@Schema(description = "每个段落的最小字符数", requiredMode = Schema.RequiredMode.REQUIRED, example = "350")
@NotNull(message = "每个段落的最小字符数不能为空")
private Integer minSegmentWordCount;
@Schema(description = "丢弃阈值:低于此阈值的段落会被丢弃", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
@NotNull(message = "丢弃阈值不能为空")
private Integer minChunkLengthToEmbed;
@Schema(description = "最大段落数", requiredMode = Schema.RequiredMode.REQUIRED, example = "10000")
@NotNull(message = "最大段落数不能为空")
private Integer maxNumSegments;
@Schema(description = "分块是否保留分隔符", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "分块是否保留分隔符不能为空")
private Boolean keepSeparator;
} }

View File

@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI 知识库的分页 Request VO")
@Data
public class AiKnowledgePageReqVO extends PageParam {
@Schema(description = "知识库名称", example = "Java 开发手册")
private String name;
}

View File

@ -9,7 +9,7 @@ import java.util.List;
@Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO") @Schema(description = "管理后台 - AI 知识库更新【我的】 Request VO")
@Data @Data
public class AiKnowledgeUpdateMyReqVO { public class AiKnowledgeUpdateReqVO {
@Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204") @Schema(description = "对话编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1204")
@NotNull(message = "知识库编号不能为空") @NotNull(message = "知识库编号不能为空")
@ -22,7 +22,7 @@ public class AiKnowledgeUpdateMyReqVO {
@Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "") @Schema(description = "知识库描述", requiredMode = Schema.RequiredMode.REQUIRED, example = "")
private String description; private String description;
@Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "[1]") @Schema(description = "可见权限,只能选择哪些人可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
private List<Long> visibilityPermissions; private List<Long> visibilityPermissions;
@Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @Schema(description = "嵌入模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")

View File

@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "管理后台 - AI 知识库段落召回 Request VO")
@Data
public class AiKnowledgeSegmentSearchReqVO {
@Schema(description = "知识库编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "24790")
private Long knowledgeId;
@Schema(description = "内容", requiredMode = Schema.RequiredMode.REQUIRED, example = "Java 学习路线")
private String content;
}

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.chat; package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
@ -64,6 +65,13 @@ public class AiChatConversationDO extends BaseDO {
*/ */
private Long roleId; private Long roleId;
/**
* 知识库编号
* <p>
* 关联 {@link AiKnowledgeDO#getId()}
*/
private Long knowledgeId;
/** /**
* 模型编号 * 模型编号
* *

View File

@ -1,13 +1,18 @@
package cn.iocoder.yudao.module.ai.dal.dataobject.chat; package cn.iocoder.yudao.module.ai.dal.dataobject.chat;
import com.baomidou.mybatisplus.annotation.TableId;
import org.springframework.ai.chat.messages.MessageType;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*; import lombok.*;
import org.springframework.ai.chat.messages.MessageType;
import java.util.List;
/** /**
* AI Chat 消息 DO * AI Chat 消息 DO
@ -66,6 +71,15 @@ public class AiChatMessageDO extends BaseDO {
*/ */
private Long roleId; private Long roleId;
/**
* 段落编号数组
*
* 关联 {@link AiKnowledgeSegmentDO#getId()} 字段
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<Long> segmentIds;
/** /**
* 模型标志 * 模型标志
*/ */

View File

@ -2,10 +2,10 @@ package cn.iocoder.yudao.module.ai.dal.dataobject.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.Data; import lombok.Data;
import java.util.List; import java.util.List;
@ -38,11 +38,13 @@ public class AiKnowledgeDO extends BaseDO {
* 知识库描述 * 知识库描述
*/ */
private String description; private String description;
// TODO @新如果全部可见需要怎么设置
/** /**
* 可见权限,只能选择哪些人可见 * 可见权限,选择哪些人可见
* <p>
* -1 所有人可见其他为各自用户编号
*/ */
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = LongListTypeHandler.class)
private List<Long> visibilityPermissions; private List<Long> visibilityPermissions;
/** /**
* 嵌入模型编号 * 嵌入模型编号
@ -52,10 +54,21 @@ public class AiKnowledgeDO extends BaseDO {
* 模型标识 * 模型标识
*/ */
private String model; private String model;
/**
* topK
*/
private Integer topK;
/**
* 相似度阈值
*/
private Double similarityThreshold;
/** /**
* 状态 * 状态
* <p> * <p>
* 枚举 {@link CommonStatusEnum} * 枚举 {@link CommonStatusEnum}
*/ */
private Integer status; private Integer status;
} }

View File

@ -23,7 +23,7 @@ public class AiKnowledgeDocumentDO extends BaseDO {
private Long id; private Long id;
/** /**
* 知识库编号 * 知识库编号
* * <p>
* 关联 {@link AiKnowledgeDO#getId()} * 关联 {@link AiKnowledgeDO#getId()}
*/ */
private Long knowledgeId; private Long knowledgeId;
@ -40,13 +40,39 @@ public class AiKnowledgeDocumentDO extends BaseDO {
*/ */
private String url; private String url;
/** /**
* token 数量 * 文档 token 数量
*/ */
private Integer tokens; private Integer tokens;
/** /**
* 字符数 * 文档字符数
*/ */
private Integer wordCount; private Integer wordCount;
// ========== 自定义分段所用参数 ==========
// TODO @新3defaultChunkSizedefaultChunkSizeminChunkSizeCharsmaxNumChunks 这几个字段的命名可能要微信一起讨论下尽量命名保持风格统一哈
/**
* 每个文本块的目标 token
*/
private Integer defaultSegmentTokens;
/**
* 每个文本块的最小字符数
*/
private Integer minSegmentWordCount;
/**
* 低于此值的块会被丢弃
*/
private Integer minChunkLengthToEmbed;
/**
* 最大块数
*/
private Integer maxNumSegments;
/**
* 分块是否保留分隔符
*/
private Boolean keepSeparator;
// ===================================
/** /**
* 切片状态 * 切片状态
* <p> * <p>

View File

@ -28,13 +28,13 @@ public class AiKnowledgeSegmentDO extends BaseDO {
private String vectorId; private String vectorId;
/** /**
* 知识库编号 * 知识库编号
* * <p>
* 关联 {@link AiKnowledgeDO#getId()} * 关联 {@link AiKnowledgeDO#getId()}
*/ */
private Long knowledgeId; private Long knowledgeId;
/** /**
* 文档编号 * 文档编号
* * <p>
* 关联 {@link AiKnowledgeDocumentDO#getId()} * 关联 {@link AiKnowledgeDocumentDO#getId()}
*/ */
private Long documentId; private Long documentId;
@ -52,7 +52,7 @@ public class AiKnowledgeSegmentDO extends BaseDO {
private Integer tokens; private Integer tokens;
/** /**
* 状态 * 状态
* * <p>
* 枚举 {@link CommonStatusEnum} * 枚举 {@link CommonStatusEnum}
*/ */
private Integer status; private Integer status;

View File

@ -1,10 +1,10 @@
package cn.iocoder.yudao.module.ai.dal.mysql.knowledge; package cn.iocoder.yudao.module.ai.dal.mysql.knowledge;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
@ -16,10 +16,11 @@ import org.apache.ibatis.annotations.Mapper;
@Mapper @Mapper
public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> { public interface AiKnowledgeMapper extends BaseMapperX<AiKnowledgeDO> {
default PageResult<AiKnowledgeDO> selectPageByMy(Long userId, PageParam pageReqVO) { default PageResult<AiKnowledgeDO> selectPage(Long userId, AiKnowledgePageReqVO pageReqVO) {
return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>() return selectPage(pageReqVO, new LambdaQueryWrapperX<AiKnowledgeDO>()
.eq(AiKnowledgeDO::getUserId, userId)
.eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus()) .eq(AiKnowledgeDO::getStatus, CommonStatusEnum.ENABLE.getStatus())
.likeIfPresent(AiKnowledgeDO::getName, pageReqVO.getName())
.and(e -> e.apply("FIND_IN_SET(" + userId + ",visibility_permissions)").or(m -> m.apply("FIND_IN_SET(-1,visibility_permissions)")))
.orderByDesc(AiKnowledgeDO::getId)); .orderByDesc(AiKnowledgeDO::getId));
} }
} }

View File

@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowle
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/** /**
* AI 知识库-分片 Mapper * AI 知识库-分片 Mapper
* *
@ -22,4 +24,11 @@ public interface AiKnowledgeSegmentMapper extends BaseMapperX<AiKnowledgeSegment
.likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword()) .likeIfPresent(AiKnowledgeSegmentDO::getContent, reqVO.getKeyword())
.orderByDesc(AiKnowledgeSegmentDO::getId)); .orderByDesc(AiKnowledgeSegmentDO::getId));
} }
default List<AiKnowledgeSegmentDO> selectListByVectorIds(List<String> vectorIdList) {
return selectList(new LambdaQueryWrapperX<AiKnowledgeSegmentDO>()
.in(AiKnowledgeSegmentDO::getVectorId, vectorIdList)
.orderByDesc(AiKnowledgeSegmentDO::getId));
}
} }

View File

@ -13,6 +13,7 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatConversationMapper;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@ -22,6 +23,7 @@ import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -45,6 +47,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
private AiChatModelService chatModalService; private AiChatModelService chatModalService;
@Resource @Resource
private AiChatRoleService chatRoleService; private AiChatRoleService chatRoleService;
@Resource
private AiKnowledgeService knowledgeService;
@Override @Override
public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) { public Long createChatConversationMy(AiChatConversationCreateMyReqVO createReqVO, Long userId) {
@ -56,9 +60,14 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
Assert.notNull(model, "必须找到默认模型"); Assert.notNull(model, "必须找到默认模型");
validateChatModel(model); validateChatModel(model);
// 1.3 校验知识库
if (Objects.nonNull(createReqVO.getKnowledgeId())) {
knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId());
}
// 2. 创建 AiChatConversationDO 聊天对话 // 2. 创建 AiChatConversationDO 聊天对话
AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false) AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
.setModelId(model.getId()).setModel(model.getModel()) .setModelId(model.getId()).setModel(model.getModel()).setKnowledgeId(createReqVO.getKnowledgeId())
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts()); .setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
if (role != null) { if (role != null) {
conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage()); conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
@ -82,6 +91,11 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
model = chatModalService.validateChatModel(updateReqVO.getModelId()); model = chatModalService.validateChatModel(updateReqVO.getModelId());
} }
// 1.3 校验知识库是否存在
if (updateReqVO.getKnowledgeId() != null) {
knowledgeService.validateKnowledgeExists(updateReqVO.getKnowledgeId());
}
// 2. 更新对话信息 // 2. 更新对话信息
AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class); AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class);
if (Boolean.TRUE.equals(updateReqVO.getPinned())) { if (Boolean.TRUE.equals(updateReqVO.getPinned())) {

View File

@ -12,21 +12,29 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessagePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO; import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.message.AiChatMessageSendRespVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatMessageDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper; import cn.iocoder.yudao.module.ai.dal.mysql.chat.AiChatMessageMapper;
import cn.iocoder.yudao.module.ai.enums.AiChatRoleEnum;
import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants; import cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants;
import cn.iocoder.yudao.module.ai.service.knowledge.AiKnowledgeSegmentService;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService; import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.MessageType;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.StreamingChatModel; import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.chat.prompt.PromptTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
@ -59,6 +67,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
private AiChatModelService chatModalService; private AiChatModelService chatModalService;
@Resource @Resource
private AiApiKeyService apiKeyService; private AiApiKeyService apiKeyService;
@Resource
private AiKnowledgeSegmentService knowledgeSegmentService;
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) { public AiChatMessageSendRespVO sendMessage(AiChatMessageSendReqVO sendReqVO, Long userId) {
@ -80,13 +90,16 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
// 3.2 创建 chat 需要的 Prompt // 3.2 召回段落
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
// 3.3 创建 chat 需要的 Prompt
Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
ChatResponse chatResponse = chatModel.call(prompt); ChatResponse chatResponse = chatModel.call(prompt);
// 3.3 段式返回 // 3.4 段式返回
String newContent = chatResponse.getResult().getOutput().getContent(); String newContent = chatResponse.getResult().getOutput().getContent();
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent)); chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId)).setContent(newContent));
return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class)) return new AiChatMessageSendRespVO().setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent)); .setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class).setContent(newContent));
} }
@ -111,11 +124,15 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model, AiChatMessageDO assistantMessage = createChatMessage(conversation.getId(), userMessage.getId(), model,
userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext()); userId, conversation.getRoleId(), MessageType.ASSISTANT, "", sendReqVO.getUseContext());
// 3.2 构建 Prompt并进行调用
Prompt prompt = buildPrompt(conversation, historyMessages, model, sendReqVO); // 3.2 召回段落
List<AiKnowledgeSegmentDO> segmentList = recallSegment(sendReqVO.getContent(), conversation.getKnowledgeId());
// 3.3 构建 Prompt并进行调用
Prompt prompt = buildPrompt(conversation, historyMessages, segmentList, model, sendReqVO);
Flux<ChatResponse> streamResponse = chatModel.stream(prompt); Flux<ChatResponse> streamResponse = chatModel.stream(prompt);
// 3.3 流式返回 // 3.4 流式返回
// TODO 注意Schedulers.immediate() 目的是避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题 // TODO 注意Schedulers.immediate() 目的是避免默认 Schedulers.parallel() 并发消费 chunk 导致 SSE 响应前端会乱序问题
StringBuffer contentBuffer = new StringBuffer(); StringBuffer contentBuffer = new StringBuffer();
return streamResponse.map(chunk -> { return streamResponse.map(chunk -> {
@ -128,7 +145,8 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
}).doOnComplete(() -> { }).doOnComplete(() -> {
// 忽略租户因为 Flux 异步无法透传租户 // 忽略租户因为 Flux 异步无法透传租户
TenantUtils.executeIgnore(() -> TenantUtils.executeIgnore(() ->
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(contentBuffer.toString()))); chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setSegmentIds(convertList(segmentList, AiKnowledgeSegmentDO::getId))
.setContent(contentBuffer.toString())));
}).doOnError(throwable -> { }).doOnError(throwable -> {
log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable); log.error("[sendChatMessageStream][userId({}) sendReqVO({}) 发生异常]", userId, sendReqVO, throwable);
// 忽略租户因为 Flux 异步无法透传租户 // 忽略租户因为 Flux 异步无法透传租户
@ -137,18 +155,35 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
}).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR))); }).onErrorResume(error -> Flux.just(error(ErrorCodeConstants.CHAT_STREAM_ERROR)));
} }
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages, private List<AiKnowledgeSegmentDO> recallSegment(String content, Long knowledgeId) {
if (Objects.isNull(knowledgeId)) {
return Collections.emptyList();
}
return knowledgeSegmentService.similaritySearch(new AiKnowledgeSegmentSearchReqVO().setKnowledgeId(knowledgeId).setContent(content));
}
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,List<AiKnowledgeSegmentDO> segmentList,
AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) { AiChatModelDO model, AiChatMessageSendReqVO sendReqVO) {
// 1. 构建 Prompt Message 列表 // 1. 构建 Prompt Message 列表
List<Message> chatMessages = new ArrayList<>(); List<Message> chatMessages = new ArrayList<>();
// 1.1 system context 角色设定
// 1.1 召回内容消息构建
if (CollUtil.isNotEmpty(segmentList)) {
PromptTemplate promptTemplate = new PromptTemplate(AiChatRoleEnum.AI_KNOWLEDGE_ROLE.getSystemMessage());
StringBuilder infoBuilder = StrUtil.builder();
segmentList.forEach(segment -> infoBuilder.append(System.lineSeparator()).append(segment.getContent()));
Message message = promptTemplate.createMessage(Map.of("info", infoBuilder.toString()));
chatMessages.add(message);
}
// 1.2 system context 角色设定
if (StrUtil.isNotBlank(conversation.getSystemMessage())) { if (StrUtil.isNotBlank(conversation.getSystemMessage())) {
chatMessages.add(new SystemMessage(conversation.getSystemMessage())); chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
} }
// 1.2 history message 历史消息 // 1.3 history message 历史消息
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO); List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent()))); contextMessages.forEach(message -> chatMessages.add(AiUtils.buildMessage(message.getType(), message.getContent())));
// 1.3 user message 新发送消息 // 1.4 user message 新发送消息
chatMessages.add(new UserMessage(sendReqVO.getContent())); chatMessages.add(new UserMessage(sendReqVO.getContent()));
// 2. 构建 ChatOptions 对象 // 2. 构建 ChatOptions 对象
@ -160,7 +195,7 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
/** /**
* 从历史消息中获得倒序的 n 组消息作为消息上下文 * 从历史消息中获得倒序的 n 组消息作为消息上下文
* * <p>
* n 指的是 user + assistant 形成一组 * n 指的是 user + assistant 形成一组
* *
* @param messages 消息列表 * @param messages 消息列表

View File

@ -9,15 +9,11 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.document.AiKnowledgeDocumentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeDocumentCreateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDocumentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeDocumentMapper;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum; import cn.iocoder.yudao.module.ai.enums.knowledge.AiKnowledgeDocumentStatusEnum;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document; import org.springframework.ai.document.Document;
@ -48,24 +44,16 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
@Resource @Resource
private AiKnowledgeSegmentMapper segmentMapper; private AiKnowledgeSegmentMapper segmentMapper;
@Resource
private TokenTextSplitter tokenTextSplitter;
@Resource @Resource
private TokenCountEstimator tokenCountEstimator; private TokenCountEstimator tokenCountEstimator;
@Resource
private AiApiKeyService apiKeyService;
@Resource @Resource
private AiKnowledgeService knowledgeService; private AiKnowledgeService knowledgeService;
@Resource
private AiChatModelService chatModelService;
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) { public Long createKnowledgeDocument(AiKnowledgeDocumentCreateReqVO createReqVO) {
// 0. 校验 // 0. 校验并获取向量存储实例
AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(createReqVO.getKnowledgeId()); VectorStore vectorStore = knowledgeService.getVectorStoreById(createReqVO.getKnowledgeId());
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
// 1.1 下载文档 // 1.1 下载文档
TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl())); TikaDocumentReader loader = new TikaDocumentReader(downloadFile(createReqVO.getUrl()));
@ -82,6 +70,9 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
return documentId; return documentId;
} }
// 2 构造文本分段器
TokenTextSplitter tokenTextSplitter = new TokenTextSplitter(createReqVO.getDefaultSegmentTokens(), createReqVO.getMinSegmentWordCount(), createReqVO.getMinChunkLengthToEmbed(),
createReqVO.getMaxNumSegments(), createReqVO.getKeepSeparator());
// 2.1 文档分段 // 2.1 文档分段
List<Document> segments = tokenTextSplitter.apply(documents); List<Document> segments = tokenTextSplitter.apply(documents);
// 2.2 分段内容入库 // 2.2 分段内容入库
@ -92,9 +83,7 @@ public class AiKnowledgeDocumentServiceImpl implements AiKnowledgeDocumentServic
.setStatus(CommonStatusEnum.ENABLE.getStatus())); .setStatus(CommonStatusEnum.ENABLE.getStatus()));
segmentMapper.insertBatch(segmentDOList); segmentMapper.insertBatch(segmentDOList);
// 3.1 获取向量存储实例 // 3. 向量化并存储
VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
// 3.2 向量化并存储
segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId())); segments.forEach(segment -> segment.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, createReqVO.getKnowledgeId()));
vectorStore.add(segments); vectorStore.add(segments);
return documentId; return documentId;

View File

@ -2,10 +2,13 @@ package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import java.util.List;
/** /**
* AI 知识库段落 Service 接口 * AI 知识库段落 Service 接口
* *
@ -35,4 +38,12 @@ public interface AiKnowledgeSegmentService {
*/ */
void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO); void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO);
/**
* 召回段落
*
* @param reqVO 召回请求信息
* @return 召回的段落
*/
List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO);
} }

View File

@ -1,16 +1,34 @@
package cn.iocoder.yudao.module.ai.service.knowledge; package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentPageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentSearchReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.segment.AiKnowledgeSegmentUpdateStatusReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeSegmentDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeSegmentMapper;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.filter.FilterExpressionBuilder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_SEGMENT_NOT_EXISTS;
/** /**
* AI 知识库分片 Service 实现类 * AI 知识库分片 Service 实现类
* *
@ -23,6 +41,13 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
@Resource @Resource
private AiKnowledgeSegmentMapper segmentMapper; private AiKnowledgeSegmentMapper segmentMapper;
@Resource
private AiKnowledgeService knowledgeService;
@Resource
private AiChatModelService chatModelService;
@Resource
private AiApiKeyService apiKeyService;
@Override @Override
public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) { public PageResult<AiKnowledgeSegmentDO> getKnowledgeSegmentPage(AiKnowledgeSegmentPageReqVO pageReqVO) {
return segmentMapper.selectPage(pageReqVO); return segmentMapper.selectPage(pageReqVO);
@ -30,13 +55,80 @@ public class AiKnowledgeSegmentServiceImpl implements AiKnowledgeSegmentService
@Override @Override
public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) { public void updateKnowledgeSegment(AiKnowledgeSegmentUpdateReqVO reqVO) {
segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class)); // 1. 校验
// TODO @xin 重新向量化 AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId());
// 2.1 获取知识库向量实例
VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId());
// 2.2 删除原向量
vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
// 2.3 重新向量化
Document document = new Document(reqVO.getContent());
document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId());
vectorStore.add(List.of(document));
// 3. 更新段落内容
AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class);
knowledgeSegment.setVectorId(document.getId());
segmentMapper.updateById(knowledgeSegment);
} }
@Override @Override
public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) { public void updateKnowledgeSegmentStatus(AiKnowledgeSegmentUpdateStatusReqVO reqVO) {
segmentMapper.updateById(BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class)); // 0 校验
// TODO @xin 1.禁用删除向量 2.启用重新向量化 AiKnowledgeSegmentDO oldKnowledgeSegment = validateKnowledgeSegmentExists(reqVO.getId());
// 1 获取知识库向量实例
VectorStore vectorStore = knowledgeService.getVectorStoreById(oldKnowledgeSegment.getKnowledgeId());
AiKnowledgeSegmentDO knowledgeSegment = BeanUtils.toBean(reqVO, AiKnowledgeSegmentDO.class);
if (Objects.equals(reqVO.getStatus(), CommonStatusEnum.ENABLE.getStatus())) {
// 2.1 启用重新向量化
Document document = new Document(oldKnowledgeSegment.getContent());
document.getMetadata().put(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, oldKnowledgeSegment.getKnowledgeId());
vectorStore.add(List.of(document));
knowledgeSegment.setVectorId(document.getId());
} else {
// 2.2 禁用删除向量
vectorStore.delete(List.of(oldKnowledgeSegment.getVectorId()));
knowledgeSegment.setVectorId("");
} }
// 3 更新段落状态
segmentMapper.updateById(knowledgeSegment);
}
@Override
public List<AiKnowledgeSegmentDO> similaritySearch(AiKnowledgeSegmentSearchReqVO reqVO) {
// 1. 校验
AiKnowledgeDO knowledge = knowledgeService.validateKnowledgeExists(reqVO.getKnowledgeId());
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
// 2. 获取向量存储实例
VectorStore vectorStore = apiKeyService.getOrCreateVectorStore(model.getKeyId());
// 3.1 向量检索
List<Document> documentList = vectorStore.similaritySearch(SearchRequest.query(reqVO.getContent())
.withTopK(knowledge.getTopK())
.withSimilarityThreshold(knowledge.getSimilarityThreshold())
.withFilterExpression(new FilterExpressionBuilder().eq(AiKnowledgeSegmentDO.FIELD_KNOWLEDGE_ID, reqVO.getKnowledgeId()).build()));
if (CollUtil.isEmpty(documentList)) {
return ListUtil.empty();
}
// 3.2 段落召回
return segmentMapper.selectListByVectorIds(CollUtil.getFieldValues(documentList, "id", String.class));
}
/**
* 校验段落是否存在
*
* @param id 文档编号
* @return 段落信息
*/
private AiKnowledgeSegmentDO validateKnowledgeSegmentExists(Long id) {
AiKnowledgeSegmentDO knowledgeSegment = segmentMapper.selectById(id);
if (knowledgeSegment == null) {
throw exception(KNOWLEDGE_SEGMENT_NOT_EXISTS);
}
return knowledgeSegment;
}
} }

View File

@ -1,10 +1,11 @@
package cn.iocoder.yudao.module.ai.service.knowledge; package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import org.springframework.ai.vectorstore.VectorStore;
/** /**
* AI 知识库-基础信息 Service 接口 * AI 知识库-基础信息 Service 接口
@ -14,23 +15,21 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
public interface AiKnowledgeService { public interface AiKnowledgeService {
/** /**
* 创建我的知识库 * 创建知识库
* *
* @param createReqVO 创建信息 * @param createReqVO 创建信息
* @param userId 用户编号 * @param userId 用户编号
* @return 编号 * @return 编号
*/ */
Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId); Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId);
/** /**
* 创建我的知识库 * 更新知识库
* *
* @param updateReqVO 更新信息 * @param updateReqVO 更新信息
* @param userId 用户编号 * @param userId 用户编号
*/ */
void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId); void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId);
/** /**
* 校验知识库是否存在 * 校验知识库是否存在
@ -40,11 +39,20 @@ public interface AiKnowledgeService {
AiKnowledgeDO validateKnowledgeExists(Long id); AiKnowledgeDO validateKnowledgeExists(Long id);
/** /**
* 获得我的知识库分页 * 获得知识库分页
* *
* @param userId 用户编号 * @param userId 用户编号
* @param pageReqVO 分页查询 * @param pageReqVO 分页查询
* @return 知识库分页 * @return 知识库分页
*/ */
PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO); PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO);
/**
* 根据知识库编号获取向量存储实例
*
* @param id 知识库编号
* @return 向量存储实例
*/
VectorStore getVectorStoreById(Long id);
} }

View File

@ -2,17 +2,19 @@ package cn.iocoder.yudao.module.ai.service.knowledge;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeCreateReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateMyReqVO; import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgePageReqVO;
import cn.iocoder.yudao.module.ai.controller.admin.knowledge.vo.knowledge.AiKnowledgeUpdateReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO; import cn.iocoder.yudao.module.ai.dal.dataobject.knowledge.AiKnowledgeDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper; import cn.iocoder.yudao.module.ai.dal.mysql.knowledge.AiKnowledgeMapper;
import cn.iocoder.yudao.module.ai.service.model.AiApiKeyService;
import cn.iocoder.yudao.module.ai.service.model.AiChatModelService; import cn.iocoder.yudao.module.ai.service.model.AiChatModelService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -27,16 +29,18 @@ import static cn.iocoder.yudao.module.ai.enums.ErrorCodeConstants.KNOWLEDGE_NOT_
@Slf4j @Slf4j
public class AiKnowledgeServiceImpl implements AiKnowledgeService { public class AiKnowledgeServiceImpl implements AiKnowledgeService {
@Resource
private AiChatModelService chatModalService;
@Resource @Resource
private AiKnowledgeMapper knowledgeMapper; private AiKnowledgeMapper knowledgeMapper;
@Resource
private AiChatModelService chatModelService;
@Resource
private AiApiKeyService apiKeyService;
@Override @Override
public Long createKnowledgeMy(AiKnowledgeCreateMyReqVO createReqVO, Long userId) { public Long createKnowledge(AiKnowledgeCreateReqVO createReqVO, Long userId) {
// 1. 校验模型配置 // 1. 校验模型配置
AiChatModelDO model = chatModalService.validateChatModel(createReqVO.getModelId()); AiChatModelDO model = chatModelService.validateChatModel(createReqVO.getModelId());
// 2. 插入知识库 // 2. 插入知识库
AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class) AiKnowledgeDO knowledgeBase = BeanUtils.toBean(createReqVO, AiKnowledgeDO.class)
@ -46,14 +50,14 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
} }
@Override @Override
public void updateKnowledgeMy(AiKnowledgeUpdateMyReqVO updateReqVO, Long userId) { public void updateKnowledge(AiKnowledgeUpdateReqVO updateReqVO, Long userId) {
// 1.1 校验知识库存在 // 1.1 校验知识库存在
AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId()); AiKnowledgeDO knowledgeBaseDO = validateKnowledgeExists(updateReqVO.getId());
if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) { if (ObjUtil.notEqual(knowledgeBaseDO.getUserId(), userId)) {
throw exception(KNOWLEDGE_NOT_EXISTS); throw exception(KNOWLEDGE_NOT_EXISTS);
} }
// 1.2 校验模型配置 // 1.2 校验模型配置
AiChatModelDO model = chatModalService.validateChatModel(updateReqVO.getModelId()); AiChatModelDO model = chatModelService.validateChatModel(updateReqVO.getModelId());
// 2. 更新知识库 // 2. 更新知识库
AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class); AiKnowledgeDO updateDO = BeanUtils.toBean(updateReqVO, AiKnowledgeDO.class);
@ -71,8 +75,16 @@ public class AiKnowledgeServiceImpl implements AiKnowledgeService {
} }
@Override @Override
public PageResult<AiKnowledgeDO> getKnowledgePageMy(Long userId, PageParam pageReqVO) { public PageResult<AiKnowledgeDO> getKnowledgePage(Long userId, AiKnowledgePageReqVO pageReqVO) {
return knowledgeMapper.selectPageByMy(userId, pageReqVO); return knowledgeMapper.selectPage(userId, pageReqVO);
}
@Override
public VectorStore getVectorStoreById(Long id) {
AiKnowledgeDO knowledge = validateKnowledgeExists(id);
AiChatModelDO model = chatModelService.validateChatModel(knowledge.getModelId());
// 创建或获取 VectorStore 对象
return apiKeyService.getOrCreateVectorStore(model.getKeyId());
} }
} }

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.service.model;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum; import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@ -39,8 +38,6 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
@Resource @Resource
private AiModelFactory modelFactory; private AiModelFactory modelFactory;
@Resource
private AiVectorStoreFactory vectorFactory;
@Override @Override
public Long createApiKey(AiApiKeySaveReqVO createReqVO) { public Long createApiKey(AiApiKeySaveReqVO createReqVO) {
@ -149,7 +146,8 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
public VectorStore getOrCreateVectorStore(Long id) { public VectorStore getOrCreateVectorStore(Long id) {
AiApiKeyDO apiKey = validateApiKey(id); AiApiKeyDO apiKey = validateApiKey(id);
AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform()); AiPlatformEnum platform = AiPlatformEnum.validatePlatform(apiKey.getPlatform());
return vectorFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl()); // 创建或获取 VectorStore 对象
return modelFactory.getOrCreateVectorStore(getEmbeddingModel(id), platform, apiKey.getApiKey(), apiKey.getUrl());
} }
} }

View File

@ -46,11 +46,13 @@
</dependency> </dependency>
<!-- 向量化,基于 Redis 存储Tika 解析内容 --> <!-- 向量化,基于 Redis 存储Tika 解析内容 -->
<dependency>
<groupId>${spring-ai.groupId}</groupId> <!-- 暂不做经济型,先注释 TODO 经济型是啥呀? -->
<artifactId>spring-ai-transformers-spring-boot-starter</artifactId> <!-- <dependency>-->
<version>${spring-ai.version}</version> <!-- <groupId>${spring-ai.groupId}</groupId>-->
</dependency> <!-- <artifactId>spring-ai-transformers-spring-boot-starter</artifactId>-->
<!-- <version>${spring-ai.version}</version>-->
<!-- </dependency>-->
<dependency> <dependency>
<groupId>${spring-ai.groupId}</groupId> <groupId>${spring-ai.groupId}</groupId>
<artifactId>spring-ai-tika-document-reader</artifactId> <artifactId>spring-ai-tika-document-reader</artifactId>

View File

@ -2,8 +2,6 @@ package cn.iocoder.yudao.framework.ai.config;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl; import cn.iocoder.yudao.framework.ai.core.factory.AiModelFactoryImpl;
import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactory;
import cn.iocoder.yudao.framework.ai.core.factory.AiVectorStoreFactoryImpl;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions; import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatOptions;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
@ -38,11 +36,6 @@ public class YudaoAiAutoConfiguration {
return new AiModelFactoryImpl(); return new AiModelFactoryImpl();
} }
@Bean
public AiVectorStoreFactory aiVectorFactory() {
return new AiVectorStoreFactoryImpl();
}
// ========== 各种 AI Client 创建 ========== // ========== 各种 AI Client 创建 ==========
@ -89,7 +82,7 @@ public class YudaoAiAutoConfiguration {
// TODO @xin 免费版本 // TODO @xin 免费版本
// @Bean // @Bean
// @Lazy // TODO 芋艿临时注释避免无法启动 // @Lazy // TODO 芋艿临时注释避免无法启动
// public EmbeddingModel transformersEmbeddingClient() { // public TransformersEmbeddingModel transformersEmbeddingClient() {
// return new TransformersEmbeddingModel(MetadataMode.EMBED); // return new TransformersEmbeddingModel(MetadataMode.EMBED);
// } // }
@ -98,23 +91,24 @@ public class YudaoAiAutoConfiguration {
*/ */
// @Bean // @Bean
// @Lazy // TODO 芋艿临时注释避免无法启动 // @Lazy // TODO 芋艿临时注释避免无法启动
// public RedisVectorStore vectorStore(TongYiTextEmbeddingModel tongYiTextEmbeddingModel, RedisVectorStoreProperties properties, // public RedisVectorStore vectorStore(TransformersEmbeddingModel embeddingModel, RedisVectorStoreProperties properties,
// RedisProperties redisProperties) { // RedisProperties redisProperties) {
// var config = RedisVectorStore.RedisVectorStoreConfig.builder() // var config = RedisVectorStore.RedisVectorStoreConfig.builder()
// .withIndexName(properties.getIndex()) // .withIndexName(properties.getIndex())
// .withPrefix(properties.getPrefix()) // .withPrefix(properties.getPrefix())
// .withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC))
// .build(); // .build();
// //
// RedisVectorStore redisVectorStore = new RedisVectorStore(config, tongYiTextEmbeddingModel, // RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
// new JedisPooled(redisProperties.getHost(), redisProperties.getPort()), // new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
// properties.isInitializeSchema()); // properties.isInitializeSchema());
// redisVectorStore.afterPropertiesSet(); // redisVectorStore.afterPropertiesSet();
// return redisVectorStore; // return redisVectorStore;
// } // }
@Bean @Bean
@Lazy // TODO 芋艿临时注释避免无法启动 @Lazy // TODO 芋艿临时注释避免无法启动
public TokenTextSplitter tokenTextSplitter() { public TokenTextSplitter tokenTextSplitter() {
//TODO @xin 配置提取
return new TokenTextSplitter(500, 100, 5, 10000, true); return new TokenTextSplitter(500, 100, 5, 10000, true);
} }

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageModel;
import org.springframework.ai.vectorstore.VectorStore;
/** /**
* AI Model 模型工厂的接口类 * AI Model 模型工厂的接口类
@ -92,4 +93,17 @@ public interface AiModelFactory {
*/ */
EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url); EmbeddingModel getOrCreateEmbeddingModel(AiPlatformEnum platform, String apiKey, String url);
/**
* 基于指定配置获得 VectorStore 对象
* <p>
* 如果不存在则进行创建
*
* @param embeddingModel 嵌入模型
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @return VectorStore 对象
*/
VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
} }

View File

@ -13,6 +13,7 @@ import cn.iocoder.yudao.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.framework.ai.core.model.xinghuo.XingHuoChatModel;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration; import com.alibaba.cloud.ai.tongyi.TongYiAutoConfiguration;
import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties; import com.alibaba.cloud.ai.tongyi.TongYiConnectionProperties;
import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel; import com.alibaba.cloud.ai.tongyi.chat.TongYiChatModel;
@ -54,13 +55,18 @@ import org.springframework.ai.qianfan.api.QianFanApi;
import org.springframework.ai.qianfan.api.QianFanImageApi; import org.springframework.ai.qianfan.api.QianFanImageApi;
import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.StabilityAiImageModel;
import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.ai.stabilityai.api.StabilityAiApi;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.zhipuai.ZhiPuAiChatModel; import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
import org.springframework.ai.zhipuai.ZhiPuAiImageModel; import org.springframework.ai.zhipuai.ZhiPuAiImageModel;
import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiApi;
import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.retry.support.RetryTemplate; import org.springframework.retry.support.RetryTemplate;
import org.springframework.web.client.ResponseErrorHandler; import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestClient; import org.springframework.web.client.RestClient;
import redis.clients.jedis.JedisPooled;
import redis.clients.jedis.search.Schema;
import java.util.List; import java.util.List;
@ -191,6 +197,25 @@ public class AiModelFactoryImpl implements AiModelFactory {
}); });
} }
@Override
public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
String prefix = StrUtil.format("{}#{}:", platform.getPlatform(), apiKey);
var config = RedisVectorStore.RedisVectorStoreConfig.builder()
.withIndexName(cacheKey)
.withPrefix(prefix)
.withMetadataFields(new RedisVectorStore.MetadataField("knowledgeId", Schema.FieldType.NUMERIC))
.build();
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
true);
redisVectorStore.afterPropertiesSet();
return redisVectorStore;
});
}
private static String buildClientCacheKey(Class<?> clazz, Object... params) { private static String buildClientCacheKey(Class<?> clazz, Object... params) {
if (ArrayUtil.isEmpty(params)) { if (ArrayUtil.isEmpty(params)) {
return clazz.getName(); return clazz.getName();

View File

@ -1,28 +0,0 @@
package cn.iocoder.yudao.framework.ai.core.factory;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.VectorStore;
// TODO @xin也放到 AiModelFactory 里面好了后续改成 AiFactory
/**
* AI Vector 模型工厂的接口类
*
* @author xiaoxin
*/
public interface AiVectorStoreFactory {
/**
* 基于指定配置获得 VectorStore 对象
* <p>
* 如果不存在则进行创建
*
* @param embeddingModel 嵌入模型
* @param platform 平台
* @param apiKey API KEY
* @param url API URL
* @return VectorStore 对象
*/
VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url);
}

View File

@ -1,52 +0,0 @@
package cn.iocoder.yudao.framework.ai.core.factory;
import cn.hutool.core.lang.Singleton;
import cn.hutool.core.lang.func.Func0;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.ai.core.enums.AiPlatformEnum;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.RedisVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import redis.clients.jedis.JedisPooled;
/**
* AI Vector 模型工厂的实现类
* 使用 redisVectorStore 实现 VectorStore
*
* @author xiaoxin
*/
public class AiVectorStoreFactoryImpl implements AiVectorStoreFactory {
@Override
public VectorStore getOrCreateVectorStore(EmbeddingModel embeddingModel, AiPlatformEnum platform, String apiKey, String url) {
String cacheKey = buildClientCacheKey(VectorStore.class, platform, apiKey, url);
return Singleton.get(cacheKey, (Func0<VectorStore>) () -> {
// TODO 芋艿 @xin 这两个配置取哪好呢
// TODO 不同模型的向量维度可能会不一样目前看貌似是以 index 来做区分的维度不一样存不到一个 index
// TODO 回复好的哈
String index = "default-index";
String prefix = "default:";
var config = RedisVectorStore.RedisVectorStoreConfig.builder()
.withIndexName(index)
.withPrefix(prefix)
.build();
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
RedisVectorStore redisVectorStore = new RedisVectorStore(config, embeddingModel,
new JedisPooled(redisProperties.getHost(), redisProperties.getPort()),
true);
redisVectorStore.afterPropertiesSet();
return redisVectorStore;
});
}
private static String buildClientCacheKey(Class<?> clazz, Object... params) {
if (ArrayUtil.isEmpty(params)) {
return clazz.getName();
}
return StrUtil.format("{}#{}", clazz.getName(), ArrayUtil.join(params, "_"));
}
}

View File

@ -18,8 +18,8 @@ public class BpmProcessInstancePageReqVO extends PageParam {
@Schema(description = "流程名称", example = "芋道") @Schema(description = "流程名称", example = "芋道")
private String name; private String name;
@Schema(description = "流程定义的编号", example = "2048") @Schema(description = "流程定义的标识", example = "2048")
private String processDefinitionId; private String processDefinitionKey; // 精准匹配
@Schema(description = "流程实例的状态", example = "1") @Schema(description = "流程实例的状态", example = "1")
@InEnum(BpmProcessInstanceStatusEnum.class) @InEnum(BpmProcessInstanceStatusEnum.class)

View File

@ -30,9 +30,6 @@ public class CrmCluePageReqVO extends PageParam {
@InEnum(CrmSceneTypeEnum.class) @InEnum(CrmSceneTypeEnum.class)
private Integer sceneType; // 场景类型 null 时则表示全部 private Integer sceneType; // 场景类型 null 时则表示全部
@Schema(description = "是否为公海数据", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
private Boolean pool; // null 则表示为不是公海数据
@Schema(description = "所属行业", example = "1") @Schema(description = "所属行业", example = "1")
private Integer industryId; private Integer industryId;

View File

@ -47,7 +47,7 @@ public interface CrmBusinessMapper extends BaseMapperX<CrmBusinessDO> {
MPJLambdaWrapperX<CrmBusinessDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmBusinessDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_BUSINESS.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_BUSINESS.getType(),
CrmBusinessDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE); CrmBusinessDO::getId, userId, pageReqVO.getSceneType());
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmBusinessDO.class) query.selectAll(CrmBusinessDO.class)
.likeIfPresent(CrmBusinessDO::getName, pageReqVO.getName()) .likeIfPresent(CrmBusinessDO::getName, pageReqVO.getName())

View File

@ -10,9 +10,6 @@ import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils; import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.List;
/** /**
* 线索 Mapper * 线索 Mapper
* *
@ -25,7 +22,7 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(),
CrmClueDO::getId, userId, pageReqVO.getSceneType(), pageReqVO.getPool()); CrmClueDO::getId, userId, pageReqVO.getSceneType());
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmClueDO.class) query.selectAll(CrmClueDO.class)
.likeIfPresent(CrmClueDO::getName, pageReqVO.getName()) .likeIfPresent(CrmClueDO::getName, pageReqVO.getName())
@ -40,20 +37,11 @@ public interface CrmClueMapper extends BaseMapperX<CrmClueDO> {
return selectJoinPage(pageReqVO, CrmClueDO.class, query); return selectJoinPage(pageReqVO, CrmClueDO.class, query);
} }
default List<CrmClueDO> selectBatchIds(Collection<Long> ids, Long userId) {
MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(), ids, userId);
query.selectAll(CrmClueDO.class).in(CrmClueDO::getId, ids).orderByDesc(CrmClueDO::getId);
// 拼接自身的查询条件
return selectJoinList(CrmClueDO.class, query);
}
default Long selectCountByFollow(Long userId) { default Long selectCountByFollow(Long userId) {
MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmClueDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CLUE.getType(),
CrmClueDO::getId, userId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmClueDO::getId, userId, CrmSceneTypeEnum.OWNER.getType());
// 未跟进 + 未转化 // 未跟进 + 未转化
query.eq(CrmClueDO::getFollowUpStatus, false) query.eq(CrmClueDO::getFollowUpStatus, false)
.eq(CrmClueDO::getTransformStatus, false); .eq(CrmClueDO::getTransformStatus, false);

View File

@ -56,7 +56,7 @@ public interface CrmContactMapper extends BaseMapperX<CrmContactDO> {
MPJLambdaWrapperX<CrmContactDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmContactDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTACT.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTACT.getType(),
CrmContactDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE); CrmContactDO::getId, userId, pageReqVO.getSceneType());
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmContactDO.class) query.selectAll(CrmContactDO.class)
.likeIfPresent(CrmContactDO::getName, pageReqVO.getName()) .likeIfPresent(CrmContactDO::getName, pageReqVO.getName())

View File

@ -15,7 +15,6 @@ import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
/** /**
@ -54,7 +53,7 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(),
CrmContractDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE); CrmContractDO::getId, userId, pageReqVO.getSceneType());
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmContractDO.class) query.selectAll(CrmContractDO.class)
.likeIfPresent(CrmContractDO::getNo, pageReqVO.getNo()) .likeIfPresent(CrmContractDO::getNo, pageReqVO.getNo())
@ -77,15 +76,6 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
return selectJoinPage(pageReqVO, CrmContractDO.class, query); return selectJoinPage(pageReqVO, CrmContractDO.class, query);
} }
default List<CrmContractDO> selectBatchIds(Collection<Long> ids, Long userId) {
MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>();
// 构建数据权限连表条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(), ids, userId);
// 拼接自身的查询条件
query.selectAll(CrmContractDO.class).in(CrmContractDO::getId, ids).orderByDesc(CrmContractDO::getId);
return selectJoinList(CrmContractDO.class, query);
}
default Long selectCountByContactId(Long contactId) { default Long selectCountByContactId(Long contactId) {
return selectCount(CrmContractDO::getSignContactId, contactId); return selectCount(CrmContractDO::getSignContactId, contactId);
} }
@ -98,7 +88,7 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(),
CrmContractDO::getId, userId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmContractDO::getId, userId, CrmSceneTypeEnum.OWNER.getType());
// 未审核 // 未审核
query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus()); query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus());
return selectCount(query); return selectCount(query);
@ -108,7 +98,7 @@ public interface CrmContractMapper extends BaseMapperX<CrmContractDO> {
MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmContractDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CONTRACT.getType(),
CrmContractDO::getId, userId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmContractDO::getId, userId, CrmSceneTypeEnum.OWNER.getType());
// 即将到期 // 即将到期
LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now()); LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());

View File

@ -20,7 +20,6 @@ import org.springframework.lang.Nullable;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
/** /**
@ -52,8 +51,12 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Long ownerUserId) { default PageResult<CrmCustomerDO> selectPage(CrmCustomerPageReqVO pageReqVO, Long ownerUserId) {
MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
if (Boolean.TRUE.equals(pageReqVO.getPool())) {
query.isNull(CrmCustomerDO::getOwnerUserId);
} else {
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
CrmCustomerDO::getId, ownerUserId, pageReqVO.getSceneType(), pageReqVO.getPool()); CrmCustomerDO::getId, ownerUserId, pageReqVO.getSceneType());
}
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmCustomerDO.class) query.selectAll(CrmCustomerDO.class)
.likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName()) .likeIfPresent(CrmCustomerDO::getName, pageReqVO.getName())
@ -81,15 +84,6 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
return selectJoinPage(pageReqVO, CrmCustomerDO.class, query); return selectJoinPage(pageReqVO, CrmCustomerDO.class, query);
} }
default List<CrmCustomerDO> selectBatchIds(Collection<Long> ids, Long ownerUserId) {
MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(), ids, ownerUserId);
// 拼接自身的查询条件
query.selectAll(CrmCustomerDO.class).in(CrmCustomerDO::getId, ids).orderByDesc(CrmCustomerDO::getId);
return selectJoinList(CrmCustomerDO.class, query);
}
default CrmCustomerDO selectByCustomerName(String name) { default CrmCustomerDO selectByCustomerName(String name) {
return selectOne(CrmCustomerDO::getName, name); return selectOne(CrmCustomerDO::getName, name);
} }
@ -102,9 +96,9 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
} }
default Long selectPutPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO, default Long selectPutPoolRemindCustomerCount(CrmCustomerPageReqVO pageReqVO,
CrmCustomerPoolConfigDO poolConfigDO, CrmCustomerPoolConfigDO poolConfig,
Long userId) { Long userId) {
final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfigDO, userId); final MPJLambdaWrapperX<CrmCustomerDO> query = buildPutPoolRemindCustomerQuery(pageReqVO, poolConfig, userId);
return selectCount(query); return selectCount(query);
} }
@ -114,7 +108,7 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
CrmCustomerDO::getId, ownerUserId, pageReqVO.getSceneType(), null); CrmCustomerDO::getId, ownerUserId, pageReqVO.getSceneType());
// 未锁定 + 未成交 // 未锁定 + 未成交
query.eq(CrmCustomerDO::getLockStatus, false).eq(CrmCustomerDO::getDealStatus, false); query.eq(CrmCustomerDO::getLockStatus, false).eq(CrmCustomerDO::getDealStatus, false);
@ -168,7 +162,7 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
CrmCustomerDO::getId, ownerUserId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmCustomerDO::getId, ownerUserId, CrmSceneTypeEnum.OWNER.getType());
// 今天需联系 // 今天需联系
LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now()); LocalDateTime endOfToday = LocalDateTimeUtil.endOfDay(LocalDateTime.now());
@ -180,7 +174,7 @@ public interface CrmCustomerMapper extends BaseMapperX<CrmCustomerDO> {
MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmCustomerDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_CUSTOMER.getType(),
CrmCustomerDO::getId, ownerUserId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmCustomerDO::getId, ownerUserId, CrmSceneTypeEnum.OWNER.getType());
// 未跟进 // 未跟进
query.eq(CrmClueDO::getFollowUpStatus, false); query.eq(CrmClueDO::getFollowUpStatus, false);
return selectCount(query); return selectCount(query);

View File

@ -48,7 +48,7 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
MPJLambdaWrapperX<CrmReceivableDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmReceivableDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(),
CrmReceivableDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE); CrmReceivableDO::getId, userId, pageReqVO.getSceneType());
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmReceivableDO.class) query.selectAll(CrmReceivableDO.class)
.eqIfPresent(CrmReceivableDO::getNo, pageReqVO.getNo()) .eqIfPresent(CrmReceivableDO::getNo, pageReqVO.getNo())
@ -59,20 +59,11 @@ public interface CrmReceivableMapper extends BaseMapperX<CrmReceivableDO> {
return selectJoinPage(pageReqVO, CrmReceivableDO.class, query); return selectJoinPage(pageReqVO, CrmReceivableDO.class, query);
} }
default List<CrmReceivableDO> selectBatchIds(Collection<Long> ids, Long userId) {
MPJLambdaWrapperX<CrmReceivableDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(), ids, userId);
// 拼接自身的查询条件
query.selectAll(CrmReceivableDO.class).in(CrmReceivableDO::getId, ids).orderByDesc(CrmReceivableDO::getId);
return selectJoinList(CrmReceivableDO.class, query);
}
default Long selectCountByAudit(Long userId) { default Long selectCountByAudit(Long userId) {
MPJLambdaWrapperX<CrmReceivableDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmReceivableDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE.getType(),
CrmReceivableDO::getId, userId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmReceivableDO::getId, userId, CrmSceneTypeEnum.OWNER.getType());
// 未审核 // 未审核
query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus()); query.eq(CrmContractDO::getAuditStatus, CrmAuditStatusEnum.PROCESS.getStatus());
return selectCount(query); return selectCount(query);

View File

@ -13,8 +13,6 @@ import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Objects; import java.util.Objects;
/** /**
@ -48,7 +46,7 @@ public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO
MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件 // 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(),
CrmReceivablePlanDO::getId, userId, pageReqVO.getSceneType(), Boolean.FALSE); CrmReceivablePlanDO::getId, userId, pageReqVO.getSceneType());
// 拼接自身的查询条件 // 拼接自身的查询条件
query.selectAll(CrmReceivablePlanDO.class) query.selectAll(CrmReceivablePlanDO.class)
.eqIfPresent(CrmReceivablePlanDO::getCustomerId, pageReqVO.getCustomerId()) .eqIfPresent(CrmReceivablePlanDO::getCustomerId, pageReqVO.getCustomerId())
@ -74,20 +72,11 @@ public interface CrmReceivablePlanMapper extends BaseMapperX<CrmReceivablePlanDO
return selectJoinPage(pageReqVO, CrmReceivablePlanDO.class, query); return selectJoinPage(pageReqVO, CrmReceivablePlanDO.class, query);
} }
default List<CrmReceivablePlanDO> selectBatchIds(Collection<Long> ids, Long userId) {
MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>();
// 拼接数据权限的查询条件
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), ids, userId);
// 拼接自身的查询条件
query.selectAll(CrmReceivablePlanDO.class).in(CrmReceivablePlanDO::getId, ids).orderByDesc(CrmReceivablePlanDO::getId);
return selectJoinList(CrmReceivablePlanDO.class, query);
}
default Long selectReceivablePlanCountByRemind(Long userId) { default Long selectReceivablePlanCountByRemind(Long userId) {
MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>(); MPJLambdaWrapperX<CrmReceivablePlanDO> query = new MPJLambdaWrapperX<>();
// 我负责的 + 非公海 // 我负责的 + 非公海
CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(), CrmPermissionUtils.appendPermissionCondition(query, CrmBizTypeEnum.CRM_RECEIVABLE_PLAN.getType(),
CrmReceivablePlanDO::getId, userId, CrmSceneTypeEnum.OWNER.getType(), Boolean.FALSE); CrmReceivablePlanDO::getId, userId, CrmSceneTypeEnum.OWNER.getType());
// 未回款 + 已逾期 + 今天开始提醒 // 未回款 + 已逾期 + 今天开始提醒
LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now()); LocalDateTime beginOfToday = LocalDateTimeUtil.beginOfDay(LocalDateTime.now());
query.isNull(CrmReceivablePlanDO::getReceivableId) // 未回款 query.isNull(CrmReceivablePlanDO::getReceivableId) // 未回款

View File

@ -9,8 +9,10 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum; import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission; import cn.iocoder.yudao.module.crm.framework.permission.core.annotations.CrmPermission;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService; import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.util.CrmPermissionUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint; import org.aspectj.lang.JoinPoint;
@ -38,6 +40,9 @@ public class CrmPermissionAspect {
@Resource @Resource
private CrmPermissionService crmPermissionService; private CrmPermissionService crmPermissionService;
@Resource
private AdminUserApi adminUserApi;
@Before("@annotation(crmPermission)") @Before("@annotation(crmPermission)")
public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) { public void doBefore(JoinPoint joinPoint, CrmPermission crmPermission) {
// 1.1 获取相关属性值 // 1.1 获取相关属性值
@ -65,46 +70,73 @@ public class CrmPermissionAspect {
if (CrmPermissionUtils.isCrmAdmin()) { if (CrmPermissionUtils.isCrmAdmin()) {
return; return;
} }
// 1.1 没有数据权限的情况 // 特殊没有数据权限的情况针对 READ 的特殊处理
if (CollUtil.isEmpty(bizPermissions)) { if (CollUtil.isEmpty(bizPermissions)) {
// 公海数据如果没有团队成员大家也因该有读权限才对 // 1.1 公海数据如果没有团队成员大家也应该有 READ 权限才对
if (CrmPermissionLevelEnum.isRead(permissionLevel)) { if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
return; return;
} }
// 没有数据权限的情况下超出了读权限直接报错避免后面校验空指针 // 没有数据权限的情况下超出了读权限直接报错避免后面校验空指针
throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType)); throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
} else { // 1.2 有数据权限但是没有负责人的情况 } else { // 1.2 有数据权限但是没有负责人的情况
if (!anyMatch(bizPermissions, item -> CrmPermissionLevelEnum.isOwner(item.getLevel()))) { if (!anyMatch(bizPermissions, item -> CrmPermissionLevelEnum.isOwner(item.getLevel()))
if (CrmPermissionLevelEnum.isRead(permissionLevel)) { && CrmPermissionLevelEnum.isRead(permissionLevel)) {
return; return;
} }
} }
}
// 2.1 情况一如果自己是负责人则默认有所有权限 // 2. 只考虑自的身权限
CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, permission -> ObjUtil.equal(permission.getUserId(), getUserId())); Long userId = getUserId();
CrmPermissionDO userPermission = CollUtil.findOne(bizPermissions, permission -> ObjUtil.equal(permission.getUserId(), userId));
if (userPermission != null) { if (userPermission != null) {
if (CrmPermissionLevelEnum.isOwner(userPermission.getLevel())) { if (isUserPermissionValid(userPermission, permissionLevel)) {
return; return;
} }
}
// 3. 考虑下级的权限
List<AdminUserRespDTO> subordinateUserIds = adminUserApi.getUserListBySubordinate(userId);
for (Long subordinateUserId : convertSet(subordinateUserIds, AdminUserRespDTO::getId)) {
CrmPermissionDO subordinatePermission = CollUtil.findOne(bizPermissions,
permission -> ObjUtil.equal(permission.getUserId(), subordinateUserId));
if (subordinatePermission != null && isUserPermissionValid(subordinatePermission, permissionLevel)) {
return;
}
}
// 4. 没有权限抛出异常
log.info("[doBefore][userId({}) 要求权限({}) 实际权限({}) 数据校验错误]", // 打个 info 日志方便后续排查问题审计
userId, permissionLevel, toJsonString(userPermission));
throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
}
/**
* 校验用户权限是否有效
*
* @param userPermission 用户拥有的权限
* @param permissionLevel 需要的权限级别
* @return 是否有效
*/
@SuppressWarnings("RedundantIfStatement")
private boolean isUserPermissionValid(CrmPermissionDO userPermission, Integer permissionLevel) {
// 2.1 情况一如果自己是负责人则默认有所有权限
if (CrmPermissionLevelEnum.isOwner(userPermission.getLevel())) {
return true;
}
// 2.2 情况二校验自己是否有读权限 // 2.2 情况二校验自己是否有读权限
if (CrmPermissionLevelEnum.isRead(permissionLevel)) { if (CrmPermissionLevelEnum.isRead(permissionLevel)) {
if (CrmPermissionLevelEnum.isRead(userPermission.getLevel()) // 校验当前用户是否有读权限 if (CrmPermissionLevelEnum.isRead(userPermission.getLevel()) // 校验当前用户是否有读权限
|| CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限 || CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
return; return true;
} }
} }
// 2.3 情况三校验自己是否有写权限 // 2.3 情况三校验自己是否有写权限
if (CrmPermissionLevelEnum.isWrite(permissionLevel)) { if (CrmPermissionLevelEnum.isWrite(permissionLevel)) {
if (CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限 if (CrmPermissionLevelEnum.isWrite(userPermission.getLevel())) { // 校验当前用户是否有写权限
return; return true;
} }
} }
} return false;
// 2.4 没有权限抛出异常
log.info("[doBefore][userId({}) 要求权限({}) 实际权限({}) 数据校验错误]", // 打个 info 日志方便后续排查问题审计
getUserId(), permissionLevel, toJsonString(userPermission));
throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(bizType));
} }
/** /**

View File

@ -8,8 +8,6 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.clue.CrmClueDO;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
/** /**
* 线索 Service 接口 * 线索 Service 接口
@ -57,14 +55,6 @@ public interface CrmClueService {
*/ */
CrmClueDO getClue(Long id); CrmClueDO getClue(Long id);
/**
* 获得线索列表
*
* @param ids 编号
* @return 线索列表
*/
List<CrmClueDO> getClueList(Collection<Long> ids, Long userId);
/** /**
* 获得线索分页 * 获得线索分页
* *

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.crm.service.clue; package cn.iocoder.yudao.module.crm.service.clue;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@ -32,7 +31,6 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -220,14 +218,6 @@ public class CrmClueServiceImpl implements CrmClueService {
return clueMapper.selectById(id); return clueMapper.selectById(id);
} }
@Override
public List<CrmClueDO> getClueList(Collection<Long> ids, Long userId) {
if (CollUtil.isEmpty(ids)) {
return ListUtil.empty();
}
return clueMapper.selectBatchIds(ids, userId);
}
@Override @Override
public PageResult<CrmClueDO> getCluePage(CrmCluePageReqVO pageReqVO, Long userId) { public PageResult<CrmClueDO> getCluePage(CrmCluePageReqVO pageReqVO, Long userId) {
return clueMapper.selectPage(pageReqVO, userId); return clueMapper.selectPage(pageReqVO, userId);

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.crm.util; package cn.iocoder.yudao.module.crm.util;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO; import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
@ -15,7 +14,6 @@ import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.github.yulichang.autoconfigure.MybatisPlusJoinProperties; import com.github.yulichang.autoconfigure.MybatisPlusJoinProperties;
import com.github.yulichang.wrapper.MPJLambdaWrapper; import com.github.yulichang.wrapper.MPJLambdaWrapper;
import java.util.Collection;
import java.util.List; import java.util.List;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@ -39,37 +37,33 @@ public class CrmPermissionUtils {
} }
/** /**
* 构造 CRM 数据类型数据分页查询条件 * 构造 CRM 数据类型数据分页查询条件
* *
* @param query 连表查询对象 * @param query 连表查询对象
* @param bizType 数据类型 {@link CrmBizTypeEnum} * @param bizType 数据类型 {@link CrmBizTypeEnum}
* @param bizId 数据编号 * @param bizId 数据编号
* @param userId 用户编号 * @param userId 用户编号
* @param sceneType 场景类型 * @param sceneType 场景类型
* @param pool 公海
*/ */
public static <T extends MPJLambdaWrapper<?>, S> void appendPermissionCondition(T query, Integer bizType, SFunction<S, ?> bizId, public static <T extends MPJLambdaWrapper<?>, S> void appendPermissionCondition(T query, Integer bizType, SFunction<S, ?> bizId,
Long userId, Integer sceneType, Boolean pool) { Long userId, Integer sceneType) {
MybatisPlusJoinProperties mybatisPlusJoinProperties = SpringUtil.getBean(MybatisPlusJoinProperties.class); MybatisPlusJoinProperties mybatisPlusJoinProperties = SpringUtil.getBean(MybatisPlusJoinProperties.class);
final String ownerUserIdField = mybatisPlusJoinProperties.getTableAlias() + ".owner_user_id"; final String ownerUserIdField = mybatisPlusJoinProperties.getTableAlias() + ".owner_user_id";
// 1. 构建数据权限连表条件 // 场景一我负责的数据
if (!CrmPermissionUtils.isCrmAdmin() && ObjUtil.notEqual(pool, Boolean.TRUE)) { // 管理员公海不需要数据权限
query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType)
.eq(CrmPermissionDO::getBizId, bizId) // 只能使用 SFunction 如果传 id 解析出来的 sql 不对
.eq(CrmPermissionDO::getUserId, userId));
}
// 2.1 场景一我负责的数据
if (CrmSceneTypeEnum.isOwner(sceneType)) { if (CrmSceneTypeEnum.isOwner(sceneType)) {
query.eq(ownerUserIdField, userId); query.eq(ownerUserIdField, userId);
} }
// 2.2 场景二我参与的数据 // 场景二我参与的数据我有读或写权限并且不是负责人
if (CrmSceneTypeEnum.isInvolved(sceneType)) { if (CrmSceneTypeEnum.isInvolved(sceneType)) {
if (CrmPermissionUtils.isCrmAdmin()) { // 特殊逻辑如果是超管直接查询所有不过滤数据权限
return;
}
query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType) query.innerJoin(CrmPermissionDO.class, on -> on.eq(CrmPermissionDO::getBizType, bizType)
.eq(CrmPermissionDO::getBizId, bizId) .eq(CrmPermissionDO::getBizId, bizId)
.in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel())); .in(CrmPermissionDO::getLevel, CrmPermissionLevelEnum.READ.getLevel(), CrmPermissionLevelEnum.WRITE.getLevel()));
query.ne(ownerUserIdField, userId); query.ne(ownerUserIdField, userId);
} }
// 2.3 场景三下属负责的数据 // 场景三下属负责的数据下属是负责人
if (CrmSceneTypeEnum.isSubordinate(sceneType)) { if (CrmSceneTypeEnum.isSubordinate(sceneType)) {
AdminUserApi adminUserApi = SpringUtil.getBean(AdminUserApi.class); AdminUserApi adminUserApi = SpringUtil.getBean(AdminUserApi.class);
List<AdminUserRespDTO> subordinateUsers = adminUserApi.getUserListBySubordinate(userId); List<AdminUserRespDTO> subordinateUsers = adminUserApi.getUserListBySubordinate(userId);
@ -79,30 +73,6 @@ public class CrmPermissionUtils {
query.in(ownerUserIdField, convertSet(subordinateUsers, AdminUserRespDTO::getId)); query.in(ownerUserIdField, convertSet(subordinateUsers, AdminUserRespDTO::getId));
} }
} }
// 3. 拼接公海的查询条件
if (ObjUtil.equal(pool, Boolean.TRUE)) { // 情况一公海
query.isNull(ownerUserIdField);
} else { // 情况二不是公海
query.isNotNull(ownerUserIdField);
}
}
/**
* 构造 CRM 数据类型批量数据查询条件
*
* @param query 连表查询对象
* @param bizType 数据类型 {@link CrmBizTypeEnum}
* @param bizIds 数据编号
* @param userId 用户编号
*/
public static <T extends MPJLambdaWrapper<?>> void appendPermissionCondition(T query, Integer bizType, Collection<Long> bizIds, Long userId) {
if (isCrmAdmin()) {// 管理员不需要数据权限
return;
}
query.innerJoin(CrmPermissionDO.class, on ->
on.eq(CrmPermissionDO::getBizType, bizType).in(CrmPermissionDO::getBizId, bizIds)
.eq(CollUtil.isNotEmpty(bizIds), CrmPermissionDO::getUserId, userId));
} }
} }

View File

@ -2,19 +2,20 @@ package cn.iocoder.yudao.module.infra.controller.app.file;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FileCreateReqVO;
import cn.iocoder.yudao.module.infra.controller.admin.file.vo.file.FilePresignedUrlRespVO;
import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO; import cn.iocoder.yudao.module.infra.controller.app.file.vo.AppFileUploadReqVO;
import cn.iocoder.yudao.module.infra.service.file.FileService; import cn.iocoder.yudao.module.infra.service.file.FileService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.annotation.security.PermitAll;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import jakarta.annotation.Resource;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "用户 App - 文件存储") @Tag(name = "用户 App - 文件存储")
@ -29,10 +30,25 @@ public class AppFileController {
@PostMapping("/upload") @PostMapping("/upload")
@Operation(summary = "上传文件") @Operation(summary = "上传文件")
@PermitAll
public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception { public CommonResult<String> uploadFile(AppFileUploadReqVO uploadReqVO) throws Exception {
MultipartFile file = uploadReqVO.getFile(); MultipartFile file = uploadReqVO.getFile();
String path = uploadReqVO.getPath(); String path = uploadReqVO.getPath();
return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream()))); return success(fileService.createFile(file.getOriginalFilename(), path, IoUtil.readBytes(file.getInputStream())));
} }
@GetMapping("/presigned-url")
@Operation(summary = "获取文件预签名地址", description = "模式二:前端上传文件:用于前端直接上传七牛、阿里云 OSS 等文件存储器")
@PermitAll
public CommonResult<FilePresignedUrlRespVO> getFilePresignedUrl(@RequestParam("path") String path) throws Exception {
return success(fileService.getFilePresignedUrl(path));
}
@PostMapping("/create")
@Operation(summary = "创建文件", description = "模式二:前端上传文件:配合 presigned-url 接口,记录上传了上传的文件")
@PermitAll
public CommonResult<Long> createFile(@Valid @RequestBody FileCreateReqVO createReqVO) {
return success(fileService.createFile(createReqVO));
}
} }

View File

@ -13,7 +13,7 @@ public interface CodegenColumnMapper extends BaseMapperX<CodegenColumnDO> {
default List<CodegenColumnDO> selectListByTableId(Long tableId) { default List<CodegenColumnDO> selectListByTableId(Long tableId) {
return selectList(new LambdaQueryWrapperX<CodegenColumnDO>() return selectList(new LambdaQueryWrapperX<CodegenColumnDO>()
.eq(CodegenColumnDO::getTableId, tableId) .eq(CodegenColumnDO::getTableId, tableId)
.orderByAsc(CodegenColumnDO::getId)); .orderByAsc(CodegenColumnDO::getOrdinalPosition));
} }
default void deleteListByTableId(Long tableId) { default void deleteListByTableId(Long tableId) {

View File

@ -34,4 +34,10 @@ public class CodegenProperties {
@NotNull(message = "代码生成的前端类型不能为空") @NotNull(message = "代码生成的前端类型不能为空")
private Integer frontType; private Integer frontType;
/**
* 是否生成单元测试
*/
@NotNull(message = "是否生成单元测试不能为空")
private Boolean unitTestEnable;
} }

View File

@ -22,13 +22,14 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import jakarta.annotation.Resource;
import java.util.*; import java.util.*;
import java.util.function.BiPredicate; import java.util.function.BiPredicate;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
@ -179,11 +180,18 @@ public class CodegenServiceImpl implements CodegenService {
&& tableField.getMetaInfo().isNullable() == codegenColumn.getNullable() && tableField.getMetaInfo().isNullable() == codegenColumn.getNullable()
&& tableField.isKeyFlag() == codegenColumn.getPrimaryKey() && tableField.isKeyFlag() == codegenColumn.getPrimaryKey()
&& tableField.getComment().equals(codegenColumn.getColumnComment()); && tableField.getComment().equals(codegenColumn.getColumnComment());
Set<String> modifyFieldNames = tableFields.stream() Set<String> modifyFieldNames = IntStream.range(0, tableFields.size()).mapToObj(index -> {
.filter(tableField -> codegenColumnDOMap.get(tableField.getColumnName()) != null TableField tableField = tableFields.get(index);
&& !primaryKeyPredicate.test(tableField, codegenColumnDOMap.get(tableField.getColumnName()))) String columnName = tableField.getColumnName();
.map(TableField::getColumnName) CodegenColumnDO codegenColumn = codegenColumnDOMap.get(columnName);
.collect(Collectors.toSet()); if (codegenColumn == null) {
return null;
}
if (!primaryKeyPredicate.test(tableField, codegenColumn) || codegenColumn.getOrdinalPosition() != index) {
return columnName;
}
return null;
}).filter(Objects::nonNull).collect(Collectors.toSet());
// 3.2 计算需要删除的字段 // 3.2 计算需要删除的字段
Set<String> tableFieldNames = convertSet(tableFields, TableField::getName); Set<String> tableFieldNames = convertSet(tableFields, TableField::getName);
Set<Long> deleteColumnIds = codegenColumns.stream() Set<Long> deleteColumnIds = codegenColumns.stream()

View File

@ -398,6 +398,11 @@ public class CodegenEngine {
Map<String, String> templates = new LinkedHashMap<>(); Map<String, String> templates = new LinkedHashMap<>();
templates.putAll(SERVER_TEMPLATES); templates.putAll(SERVER_TEMPLATES);
templates.putAll(FRONT_TEMPLATES.row(frontType)); templates.putAll(FRONT_TEMPLATES.row(frontType));
// 如果禁用单元测试则移除对应的模版
if (Boolean.FALSE.equals(codegenProperties.getUnitTestEnable())) {
templates.remove(javaTemplatePath("test/serviceTest"));
templates.remove("codegen/sql/h2.vm");
}
return templates; return templates;
} }

View File

@ -5,7 +5,6 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils; import cn.iocoder.yudao.framework.mybatis.core.util.JdbcUtils;
import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO; import cn.iocoder.yudao.module.infra.dal.dataobject.db.DataSourceConfigDO;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig;
@ -18,7 +17,6 @@ import org.springframework.stereotype.Service;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -49,12 +47,11 @@ public class DatabaseTableServiceImpl implements DatabaseTableService {
// 获得数据源配置 // 获得数据源配置
DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId); DataSourceConfigDO config = dataSourceConfigService.getDataSourceConfig(dataSourceConfigId);
Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId); Assert.notNull(config, "数据源({}) 不存在!", dataSourceConfigId);
DbType dbType = JdbcUtils.getDbType(config.getUrl());
// 使用 MyBatis Plus Generator 解析表结构 // 使用 MyBatis Plus Generator 解析表结构
DataSourceConfig.Builder dataSourceConfigBuilder = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(), DataSourceConfig.Builder dataSourceConfigBuilder = new DataSourceConfig.Builder(config.getUrl(), config.getUsername(),
config.getPassword()); config.getPassword());
if (Objects.equals(dbType, DbType.SQL_SERVER)) { // 特殊SQLServer jdbc 非标准参见 https://github.com/baomidou/mybatis-plus/issues/5419 if (JdbcUtils.isSQLServer(config.getUrl())) { // 特殊SQLServer jdbc 非标准参见 https://github.com/baomidou/mybatis-plus/issues/5419
dataSourceConfigBuilder.databaseQueryClass(SQLQuery.class); dataSourceConfigBuilder.databaseQueryClass(SQLQuery.class);
} }
StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图业务上一般用不到 StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder().enableSkipView(); // 忽略视图业务上一般用不到

View File

@ -24,12 +24,11 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.generator.config.po.TableField; import com.baomidou.mybatisplus.generator.config.po.TableField;
import com.baomidou.mybatisplus.generator.config.po.TableInfo; import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import org.junit.jupiter.api.Disabled; import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import jakarta.annotation.Resource;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
@ -234,17 +233,16 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
} }
@Test @Test
@Disabled // TODO @芋艿这个单测会随机性失败需要定位下
public void testSyncCodegenFromDB() { public void testSyncCodegenFromDB() {
// mock 数据CodegenTableDO // mock 数据CodegenTableDO
CodegenTableDO table = randomPojo(CodegenTableDO.class, o -> o.setTableName("t_yunai") CodegenTableDO table = randomPojo(CodegenTableDO.class, o -> o.setTableName("t_yunai")
.setDataSourceConfigId(1L).setScene(CodegenSceneEnum.ADMIN.getScene())); .setDataSourceConfigId(1L).setScene(CodegenSceneEnum.ADMIN.getScene()));
codegenTableMapper.insert(table); codegenTableMapper.insert(table);
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()) CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setColumnName("id")); .setColumnName("id").setPrimaryKey(true).setOrdinalPosition(0));
codegenColumnMapper.insert(column01); codegenColumnMapper.insert(column01);
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId()) CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setColumnName("name")); .setColumnName("name").setOrdinalPosition(1));
codegenColumnMapper.insert(column02); codegenColumnMapper.insert(column02);
// 准备参数 // 准备参数
Long tableId = table.getId(); Long tableId = table.getId();
@ -263,7 +261,7 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
when(databaseTableService.getTable(eq(1L), eq("t_yunai"))) when(databaseTableService.getTable(eq(1L), eq("t_yunai")))
.thenReturn(tableInfo); .thenReturn(tableInfo);
// mock 方法CodegenTableDO // mock 方法CodegenTableDO
List<CodegenColumnDO> newColumns = randomPojoList(CodegenColumnDO.class); List<CodegenColumnDO> newColumns = randomPojoList(CodegenColumnDO.class, 2);
when(codegenBuilder.buildColumns(eq(table.getId()), argThat(tableFields -> { when(codegenBuilder.buildColumns(eq(table.getId()), argThat(tableFields -> {
assertEquals(2, tableFields.size()); assertEquals(2, tableFields.size());
assertSame(tableInfo.getFields(), tableFields); assertSame(tableInfo.getFields(), tableFields);
@ -457,9 +455,11 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
.setTemplateType(CodegenTemplateTypeEnum.ONE.getType())); .setTemplateType(CodegenTemplateTypeEnum.ONE.getType()));
codegenTableMapper.insert(table); codegenTableMapper.insert(table);
// mock 数据CodegenColumnDO // mock 数据CodegenColumnDO
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())); CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(1));
codegenColumnMapper.insert(column01); codegenColumnMapper.insert(column01);
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())); CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(2));
codegenColumnMapper.insert(column02); codegenColumnMapper.insert(column02);
// mock 执行生成 // mock 执行生成
Map<String, String> codes = MapUtil.of(randomString(), randomString()); Map<String, String> codes = MapUtil.of(randomString(), randomString());
@ -486,9 +486,11 @@ public class CodegenServiceImplTest extends BaseDbUnitTest {
.setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType())); .setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType()));
codegenTableMapper.insert(table); codegenTableMapper.insert(table);
// mock 数据CodegenColumnDO // mock 数据CodegenColumnDO
CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())); CodegenColumnDO column01 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(1));
codegenColumnMapper.insert(column01); codegenColumnMapper.insert(column01);
CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())); CodegenColumnDO column02 = randomPojo(CodegenColumnDO.class, o -> o.setTableId(table.getId())
.setOrdinalPosition(2));
codegenColumnMapper.insert(column02); codegenColumnMapper.insert(column02);
// mock 数据sub CodegenTableDO // mock 数据sub CodegenTableDO
CodegenTableDO subTable = randomPojo(CodegenTableDO.class, CodegenTableDO subTable = randomPojo(CodegenTableDO.class,

25
yudao-module-iot/pom.xml Normal file
View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modules>
<module>yudao-module-iot-api</module>
<module>yudao-module-iot-biz</module>
</modules>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-iot</artifactId>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>
物联网模块
<!-- TODO 芋艿:需要补充下说明! -->
</description>
</project>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-iot</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>yudao-module-iot-api</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
物联网 模块 API暴露给其它模块调用
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,6 @@
/**
* 占位
*
* TODO 芋艿后续删除
*/
package cn.iocoder.yudao.module.iot.api;

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.iot.enums;
import cn.iocoder.yudao.framework.common.exception.ErrorCode;
/**
* iot 错误码枚举类
* <p>
* iot 系统使用 1-050-000-000
*/
public interface ErrorCodeConstants {
// ========== IoT 产品相关 1-050-001-000 ============
ErrorCode PRODUCT_NOT_EXISTS = new ErrorCode(1_050_001_000, "产品不存在");
ErrorCode PRODUCT_IDENTIFICATION_EXISTS = new ErrorCode(1_050_001_001, "产品标识已经存在");
ErrorCode PRODUCT_STATUS_NOT_DELETE = new ErrorCode(1_050_001_002, "产品状是发布状态,不允许删除");
// ========== IoT 产品物模型 1-050-002-000 ============
ErrorCode THINK_MODEL_FUNCTION_NOT_EXISTS = new ErrorCode(1_050_002_000, "产品物模型不存在");
ErrorCode THINK_MODEL_FUNCTION_EXISTS_BY_PRODUCT_KEY = new ErrorCode(1_050_002_001, "ProductKey 对应的产品物模型已存在");
ErrorCode THINK_MODEL_FUNCTION_IDENTIFIER_EXISTS = new ErrorCode(1_050_002_002, "存在重复的功能标识符。");
ErrorCode THINK_MODEL_FUNCTION_NAME_EXISTS = new ErrorCode(1_050_002_003, "存在重复的功能名称。");
ErrorCode THINK_MODEL_FUNCTION_IDENTIFIER_INVALID = new ErrorCode(1_050_002_003, "产品物模型标识无效");
// ========== IoT 设备 1-050-003-000 ============
ErrorCode DEVICE_NOT_EXISTS = new ErrorCode(1_050_003_000, "设备不存在");
ErrorCode DEVICE_NAME_EXISTS = new ErrorCode(1_050_003_001, "设备名称在同一产品下必须唯一");
ErrorCode DEVICE_HAS_CHILDREN = new ErrorCode(1_050_003_002, "有子设备,不允许删除");
ErrorCode DEVICE_NAME_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_003, "设备名称不能修改");
ErrorCode DEVICE_PRODUCT_CANNOT_BE_MODIFIED = new ErrorCode(1_050_003_004, "产品不能修改");
ErrorCode DEVICE_INVALID_DEVICE_STATUS = new ErrorCode(1_050_003_005, "无效的设备状态");
}

View File

@ -0,0 +1,55 @@
package cn.iocoder.yudao.module.iot.enums.device;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.Getter;
import java.util.Arrays;
/**
* IoT 设备状态枚举
*
* @author haohao
*/
@Getter
public enum IotDeviceStatusEnum implements IntArrayValuable {
INACTIVE(0, "未激活"),
ONLINE(1, "在线"),
OFFLINE(2, "离线"),
DISABLED(3, "已禁用");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotDeviceStatusEnum::getStatus).toArray();
/**
* 状态
*/
private final Integer status;
/**
* 状态名
*/
private final String name;
IotDeviceStatusEnum(Integer status, String name) {
this.status = status;
this.name = name;
}
public static IotDeviceStatusEnum fromStatus(Integer status) {
for (IotDeviceStatusEnum value : values()) {
if (value.getStatus().equals(status)) {
return value;
}
}
return null;
}
public static boolean isValidStatus(Integer status) {
return fromStatus(status) != null;
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.iot.enums.product;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* IOT 访问方式枚举类
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotAccessModeEnum {
READ("r"),
WRITE("w"),
READ_WRITE("rw");
private final String mode;
}

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 产品数据格式枚举类
*
* @author ahh
* @see <a href="https://help.aliyun.com/zh/iot/user-guide/message-parsing">阿里云 - 什么是消息解析</a>
*/
@AllArgsConstructor
@Getter
public enum IotDataFormatEnum implements IntArrayValuable {
JSON(0, "标准数据格式JSON"),
CUSTOMIZE(1, "透传/自定义");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotDataFormatEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IOT 联网方式枚举类
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotNetTypeEnum implements IntArrayValuable {
WIFI(0, "Wi-Fi"),
CELLULAR(1, "Cellular"),
ETHERNET(2, "Ethernet"),
OTHER(3, "其他");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotNetTypeEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IOT 产品的设备类型
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotProductDeviceTypeEnum implements IntArrayValuable {
DIRECT(0, "直连设备"),
GATEWAY_CHILD(1, "网关子设备"),
GATEWAY(2, "网关设备");
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotProductDeviceTypeEnum::getType).toArray();
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IOT 产品功能物模型类型枚举类
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotProductFunctionTypeEnum implements IntArrayValuable {
PROPERTY(1, "属性"),
SERVICE(2, "服务"),
EVENT(3, "事件");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotProductFunctionTypeEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IOT 产品的状态枚举类
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotProductStatusEnum implements IntArrayValuable {
UNPUBLISHED(0, "开发中"),
PUBLISHED(1, "已发布");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotProductStatusEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IOT 接入网关协议枚举类
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotProtocolTypeEnum implements IntArrayValuable {
CUSTOM(0, "自定义"),
MODBUS(1, "Modbus"),
OPC_UA(2, "OPC UA"),
ZIGBEE(3, "ZigBee"),
BLE(4, "BLE");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotProtocolTypeEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.iot.enums.product;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* IOT 数据校验级别枚举类
*
* @author ahh
*/
@AllArgsConstructor
@Getter
public enum IotValidateTypeEnum implements IntArrayValuable {
WEAK(0, "弱校验"),
NONE(1, "免校验");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(IotValidateTypeEnum::getType).toArray();
/**
* 类型
*/
private final Integer type;
/**
* 描述
*/
private final String description;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,64 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>yudao-module-iot</artifactId>
<groupId>cn.iocoder.boot</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>yudao-module-iot-biz</artifactId>
<name>${project.artifactId}</name>
<description>
物联网 模块,主要实现 产品管理、设备管理、协议管理等功能。
<!-- TODO 芋艿:后续补充下 -->
</description>
<dependencies>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-iot-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-mybatis</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-test</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-excel</artifactId>
</dependency>
<!-- MQTT -->
<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.iot.controller.admin.device;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDevicePageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceSaveReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.IotDeviceStatusUpdateReqVO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.IotDeviceDO;
import cn.iocoder.yudao.module.iot.service.device.IotDeviceService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - IoT 设备")
@RestController
@RequestMapping("/iot/device")
@Validated
public class IotDeviceController {
@Resource
private IotDeviceService deviceService;
@PostMapping("/create")
@Operation(summary = "创建设备")
@PreAuthorize("@ss.hasPermission('iot:device:create')")
public CommonResult<Long> createDevice(@Valid @RequestBody IotDeviceSaveReqVO createReqVO) {
return success(deviceService.createDevice(createReqVO));
}
@PutMapping("/update-status")
@Operation(summary = "更新设备状态")
@PreAuthorize("@ss.hasPermission('iot:device:update')")
public CommonResult<Boolean> updateDeviceStatus(@Valid @RequestBody IotDeviceStatusUpdateReqVO updateReqVO) {
deviceService.updateDeviceStatus(updateReqVO);
return success(true);
}
@PutMapping("/update")
@Operation(summary = "更新设备")
@PreAuthorize("@ss.hasPermission('iot:device:update')")
public CommonResult<Boolean> updateDevice(@Valid @RequestBody IotDeviceSaveReqVO updateReqVO) {
deviceService.updateDevice(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除设备")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('iot:device:delete')")
public CommonResult<Boolean> deleteDevice(@RequestParam("id") Long id) {
deviceService.deleteDevice(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得设备")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<IotDeviceRespVO> getDevice(@RequestParam("id") Long id) {
IotDeviceDO device = deviceService.getDevice(id);
return success(BeanUtils.toBean(device, IotDeviceRespVO.class));
}
@GetMapping("/page")
@Operation(summary = "获得设备分页")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<PageResult<IotDeviceRespVO>> getDevicePage(@Valid IotDevicePageReqVO pageReqVO) {
PageResult<IotDeviceDO> pageResult = deviceService.getDevicePage(pageReqVO);
return success(BeanUtils.toBean(pageResult, IotDeviceRespVO.class));
}
@GetMapping("/count")
@Operation(summary = "获得设备数量")
@Parameter(name = "productId", description = "产品编号", example = "1")
@PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<Long> getDeviceCount(@RequestParam("productId") Long productId) {
return success(deviceService.getDeviceCountByProductId(productId));
}
}

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.iot.controller.admin.device.vo;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.iot.enums.device.IotDeviceStatusEnum;
import cn.iocoder.yudao.module.iot.enums.product.IotProductDeviceTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.format.annotation.DateTimeFormat;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@Schema(description = "管理后台 - IoT 设备分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class IotDevicePageReqVO extends PageParam {
// TODO @芋艿需要去掉一些多余的字段
@Schema(description = "设备唯一标识符", example = "24602")
private String deviceKey;
@Schema(description = "设备名称", example = "王五")
private String deviceName;
@Schema(description = "备注名称", example = "张三")
private String nickname;
@Schema(description = "产品编号", example = "26202")
private Long productId;
@Schema(description = "产品标识")
private String productKey;
@Schema(description = "设备类型", example = "1")
@InEnum(IotProductDeviceTypeEnum.class)
private Integer deviceType;
@Schema(description = "网关设备 ID", example = "16380")
private Long gatewayId;
@Schema(description = "设备状态", example = "1")
@InEnum(IotDeviceStatusEnum.class)
private Integer status;
@Schema(description = "设备状态最后更新时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] statusLastUpdateTime;
@Schema(description = "最后上线时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] lastOnlineTime;
@Schema(description = "最后离线时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] lastOfflineTime;
@Schema(description = "设备激活时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] activeTime;
@Schema(description = "设备密钥,用于设备认证,需安全存储")
private String deviceSecret;
@Schema(description = "MQTT 客户端 ID", example = "24602")
private String mqttClientId;
@Schema(description = "MQTT 用户名", example = "芋艿")
private String mqttUsername;
@Schema(description = "MQTT 密码")
private String mqttPassword;
@Schema(description = "认证类型(如一机一密、动态注册)", example = "2")
private String authType;
@Schema(description = "创建时间")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;
}

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