Compare commits

..

2 Commits

Author SHA1 Message Date
Cursor Agent
a9222d9749 Add tenant-aware WebSocket message sender and AOP support 2025-06-16 07:15:51 +00:00
Cursor Agent
e454bfd959 Add WebSocket methods for sending messages to specific tenants 2025-06-16 06:24:54 +00:00
66 changed files with 931 additions and 707 deletions

13
pom.xml
View File

@@ -41,9 +41,7 @@
<maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version> <maven-surefire-plugin.version>3.2.2</maven-surefire-plugin.version>
<maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.14.0</maven-compiler-plugin.version>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- maven-surefire-plugin 暂时无法通过 bom 的依赖读取(兼容老版本 IDEA 2024 及以前版本) --> <!-- 看看咋放到 bom 里 -->
<lombok.version>1.18.38</lombok.version>
<spring.boot.version>2.7.18</spring.boot.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> </properties>
@@ -81,19 +79,10 @@
<path> <path>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId> <artifactId>spring-boot-configuration-processor</artifactId>
<version>${spring.boot.version}</version>
</path> </path>
<path> <path>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<!-- 确保 Lombok 生成的 getter/setter 方法能被 MapStruct 正确识别,
避免出现 No property named “xxx" exists 的编译错误 -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path> </path>
<path> <path>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>

View File

@@ -1,208 +1,253 @@
-- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql -- ----------------------------
-- Thanks to Patrick Lightbody for submitting this... -- qrtz_blob_triggers
-- -- ----------------------------
-- In your Quartz properties file, you'll need to set CREATE TABLE qrtz_blob_triggers
-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate
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_SIMPLE_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS;
DROP TABLE IF EXISTS QRTZ_SIMPROP_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;
CREATE TABLE QRTZ_JOB_DETAILS
( (
SCHED_NAME VARCHAR(120) NOT NULL, sched_name varchar(120) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL, trigger_name varchar(190) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL, trigger_group varchar(190) NOT NULL,
DESCRIPTION VARCHAR(250) NULL, blob_data bytea NULL,
JOB_CLASS_NAME VARCHAR(250) NOT NULL, PRIMARY KEY (sched_name, trigger_name, trigger_group)
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 CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group);
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
JOB_NAME VARCHAR(200) NOT NULL,
JOB_GROUP VARCHAR(200) NOT NULL,
DESCRIPTION VARCHAR(250) NULL,
NEXT_FIRE_TIME BIGINT NULL,
PREV_FIRE_TIME BIGINT NULL,
PRIORITY INTEGER NULL,
TRIGGER_STATE VARCHAR(16) NOT NULL,
TRIGGER_TYPE VARCHAR(8) NOT NULL,
START_TIME BIGINT NOT NULL,
END_TIME BIGINT NULL,
CALENDAR_NAME VARCHAR(200) 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 -- ----------------------------
-- qrtz_calendars
-- ----------------------------
CREATE TABLE qrtz_calendars
( (
SCHED_NAME VARCHAR(120) NOT NULL, sched_name varchar(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL, calendar_name varchar(190) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL, calendar bytea NOT NULL,
REPEAT_COUNT BIGINT NOT NULL, PRIMARY KEY (sched_name, calendar_name)
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)
);
CREATE TABLE QRTZ_CRON_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
CRON_EXPRESSION VARCHAR(120) NOT NULL,
TIME_ZONE_ID VARCHAR(80),
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)
);
CREATE TABLE QRTZ_SIMPROP_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
STR_PROP_1 VARCHAR(512) NULL,
STR_PROP_2 VARCHAR(512) NULL,
STR_PROP_3 VARCHAR(512) NULL,
INT_PROP_1 INT NULL,
INT_PROP_2 INT NULL,
LONG_PROP_1 BIGINT NULL,
LONG_PROP_2 BIGINT NULL,
DEC_PROP_1 NUMERIC(13, 4) NULL,
DEC_PROP_2 NUMERIC(13, 4) NULL,
BOOL_PROP_1 BOOL NULL,
BOOL_PROP_2 BOOL 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)
);
CREATE TABLE QRTZ_BLOB_TRIGGERS
(
SCHED_NAME VARCHAR(120) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) 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)
);
CREATE TABLE QRTZ_CALENDARS
(
SCHED_NAME VARCHAR(120) NOT NULL,
CALENDAR_NAME VARCHAR(200) NOT NULL,
CALENDAR BYTEA NOT NULL,
PRIMARY KEY (SCHED_NAME, CALENDAR_NAME)
); );
CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS -- ----------------------------
-- qrtz_cron_triggers
-- ----------------------------
CREATE TABLE qrtz_cron_triggers
( (
SCHED_NAME VARCHAR(120) NOT NULL, sched_name varchar(120) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL, trigger_name varchar(190) NOT NULL,
PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) 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)
); );
CREATE TABLE QRTZ_FIRED_TRIGGERS -- @formatter:off
( BEGIN;
SCHED_NAME VARCHAR(120) NOT NULL,
ENTRY_ID VARCHAR(95) NOT NULL,
TRIGGER_NAME VARCHAR(200) NOT NULL,
TRIGGER_GROUP VARCHAR(200) NOT NULL,
INSTANCE_NAME VARCHAR(200) NOT NULL,
FIRED_TIME BIGINT NOT NULL,
SCHED_TIME BIGINT NOT NULL,
PRIORITY INTEGER NOT NULL,
STATE VARCHAR(16) NOT NULL,
JOB_NAME VARCHAR(200) NULL,
JOB_GROUP VARCHAR(200) NULL,
IS_NONCONCURRENT BOOL NULL,
REQUESTS_RECOVERY BOOL NULL,
PRIMARY KEY (SCHED_NAME, ENTRY_ID)
);
CREATE TABLE QRTZ_SCHEDULER_STATE
(
SCHED_NAME VARCHAR(120) NOT NULL,
INSTANCE_NAME VARCHAR(200) 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 VARCHAR(120) NOT NULL,
LOCK_NAME VARCHAR(40) NOT NULL,
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);
COMMIT; 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

