From e7c9e3dc231d112639340b292500b29760f09996 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 11:16:14 +0800 Subject: [PATCH 01/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90INFRA=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E3=80=91=E6=9B=B4=E6=96=B0=20pgsql?= =?UTF-8?q?=20quartz=20sql?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/postgresql/quartz.sql | 415 +++++++++++++++++--------------------- 1 file changed, 185 insertions(+), 230 deletions(-) diff --git a/sql/postgresql/quartz.sql b/sql/postgresql/quartz.sql index 4ec390c527..46bb938431 100644 --- a/sql/postgresql/quartz.sql +++ b/sql/postgresql/quartz.sql @@ -1,253 +1,208 @@ --- ---------------------------- --- qrtz_blob_triggers --- ---------------------------- -CREATE TABLE qrtz_blob_triggers +-- 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... +-- +-- In your Quartz properties file, you'll need to set +-- 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, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - blob_data bytea NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + JOB_CLASS_NAME VARCHAR(250) NOT NULL, + IS_DURABLE BOOL NOT NULL, + IS_NONCONCURRENT BOOL NOT NULL, + IS_UPDATE_DATA BOOL NOT NULL, + REQUESTS_RECOVERY BOOL NOT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) ); -CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- qrtz_calendars --- ---------------------------- -CREATE TABLE qrtz_calendars +CREATE TABLE QRTZ_TRIGGERS ( - sched_name varchar(120) NOT NULL, - calendar_name varchar(190) NOT NULL, - calendar bytea NOT NULL, - PRIMARY KEY (sched_name, calendar_name) + 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 +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + REPEAT_COUNT BIGINT NOT NULL, + REPEAT_INTERVAL BIGINT NOT NULL, + TIMES_TRIGGERED BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +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) ); --- ---------------------------- --- qrtz_cron_triggers --- ---------------------------- -CREATE TABLE qrtz_cron_triggers +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - cron_expression varchar(120) NOT NULL, - time_zone_id varchar(80) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) ); --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_fired_triggers --- ---------------------------- -CREATE TABLE 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) + 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 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 +CREATE TABLE QRTZ_SCHEDULER_STATE ( - 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) + 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 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 +CREATE TABLE QRTZ_LOCKS ( - sched_name varchar(120) NOT NULL, - lock_name varchar(40) NOT NULL, - PRIMARY KEY (sched_name, lock_name) + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR(40) NOT NULL, + PRIMARY KEY (SCHED_NAME, LOCK_NAME) ); --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on +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); --- ---------------------------- --- 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) -); +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); --- ---------------------------- --- 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 +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); --- ---------------------------- --- 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); +COMMIT; \ No newline at end of file From 64516b22106294f541c07a443cc6358fda56c1a0 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 16:06:41 +0800 Subject: [PATCH 02/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90INFRA=20=E5=9F=BA?= =?UTF-8?q?=E7=A1=80=E8=AE=BE=E6=96=BD=E3=80=91=E6=96=87=E4=BB=B6=E4=B8=8A?= =?UTF-8?q?=E4=BC=A0=E6=97=B6=EF=BC=8Cdirectory=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BB=BB=E6=84=8F=E8=B7=AF=E5=BE=84=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/admin/file/vo/file/FileUploadReqVO.java | 9 +++++++++ .../infra/controller/app/file/vo/AppFileUploadReqVO.java | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java index 4096f477e3..44e8b65d76 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -1,6 +1,9 @@ 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 jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @@ -16,4 +19,10 @@ public class FileUploadReqVO { @Schema(description = "文件目录", example = "XXX/YYY") private String directory; + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java index fde120a067..d10a21cc49 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java @@ -1,6 +1,9 @@ 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 jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @@ -16,4 +19,10 @@ public class AppFileUploadReqVO { @Schema(description = "文件目录", example = "XXX/YYY") private String directory; + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + } From a54e743a884eb328eadbcb9b6bfd1573bc554691 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 16:15:18 +0800 Subject: [PATCH 03/12] =?UTF-8?q?feat=EF=BC=9A=E3=80=90SYSTEM=20=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=E7=AE=A1=E7=90=86=E3=80=91=E6=89=8B=E6=9C=BA=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E7=99=BB=E5=BD=95=EF=BC=8C=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E9=99=90=E6=B5=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/system/controller/admin/auth/AuthController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index d9269470d0..e41aee576d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -127,6 +127,8 @@ public class AuthController { @PostMapping("/sms-login") @PermitAll @Operation(summary = "使用短信验证码登录") + // 可按需开启限流:https://github.com/YunaiV/ruoyi-vue-pro/issues/851 + // @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile") public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { return success(authService.smsLogin(reqVO)); } From a9c7b584ccbf2ad377893c0316fd70df5bbbc40d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Sun, 13 Jul 2025 17:06:40 +0800 Subject: [PATCH 04/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90MALL=20=E5=95=86?= =?UTF-8?q?=E5=9F=8E=E7=AE=A1=E7=90=86=E3=80=91=E4=BC=98=E6=83=A0=E5=8A=B5?= =?UTF-8?q?=E6=89=A3=E5=87=8F=E6=97=B6=EF=BC=8C=E5=A2=9E=E5=8A=A0=20WHERE?= =?UTF-8?q?=20=E4=B9=90=E8=A7=82=E9=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dal/dataobject/coupon/CouponTemplateDO.java | 8 ++++++-- .../dal/mysql/coupon/CouponTemplateMapper.java | 12 +++++++++--- .../promotion/service/coupon/CouponServiceImpl.java | 3 +-- .../service/coupon/CouponTemplateServiceImpl.java | 10 ++++++---- 4 files changed, 22 insertions(+), 11 deletions(-) diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index 91216bf184..59ee95fb12 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -31,9 +31,13 @@ import java.util.List; public class CouponTemplateDO extends BaseDO { /** - * 不限制领取数量 + * 领取数量 - 不限制 */ - public static final Integer TIME_LIMIT_COUNT_MAX = -1; + public static final Integer TAKE_LIMIT_COUNT_MAX = -1; + /** + * 发放数量 - 不限制 + */ + public static final Integer TOTAL_COUNT_MAX = -1; // ========== 基本信息 BEGIN ========== /** diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java index 3096a49f3c..84e98f3dbd 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java @@ -40,10 +40,16 @@ public interface CouponTemplateMapper extends BaseMapperX { .orderByDesc(CouponTemplateDO::getId)); } - default void updateTakeCount(Long id, Integer incrCount) { - update(null, new LambdaUpdateWrapper() + default int updateTakeCount(Long id, Integer incrCount) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() .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 selectListByTakeType(Integer takeType) { diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index e6f82a69fc..e175807503 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -137,7 +137,6 @@ public class CouponServiceImpl implements CouponService { // 4. 增加优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); - return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @@ -281,7 +280,7 @@ public class CouponServiceImpl implements CouponService { } // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { throw exception(COUPON_TEMPLATE_NOT_ENOUGH); } diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java index 175e33b197..bdd8b32825 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java @@ -22,8 +22,7 @@ import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL; +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; /** * 优惠劵模板 Service 实现类 @@ -60,7 +59,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); } @@ -116,7 +115,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { @Override public void updateCouponTemplateTakeCount(Long id, int incrCount) { - couponTemplateMapper.updateTakeCount(id, incrCount); + int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount); + if (updateCount == 0) { + throw exception(COUPON_TEMPLATE_NOT_ENOUGH); + } } @Override From af94536a0606e62f3288687b4f4fdee1fce74feb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 13:10:51 +0800 Subject: [PATCH 05/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90CRM=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E3=80=91=E4=BF=AE=E6=94=B9=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E6=97=B6=EF=BC=8C=E9=81=BF=E5=85=8D=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E5=87=BA=E7=8E=B0=E2=80=9C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E8=B4=9F=E8=B4=A3=E4=BA=BA=E2=80=9D=E7=9A=84=E6=83=85=E5=86=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/business/CrmBusinessServiceImpl.java | 1 + .../crm/service/clue/CrmClueServiceImpl.java | 13 +++++++------ .../service/contact/CrmContactServiceImpl.java | 1 + .../service/contract/CrmContractServiceImpl.java | 9 +++++---- .../service/customer/CrmCustomerServiceImpl.java | 1 + .../receivable/CrmReceivablePlanServiceImpl.java | 1 + .../receivable/CrmReceivableServiceImpl.java | 15 ++++++++------- 7 files changed, 24 insertions(+), 17 deletions(-) diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java index 6e75f23daa..71b1884cc2 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java @@ -142,6 +142,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService { updateBusinessProduct(updateObj.getId(), businessProducts); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldBusiness.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class)); LogRecordContext.putVariable("businessName", oldBusiness.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java index c8c850ab48..0250fe14f5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java @@ -92,19 +92,20 @@ public class CrmClueServiceImpl implements CrmClueService { @Transactional(rollbackFor = Exception.class) @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = CRM_CLUE_UPDATE_SUCCESS) - @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER) - public void updateClue(CrmClueSaveReqVO updateReq) { - Assert.notNull(updateReq.getId(), "线索编号不能为空"); + @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.OWNER) + public void updateClue(CrmClueSaveReqVO updateReqVO) { + Assert.notNull(updateReqVO.getId(), "线索编号不能为空"); // 1.1 校验线索是否存在 - CrmClueDO oldClue = validateClueExists(updateReq.getId()); + CrmClueDO oldClue = validateClueExists(updateReqVO.getId()); // 1.2 校验关联数据 - validateRelationDataExists(updateReq); + validateRelationDataExists(updateReqVO); // 2. 更新线索 - CrmClueDO updateObj = BeanUtils.toBean(updateReq, CrmClueDO.class); + CrmClueDO updateObj = BeanUtils.toBean(updateReqVO, CrmClueDO.class); clueMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable("clueName", oldClue.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java index 2819c528cd..958fc7520d 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java @@ -114,6 +114,7 @@ public class CrmContactServiceImpl implements CrmContactService { contactMapper.updateById(updateObj); // 3. 记录操作日志 + updateReqVO.setOwnerUserId(oldContact.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class)); LogRecordContext.putVariable("contactName", oldContact.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java index 12236bf5bc..d25d7c0046 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java @@ -140,9 +140,9 @@ public class CrmContractServiceImpl implements CrmContractService { Assert.notNull(updateReqVO.getId(), "合同编号不能为空"); updateReqVO.setOwnerUserId(null); // 不允许更新的字段 // 1.1 校验存在 - CrmContractDO contract = validateContractExists(updateReqVO.getId()); + CrmContractDO oldContract = validateContractExists(updateReqVO.getId()); // 1.2 只有草稿、审批中,可以编辑; - if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), + if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT); } @@ -159,8 +159,9 @@ public class CrmContractServiceImpl implements CrmContractService { updateContractProduct(updateReqVO.getId(), contractProducts); // 3. 记录操作日志上下文 - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contract, CrmContractSaveReqVO.class)); - LogRecordContext.putVariable("contractName", contract.getName()); + updateReqVO.setOwnerUserId(oldContract.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class)); + LogRecordContext.putVariable("contractName", oldContract.getName()); } private void updateContractProduct(Long id, List newList) { diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java index 184369c791..11ac107d98 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java @@ -137,6 +137,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService { customerMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldCustomer.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable("customerName", oldCustomer.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java index b18c1a0c4f..5005508bbb 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java @@ -104,6 +104,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService { receivablePlanMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldReceivablePlan.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class)); LogRecordContext.putVariable("receivablePlan", oldReceivablePlan); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java index c1ba3a7c43..642c8e6872 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java @@ -162,14 +162,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService { Assert.notNull(updateReqVO.getId(), "回款编号不能为空"); updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段 // 1.1 校验存在 - CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId()); - updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId()) - .setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值 + CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId()); + updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId()) + .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值 // 1.2 校验可回款金额超过上限 validateReceivablePriceExceedsLimit(updateReqVO); // 1.3 只有草稿、审批中,可以编辑; - if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), + if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED); } @@ -179,9 +179,10 @@ public class CrmReceivableServiceImpl implements CrmReceivableService { receivableMapper.updateById(updateObj); // 3. 记录操作日志上下文 - LogRecordContext.putVariable("receivable", receivable); - LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId())); - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class)); + updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 + LogRecordContext.putVariable("oldReceivable", oldReceivable); + LogRecordContext.putVariable("period", getReceivablePeriod(oldReceivable.getPlanId())); + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableSaveReqVO.class)); } private Integer getReceivablePeriod(Long planId) { From c96f6bb3607481fcac7cb10be17be4e2e3477299 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 13:19:14 +0800 Subject: [PATCH 06/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90CRM=20=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AE=A1=E7=90=86=E3=80=91CRM=20=E8=B6=85=E7=AE=A1?= =?UTF-8?q?=EF=BC=8C=E6=97=A0=E6=B3=95=E5=BC=BA=E5=88=B6=E8=BD=AC=E7=A7=BB?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../crm/service/permission/CrmPermissionServiceImpl.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java index b479443390..6e2c37c89a 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java @@ -210,12 +210,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService { CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId( transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId()); String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType()); - if (oldPermission == null // 不是拥有者,并且不是超管 - || (!isOwner(oldPermission.getLevel()) && !CrmPermissionUtils.isCrmAdmin())) { + if ((oldPermission == null || !isOwner(oldPermission.getLevel())) + && !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管 throw exception(CRM_PERMISSION_DENIED, bizTypeName); } // 1.1 校验转移对象是否已经是该负责人 - if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { + if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); } // 1.2 校验新负责人是否存在 From e50250449a1d38128d6577917aa033e1414731a9 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 19:50:02 +0800 Subject: [PATCH 07/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91UserProfileQueryToolFunction=20?= =?UTF-8?q?=E6=B2=A1=E6=9C=89=E5=8F=82=E6=95=B0=EF=BC=8C=E4=BC=9A=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tool/UserProfileQueryToolFunction.java | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java index 6e0ba51c9e..5656d39292 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java @@ -7,6 +7,8 @@ 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.dto.AdminUserRespDTO; import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; import jakarta.annotation.Resource; import lombok.AllArgsConstructor; import lombok.Data; @@ -17,7 +19,7 @@ import org.springframework.stereotype.Component; import java.util.function.BiFunction; /** - * 工具:当前用户信息查询 + * 工具:用户信息查询 * * 同时,也是展示 ToolContext 上下文的使用 * @@ -31,8 +33,17 @@ public class UserProfileQueryToolFunction private AdminUserApi adminUserApi; @Data - @JsonClassDescription("当前用户信息查询") - public static class Request { } + @JsonClassDescription("用户信息查询") + public static class Request { + + /** + * 用户编号 + */ + @JsonProperty(value = "id") + @JsonPropertyDescription("用户编号,例如说:1。如果查询自己,则 id 为空") + private Long id; + + } @Data @AllArgsConstructor @@ -61,13 +72,19 @@ public class UserProfileQueryToolFunction @Override public Response apply(Request request, ToolContext toolContext) { - LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID); - if (loginUser == null | tenantId == null) { - return null; + if (tenantId == null) { + return new Response(); + } + if (request.getId() == null) { + LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); + if (loginUser == null) { + return new Response(); + } + request.setId(loginUser.getId()); } return TenantUtils.execute(tenantId, () -> { - AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId()); + AdminUserRespDTO user = adminUserApi.getUser(request.getId()); return BeanUtils.toBean(user, Response.class); }); } From c789418a7bd9aedd1097de1b160141b7dded92eb Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 20:34:42 +0800 Subject: [PATCH 08/12] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91=E4=BE=9D=E8=B5=96=20spring=20ai=20?= =?UTF-8?q?=E5=8D=87=E7=BA=A7=E5=88=B0=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/pom.xml | 38 ++++++++++--------- .../ai/config/AiAutoConfiguration.java | 8 ++-- .../siliconflow/SiliconFlowImageModel.java | 2 +- .../ai/service/image/AiImageServiceImpl.java | 16 ++++---- .../iocoder/yudao/module/ai/util/AiUtils.java | 14 +++---- .../core/model/chat/OpenAIChatModelTests.java | 4 +- .../core/model/chat/TongYiChatModelTests.java | 11 ++++-- .../model/image/OpenAiImageModelTests.java | 6 +-- .../src/main/resources/application-dev.yaml | 4 +- .../src/main/resources/application-local.yaml | 4 +- 10 files changed, 56 insertions(+), 51 deletions(-) diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 4b02a7e00f..95c836777a 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -19,7 +19,8 @@ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno - 1.0.0-M6 + 1.0.0 + 1.0.0.2 1.0.2 @@ -75,65 +76,66 @@ org.springframework.ai - spring-ai-openai-spring-boot-starter + spring-ai-starter-model-openai ${spring-ai.version} org.springframework.ai - spring-ai-azure-openai-spring-boot-starter + spring-ai-starter-model-azure-openai ${spring-ai.version} org.springframework.ai - spring-ai-ollama-spring-boot-starter + spring-ai-starter-model-ollama ${spring-ai.version} org.springframework.ai - spring-ai-stability-ai-spring-boot-starter + spring-ai-starter-model-stability-ai ${spring-ai.version} com.alibaba.cloud.ai - spring-ai-alibaba-starter - ${spring-ai.version}.1 + spring-ai-alibaba-starter-dashscope + ${alibaba-ai.version} - org.springframework.ai - spring-ai-qianfan-spring-boot-starter - ${spring-ai.version} + org.springaicommunity + qianfan-spring-boot-starter + 1.0.0 org.springframework.ai - spring-ai-zhipuai-spring-boot-starter + spring-ai-starter-model-zhipuai ${spring-ai.version} org.springframework.ai - spring-ai-minimax-spring-boot-starter + spring-ai-starter-model-minimax ${spring-ai.version} - org.springframework.ai - spring-ai-moonshot-spring-boot-starter - ${spring-ai.version} + + org.springaicommunity + moonshot-spring-boot-starter + 1.0.0 org.springframework.ai - spring-ai-qdrant-store + spring-ai-starter-vector-store-qdrant ${spring-ai.version} org.springframework.ai - spring-ai-redis-store + spring-ai-starter-vector-store-redis ${spring-ai.version} @@ -144,7 +146,7 @@ org.springframework.ai - spring-ai-milvus-store + spring-ai-starter-vector-store-milvus ${spring-ai.version} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index a28d726b90..ae8a7e75f6 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -14,10 +14,6 @@ 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.xinghuo.XingHuoChatModel; 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.TokenCountBatchingStrategy; import org.springframework.ai.model.tool.ToolCallingManager; @@ -26,6 +22,10 @@ import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; 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.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java index 43f8ad2168..44a652309e 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java @@ -89,7 +89,7 @@ public class SiliconFlowImageModel implements ImageModel { var observationContext = ImageModelObservationContext.builder() .imagePrompt(imagePrompt) .provider(SiliconFlowApiConstants.PROVIDER_NAME) - .requestOptions(imagePrompt.getOptions()) + .imagePrompt(imagePrompt) .build(); return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 671098a704..79214a0325 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -9,9 +9,6 @@ import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; 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.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; @@ -24,17 +21,20 @@ 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.mysql.image.AiImageMapper; 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.infra.api.file.FileApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springaicommunity.qianfan.QianFanImageOptions; import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageOptions; -import org.springframework.ai.qianfan.QianFanImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.scheduling.annotation.Async; @@ -140,10 +140,10 @@ public class AiImageServiceImpl implements AiImageService { private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { // https://platform.openai.com/docs/api-reference/images/create - return OpenAiImageOptions.builder().withModel(model.getModel()) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) - .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 - .withResponseFormat("b64_json") + return OpenAiImageOptions.builder().model(model.getModel()) + .height(draw.getHeight()).width(draw.getWidth()) + .style(MapUtil.getStr(draw.getOptions(), "style")) // 风格 + .responseFormat("b64_json") .build(); } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) { // https://docs.siliconflow.cn/cn/api-reference/images/images-generations diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java index ac3ff39a49..0744ff6307 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java @@ -2,18 +2,18 @@ package cn.iocoder.yudao.module.ai.util; import cn.hutool.core.util.ObjUtil; 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.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; 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.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.moonshot.MoonshotChatOptions; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import java.util.Collections; @@ -43,18 +43,18 @@ public class AiUtils { switch (platform) { case TONG_YI: return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) - .withFunctions(toolNames).withToolContext(toolContext).build(); + .withToolNames(toolNames).withToolContext(toolContext).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case MINI_MAX: return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case MOONSHOT: return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java index ff866fe40b..c650fd0420 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java @@ -25,10 +25,10 @@ public class OpenAIChatModelTests { private final OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.holdai.top") - .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey + .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey .build()) .defaultOptions(OpenAiChatOptions.builder() - .model(OpenAiApi.ChatModel.GPT_4_O) // 模型 + .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型 .temperature(0.7) .build()) .build(); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java index 4f0efdb20c..4f2e27edd2 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java @@ -22,14 +22,17 @@ import java.util.List; */ public class TongYiChatModelTests { - private final DashScopeChatModel chatModel = new DashScopeChatModel( - new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), - DashScopeChatOptions.builder() + private final DashScopeChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") + .build()) + .defaultOptions( DashScopeChatOptions.builder() .withModel("qwen1.5-72b-chat") // 模型 // .withModel("deepseek-r1") // 模型(deepseek-r1) // .withModel("deepseek-v3") // 模型(deepseek-v3) // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) - .build()); + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java index 49015b9b9e..1b124529de 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java @@ -18,7 +18,7 @@ public class OpenAiImageModelTests { private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() .baseUrl("https://api.holdai.top") // apiKey - .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") + .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") .build()); @Test @@ -26,8 +26,8 @@ public class OpenAiImageModelTests { public void testCall() { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .withModel(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 - .withHeight(256).withWidth(256) + .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 + .height(256).width(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index db07245c63..344e97ecf8 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -6,8 +6,8 @@ server: spring: autoconfigure: exclude: - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 2849775e62..5d23b9b70d 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -10,8 +10,8 @@ spring: - 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.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 From 3d0eb77148c5869c42699ba1f2831e4ed89f01ab Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 20:56:00 +0800 Subject: [PATCH 09/12] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91=E5=BC=95=E5=85=A5=20spring-ai-start?= =?UTF-8?q?er-model-deepseek=20=E4=BE=9D=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- yudao-module-ai/pom.xml | 31 ++++++++----- .../ai/config/AiAutoConfiguration.java | 28 ------------ .../ai/config/YudaoAiProperties.java | 19 -------- .../model/deepseek/DeepSeekChatModel.java | 45 ------------------- .../model/chat/DeepSeekChatModelTests.java | 18 +++----- 5 files changed, 26 insertions(+), 115 deletions(-) delete mode 100644 yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 95c836777a..4a2323bcfa 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -84,6 +84,11 @@ spring-ai-starter-model-azure-openai ${spring-ai.version} + + org.springframework.ai + spring-ai-starter-model-deepseek + ${spring-ai.version} + org.springframework.ai spring-ai-starter-model-ollama @@ -94,18 +99,6 @@ spring-ai-starter-model-stability-ai ${spring-ai.version} - - - com.alibaba.cloud.ai - spring-ai-alibaba-starter-dashscope - ${alibaba-ai.version} - - - - org.springaicommunity - qianfan-spring-boot-starter - 1.0.0 - org.springframework.ai @@ -117,6 +110,20 @@ spring-ai-starter-model-minimax ${spring-ai.version} + + + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + ${alibaba-ai.version} + + + + + org.springaicommunity + qianfan-spring-boot-starter + 1.0.0 + org.springaicommunity diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index ae8a7e75f6..4ff7c9e4dc 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -5,7 +5,6 @@ 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.AiModelFactoryImpl; 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.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -52,33 +51,6 @@ public class AiAutoConfiguration { // ========== 各种 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 @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java index 7f8046768a..7c26aa89ca 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java @@ -13,12 +13,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @Data public class YudaoAiProperties { - /** - * DeepSeek - */ - @SuppressWarnings("SpellCheckingInspection") - private DeepSeekProperties deepseek; - /** * 字节豆包 */ @@ -60,19 +54,6 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") 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 public static class DouBaoProperties { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java deleted file mode 100644 index d603abf6b0..0000000000 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java +++ /dev/null @@ -1,45 +0,0 @@ -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 stream(Prompt prompt) { - return openAiChatModel.stream(prompt); - } - - @Override - public ChatOptions getDefaultOptions() { - return openAiChatModel.getDefaultOptions(); - } - -} diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java index d20a1761f6..7b51df1662 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java @@ -1,6 +1,5 @@ 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.Test; import org.springframework.ai.chat.messages.Message; @@ -8,9 +7,9 @@ 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.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -23,19 +22,16 @@ import java.util.List; */ public class DeepSeekChatModelTests { - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() - .baseUrl(DeepSeekChatModel.BASE_URL) - .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey + private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model("deepseek-chat") // 模型 .temperature(0.7) .build()) .build(); - private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); - @Test @Disabled public void testCall() { From 750709d706fdad1fdde16e6fab7a75de238ea879 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 21:40:00 +0800 Subject: [PATCH 10/12] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91deepseek=E3=80=81azure=E3=80=81baich?= =?UTF-8?q?uan=E3=80=81moonshot=20=E9=80=82=E9=85=8D=201.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/ai/core/AiModelFactoryImpl.java | 223 +++++++++++------- .../model/chat/AzureOpenAIChatModelTests.java | 14 +- .../model/chat/BaiChuanChatModelTests.java | 3 +- .../model/chat/MoonshotChatModelTests.java | 20 +- .../image/StabilityAiImageModelTests.java | 4 +- .../src/main/resources/application.yaml | 9 +- 6 files changed, 160 insertions(+), 113 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index f258ffaf1b..2aeecab917 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -8,11 +8,11 @@ import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; 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.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.deepseek.DeepSeekChatModel; 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.midjourney.api.MidjourneyApi; @@ -22,8 +22,9 @@ 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.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; -import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; -import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; @@ -32,47 +33,55 @@ import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.KeyCredential; import io.micrometer.observation.ObservationRegistry; import io.milvus.client.MilvusServiceClient; import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import lombok.SneakyThrows; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties; -import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration; -import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration; -import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; -import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; -import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails; -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.springaicommunity.moonshot.MoonshotChatModel; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.moonshot.api.MoonshotApi; +import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration; +import org.springaicommunity.qianfan.QianFanChatModel; +import org.springaicommunity.qianfan.QianFanEmbeddingModel; +import org.springaicommunity.qianfan.QianFanEmbeddingOptions; +import org.springaicommunity.qianfan.QianFanImageModel; +import org.springaicommunity.qianfan.api.QianFanApi; +import org.springaicommunity.qianfan.api.QianFanImageApi; +import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration; +import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; 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.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.image.ImageModel; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; +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.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; @@ -84,21 +93,23 @@ import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiImageApi; 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.api.StabilityAiApi; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; 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.VectorStoreObservationConvention; 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.autoconfigure.RedisVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; @@ -190,7 +201,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); case BAI_CHUAN: - return SpringUtil.getBean(AzureOpenAiChatModel.class); + return SpringUtil.getBean(BaiChuanChatModel.class); case OPENAI: return SpringUtil.getBean(OpenAiChatModel.class); case AZURE_OPENAI: @@ -319,27 +330,34 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 spring-ai 客户端的方法 ========== /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 + * 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法 */ private static DashScopeChatModel buildTongYiChatModel(String key) { - DashScopeApi dashScopeApi = new DashScopeApi(key); + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build(); DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) .withTemperature(0.7).build(); - return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 + * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法 */ private static DashScopeImageModel buildTongYiImagesModel(String key) { DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); - return new DashScopeImageModel(dashScopeImageApi); + return DashScopeImageModel.builder() + .dashScopeApi(dashScopeImageApi) + .build(); } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 + * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { + // TODO @芋艿:未测试 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -349,9 +367,10 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 + * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { + // TODO @芋艿:未测试 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -361,12 +380,17 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link AiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} + * 可参考 {@link DeepSeekChatAutoConfiguration} 的 deepSeekChatModel 方法 */ private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { - YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() - .setApiKey(apiKey); - return new AiAutoConfiguration().buildDeepSeekChatModel(properties); + DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build(); + DeepSeekChatOptions options = DeepSeekChatOptions.builder().model(DeepSeekApi.DEFAULT_CHAT_MODEL) + .temperature(0.7).build(); + return DeepSeekChatModel.builder() + .deepSeekApi(deepSeekApi) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -397,17 +421,18 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 + * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) : new ZhiPuAiApi(url, apiKey); ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE, + getObservationRegistry().getIfAvailable()); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 + * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) @@ -416,23 +441,30 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法 + * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 */ private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) : new MiniMaxApi(url, apiKey); MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); } /** - * 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法 + * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 */ private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { - MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) - : new MoonshotApi(url, apiKey); + MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder() + .apiKey(apiKey); + if (StrUtil.isNotEmpty(url)) { + moonshotApiBuilder.baseUrl(url); + } MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); - return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return MoonshotChatModel.builder() + .moonshotApi(moonshotApiBuilder.build()) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -456,33 +488,32 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 + * 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法 */ private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); - return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); + return OpenAiChatModel.builder() + .openAiApi(openAiApi) + .toolCallingManager(getToolCallingManager()) + .build(); } - // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** - * 可参考 {@link AzureOpenAiAutoConfiguration} + * 可参考 {@link AzureOpenAiChatAutoConfiguration} */ private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) { - AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); - // 创建 OpenAIClient 对象 - AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - connectionProperties.setEndpoint(url); - OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); - // 获取 AzureOpenAiChatProperties 对象 - AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, - getToolCallingManager(), null, null); + // TODO @芋艿:使用前,请测试,暂时没密钥!!! + OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() + .endpoint(url).credential(new KeyCredential(apiKey)); + return AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAIClientBuilder) + .toolCallingManager(getToolCallingManager()) + .build(); } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 + * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 */ private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); @@ -494,16 +525,18 @@ public class AiModelFactoryImpl implements AiModelFactory { * 创建 SiliconFlowImageModel 对象 */ private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) { + // TODO @芋艿:未测试 url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL); SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken); return new SiliconFlowImageModel(openAiApi); } /** - * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 + * 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { - OllamaApi ollamaApi = new OllamaApi(url); + // TODO @芋艿:未测试 + OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); } @@ -519,16 +552,16 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 + * 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { - DashScopeApi dashScopeApi = new DashScopeApi(apiKey); + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 + * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 */ private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) @@ -538,7 +571,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法 + * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法 */ private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) @@ -548,7 +581,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法 + * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法 */ private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { List keys = StrUtil.split(key, '|'); @@ -561,13 +594,13 @@ public class AiModelFactoryImpl implements AiModelFactory { } private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { - OllamaApi ollamaApi = new OllamaApi(url); + OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法 + * 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法 */ private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); @@ -576,21 +609,19 @@ public class AiModelFactoryImpl implements AiModelFactory { return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties); } - // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** - * 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 + * 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 */ private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) { - AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); - // 创建 OpenAIClient 对象 - AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - connectionProperties.setEndpoint(url); - OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 + AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration(); + // 创建 OpenAIClientBuilder 对象 + OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() + .endpoint(url).credential(new KeyCredential(apiKey)); // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties, - null, null); + return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties, + getObservationRegistry(), getEmbeddingModelObservationConvention()); } // ========== 各种创建 VectorStore 的方法 ========== @@ -657,10 +688,11 @@ public class AiModelFactoryImpl implements AiModelFactory { RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort()); // 创建 RedisVectorStoreProperties 对象 + // TODO @芋艿:index-name 可能影响索引名; RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration(); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) - .indexName(properties.getIndex()).prefix(properties.getPrefix()) + .indexName(properties.getIndexName()).prefix(properties.getPrefix()) .initializeSchema(properties.isInitializeSchema()) .metadataFields(convertList(metadataFields.entrySet(), entry -> { String fieldName = entry.getKey(); @@ -730,10 +762,12 @@ public class AiModelFactoryImpl implements AiModelFactory { private static ObjectProvider getCustomObservationConvention() { return new ObjectProvider<>() { + @Override public VectorStoreObservationConvention getObject() throws BeansException { return new DefaultVectorStoreObservationConvention(); } + }; } @@ -745,8 +779,15 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(ToolCallingManager.class); } - private static FunctionCallbackResolver getFunctionCallbackResolver() { - return SpringUtil.getBean(FunctionCallbackResolver.class); + private static ObjectProvider getEmbeddingModelObservationConvention() { + return new ObjectProvider<>() { + + @Override + public EmbeddingModelObservationConvention getObject() throws BeansException { + return SpringUtil.getBean(EmbeddingModelObservationConvention.class); + } + + }; } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java index 5c924a5823..69776d8e68 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; -import com.azure.core.util.ClientOptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; @@ -17,7 +16,7 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; -import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; +import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; /** * {@link AzureOpenAiChatModel} 集成测试 @@ -29,10 +28,13 @@ public class AzureOpenAIChatModelTests { // TODO @芋艿:晚点在调整 private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() .endpoint("https://eastusprejade.openai.azure.com") - .credential(new AzureKeyCredential("xxx")) - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); - private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, - AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); + .credential(new AzureKeyCredential("xxx")); + private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAiApi) + .defaultOptions(AzureOpenAiChatOptions.builder() + .deploymentName(DEFAULT_DEPLOYMENT_NAME) + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java index d1cc381fb9..06b0b25653 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java @@ -1,7 +1,6 @@ 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.deepseek.DeepSeekChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -35,7 +34,7 @@ public class BaiChuanChatModelTests { .build()) .build(); - private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); + private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java index 7de7fd709c..992334b4d9 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java @@ -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.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.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.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -22,11 +22,15 @@ import java.util.List; */ public class MoonshotChatModelTests { - private final MoonshotChatModel chatModel = new MoonshotChatModel( - new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥 - MoonshotChatOptions.builder() - .model("moonshot-v1-8k") // 模型 - .build()); + private final MoonshotChatModel chatModel = MoonshotChatModel.builder() + .moonshotApi(MoonshotApi.builder() + .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 + .build()) + .defaultOptions(MoonshotChatOptions.builder() + .model("kimi-k2-0711-preview") // 模型 + .build()) + .build(); + @Test @Disabled public void testCall() { diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java index b58e6df00e..8cf556d9f8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java @@ -31,8 +31,8 @@ public class StabilityAiImageModelTests { public void testCall() { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .withModel("stable-diffusion-v1-6") - .withHeight(320).withWidth(320) + .model("stable-diffusion-v1-6") + .height(320).width(320) .build(); ImagePrompt prompt = new ImagePrompt("great wall", options); diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 37b783d57d..b9658e9746 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -188,13 +188,14 @@ spring: api-key: xxxx moonshot: # 月之暗灭(KIMI) api-key: sk-abc + deepseek: # DeepSeek + api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + chat: + options: + model: deepseek-chat yudao: ai: - deep-seek: # DeepSeek - enable: true - api-key: sk-e94db327cc7d457d99a8de8810fc6b12 - model: deepseek-chat doubao: # 字节豆包 enable: true api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 From 39ecf5ebe5aa8489a7ec44fd9bf31226c2f6887d Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 22:42:56 +0800 Subject: [PATCH 11/12] =?UTF-8?q?feat=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91=E6=96=87=E5=BF=83=E4=B8=80=E8=A8=80?= =?UTF-8?q?=E5=AD=98=E5=9C=A8=20springai=20=E6=8E=A5=E5=85=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98=EF=BC=8C=E6=97=A0=E6=B3=95=E4=BD=BF=E7=94=A8=20https:?= =?UTF-8?q?//github.com/spring-ai-community/qianfan/issues/6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/ai/core/AiModelFactoryImpl.java | 16 ++-- .../core/model/chat/LlamaChatModelTests.java | 90 ++++++++----------- .../core/model/chat/OllamaChatModelTests.java | 4 +- .../core/model/chat/YiYanChatModelTests.java | 10 +-- .../core/model/image/QianFanImageTests.java | 6 +- 5 files changed, 59 insertions(+), 67 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index 2aeecab917..759c0a038f 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -357,7 +357,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { - // TODO @芋艿:未测试 + // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -370,7 +370,7 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { - // TODO @芋艿:未测试 + // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -525,7 +525,6 @@ public class AiModelFactoryImpl implements AiModelFactory { * 创建 SiliconFlowImageModel 对象 */ private SiliconFlowImageModel buildSiliconFlowImageModel(String apiToken, String url) { - // TODO @芋艿:未测试 url = StrUtil.blankToDefault(url, SiliconFlowApiConstants.DEFAULT_BASE_URL); SiliconFlowImageApi openAiApi = new SiliconFlowImageApi(url, apiToken); return new SiliconFlowImageModel(openAiApi); @@ -535,9 +534,11 @@ public class AiModelFactoryImpl implements AiModelFactory { * 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { - // TODO @芋艿:未测试 OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); - return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); + return OllamaChatModel.builder() + .ollamaApi(ollamaApi) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -596,7 +597,10 @@ public class AiModelFactoryImpl implements AiModelFactory { private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); - return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); + return OllamaEmbeddingModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(ollamaOptions) + .build(); } /** diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java index 153342d44c..69e2c1daaa 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java @@ -1,20 +1,6 @@ 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.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} 集成测试 @@ -23,43 +9,43 @@ import java.util.List; */ public class LlamaChatModelTests { - private final OllamaChatModel chatModel = OllamaChatModel.builder() - .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 - .defaultOptions(OllamaOptions.builder() - .model(OllamaModel.LLAMA3.getName()) // 模型 - .build()) - .build(); - - @Test - @Disabled - public void testCall() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = chatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - System.out.println(response.getResult().getOutput()); - } - - @Test - @Disabled - public void testStream() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = chatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } +// private final OllamaChatModel chatModel = OllamaChatModel.builder() +// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 +// .defaultOptions(OllamaOptions.builder() +// .model(OllamaModel.LLAMA3.getName()) // 模型 +// .build()) +// .build(); +// +// @Test +// @Disabled +// public void testCall() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// ChatResponse response = chatModel.call(new Prompt(messages)); +// // 打印结果 +// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// } +// +// @Test +// @Disabled +// public void testStream() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// Flux flux = chatModel.stream(new Prompt(messages)); +// // 打印结果 +// flux.doOnNext(response -> { +//// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// }).then().block(); +// } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java index f86e67a667..d2bf68812b 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java @@ -23,7 +23,9 @@ import java.util.List; public class OllamaChatModelTests { private final OllamaChatModel chatModel = OllamaChatModel.builder() - .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .ollamaApi(OllamaApi.builder() + .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址 + .build()) .defaultOptions(OllamaOptions.builder() // .model("qwen") // 模型(https://ollama.com/library/qwen) .model("deepseek-r1") // 模型(https://ollama.com/library/deepseek-r1) diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java index ab6f642437..cb7be2a296 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java @@ -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.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.UserMessage; import org.springframework.ai.chat.model.ChatResponse; 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 java.util.ArrayList; @@ -23,9 +23,9 @@ import java.util.List; public class YiYanChatModelTests { private final QianFanChatModel chatModel = new QianFanChatModel( - new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥 + new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥 QianFanChatOptions.builder() - .model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue()) + .model("ERNIE-4.5-8K-Preview") .build() ); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java index 8f44ab9ad1..156360f255 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java @@ -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.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.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; From ea5b12f21e2c0eb3e9e833b12e1af3bfad0681f1 Mon Sep 17 00:00:00 2001 From: YunaiV Date: Mon, 14 Jul 2025 22:44:50 +0800 Subject: [PATCH 12/12] =?UTF-8?q?fix=EF=BC=9A=E3=80=90AI=20=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E3=80=91RedisVectorStore=20=E6=94=AF?= =?UTF-8?q?=E6=8C=81=20username=E3=80=81password?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/ai/framework/ai/core/AiModelFactoryImpl.java | 5 ++--- yudao-server/src/main/resources/application.yaml | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index 759c0a038f..f7b42e30ae 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -690,10 +690,9 @@ public class AiModelFactoryImpl implements AiModelFactory { Map> metadataFields) { // 创建 JedisPooled 对象 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 对象 - // TODO @芋艿:index-name 可能影响索引名; - RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration(); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) .indexName(properties.getIndexName()).prefix(properties.getPrefix()) diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index b9658e9746..12a4abf896 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -150,7 +150,7 @@ spring: vectorstore: # 向量存储 redis: initialize-schema: true - index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 + index-name: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 qdrant: initialize-schema: true