@@ -18,7 +18,7 @@
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.framework.version>5.3.39</spring.framework.version> <spring.framework.version>5.3.39</spring.framework.version>
<spring.security.version>5.8.16</spring.security.version> <spring.security.version>5.8.14</spring.security.version>
<spring.boot.version>2.7.18</spring.boot.version> <spring.boot.version>2.7.18</spring.boot.version>
<!-- Web 相关 --> <!-- Web 相关 -->
<springdoc.version>1.8.0</springdoc.version> <springdoc.version>1.8.0</springdoc.version>
@@ -53,7 +53,7 @@
<!-- 工具类相关 --> <!-- 工具类相关 -->
<anji-plus-captcha.version>1.4.0</anji-plus-captcha.version> <anji-plus-captcha.version>1.4.0</anji-plus-captcha.version>
<jsoup.version>1.18.3</jsoup.version> <jsoup.version>1.18.3</jsoup.version>
<lombok.version>1.18.38</lombok.version> <lombok.version>1.18.36</lombok.version>
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<hutool.version>5.8.35</hutool.version> <hutool.version>5.8.35</hutool.version>
<easyexcel.version>4.0.3</easyexcel.version> <easyexcel.version>4.0.3</easyexcel.version>
@@ -76,8 +76,7 @@
<awssdk.version>2.30.14</awssdk.version> <awssdk.version>2.30.14</awssdk.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version> <justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>2.1.0</jimureport.version> <jimureport.version>1.9.4</jimureport.version>
<jimubi.version>1.9.5</jimubi.version>
<weixin-java.version>4.7.5.B</weixin-java.version> <weixin-java.version>4.7.5.B</weixin-java.version>
<!-- 专属于 JDK8 安全漏洞升级 --> <!-- 专属于 JDK8 安全漏洞升级 -->
<logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 --> <logback.version>1.2.13</logback.version> <!-- 无法使用 1.3.X 版本,启动会报错 -->
@@ -627,7 +626,7 @@
<dependency> <dependency>
<groupId>org.jeecgframework.jimureport</groupId> <groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot-starter</artifactId> <artifactId>jimubi-spring-boot-starter</artifactId>
<version>${jimubi.version}</version> <version>${jimureport.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>com.github.jsqlparser</groupId> <groupId>com.github.jsqlparser</groupId>

View File

@@ -7,7 +7,6 @@ import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
/** /**
* 异步任务 Configuration * 异步任务 Configuration
@@ -22,21 +21,14 @@ public class YudaoAsyncAutoConfiguration {
@Override @Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 处理 ThreadPoolTaskExecutor if (!(bean instanceof ThreadPoolTaskExecutor)) {
if (bean instanceof ThreadPoolTaskExecutor) { return bean;
}
// 修改提交的任务,接入 TransmittableThreadLocal
ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean;
executor.setTaskDecorator(TtlRunnable::get); executor.setTaskDecorator(TtlRunnable::get);
return executor; return executor;
} }
// 处理 SimpleAsyncTaskExecutor
// 参考 https://t.zsxq.com/CBoks 增加
if (bean instanceof SimpleAsyncTaskExecutor) {
SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean;
executor.setTaskDecorator(TtlRunnable::get);
return executor;
}
return bean;
}
}; };
} }

View File

@@ -24,8 +24,8 @@
<!-- Web 相关 --> <!-- Web 相关 -->
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId> <artifactId>yudao-spring-boot-starter-web</artifactId>
<scope>provided</scope> <!-- 设置为 provided只有 DefaultDBFieldHandler 使用到 --> <scope>provided</scope> <!-- 设置为 provided只有 OncePerRequestFilter 使用到 -->
</dependency> </dependency>
<!-- DB 相关 --> <!-- DB 相关 -->

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.framework.mybatis.core.handler; package cn.iocoder.yudao.framework.mybatis.core.handler;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler; import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject; import org.apache.ibatis.reflection.MetaObject;
@@ -32,7 +32,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
baseDO.setUpdateTime(current); baseDO.setUpdateTime(current);
} }
Long userId = SecurityFrameworkUtils.getLoginUserId(); Long userId = WebFrameworkUtils.getLoginUserId();
// 当前登录用户不为空,创建人为空,则当前登录用户为创建人 // 当前登录用户不为空,创建人为空,则当前登录用户为创建人
if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) {
baseDO.setCreator(userId.toString()); baseDO.setCreator(userId.toString());
@@ -54,7 +54,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler {
// 当前登录用户不为空,更新人为空,则当前登录用户为更新人 // 当前登录用户不为空,更新人为空,则当前登录用户为更新人
Object modifier = getFieldValByName("updater", metaObject); Object modifier = getFieldValByName("updater", metaObject);
Long userId = SecurityFrameworkUtils.getLoginUserId(); Long userId = WebFrameworkUtils.getLoginUserId();
if (Objects.nonNull(userId) && Objects.isNull(modifier)) { if (Objects.nonNull(userId) && Objects.isNull(modifier)) {
setFieldValByName("updater", userId.toString(), metaObject); setFieldValByName("updater", userId.toString(), metaObject);
} }

View File

@@ -42,7 +42,6 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) { default PageResult<T> selectPage(PageParam pageParam, Collection<SortingField> sortingFields, @Param("ew") Wrapper<T> queryWrapper) {
// 特殊:不分页,直接查询全部 // 特殊:不分页,直接查询全部
if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) {
MyBatisUtils.addOrder(queryWrapper, sortingFields);
List<T> list = selectList(queryWrapper); List<T> list = selectList(queryWrapper);
return new PageResult<>(list, (long) list.size()); return new PageResult<>(list, (long) list.size());
} }

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.framework.mybatis.core.util; package cn.iocoder.yudao.framework.mybatis.core.util;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.lang.func.Func1; import cn.hutool.core.lang.func.Func1;
import cn.hutool.core.lang.func.LambdaUtil; import cn.hutool.core.lang.func.LambdaUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
@@ -8,8 +8,6 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.SortingField; import cn.iocoder.yudao.framework.common.pojo.SortingField;
import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum; import cn.iocoder.yudao.framework.mybatis.core.enums.DbTypeEnum;
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.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.OrderItem; import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.core.toolkit.StringPool; import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
@@ -22,6 +20,7 @@ import net.sf.jsqlparser.schema.Table;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.stream.Collectors;
/** /**
* MyBatis 工具类 * MyBatis 工具类
@@ -38,27 +37,15 @@ public class MyBatisUtils {
// 页码 + 数量 // 页码 + 数量
Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); Page<T> page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize());
// 排序字段 // 排序字段
if (CollUtil.isNotEmpty(sortingFields)) { if (!CollectionUtil.isEmpty(sortingFields)) {
for (SortingField sortingField : sortingFields) { page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder())
page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder())) ? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField()))
.setColumn(StrUtil.toUnderlineCase(sortingField.getField()))); : OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField())))
} .collect(Collectors.toList()));
} }
return page; return page;
} }
public static <T> void addOrder(Wrapper<T> wrapper, Collection<SortingField> sortingFields) {
if (CollUtil.isEmpty(sortingFields)) {
return;
}
QueryWrapper<T> query = (QueryWrapper<T>) wrapper;
for (SortingField sortingField : sortingFields) {
query.orderBy(true,
SortingField.ORDER_ASC.equals(sortingField.getOrder()),
StrUtil.toUnderlineCase(sortingField.getField()));
}
}
/** /**
* 将拦截器添加到链中 * 将拦截器添加到链中
* 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置

View File

@@ -126,11 +126,9 @@ public class SecurityFrameworkUtils {
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
// 原因是Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 // 原因是Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
if (request != null) {
WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
} }
}
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
// 创建 UsernamePasswordAuthenticationToken 对象 // 创建 UsernamePasswordAuthenticationToken 对象

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.framework.websocket.config;
import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration; import cn.iocoder.yudao.framework.mq.redis.config.YudaoRedisMQConsumerAutoConfiguration;
import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate; import cn.iocoder.yudao.framework.mq.redis.core.RedisMQTemplate;
import cn.iocoder.yudao.framework.websocket.core.aop.TenantWebSocketAspect;
import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler; import cn.iocoder.yudao.framework.websocket.core.handler.JsonWebSocketMessageHandler;
import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener; import cn.iocoder.yudao.framework.websocket.core.listener.WebSocketMessageListener;
import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor; import cn.iocoder.yudao.framework.websocket.core.security.LoginUserHandshakeInterceptor;
@@ -15,6 +16,8 @@ import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMess
import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender; import cn.iocoder.yudao.framework.websocket.core.sender.redis.RedisWebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer; import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageConsumer;
import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender; import cn.iocoder.yudao.framework.websocket.core.sender.rocketmq.RocketMQWebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.sender.TenantWebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator; import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionHandlerDecorator;
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager; import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManager;
import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl; import cn.iocoder.yudao.framework.websocket.core.session.WebSocketSessionManagerImpl;
@@ -27,6 +30,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.web.socket.WebSocketHandler; import org.springframework.web.socket.WebSocketHandler;
import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.config.annotation.EnableWebSocket;
@@ -82,10 +86,31 @@ public class YudaoWebSocketAutoConfiguration {
return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties); return new WebSocketAuthorizeRequestsCustomizer(webSocketProperties);
} }
/**
* 创建租户感知的WebSocket消息发送器
* 自动包装实际的发送器,提供租户隔离功能
*/
@Bean
@Primary
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true)
public TenantWebSocketMessageSender tenantWebSocketMessageSender(WebSocketMessageSender webSocketMessageSender) {
return new TenantWebSocketMessageSender(webSocketMessageSender);
}
/**
* 创建租户WebSocket AOP切面
* 自动处理带有 @TenantAware 注解的方法
*/
@Bean
@ConditionalOnProperty(prefix = "yudao.tenant", value = "enable", matchIfMissing = true)
public TenantWebSocketAspect tenantWebSocketAspect() {
return new TenantWebSocketAspect();
}
// ==================== Sender 相关 ==================== // ==================== Sender 相关 ====================
@Configuration @Configuration
@ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local") @ConditionalOnProperty(prefix = "yudao.websocket", name = "sender-type", havingValue = "local", matchIfMissing = true)
public class LocalWebSocketMessageSenderConfiguration { public class LocalWebSocketMessageSenderConfiguration {
@Bean @Bean

View File

@@ -0,0 +1,30 @@
package cn.iocoder.yudao.framework.websocket.core.annotation;
import java.lang.annotation.*;
/**
* 租户感知注解
*
* 用于标记需要进行租户隔离处理的WebSocket消息发送方法
*
* @author 芋道源码
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface TenantAware {
/**
* 租户ID参数名
* 当方法参数中包含租户ID时指定参数名
*/
String tenantIdParam() default "tenantId";
/**
* 是否强制要求租户上下文
* 如果为true当没有租户上下文时会抛出异常
* 如果为false当没有租户上下文时会忽略消息发送
*/
boolean required() default false;
}

View File

@@ -0,0 +1,88 @@
package cn.iocoder.yudao.framework.websocket.core.aop;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.framework.websocket.core.annotation.TenantAware;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.core.annotation.AnnotationUtils;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
/**
* 租户感知的WebSocket AOP切面
*
* 自动处理带有 {@link TenantAware} 注解的方法的租户上下文
*
* @author 芋道源码
*/
@Aspect
@Slf4j
public class TenantWebSocketAspect {
@Around("@annotation(cn.iocoder.yudao.framework.websocket.core.annotation.TenantAware) || " +
"@within(cn.iocoder.yudao.framework.websocket.core.annotation.TenantAware)")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法上的注解
Method method = ((org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature()).getMethod();
TenantAware tenantAware = AnnotationUtils.findAnnotation(method, TenantAware.class);
// 如果方法上没有,尝试从类上获取
if (tenantAware == null) {
tenantAware = AnnotationUtils.findAnnotation(method.getDeclaringClass(), TenantAware.class);
}
if (tenantAware == null) {
return joinPoint.proceed();
}
// 尝试从方法参数中获取租户ID
Long tenantId = extractTenantIdFromArgs(joinPoint, tenantAware.tenantIdParam());
// 如果参数中没有租户ID尝试从当前上下文获取
if (tenantId == null) {
tenantId = TenantContextHolder.getTenantId();
}
// 根据配置决定如何处理
if (tenantId == null) {
if (tenantAware.required()) {
throw new IllegalStateException("租户上下文缺失无法发送WebSocket消息");
} else {
log.debug("[TenantWebSocketAspect] 租户上下文缺失跳过WebSocket消息发送: {}", method.getName());
return null; // 跳过执行
}
}
// 在指定租户上下文中执行
return TenantUtils.execute(tenantId, () -> {
try {
return joinPoint.proceed();
} catch (Throwable throwable) {
if (throwable instanceof RuntimeException) {
throw (RuntimeException) throwable;
}
throw new RuntimeException(throwable);
}
});
}
/**
* 从方法参数中提取租户ID
*/
private Long extractTenantIdFromArgs(ProceedingJoinPoint joinPoint, String tenantIdParam) {
Object[] args = joinPoint.getArgs();
Method method = ((org.aspectj.lang.reflect.MethodSignature) joinPoint.getSignature()).getMethod();
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (tenantIdParam.equals(parameters[i].getName()) && args[i] instanceof Long) {
return (Long) args[i];
}
}
return null;
}
}

View File

@@ -0,0 +1,65 @@
package cn.iocoder.yudao.framework.websocket.core.sender;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import lombok.RequiredArgsConstructor;
/**
* 租户感知的 WebSocket 消息发送器装饰类
*
* 自动处理租户上下文,确保消息只发送给当前租户或指定租户的用户
*
* @author 芋道源码
*/
@RequiredArgsConstructor
public class TenantWebSocketMessageSender implements WebSocketMessageSender {
private final WebSocketMessageSender delegate;
@Override
public void send(Integer userType, Long userId, String messageType, String messageContent) {
// 如果当前有租户上下文,直接发送;否则跳过(避免跨租户发送)
if (TenantContextHolder.getTenantId() != null) {
delegate.send(userType, userId, messageType, messageContent);
}
}
@Override
public void send(Integer userType, String messageType, String messageContent) {
// 如果当前有租户上下文,直接发送;否则跳过(避免跨租户发送)
if (TenantContextHolder.getTenantId() != null) {
delegate.send(userType, messageType, messageContent);
}
}
@Override
public void send(String sessionId, String messageType, String messageContent) {
// Session级别发送不受租户限制
delegate.send(sessionId, messageType, messageContent);
}
/**
* 发送消息给指定租户的指定用户
*/
public void sendToTenant(Long tenantId, Integer userType, Long userId, String messageType, String messageContent) {
TenantUtils.execute(tenantId, () -> {
delegate.send(userType, userId, messageType, messageContent);
});
}
/**
* 发送消息给指定租户的指定用户类型
*/
public void sendToTenant(Long tenantId, Integer userType, String messageType, String messageContent) {
TenantUtils.execute(tenantId, () -> {
delegate.send(userType, messageType, messageContent);
});
}
/**
* 获取原始的委托发送器(用于特殊情况下的直接访问)
*/
public WebSocketMessageSender getDelegate() {
return delegate;
}
}

View File

@@ -19,8 +19,7 @@
国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno 国外OpenAI、Ollama、Midjourney、StableDiffusion、Suno
</description> </description>
<properties> <properties>
<spring-ai.version>1.0.0</spring-ai.version> <spring-ai.version>1.0.0-M6</spring-ai.version>
<alibaba-ai.version>1.0.0.2</alibaba-ai.version>
<tinyflow.version>1.0.2</tinyflow.version> <tinyflow.version>1.0.2</tinyflow.version>
</properties> </properties>
@@ -76,73 +75,65 @@
<!-- Spring AI Model 模型接入 --> <!-- Spring AI Model 模型接入 -->
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-openai</artifactId> <artifactId>spring-ai-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-azure-openai</artifactId> <artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-deepseek</artifactId> <artifactId>spring-ai-ollama-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-ollama</artifactId> <artifactId>spring-ai-stability-ai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- 通义千问 -->
<groupId>com.alibaba.cloud.ai</groupId>
<artifactId>spring-ai-alibaba-starter</artifactId>
<version>${spring-ai.version}.1</version>
</dependency>
<dependency>
<!-- 文心一言 -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-stability-ai</artifactId> <artifactId>spring-ai-qianfan-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- 智谱 GLM --> <!-- 智谱 GLM -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-zhipuai</artifactId> <artifactId>spring-ai-zhipuai-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-model-minimax</artifactId> <artifactId>spring-ai-minimax-spring-boot-starter</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- 通义千问 --> <groupId>org.springframework.ai</groupId>
<groupId>com.alibaba.cloud.ai</groupId> <artifactId>spring-ai-moonshot-spring-boot-starter</artifactId>
<artifactId>spring-ai-alibaba-starter-dashscope</artifactId> <version>${spring-ai.version}</version>
<version>${alibaba-ai.version}</version>
</dependency>
<dependency>
<!-- 文心一言 -->
<groupId>org.springaicommunity</groupId>
<artifactId>qianfan-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<!-- 月之暗灭 -->
<groupId>org.springaicommunity</groupId>
<artifactId>moonshot-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency> </dependency>
<!-- 向量存储https://db-engines.com/en/ranking/vector+dbms --> <!-- 向量存储https://db-engines.com/en/ranking/vector+dbms -->
<dependency> <dependency>
<!-- Qdranthttps://qdrant.tech/ --> <!-- Qdranthttps://qdrant.tech/ -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-qdrant</artifactId> <artifactId>spring-ai-qdrant-store</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<!-- Redishttps://redis.io/docs/latest/develop/get-started/vector-database/ --> <!-- Redishttps://redis.io/docs/latest/develop/get-started/vector-database/ -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-redis</artifactId> <artifactId>spring-ai-redis-store</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
</dependency> </dependency>
<dependency> <dependency>
@@ -153,7 +144,7 @@
<dependency> <dependency>
<!-- Milvushttps://milvus.io/ --> <!-- Milvushttps://milvus.io/ -->
<groupId>org.springframework.ai</groupId> <groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-starter-vector-store-milvus</artifactId> <artifactId>spring-ai-milvus-store</artifactId>
<version>${spring-ai.version}</version> <version>${spring-ai.version}</version>
<exclusions> <exclusions>
<!-- 解决和 logback 的日志冲突 --> <!-- 解决和 logback 的日志冲突 -->

View File

@@ -5,6 +5,7 @@ import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory;
import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
@@ -13,6 +14,10 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy;
import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolCallingManager;
@@ -21,10 +26,6 @@ import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator;
import org.springframework.ai.tokenizer.TokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -51,6 +52,33 @@ public class AiAutoConfiguration {
// ========== 各种 AI Client 创建 ========== // ========== 各种 AI Client 创建 ==========
@Bean
@ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true")
public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) {
YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek();
return buildDeepSeekChatModel(properties);
}
public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) {
if (StrUtil.isEmpty(properties.getModel())) {
properties.setModel(DeepSeekChatModel.MODEL_DEFAULT);
}
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder()
.baseUrl(DeepSeekChatModel.BASE_URL)
.apiKey(properties.getApiKey())
.build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new DeepSeekChatModel(openAiChatModel);
}
@Bean @Bean
@ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true")
public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) {

View File

@@ -13,6 +13,12 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
@Data @Data
public class YudaoAiProperties { public class YudaoAiProperties {
/**
* DeepSeek
*/
@SuppressWarnings("SpellCheckingInspection")
private DeepSeekProperties deepseek;
/** /**
* 字节豆包 * 字节豆包
*/ */
@@ -54,6 +60,19 @@ public class YudaoAiProperties {
@SuppressWarnings("SpellCheckingInspection") @SuppressWarnings("SpellCheckingInspection")
private SunoProperties suno; private SunoProperties suno;
@Data
public static class DeepSeekProperties {
private String enable;
private String apiKey;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
@Data @Data
public static class DouBaoProperties { public static class DouBaoProperties {

View File

@@ -8,11 +8,11 @@ import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.RuntimeUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration;
import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
@@ -22,9 +22,8 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration;
import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration;
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
@@ -33,55 +32,47 @@ import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel;
import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel;
import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.KeyCredential;
import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.ObservationRegistry;
import io.milvus.client.MilvusServiceClient; import io.milvus.client.MilvusServiceClient;
import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantClient;
import io.qdrant.client.QdrantGrpcClient; import io.qdrant.client.QdrantGrpcClient;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import org.springaicommunity.moonshot.MoonshotChatModel; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration;
import org.springaicommunity.moonshot.MoonshotChatOptions; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties;
import org.springaicommunity.moonshot.api.MoonshotApi; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties;
import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration; import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties;
import org.springaicommunity.qianfan.QianFanChatModel; import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration;
import org.springaicommunity.qianfan.QianFanEmbeddingModel; import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration;
import org.springaicommunity.qianfan.QianFanEmbeddingOptions; import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration;
import org.springaicommunity.qianfan.QianFanImageModel; import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration;
import org.springaicommunity.qianfan.api.QianFanApi; import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration;
import org.springaicommunity.qianfan.api.QianFanImageApi; import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration;
import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration; import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails;
import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration; import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties;
import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel;
import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.deepseek.DeepSeekChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi;
import org.springframework.ai.document.MetadataMode; import org.springframework.ai.document.MetadataMode;
import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.BatchingStrategy;
import org.springframework.ai.embedding.EmbeddingModel; import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention;
import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageModel;
import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatModel;
import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingModel;
import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions;
import org.springframework.ai.minimax.api.MiniMaxApi; import org.springframework.ai.minimax.api.MiniMaxApi;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; import org.springframework.ai.model.function.FunctionCallbackResolver;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties;
import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration;
import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration;
import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration;
import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration;
import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration;
import org.springframework.ai.model.tool.ToolCallingManager; import org.springframework.ai.model.tool.ToolCallingManager;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration; import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration; import org.springframework.ai.moonshot.api.MoonshotApi;
import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.OllamaEmbeddingModel;
import org.springframework.ai.ollama.api.OllamaApi; import org.springframework.ai.ollama.api.OllamaApi;
@@ -93,23 +84,21 @@ import org.springframework.ai.openai.OpenAiImageModel;
import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiApi;
import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.ai.openai.api.OpenAiImageApi;
import org.springframework.ai.openai.api.common.OpenAiApiConstants; import org.springframework.ai.openai.api.common.OpenAiApiConstants;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanEmbeddingModel;
import org.springframework.ai.qianfan.QianFanEmbeddingOptions;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.api.QianFanApi;
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.SimpleVectorStore; import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; import org.springframework.ai.vectorstore.milvus.MilvusVectorStore;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientConnectionDetails;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties;
import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention; import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention;
import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention;
import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties;
import org.springframework.ai.vectorstore.redis.RedisVectorStore; import org.springframework.ai.vectorstore.redis.RedisVectorStore;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration;
import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties;
import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.*;
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;
@@ -201,7 +190,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
case XING_HUO: case XING_HUO:
return SpringUtil.getBean(XingHuoChatModel.class); return SpringUtil.getBean(XingHuoChatModel.class);
case BAI_CHUAN: case BAI_CHUAN:
return SpringUtil.getBean(BaiChuanChatModel.class); return SpringUtil.getBean(AzureOpenAiChatModel.class);
case OPENAI: case OPENAI:
return SpringUtil.getBean(OpenAiChatModel.class); return SpringUtil.getBean(OpenAiChatModel.class);
case AZURE_OPENAI: case AZURE_OPENAI:
@@ -330,34 +319,27 @@ public class AiModelFactoryImpl implements AiModelFactory {
// ========== 各种创建 spring-ai 客户端的方法 ========== // ========== 各种创建 spring-ai 客户端的方法 ==========
/** /**
* 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法 * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法
*/ */
private static DashScopeChatModel buildTongYiChatModel(String key) { private static DashScopeChatModel buildTongYiChatModel(String key) {
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build(); DashScopeApi dashScopeApi = new DashScopeApi(key);
DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL)
.withTemperature(0.7).build(); .withTemperature(0.7).build();
return DashScopeChatModel.builder() return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
.dashScopeApi(dashScopeApi)
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
* 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法 * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法
*/ */
private static DashScopeImageModel buildTongYiImagesModel(String key) { private static DashScopeImageModel buildTongYiImagesModel(String key) {
DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key);
return DashScopeImageModel.builder() return new DashScopeImageModel(dashScopeImageApi);
.dashScopeApi(dashScopeImageApi)
.build();
} }
/** /**
* 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法
*/ */
private static QianFanChatModel buildYiYanChatModel(String key) { private static QianFanChatModel buildYiYanChatModel(String key) {
// TODO spring ai qianfan 有 bug无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0); String appKey = keys.get(0);
@@ -367,10 +349,9 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法
*/ */
private QianFanImageModel buildQianFanImageModel(String key) { private QianFanImageModel buildQianFanImageModel(String key) {
// TODO spring ai qianfan 有 bug无法使用 https://github.com/spring-ai-community/qianfan/issues/6
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式");
String appKey = keys.get(0); String appKey = keys.get(0);
@@ -380,17 +361,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link DeepSeekChatAutoConfiguration} 的 deepSeekChatModel 方法 * 可参考 {@link AiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)}
*/ */
private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) {
DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build(); YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties()
DeepSeekChatOptions options = DeepSeekChatOptions.builder().model(DeepSeekApi.DEFAULT_CHAT_MODEL) .setApiKey(apiKey);
.temperature(0.7).build(); return new AiAutoConfiguration().buildDeepSeekChatModel(properties);
return DeepSeekChatModel.builder()
.deepSeekApi(deepSeekApi)
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
@@ -421,18 +397,17 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法
*/ */
private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
: new ZhiPuAiApi(url, apiKey); : new ZhiPuAiApi(url, apiKey);
ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE, return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
getObservationRegistry().getIfAvailable());
} }
/** /**
* 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法
*/ */
private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) {
ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey)
@@ -441,30 +416,23 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法
*/ */
private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey)
: new MiniMaxApi(url, apiKey); : new MiniMaxApi(url, apiKey);
MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build();
return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
} }
/** /**
* 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 * 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法
*/ */
private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) {
MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder() MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey)
.apiKey(apiKey); : new MoonshotApi(url, apiKey);
if (StrUtil.isNotEmpty(url)) {
moonshotApiBuilder.baseUrl(url);
}
MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build();
return MoonshotChatModel.builder() return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE);
.moonshotApi(moonshotApiBuilder.build())
.defaultOptions(options)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
@@ -488,32 +456,33 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法 * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法
*/ */
private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build();
return OpenAiChatModel.builder() return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build();
.openAiApi(openAiApi)
.toolCallingManager(getToolCallingManager())
.build();
} }
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
/** /**
* 可参考 {@link AzureOpenAiChatAutoConfiguration} * 可参考 {@link AzureOpenAiAutoConfiguration}
*/ */
private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) { private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) {
// TODO @芋艿:使用前,请测试,暂时没密钥!!! AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() // 创建 OpenAIClient 对象
.endpoint(url).credential(new KeyCredential(apiKey)); AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
return AzureOpenAiChatModel.builder() connectionProperties.setApiKey(apiKey);
.openAIClientBuilder(openAIClientBuilder) connectionProperties.setEndpoint(url);
.toolCallingManager(getToolCallingManager()) OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
.build(); // 获取 AzureOpenAiChatProperties 对象
AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties,
getToolCallingManager(), null, null);
} }
/** /**
* 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法
*/ */
private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
@@ -531,14 +500,11 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法 * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法
*/ */
private static OllamaChatModel buildOllamaChatModel(String url) { private static OllamaChatModel buildOllamaChatModel(String url) {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaApi ollamaApi = new OllamaApi(url);
return OllamaChatModel.builder() return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build();
.ollamaApi(ollamaApi)
.toolCallingManager(getToolCallingManager())
.build();
} }
/** /**
@@ -553,16 +519,16 @@ public class AiModelFactoryImpl implements AiModelFactory {
// ========== 各种创建 EmbeddingModel 的方法 ========== // ========== 各种创建 EmbeddingModel 的方法 ==========
/** /**
* 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法 * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法
*/ */
private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) {
DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); DashScopeApi dashScopeApi = new DashScopeApi(apiKey);
DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build();
return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions);
} }
/** /**
* 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法
*/ */
private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) {
ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey)
@@ -572,7 +538,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法 * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法
*/ */
private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) {
MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey)
@@ -582,7 +548,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
/** /**
* 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法 * 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法
*/ */
private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) {
List<String> keys = StrUtil.split(key, '|'); List<String> keys = StrUtil.split(key, '|');
@@ -595,16 +561,13 @@ public class AiModelFactoryImpl implements AiModelFactory {
} }
private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) {
OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaApi ollamaApi = new OllamaApi(url);
OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build();
return OllamaEmbeddingModel.builder() return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build();
.ollamaApi(ollamaApi)
.defaultOptions(ollamaOptions)
.build();
} }
/** /**
* 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法 * 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法
*/ */
private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) { private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) {
url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL);
@@ -613,19 +576,21 @@ public class AiModelFactoryImpl implements AiModelFactory {
return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties); return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties);
} }
// TODO @芋艿:手头暂时没密钥,使用建议再测试下
/** /**
* 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 * 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法
*/ */
private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) { private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) {
// TODO @芋艿:手头暂时没密钥,使用建议再测试下 AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration();
AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration(); // 创建 OpenAIClient 对象
// 创建 OpenAIClientBuilder 对象 AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties();
OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() connectionProperties.setApiKey(apiKey);
.endpoint(url).credential(new KeyCredential(apiKey)); connectionProperties.setEndpoint(url);
OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null);
// 获取 AzureOpenAiChatProperties 对象 // 获取 AzureOpenAiChatProperties 对象
AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class); AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class);
return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties, return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties,
getObservationRegistry(), getEmbeddingModelObservationConvention()); null, null);
} }
// ========== 各种创建 VectorStore 的方法 ========== // ========== 各种创建 VectorStore 的方法 ==========
@@ -690,12 +655,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
Map<String, Class<?>> metadataFields) { Map<String, Class<?>> metadataFields) {
// 创建 JedisPooled 对象 // 创建 JedisPooled 对象
RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class);
JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(), JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort());
redisProperties.getUsername(), redisProperties.getPassword());
// 创建 RedisVectorStoreProperties 对象 // 创建 RedisVectorStoreProperties 对象
RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration();
RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class);
RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel)
.indexName(properties.getIndexName()).prefix(properties.getPrefix()) .indexName(properties.getIndex()).prefix(properties.getPrefix())
.initializeSchema(properties.isInitializeSchema()) .initializeSchema(properties.isInitializeSchema())
.metadataFields(convertList(metadataFields.entrySet(), entry -> { .metadataFields(convertList(metadataFields.entrySet(), entry -> {
String fieldName = entry.getKey(); String fieldName = entry.getKey();
@@ -765,12 +730,10 @@ public class AiModelFactoryImpl implements AiModelFactory {
private static ObjectProvider<VectorStoreObservationConvention> getCustomObservationConvention() { private static ObjectProvider<VectorStoreObservationConvention> getCustomObservationConvention() {
return new ObjectProvider<>() { return new ObjectProvider<>() {
@Override @Override
public VectorStoreObservationConvention getObject() throws BeansException { public VectorStoreObservationConvention getObject() throws BeansException {
return new DefaultVectorStoreObservationConvention(); return new DefaultVectorStoreObservationConvention();
} }
}; };
} }
@@ -782,15 +745,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(ToolCallingManager.class); return SpringUtil.getBean(ToolCallingManager.class);
} }
private static ObjectProvider<EmbeddingModelObservationConvention> getEmbeddingModelObservationConvention() { private static FunctionCallbackResolver getFunctionCallbackResolver() {
return new ObjectProvider<>() { return SpringUtil.getBean(FunctionCallbackResolver.class);
@Override
public EmbeddingModelObservationConvention getObject() throws BeansException {
return SpringUtil.getBean(EmbeddingModelObservationConvention.class);
}
};
} }
} }

View File

@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
/**
* DeepSeek {@link ChatModel} 实现类
*
* @author fansili
*/
@Slf4j
@RequiredArgsConstructor
public class DeepSeekChatModel implements ChatModel {
public static final String BASE_URL = "https://api.deepseek.com";
public static final String MODEL_DEFAULT = "deepseek-chat";
/**
* 兼容 OpenAI 接口,进行复用
*/
private final OpenAiChatModel openAiChatModel;
@Override
public ChatResponse call(Prompt prompt) {
return openAiChatModel.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}

View File

@@ -89,7 +89,7 @@ public class SiliconFlowImageModel implements ImageModel {
var observationContext = ImageModelObservationContext.builder() var observationContext = ImageModelObservationContext.builder()
.imagePrompt(imagePrompt) .imagePrompt(imagePrompt)
.provider(SiliconFlowApiConstants.PROVIDER_NAME) .provider(SiliconFlowApiConstants.PROVIDER_NAME)
.imagePrompt(imagePrompt) .requestOptions(imagePrompt.getOptions())
.build(); .build();
return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION

View File

@@ -9,6 +9,9 @@ import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.http.HttpUtil; import cn.hutool.http.HttpUtil;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
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.image.vo.AiImageDrawReqVO; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO;
@@ -21,20 +24,17 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO;
import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper;
import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions;
import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.ai.service.model.AiModelService;
import cn.iocoder.yudao.module.infra.api.file.FileApi; import cn.iocoder.yudao.module.infra.api.file.FileApi;
import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageModel;
import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImageOptions;
import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse; import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.openai.OpenAiImageOptions; import org.springframework.ai.openai.OpenAiImageOptions;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions;
import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.ai.zhipuai.ZhiPuAiImageOptions;
import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.Async;
@@ -140,10 +140,10 @@ public class AiImageServiceImpl implements AiImageService {
private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) {
if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) {
// https://platform.openai.com/docs/api-reference/images/create // https://platform.openai.com/docs/api-reference/images/create
return OpenAiImageOptions.builder().model(model.getModel()) return OpenAiImageOptions.builder().withModel(model.getModel())
.height(draw.getHeight()).width(draw.getWidth()) .withHeight(draw.getHeight()).withWidth(draw.getWidth())
.style(MapUtil.getStr(draw.getOptions(), "style")) // 风格 .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格
.responseFormat("b64_json") .withResponseFormat("b64_json")
.build(); .build();
} else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) { } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) {
// https://docs.siliconflow.cn/cn/api-reference/images/images-generations // https://docs.siliconflow.cn/cn/api-reference/images/images-generations

View File

@@ -7,8 +7,6 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi; 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.fasterxml.jackson.annotation.JsonClassDescription; import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
@@ -19,7 +17,7 @@ import org.springframework.stereotype.Component;
import java.util.function.BiFunction; import java.util.function.BiFunction;
/** /**
* 工具:用户信息查询 * 工具:当前用户信息查询
* *
* 同时,也是展示 ToolContext 上下文的使用 * 同时,也是展示 ToolContext 上下文的使用
* *
@@ -33,17 +31,8 @@ public class UserProfileQueryToolFunction
private AdminUserApi adminUserApi; private AdminUserApi adminUserApi;
@Data @Data
@JsonClassDescription("用户信息查询") @JsonClassDescription("当前用户信息查询")
public static class Request { public static class Request { }
/**
* 用户编号
*/
@JsonProperty(value = "id")
@JsonPropertyDescription("用户编号例如说1。如果查询自己则 id 为空")
private Long id;
}
@Data @Data
@AllArgsConstructor @AllArgsConstructor
@@ -72,19 +61,13 @@ public class UserProfileQueryToolFunction
@Override @Override
public Response apply(Request request, ToolContext toolContext) { public Response apply(Request request, ToolContext toolContext) {
Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID);
if (tenantId == null) {
return new Response();
}
if (request.getId() == null) {
LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER);
if (loginUser == null) { Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID);
return new Response(); if (loginUser == null | tenantId == null) {
} return null;
request.setId(loginUser.getId());
} }
return TenantUtils.execute(tenantId, () -> { return TenantUtils.execute(tenantId, () -> {
AdminUserRespDTO user = adminUserApi.getUser(request.getId()); AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId());
return BeanUtils.toBean(user, Response.class); return BeanUtils.toBean(user, Response.class);
}); });
} }

View File

@@ -2,18 +2,18 @@ package cn.iocoder.yudao.module.ai.util;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum;
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions;
import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.messages.*;
import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.ollama.api.OllamaOptions;
import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions;
import java.util.Collections; import java.util.Collections;
@@ -43,18 +43,18 @@ public class AiUtils {
switch (platform) { switch (platform) {
case TONG_YI: case TONG_YI:
return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens)
.withToolNames(toolNames).withToolContext(toolContext).build(); .withFunctions(toolNames).withToolContext(toolContext).build();
case YI_YAN: case YI_YAN:
return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build();
case ZHI_PU: case ZHI_PU:
return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .functions(toolNames).toolContext(toolContext).build();
case MINI_MAX: case MINI_MAX:
return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .functions(toolNames).toolContext(toolContext).build();
case MOONSHOT: case MOONSHOT:
return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build(); .functions(toolNames).toolContext(toolContext).build();
case OPENAI: case OPENAI:
case DEEP_SEEK: // 复用 OpenAI 客户端 case DEEP_SEEK: // 复用 OpenAI 客户端
case DOU_BAO: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端

View File

@@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.ai.openai.OpenAIClientBuilder;
import com.azure.core.credential.AzureKeyCredential; import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.util.ClientOptions;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiChatModel;
@@ -16,7 +17,7 @@ import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME;
/** /**
* {@link AzureOpenAiChatModel} 集成测试 * {@link AzureOpenAiChatModel} 集成测试
@@ -28,13 +29,10 @@ public class AzureOpenAIChatModelTests {
// TODO @芋艿:晚点在调整 // TODO @芋艿:晚点在调整
private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder()
.endpoint("https://eastusprejade.openai.azure.com") .endpoint("https://eastusprejade.openai.azure.com")
.credential(new AzureKeyCredential("xxx")); .credential(new AzureKeyCredential("xxx"))
private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder() .clientOptions((new ClientOptions()).setApplicationId("spring-ai"));
.openAIClientBuilder(openAiApi) private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi,
.defaultOptions(AzureOpenAiChatOptions.builder() AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build());
.deploymentName(DEFAULT_DEPLOYMENT_NAME)
.build())
.build();
@Test @Test
@Disabled @Disabled

View File

@@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
@@ -34,7 +35,7 @@ public class BaiChuanChatModelTests {
.build()) .build())
.build(); .build();
private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
@Test @Test
@Disabled @Disabled

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
@@ -7,9 +8,9 @@ import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.deepseek.DeepSeekChatModel; import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.deepseek.DeepSeekChatOptions; import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.deepseek.api.DeepSeekApi; import org.springframework.ai.openai.api.OpenAiApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@@ -22,16 +23,19 @@ import java.util.List;
*/ */
public class DeepSeekChatModelTests { public class DeepSeekChatModelTests {
private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.deepSeekApi(DeepSeekApi.builder() .openAiApi(OpenAiApi.builder()
.apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey .baseUrl(DeepSeekChatModel.BASE_URL)
.apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey
.build()) .build())
.defaultOptions(DeepSeekChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
.model("deepseek-chat") // 模型 .model("deepseek-chat") // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
.build(); .build();
private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel);
@Test @Test
@Disabled @Disabled
public void testCall() { public void testCall() {

View File

@@ -1,6 +1,20 @@
package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaChatModel;
import org.springframework.ai.ollama.api.OllamaApi;
import org.springframework.ai.ollama.api.OllamaModel;
import org.springframework.ai.ollama.api.OllamaOptions;
import reactor.core.publisher.Flux;
import java.util.ArrayList;
import java.util.List;
/** /**
* {@link OllamaChatModel} 集成测试 * {@link OllamaChatModel} 集成测试
@@ -9,43 +23,43 @@ import org.springframework.ai.ollama.OllamaChatModel;
*/ */
public class LlamaChatModelTests { public class LlamaChatModelTests {
// private final OllamaChatModel chatModel = OllamaChatModel.builder() private final OllamaChatModel chatModel = OllamaChatModel.builder()
// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
// .defaultOptions(OllamaOptions.builder() .defaultOptions(OllamaOptions.builder()
// .model(OllamaModel.LLAMA3.getName()) // 模型 .model(OllamaModel.LLAMA3.getName()) // 模型
// .build()) .build())
// .build(); .build();
//
// @Test @Test
// @Disabled @Disabled
// public void testCall() { public void testCall() {
// // 准备参数 // 准备参数
// List<Message> messages = new ArrayList<>(); List<Message> messages = new ArrayList<>();
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
// messages.add(new UserMessage("1 + 1 = ")); messages.add(new UserMessage("1 + 1 = "));
//
// // 调用 // 调用
// ChatResponse response = chatModel.call(new Prompt(messages)); ChatResponse response = chatModel.call(new Prompt(messages));
// // 打印结果 // 打印结果
System.out.println(response);
System.out.println(response.getResult().getOutput());
}
@Test
@Disabled
public void testStream() {
// 准备参数
List<Message> messages = new ArrayList<>();
messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
messages.add(new UserMessage("1 + 1 = "));
// 调用
Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// 打印结果
flux.doOnNext(response -> {
// System.out.println(response); // System.out.println(response);
// System.out.println(response.getResult().getOutput()); System.out.println(response.getResult().getOutput());
// } }).then().block();
// }
// @Test
// @Disabled
// public void testStream() {
// // 准备参数
// List<Message> messages = new ArrayList<>();
// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。"));
// messages.add(new UserMessage("1 + 1 = "));
//
// // 调用
// Flux<ChatResponse> flux = chatModel.stream(new Prompt(messages));
// // 打印结果
// flux.doOnNext(response -> {
//// System.out.println(response);
// System.out.println(response.getResult().getOutput());
// }).then().block();
// }
} }

View File

@@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springaicommunity.moonshot.MoonshotChatModel;
import org.springaicommunity.moonshot.MoonshotChatOptions;
import org.springaicommunity.moonshot.api.MoonshotApi;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.SystemMessage;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.moonshot.MoonshotChatModel;
import org.springframework.ai.moonshot.MoonshotChatOptions;
import org.springframework.ai.moonshot.api.MoonshotApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@@ -22,15 +22,11 @@ import java.util.List;
*/ */
public class MoonshotChatModelTests { public class MoonshotChatModelTests {
private final MoonshotChatModel chatModel = MoonshotChatModel.builder() private final MoonshotChatModel chatModel = new MoonshotChatModel(
.moonshotApi(MoonshotApi.builder() new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥
.apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 MoonshotChatOptions.builder()
.build()) .model("moonshot-v1-8k") // 模型
.defaultOptions(MoonshotChatOptions.builder() .build());
.model("kimi-k2-0711-preview") // 模型
.build())
.build();
@Test @Test
@Disabled @Disabled
public void testCall() { public void testCall() {

View File

@@ -23,9 +23,7 @@ import java.util.List;
public class OllamaChatModelTests { public class OllamaChatModelTests {
private final OllamaChatModel chatModel = OllamaChatModel.builder() private final OllamaChatModel chatModel = OllamaChatModel.builder()
.ollamaApi(OllamaApi.builder() .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址
.baseUrl("http://127.0.0.1:11434") // Ollama 服务地址
.build())
.defaultOptions(OllamaOptions.builder() .defaultOptions(OllamaOptions.builder()
// .model("qwen") // 模型https://ollama.com/library/qwen // .model("qwen") // 模型https://ollama.com/library/qwen
.model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1 .model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1

View File

@@ -25,10 +25,10 @@ public class OpenAIChatModelTests {
private final OpenAiChatModel chatModel = OpenAiChatModel.builder() private final OpenAiChatModel chatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder() .openAiApi(OpenAiApi.builder()
.baseUrl("https://api.holdai.top") .baseUrl("https://api.holdai.top")
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey
.build()) .build())
.defaultOptions(OpenAiChatOptions.builder() .defaultOptions(OpenAiChatOptions.builder()
.model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型 .model(OpenAiApi.ChatModel.GPT_4_O) // 模型
.temperature(0.7) .temperature(0.7)
.build()) .build())
.build(); .build();

View File

@@ -22,17 +22,14 @@ import java.util.List;
*/ */
public class TongYiChatModelTests { public class TongYiChatModelTests {
private final DashScopeChatModel chatModel = DashScopeChatModel.builder() private final DashScopeChatModel chatModel = new DashScopeChatModel(
.dashScopeApi(DashScopeApi.builder() new DashScopeApi("sk-7d903764249848cfa912733146da12d1"),
.apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") DashScopeChatOptions.builder()
.build())
.defaultOptions( DashScopeChatOptions.builder()
.withModel("qwen1.5-72b-chat") // 模型 .withModel("qwen1.5-72b-chat") // 模型
// .withModel("deepseek-r1") // 模型deepseek-r1 // .withModel("deepseek-r1") // 模型deepseek-r1
// .withModel("deepseek-v3") // 模型deepseek-v3 // .withModel("deepseek-v3") // 模型deepseek-v3
// .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型deepseek-r1-distill-qwen-1.5b
.build()) .build());
.build();
@Test @Test
@Disabled @Disabled

View File

@@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanChatModel;
import org.springaicommunity.qianfan.QianFanChatOptions;
import org.springaicommunity.qianfan.api.QianFanApi;
import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.Message;
import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.messages.UserMessage;
import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.qianfan.QianFanChatModel;
import org.springframework.ai.qianfan.QianFanChatOptions;
import org.springframework.ai.qianfan.api.QianFanApi;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import java.util.ArrayList; import java.util.ArrayList;
@@ -23,9 +23,9 @@ import java.util.List;
public class YiYanChatModelTests { public class YiYanChatModelTests {
private final QianFanChatModel chatModel = new QianFanChatModel( private final QianFanChatModel chatModel = new QianFanChatModel(
new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥 new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥
QianFanChatOptions.builder() QianFanChatOptions.builder()
.model("ERNIE-4.5-8K-Preview") .model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue())
.build() .build()
); );

View File

@@ -18,7 +18,7 @@ public class OpenAiImageModelTests {
private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder()
.baseUrl("https://api.holdai.top") // apiKey .baseUrl("https://api.holdai.top") // apiKey
.apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17")
.build()); .build());
@Test @Test
@@ -26,8 +26,8 @@ public class OpenAiImageModelTests {
public void testCall() { public void testCall() {
// 准备参数 // 准备参数
ImageOptions options = OpenAiImageOptions.builder() ImageOptions options = OpenAiImageOptions.builder()
.model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 .withModel(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜
.height(256).width(256) .withHeight(256).withWidth(256)
.build(); .build();
ImagePrompt prompt = new ImagePrompt("中国长城!", options); ImagePrompt prompt = new ImagePrompt("中国长城!", options);

View File

@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image;
import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springaicommunity.qianfan.QianFanImageModel;
import org.springaicommunity.qianfan.QianFanImageOptions;
import org.springaicommunity.qianfan.api.QianFanImageApi;
import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImagePrompt;
import org.springframework.ai.image.ImageResponse; import org.springframework.ai.image.ImageResponse;
import org.springframework.ai.qianfan.QianFanImageModel;
import org.springframework.ai.qianfan.QianFanImageOptions;
import org.springframework.ai.qianfan.api.QianFanImageApi;
import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage; import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage;

View File

@@ -31,8 +31,8 @@ public class StabilityAiImageModelTests {
public void testCall() { public void testCall() {
// 准备参数 // 准备参数
ImageOptions options = OpenAiImageOptions.builder() ImageOptions options = OpenAiImageOptions.builder()
.model("stable-diffusion-v1-6") .withModel("stable-diffusion-v1-6")
.height(320).width(320) .withHeight(320).withWidth(320)
.build(); .build();
ImagePrompt prompt = new ImagePrompt("great wall", options); ImagePrompt prompt = new ImagePrompt("great wall", options);

View File

@@ -62,7 +62,6 @@ import org.springframework.transaction.support.TransactionSynchronizationManager
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.validation.Valid; import javax.validation.Valid;
import java.util.*; import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream; import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -174,7 +173,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
/** /**
* 获得用户指定 taskId 任务编号的"待办"(未审批、且可审核)的任务 * 获得用户指定 taskId 任务编号的待办(未审批、且可审核)的任务
* *
* @param userId 用户编号 * @param userId 用户编号
* @param taskId 任务编号 * @param taskId 任务编号
@@ -195,7 +194,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
/** /**
* 获得用户指定 processInstanceId 流程编号下的首个"待办"(未审批、且可审核)的任务 * 获得用户指定 processInstanceId 流程编号下的首个待办(未审批、且可审核)的任务
* *
* @param userId 用户编号 * @param userId 用户编号
* @param processInstanceId 流程编号 * @param processInstanceId 流程编号
@@ -242,7 +241,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
// 特殊:强制移除自动完成的"发起人"节点 // 特殊:强制移除自动完成的发起人节点
// 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤
tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID));
return new PageResult<>(tasks, count); return new PageResult<>(tasks, count);
@@ -298,7 +297,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
public Task validateTask(Long userId, String taskId) { public Task validateTask(Long userId, String taskId) {
Task task = validateTaskExist(taskId); Task task = validateTaskExist(taskId);
// 为什么判断 assignee 非空的情况下? // 为什么判断 assignee 非空的情况下?
// 例如说:在审批人为空时,我们会有"自动审批通过"的策略,此时 userId 为 null允许通过 // 例如说:在审批人为空时,我们会有自动审批通过的策略,此时 userId 为 null允许通过
if (StrUtil.isNotBlank(task.getAssignee()) if (StrUtil.isNotBlank(task.getAssignee())
&& ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) { && ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) {
throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF); throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
@@ -405,28 +404,19 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* @return 所有子任务列表 * @return 所有子任务列表
*/ */
private List<Task> getAllChildTaskList(Task parentTask) { private List<Task> getAllChildTaskList(Task parentTask) {
// 1. 获取流程实例的所有任务
List<Task> allTasks = taskService.createTaskQuery().processInstanceId(parentTask.getProcessInstanceId()).list();
if (CollUtil.isEmpty(allTasks)) {
return Collections.emptyList();
}
// 2. 构建父任务到子任务列表的映射关系,用于内存中快速查找
Map<String, List<Task>> childrenTaskMap = allTasks.stream()
.filter(task -> task.getParentTaskId() != null)
.collect(Collectors.groupingBy(Task::getParentTaskId));
if (CollUtil.isEmpty(childrenTaskMap)) {
return Collections.emptyList();
}
// 3. 使用栈进行遍历,广度或深度优先搜索所有子孙任务
List<Task> result = new ArrayList<>(); List<Task> result = new ArrayList<>();
// 1. 递归获取子级
Stack<Task> stack = new Stack<>(); Stack<Task> stack = new Stack<>();
stack.push(parentTask); stack.push(parentTask);
// 2. 递归遍历
while (!stack.isEmpty()) { for (int i = 0; i < Short.MAX_VALUE; i++) {
if (stack.isEmpty()) {
break;
}
// 2.1 获取子任务们
Task task = stack.pop(); Task task = stack.pop();
List<Task> childTaskList = childrenTaskMap.get(task.getId()); List<Task> childTaskList = getTaskListByParentTaskId(task.getId());
// 2.2 如果非空,则添加到 stack 进一步递归
if (CollUtil.isNotEmpty(childTaskList)) { if (CollUtil.isNotEmpty(childTaskList)) {
stack.addAll(childTaskList); stack.addAll(childTaskList);
result.addAll(childTaskList); result.addAll(childTaskList);
@@ -669,7 +659,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
/** /**
* 审批通过存在"后加签"的任务。 * 审批通过存在后加签的任务。
* <p> * <p>
* 注意该任务不能马上完成需要一个中间状态APPROVING并激活剩余所有子任务PROCESS为可审批处理 * 注意该任务不能马上完成需要一个中间状态APPROVING并激活剩余所有子任务PROCESS为可审批处理
* 如果马上完成,则会触发下一个任务,甚至如果没有下一个任务则流程实例就直接结束了! * 如果马上完成,则会触发下一个任务,甚至如果没有下一个任务则流程实例就直接结束了!
@@ -781,7 +771,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason())); BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
// 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过 // 2.3 如果当前任务时被加签的,则加它的根任务也标记成未通过
// 疑问:为什么要标记未通过呢? // 疑问:为什么要标记未通过呢?
// 回答:例如说 A 任务被向前加签除 B 任务时B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示"已取消"的任务,导致展示不出审批不通过的细节。 // 回答:例如说 A 任务被向前加签除 B 任务时B 任务被审批不通过,此时 A 会被取消。而 yudao-ui-admin-vue3 不展示已取消的任务,导致展示不出审批不通过的细节。
if (task.getParentTaskId() != null) { if (task.getParentTaskId() != null) {
String rootParentId = getTaskRootParentId(task); String rootParentId = getTaskRootParentId(task);
updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(), updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(),
@@ -1054,8 +1044,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
/** /**
* 校验任务是否可以加签,主要校验加签类型是否一致: * 校验任务是否可以加签,主要校验加签类型是否一致:
* <p> * <p>
* 1. 如果存在"向前加签"的任务,则不能"向后加签" * 1. 如果存在向前加签的任务,则不能向后加签
* 2. 如果存在"向后加签"的任务,则不能"向前加签" * 2. 如果存在向后加签的任务,则不能向前加签
* *
* @param userId 当前用户 ID * @param userId 当前用户 ID
* @param reqVO 请求参数,包含任务 ID 和加签类型 * @param reqVO 请求参数,包含任务 ID 和加签类型
@@ -1376,8 +1366,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(), Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID) if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是"主流程",发起人节点,自动通过审核 && (skipStartUserNodeFlag == null // 目的:一般是主流程,发起人节点,自动通过审核
|| Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是"子流程",发起人节点,按配置自动通过审核 || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是子流程,发起人节点,按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));

View File

@@ -142,7 +142,6 @@ public class CrmBusinessServiceImpl implements CrmBusinessService {
updateBusinessProduct(updateObj.getId(), businessProducts); updateBusinessProduct(updateObj.getId(), businessProducts);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldBusiness.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class));
LogRecordContext.putVariable("businessName", oldBusiness.getName()); LogRecordContext.putVariable("businessName", oldBusiness.getName());
} }

View File

@@ -92,20 +92,19 @@ public class CrmClueServiceImpl implements CrmClueService {
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}",
success = CRM_CLUE_UPDATE_SUCCESS) success = CRM_CLUE_UPDATE_SUCCESS)
@CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.OWNER) @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER)
public void updateClue(CrmClueSaveReqVO updateReqVO) { public void updateClue(CrmClueSaveReqVO updateReq) {
Assert.notNull(updateReqVO.getId(), "线索编号不能为空"); Assert.notNull(updateReq.getId(), "线索编号不能为空");
// 1.1 校验线索是否存在 // 1.1 校验线索是否存在
CrmClueDO oldClue = validateClueExists(updateReqVO.getId()); CrmClueDO oldClue = validateClueExists(updateReq.getId());
// 1.2 校验关联数据 // 1.2 校验关联数据
validateRelationDataExists(updateReqVO); validateRelationDataExists(updateReq);
// 2. 更新线索 // 2. 更新线索
CrmClueDO updateObj = BeanUtils.toBean(updateReqVO, CrmClueDO.class); CrmClueDO updateObj = BeanUtils.toBean(updateReq, CrmClueDO.class);
clueMapper.updateById(updateObj); clueMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class));
LogRecordContext.putVariable("clueName", oldClue.getName()); LogRecordContext.putVariable("clueName", oldClue.getName());
} }

View File

@@ -114,7 +114,6 @@ public class CrmContactServiceImpl implements CrmContactService {
contactMapper.updateById(updateObj); contactMapper.updateById(updateObj);
// 3. 记录操作日志 // 3. 记录操作日志
updateReqVO.setOwnerUserId(oldContact.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class));
LogRecordContext.putVariable("contactName", oldContact.getName()); LogRecordContext.putVariable("contactName", oldContact.getName());
} }

View File

@@ -140,9 +140,9 @@ public class CrmContractServiceImpl implements CrmContractService {
Assert.notNull(updateReqVO.getId(), "合同编号不能为空"); Assert.notNull(updateReqVO.getId(), "合同编号不能为空");
updateReqVO.setOwnerUserId(null); // 不允许更新的字段 updateReqVO.setOwnerUserId(null); // 不允许更新的字段
// 1.1 校验存在 // 1.1 校验存在
CrmContractDO oldContract = validateContractExists(updateReqVO.getId()); CrmContractDO contract = validateContractExists(updateReqVO.getId());
// 1.2 只有草稿、审批中,可以编辑; // 1.2 只有草稿、审批中,可以编辑;
if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
CrmAuditStatusEnum.PROCESS.getStatus())) { CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT); throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT);
} }
@@ -159,9 +159,8 @@ public class CrmContractServiceImpl implements CrmContractService {
updateContractProduct(updateReqVO.getId(), contractProducts); updateContractProduct(updateReqVO.getId(), contractProducts);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldContract.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contract, CrmContractSaveReqVO.class));
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class)); LogRecordContext.putVariable("contractName", contract.getName());
LogRecordContext.putVariable("contractName", oldContract.getName());
} }
private void updateContractProduct(Long id, List<CrmContractProductDO> newList) { private void updateContractProduct(Long id, List<CrmContractProductDO> newList) {

View File

@@ -137,7 +137,6 @@ public class CrmCustomerServiceImpl implements CrmCustomerService {
customerMapper.updateById(updateObj); customerMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldCustomer.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class));
LogRecordContext.putVariable("customerName", oldCustomer.getName()); LogRecordContext.putVariable("customerName", oldCustomer.getName());
} }

View File

@@ -210,12 +210,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService {
CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId( CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId(
transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId()); transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId());
String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType()); String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType());
if ((oldPermission == null || !isOwner(oldPermission.getLevel())) if (oldPermission == null // 不是拥有者,并且不是超管
&& !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管 || (!isOwner(oldPermission.getLevel()) && !CrmPermissionUtils.isCrmAdmin())) {
throw exception(CRM_PERMISSION_DENIED, bizTypeName); throw exception(CRM_PERMISSION_DENIED, bizTypeName);
} }
// 1.1 校验转移对象是否已经是该负责人 // 1.1 校验转移对象是否已经是该负责人
if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) {
throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName);
} }
// 1.2 校验新负责人是否存在 // 1.2 校验新负责人是否存在

View File

@@ -104,7 +104,6 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService {
receivablePlanMapper.updateById(updateObj); receivablePlanMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldReceivablePlan.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class)); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class));
LogRecordContext.putVariable("receivablePlan", oldReceivablePlan); LogRecordContext.putVariable("receivablePlan", oldReceivablePlan);
} }

View File

@@ -162,14 +162,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
Assert.notNull(updateReqVO.getId(), "回款编号不能为空"); Assert.notNull(updateReqVO.getId(), "回款编号不能为空");
updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段 updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段
// 1.1 校验存在 // 1.1 校验存在
CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId()); CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId());
updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId()) updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId())
.setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值 .setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值
// 1.2 校验可回款金额超过上限 // 1.2 校验可回款金额超过上限
validateReceivablePriceExceedsLimit(updateReqVO); validateReceivablePriceExceedsLimit(updateReqVO);
// 1.3 只有草稿、审批中,可以编辑; // 1.3 只有草稿、审批中,可以编辑;
if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(),
CrmAuditStatusEnum.PROCESS.getStatus())) { CrmAuditStatusEnum.PROCESS.getStatus())) {
throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED); throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED);
} }
@@ -179,10 +179,9 @@ public class CrmReceivableServiceImpl implements CrmReceivableService {
receivableMapper.updateById(updateObj); receivableMapper.updateById(updateObj);
// 3. 记录操作日志上下文 // 3. 记录操作日志上下文
updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable("receivable", receivable);
LogRecordContext.putVariable("oldReceivable", oldReceivable); LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId()));
LogRecordContext.putVariable("period", getReceivablePeriod(oldReceivable.getPlanId())); LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class));
LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableSaveReqVO.class));
} }
private Integer getReceivablePeriod(Long planId) { private Integer getReceivablePeriod(Long planId) {

View File

@@ -10,9 +10,7 @@
<if test="endTime != null"> <if test="endTime != null">
AND in_time &lt; #{endTime} AND in_time &lt; #{endTime}
</if> </if>
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) - AND deleted = 0) -
(SELECT IFNULL(SUM(total_price), 0) (SELECT IFNULL(SUM(total_price), 0)
FROM erp_purchase_return FROM erp_purchase_return
@@ -20,9 +18,7 @@
<if test="endTime != null"> <if test="endTime != null">
AND return_time &lt; #{endTime} AND return_time &lt; #{endTime}
</if> </if>
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) AND deleted = 0)
</select> </select>

View File

@@ -10,9 +10,7 @@
<if test="endTime != null"> <if test="endTime != null">
AND out_time &lt; #{endTime} AND out_time &lt; #{endTime}
</if> </if>
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) - AND deleted = 0) -
(SELECT IFNULL(SUM(total_price), 0) (SELECT IFNULL(SUM(total_price), 0)
FROM erp_sale_return FROM erp_sale_return
@@ -20,9 +18,7 @@
<if test="endTime != null"> <if test="endTime != null">
AND return_time &lt; #{endTime} AND return_time &lt; #{endTime}
</if> </if>
<if test="@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId() != null"> AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()}
AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()}
</if>
AND deleted = 0) AND deleted = 0)
</select> </select>

View File

@@ -39,6 +39,27 @@ public interface WebSocketSenderApi {
*/ */
void send(String sessionId, String messageType, String messageContent); void send(String sessionId, String messageType, String messageContent);
/**
* 发送消息给指定租户的指定用户
*
* @param tenantId 租户编号
* @param userType 用户类型
* @param userId 用户编号
* @param messageType 消息类型
* @param messageContent 消息内容JSON 格式
*/
void sendToTenant(Long tenantId, Integer userType, Long userId, String messageType, String messageContent);
/**
* 发送消息给指定租户的指定用户类型
*
* @param tenantId 租户编号
* @param userType 用户类型
* @param messageType 消息类型
* @param messageContent 消息内容JSON 格式
*/
void sendToTenant(Long tenantId, Integer userType, String messageType, String messageContent);
default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) { default void sendObject(Integer userType, Long userId, String messageType, Object messageContent) {
send(userType, userId, messageType, JsonUtils.toJsonString(messageContent)); send(userType, userId, messageType, JsonUtils.toJsonString(messageContent));
} }
@@ -51,4 +72,12 @@ public interface WebSocketSenderApi {
send(sessionId, messageType, JsonUtils.toJsonString(messageContent)); send(sessionId, messageType, JsonUtils.toJsonString(messageContent));
} }
default void sendObjectToTenant(Long tenantId, Integer userType, Long userId, String messageType, Object messageContent) {
sendToTenant(tenantId, userType, userId, messageType, JsonUtils.toJsonString(messageContent));
}
default void sendObjectToTenant(Long tenantId, Integer userType, String messageType, Object messageContent) {
sendToTenant(tenantId, userType, messageType, JsonUtils.toJsonString(messageContent));
}
} }

View File

@@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.infra.api.websocket; package cn.iocoder.yudao.module.infra.api.websocket;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.framework.websocket.core.sender.TenantWebSocketMessageSender;
import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -31,4 +33,30 @@ public class WebSocketSenderApiImpl implements WebSocketSenderApi {
webSocketMessageSender.send(sessionId, messageType, messageContent); webSocketMessageSender.send(sessionId, messageType, messageContent);
} }
@Override
public void sendToTenant(Long tenantId, Integer userType, Long userId, String messageType, String messageContent) {
// 优先使用租户感知的发送器
if (webSocketMessageSender instanceof TenantWebSocketMessageSender) {
((TenantWebSocketMessageSender) webSocketMessageSender).sendToTenant(tenantId, userType, userId, messageType, messageContent);
} else {
// 降级方案使用TenantUtils
TenantUtils.execute(tenantId, () -> {
webSocketMessageSender.send(userType, userId, messageType, messageContent);
});
}
}
@Override
public void sendToTenant(Long tenantId, Integer userType, String messageType, String messageContent) {
// 优先使用租户感知的发送器
if (webSocketMessageSender instanceof TenantWebSocketMessageSender) {
((TenantWebSocketMessageSender) webSocketMessageSender).sendToTenant(tenantId, userType, messageType, messageContent);
} else {
// 降级方案使用TenantUtils
TenantUtils.execute(tenantId, () -> {
webSocketMessageSender.send(userType, messageType, messageContent);
});
}
}
} }

View File

@@ -1,12 +1,9 @@
package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 上传文件 Request VO") @Schema(description = "管理后台 - 上传文件 Request VO")
@@ -20,10 +17,4 @@ public class FileUploadReqVO {
@Schema(description = "文件目录", example = "XXX/YYY") @Schema(description = "文件目录", example = "XXX/YYY")
private String directory; private String directory;
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
} }

View File

@@ -1,12 +1,9 @@
package cn.iocoder.yudao.module.infra.controller.app.file.vo; package cn.iocoder.yudao.module.infra.controller.app.file.vo;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.NotNull; import javax.validation.constraints.NotNull;
@Schema(description = "用户 App - 上传文件 Request VO") @Schema(description = "用户 App - 上传文件 Request VO")
@@ -20,10 +17,4 @@ public class AppFileUploadReqVO {
@Schema(description = "文件目录", example = "XXX/YYY") @Schema(description = "文件目录", example = "XXX/YYY")
private String directory; private String directory;
@AssertTrue(message = "文件目录不正确")
@JsonIgnore
public boolean isDirectoryValid() {
return !StrUtil.containsAny(directory, "..", "/", "\\");
}
} }

View File

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils;
import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import com.alibaba.ttl.TransmittableThreadLocal;
import lombok.SneakyThrows; import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.tika.Tika; import org.apache.tika.Tika;
@@ -85,8 +86,8 @@ public class FileTypeUtils {
response.setContentType(contentType); response.setContentType(contentType);
// 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题
if (StrUtil.containsIgnoreCase(contentType, "video")) { if (StrUtil.containsIgnoreCase(contentType, "video")) {
response.setHeader("Content-Length", String.valueOf(content.length)); response.setHeader("Content-Length", String.valueOf(content.length - 1));
response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); response.setHeader("Content-Range", String.valueOf(content.length - 1));
response.setHeader("Accept-Ranges", "bytes"); response.setHeader("Accept-Ranges", "bytes");
} }
// 输出附件 // 输出附件

View File

@@ -31,13 +31,9 @@ import java.util.List;
public class CouponTemplateDO extends BaseDO { public class CouponTemplateDO extends BaseDO {
/** /**
* 领取数量 - 不限制 * 不限制领取数量
*/ */
public static final Integer TAKE_LIMIT_COUNT_MAX = -1; public static final Integer TIME_LIMIT_COUNT_MAX = -1;
/**
* 发放数量 - 不限制
*/
public static final Integer TOTAL_COUNT_MAX = -1;
// ========== 基本信息 BEGIN ========== // ========== 基本信息 BEGIN ==========
/** /**

View File

@@ -40,16 +40,10 @@ public interface CouponTemplateMapper extends BaseMapperX<CouponTemplateDO> {
.orderByDesc(CouponTemplateDO::getId)); .orderByDesc(CouponTemplateDO::getId));
} }
default int updateTakeCount(Long id, Integer incrCount) { default void updateTakeCount(Long id, Integer incrCount) {
LambdaUpdateWrapper<CouponTemplateDO> updateWrapper = new LambdaUpdateWrapper<CouponTemplateDO>() update(null, new LambdaUpdateWrapper<CouponTemplateDO>()
.eq(CouponTemplateDO::getId, id) .eq(CouponTemplateDO::getId, id)
.setSql("take_count = take_count + " + incrCount); .setSql("take_count = take_count + " + incrCount));
// 增加已领取的数量incrCount 为正数),需要考虑发放数量 totalCount 的限制
if (incrCount > 0) {
updateWrapper.and(i -> i.apply("take_count < total_count")
.or().eq(CouponTemplateDO::getTotalCount, CouponTemplateDO.TOTAL_COUNT_MAX));
}
return update(updateWrapper);
} }
default List<CouponTemplateDO> selectListByTakeType(Integer takeType) { default List<CouponTemplateDO> selectListByTakeType(Integer takeType) {

View File

@@ -86,7 +86,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic
activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId));
} }
// 查找是否有其它活动,选择了该产品 // 查找是否有其它活动,选择了该产品
List<CombinationActivityDO> matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getSpuId(), spuId)); List<CombinationActivityDO> matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getId(), spuId));
if (CollUtil.isNotEmpty(matchActivityList)) { if (CollUtil.isNotEmpty(matchActivityList)) {
throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS); throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS);
} }

View File

@@ -137,6 +137,7 @@ public class CouponServiceImpl implements CouponService {
// 4. 增加优惠劵模板的领取数量 // 4. 增加优惠劵模板的领取数量
couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size());
return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId);
} }
@@ -280,7 +281,7 @@ public class CouponServiceImpl implements CouponService {
} }
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
&& couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH); throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
} }

View File

@@ -22,7 +22,8 @@ import java.util.List;
import java.util.Objects; 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.module.promotion.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS;
import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL;
/** /**
* 优惠劵模板 Service 实现类 * 优惠劵模板 Service 实现类
@@ -59,7 +60,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId());
// 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时)
if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType())
&& ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制
&& updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) {
throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount());
} }
@@ -115,10 +116,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService {
@Override @Override
public void updateCouponTemplateTakeCount(Long id, int incrCount) { public void updateCouponTemplateTakeCount(Long id, int incrCount) {
int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount); couponTemplateMapper.updateTakeCount(id, incrCount);
if (updateCount == 0) {
throw exception(COUPON_TEMPLATE_NOT_ENOUGH);
}
} }
@Override @Override

View File

@@ -76,8 +76,8 @@
<select id="selectListByPayTimeBetweenAndGroupByMonth" <select id="selectListByPayTimeBetweenAndGroupByMonth"
resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO"> resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderTrendRespVO">
SELECT DATE_FORMAT(pay_time, '%Y-%m') AS date, SELECT DATE_FORMAT(pay_time, '%Y-%m') AS date,
COUNT(1) AS order_pay_count, COUNT(1) AS orderPayCount,
SUM(pay_price) AS order_pay_price SUM(pay_price) AS orderPayPrice
FROM trade_order FROM trade_order
WHERE pay_status = TRUE WHERE pay_status = TRUE
AND create_time BETWEEN #{beginTime} AND #{endTime} AND create_time BETWEEN #{beginTime} AND #{endTime}
@@ -95,8 +95,8 @@
<select id="selectPaySummaryByPayStatusAndPayTimeBetween" <select id="selectPaySummaryByPayStatusAndPayTimeBetween"
resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO"> resultType="cn.iocoder.yudao.module.statistics.controller.admin.trade.vo.TradeOrderSummaryRespVO">
SELECT IFNULL(SUM(pay_price), 0) AS order_pay_price, SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice,
COUNT(1) AS order_pay_count COUNT(1) AS orderPayCount
FROM trade_order FROM trade_order
WHERE pay_status = #{payStatus} WHERE pay_status = #{payStatus}
AND pay_time BETWEEN #{beginTime} AND #{endTime} AND pay_time BETWEEN #{beginTime} AND #{endTime}

View File

@@ -39,8 +39,7 @@ public interface ErrorCodeConstants {
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在"); ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在");
ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】"); ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】");
ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限"); ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限");
ErrorCode ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS = new ErrorCode(1_011_000_037, "交易订单自提失败,原因:商品拼团记录不是【成功】状态"); ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足");
ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_038, "交易订单创建失败,原因:用户积分不足");
// ========== After Sale 模块 1-011-000-100 ========== // ========== After Sale 模块 1-011-000-100 ==========
ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在");

View File

@@ -141,8 +141,9 @@ public class AfterSaleController {
public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { public CommonResult<Boolean> updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) {
log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO); log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO);
if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) { if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) {
Long orderId = Long.parseLong(StrUtil.subAfter(notifyReqDTO.getMerchantRefundId(), "order-", true)); tradeOrderUpdateService.updatePaidOrderRefunded(
tradeOrderUpdateService.updatePaidOrderRefunded(orderId, notifyReqDTO.getPayRefundId()); Long.parseLong(notifyReqDTO.getMerchantRefundId()),
notifyReqDTO.getPayRefundId());
} else { } else {
afterSaleService.updateAfterSaleRefunded( afterSaleService.updateAfterSaleRefunded(
Long.parseLong(notifyReqDTO.getMerchantRefundId()), Long.parseLong(notifyReqDTO.getMerchantRefundId()),

View File

@@ -25,9 +25,6 @@ import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum;
import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum;
import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi;
import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO;
import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi;
import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO;
import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum;
import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.SocialClientApi;
import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO;
import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO;
@@ -124,8 +121,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
public SocialClientApi socialClientApi; public SocialClientApi socialClientApi;
@Resource @Resource
public PayRefundApi payRefundApi; public PayRefundApi payRefundApi;
@Resource
private CombinationRecordApi combinationRecordApi;
@Resource @Resource
private TradeOrderProperties tradeOrderProperties; private TradeOrderProperties tradeOrderProperties;
@@ -780,14 +775,6 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService {
if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) { if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) {
throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP); throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP);
} }
// 情况一:如果是拼团订单,则校验拼团是否成功
if (TradeOrderTypeEnum.isCombination(order.getType())) {
CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId(
order.getUserId(), order.getId());
if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) {
throw exception(ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS);
}
}
DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId()); DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId());
if (deliveryPickUpStore == null if (deliveryPickUpStore == null
|| !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) { || !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) {

View File

@@ -1,12 +1,10 @@
package cn.iocoder.yudao.module.mp.service.handler.user; package cn.iocoder.yudao.module.mp.service.handler.user;
import cn.hutool.core.util.ObjUtil;
import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder;
import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService;
import cn.iocoder.yudao.module.mp.service.user.MpUserService; import cn.iocoder.yudao.module.mp.service.user.MpUserService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.common.error.WxErrorException; import me.chanjar.weixin.common.error.WxErrorException;
import me.chanjar.weixin.common.error.WxMpErrorMsgEnum;
import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.common.session.WxSessionManager;
import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpMessageHandler;
import me.chanjar.weixin.mp.api.WxMpService; import me.chanjar.weixin.mp.api.WxMpService;
@@ -42,13 +40,6 @@ public class SubscribeHandler implements WxMpMessageHandler {
wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser());
} catch (WxErrorException e) { } catch (WxErrorException e) {
log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e);
// 特殊情况个人账号无接口权限https://t.zsxq.com/cLFq5
if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) {
wxMpUser = new WxMpUser();
wxMpUser.setOpenId(wxMessage.getFromUser());
wxMpUser.setSubscribe(true);
wxMpUser.setSubscribeTime(System.currentTimeMillis() / 1000L);
}
} }
// 第二步,保存粉丝信息 // 第二步,保存粉丝信息

View File

@@ -127,8 +127,6 @@ public class AuthController {
@PostMapping("/sms-login") @PostMapping("/sms-login")
@PermitAll @PermitAll
@Operation(summary = "使用短信验证码登录") @Operation(summary = "使用短信验证码登录")
// 可按需开启限流https://github.com/YunaiV/ruoyi-vue-pro/issues/851
// @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile")
public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { public CommonResult<AuthLoginRespVO> smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) {
return success(authService.smsLogin(reqVO)); return success(authService.smsLogin(reqVO));
} }

View File

@@ -38,8 +38,7 @@ public class SocialUserController {
@PostMapping("/bind") @PostMapping("/bind")
@Operation(summary = "社交绑定,使用 code 授权码") @Operation(summary = "社交绑定,使用 code 授权码")
public CommonResult<Boolean> socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { public CommonResult<Boolean> socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) {
socialUserService.bindSocialUser(new SocialUserBindReqDTO().setSocialType(reqVO.getType()) socialUserService.bindSocialUser(BeanUtils.toBean(reqVO, SocialUserBindReqDTO.class)
.setCode(reqVO.getCode()).setState(reqVO.getState())
.setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue())); .setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue()));
return CommonResult.success(true); return CommonResult.success(true);
} }

View File

@@ -138,7 +138,6 @@
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions> <executions>
<execution> <execution>
<goals> <goals>

View File

@@ -6,8 +6,8 @@ server:
spring: spring:
autoconfigure: autoconfigure:
exclude: exclude:
- org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建 - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建
- org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建 - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建
# 数据源配置项 # 数据源配置项
autoconfigure: autoconfigure:
exclude: exclude:

View File

@@ -11,8 +11,8 @@ spring:
- de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置
- de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置
- de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置
- org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建 - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant手动创建
- org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建 - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus手动创建
# 数据源配置项 # 数据源配置项
datasource: datasource:
druid: # Druid 【监控】相关的全局配置 druid: # Druid 【监控】相关的全局配置

View File

@@ -150,7 +150,7 @@ spring:
vectorstore: # 向量存储 vectorstore: # 向量存储
redis: redis:
initialize-schema: true initialize-schema: true
index-name: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行
prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构
qdrant: qdrant:
initialize-schema: true initialize-schema: true
@@ -188,14 +188,13 @@ spring:
api-key: xxxx api-key: xxxx
moonshot: # 月之暗灭KIMI moonshot: # 月之暗灭KIMI
api-key: sk-abc api-key: sk-abc
deepseek: # DeepSeek
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
chat:
options:
model: deepseek-chat
yudao: yudao:
ai: ai:
deep-seek: # DeepSeek
enable: true
api-key: sk-e94db327cc7d457d99a8de8810fc6b12
model: deepseek-chat
doubao: # 字节豆包 doubao: # 字节豆包
enable: true enable: true
api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272