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

# Conflicts:
#	yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceMqttConnectionParamsRespVO.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaFirmwareController.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/ota/IotOtaUpgradeRecordController.java
#	yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/thingmodel/vo/IotThingModelRespVO.java
This commit is contained in:
YunaiV
2025-08-02 10:50:12 +08:00
227 changed files with 1869 additions and 1242 deletions

View File

@@ -32,7 +32,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>2.6.0-SNAPSHOT</revision> <revision>2025.08-SNAPSHOT</revision>
<!-- Maven 相关 --> <!-- Maven 相关 -->
<java.version>17</java.version> <java.version>17</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
@@ -87,6 +87,13 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<version>${lombok.version}</version> <version>${lombok.version}</version>
</path> </path>
<path>
<!-- 确保 Lombok 生成的 getter/setter 方法能被 MapStruct 正确识别,
避免出现 No property named “xxx" exists 的编译错误 -->
<groupId>org.projectlombok</groupId>
<artifactId>lombok-mapstruct-binding</artifactId>
<version>0.2.0</version>
</path>
<path> <path>
<groupId>org.mapstruct</groupId> <groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId> <artifactId>mapstruct-processor</artifactId>

View File

@@ -1,253 +1,208 @@
-- ---------------------------- -- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql
-- qrtz_blob_triggers -- Thanks to Patrick Lightbody for submitting this...
-- ---------------------------- --
CREATE TABLE qrtz_blob_triggers -- 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, SCHED_NAME VARCHAR(120) NOT NULL,
trigger_name varchar(190) NOT NULL, JOB_NAME VARCHAR(200) NOT NULL,
trigger_group varchar(190) NOT NULL, JOB_GROUP VARCHAR(200) NOT NULL,
blob_data bytea NULL, DESCRIPTION VARCHAR(250) NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group) 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); CREATE TABLE QRTZ_TRIGGERS
-- ----------------------------
-- qrtz_calendars
-- ----------------------------
CREATE TABLE qrtz_calendars
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
calendar_name varchar(190) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL,
calendar bytea NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL,
PRIMARY KEY (sched_name, calendar_name) 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)
); );
-- ---------------------------- CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS
-- qrtz_cron_triggers
-- ----------------------------
CREATE TABLE qrtz_cron_triggers
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
trigger_name varchar(190) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL,
trigger_group varchar(190) NOT NULL, PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP)
cron_expression varchar(120) NOT NULL,
time_zone_id varchar(80) NULL DEFAULT NULL,
PRIMARY KEY (sched_name, trigger_name, trigger_group)
); );
-- @formatter:off CREATE TABLE QRTZ_FIRED_TRIGGERS
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_fired_triggers
-- ----------------------------
CREATE TABLE qrtz_fired_triggers
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
entry_id varchar(95) NOT NULL, ENTRY_ID VARCHAR(95) NOT NULL,
trigger_name varchar(190) NOT NULL, TRIGGER_NAME VARCHAR(200) NOT NULL,
trigger_group varchar(190) NOT NULL, TRIGGER_GROUP VARCHAR(200) NOT NULL,
instance_name varchar(190) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL,
fired_time int8 NOT NULL, FIRED_TIME BIGINT NOT NULL,
sched_time int8 NOT NULL, SCHED_TIME BIGINT NOT NULL,
priority int4 NOT NULL, PRIORITY INTEGER NOT NULL,
state varchar(16) NOT NULL, STATE VARCHAR(16) NOT NULL,
job_name varchar(190) NULL DEFAULT NULL, JOB_NAME VARCHAR(200) NULL,
job_group varchar(190) NULL DEFAULT NULL, JOB_GROUP VARCHAR(200) NULL,
is_nonconcurrent varchar(1) NULL DEFAULT NULL, IS_NONCONCURRENT BOOL NULL,
requests_recovery varchar(1) NULL DEFAULT NULL, REQUESTS_RECOVERY BOOL NULL,
PRIMARY KEY (sched_name, entry_id) PRIMARY KEY (SCHED_NAME, ENTRY_ID)
); );
CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name); CREATE TABLE QRTZ_SCHEDULER_STATE
CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery);
CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group);
CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group);
CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group);
CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group);
-- ----------------------------
-- qrtz_job_details
-- ----------------------------
CREATE TABLE qrtz_job_details
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
job_name varchar(190) NOT NULL, INSTANCE_NAME VARCHAR(200) NOT NULL,
job_group varchar(190) NOT NULL, LAST_CHECKIN_TIME BIGINT NOT NULL,
description varchar(250) NULL DEFAULT NULL, CHECKIN_INTERVAL BIGINT NOT NULL,
job_class_name varchar(250) NOT NULL, PRIMARY KEY (SCHED_NAME, INSTANCE_NAME)
is_durable varchar(1) NOT NULL,
is_nonconcurrent varchar(1) NOT NULL,
is_update_data varchar(1) NOT NULL,
requests_recovery varchar(1) NOT NULL,
job_data bytea NULL,
PRIMARY KEY (sched_name, job_name, job_group)
); );
CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery); CREATE TABLE QRTZ_LOCKS
CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group);
-- @formatter:off
BEGIN;
COMMIT;
-- @formatter:on
-- ----------------------------
-- qrtz_locks
-- ----------------------------
CREATE TABLE qrtz_locks
( (
sched_name varchar(120) NOT NULL, SCHED_NAME VARCHAR(120) NOT NULL,
lock_name varchar(40) NOT NULL, LOCK_NAME VARCHAR(40) NOT NULL,
PRIMARY KEY (sched_name, lock_name) PRIMARY KEY (SCHED_NAME, LOCK_NAME)
); );
-- @formatter:off CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY
BEGIN; ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY);
COMMIT; CREATE INDEX IDX_QRTZ_J_GRP
-- @formatter:on ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP);
-- ---------------------------- CREATE INDEX IDX_QRTZ_T_J
-- qrtz_paused_trigger_grps ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
-- ---------------------------- CREATE INDEX IDX_QRTZ_T_JG
CREATE TABLE qrtz_paused_trigger_grps ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP);
( CREATE INDEX IDX_QRTZ_T_C
sched_name varchar(120) NOT NULL, ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME);
trigger_group varchar(190) NOT NULL, CREATE INDEX IDX_QRTZ_T_G
PRIMARY KEY (sched_name, trigger_group) ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
); CREATE INDEX IDX_QRTZ_T_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_N_G_STATE
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME
ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST
ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE);
CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP
ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE);
-- ---------------------------- CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME
-- qrtz_scheduler_state ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME);
-- ---------------------------- CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY
CREATE TABLE qrtz_scheduler_state ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY);
( CREATE INDEX IDX_QRTZ_FT_J_G
sched_name varchar(120) NOT NULL, ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP);
instance_name varchar(190) NOT NULL, CREATE INDEX IDX_QRTZ_FT_JG
last_checkin_time int8 NOT NULL, ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP);
checkin_interval int8 NOT NULL, CREATE INDEX IDX_QRTZ_FT_T_G
PRIMARY KEY (sched_name, instance_name) ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP);
); CREATE INDEX IDX_QRTZ_FT_TG
ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP);
-- @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
-- ---------------------------- COMMIT;
-- FK: qrtz_blob_triggers
-- ----------------------------
ALTER TABLE qrtz_blob_triggers
ADD CONSTRAINT qrtz_blob_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
trigger_name,
trigger_group);
-- ----------------------------
-- FK: qrtz_cron_triggers
-- ----------------------------
ALTER TABLE qrtz_cron_triggers
ADD CONSTRAINT qrtz_cron_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- FK: qrtz_simple_triggers
-- ----------------------------
ALTER TABLE qrtz_simple_triggers
ADD CONSTRAINT qrtz_simple_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name,
trigger_name,
trigger_group);
-- ----------------------------
-- FK: qrtz_simprop_triggers
-- ----------------------------
ALTER TABLE qrtz_simprop_triggers
ADD CONSTRAINT qrtz_simprop_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group);
-- ----------------------------
-- FK: qrtz_triggers
-- ----------------------------
ALTER TABLE qrtz_triggers
ADD CONSTRAINT qrtz_triggers_ibfk_1 FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group);

View File

@@ -17,6 +17,7 @@ uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql
import argparse import argparse
import pathlib import pathlib
import re import re
import sys
import time import time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import Dict, Generator, Optional, Tuple, Union from typing import Dict, Generator, Optional, Tuple, Union
@@ -293,8 +294,10 @@ class Convertor(ABC):
# 将parse失败的脚本打印出来 # 将parse失败的脚本打印出来
if error_scripts: if error_scripts:
print("!!! 以下内容无法正常解析", file=sys.stderr)
for script in error_scripts: for script in error_scripts:
print(script) # print to stderr
print(script, file=sys.stderr)
class PostgreSQLConvertor(Convertor): class PostgreSQLConvertor(Convertor):

View File

@@ -14,7 +14,7 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<properties> <properties>
<revision>2.6.0-SNAPSHOT</revision> <revision>2025.08-SNAPSHOT</revision>
<flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version> <flatten-maven-plugin.version>1.6.0</flatten-maven-plugin.version>
<!-- 统一依赖管理 --> <!-- 统一依赖管理 -->
<spring.boot.version>3.4.5</spring.boot.version> <spring.boot.version>3.4.5</spring.boot.version>
@@ -24,9 +24,9 @@
<!-- DB 相关 --> <!-- DB 相关 -->
<druid.version>1.2.24</druid.version> <druid.version>1.2.24</druid.version>
<mybatis.version>3.5.19</mybatis.version> <mybatis.version>3.5.19</mybatis.version>
<mybatis-plus.version>3.5.10.1</mybatis-plus.version> <mybatis-plus.version>3.5.12</mybatis-plus.version>
<mybatis-plus-join.version>1.5.4</mybatis-plus-join.version>
<dynamic-datasource.version>4.3.1</dynamic-datasource.version> <dynamic-datasource.version>4.3.1</dynamic-datasource.version>
<mybatis-plus-join.version>1.4.13</mybatis-plus-join.version>
<easy-trans.version>3.0.6</easy-trans.version> <easy-trans.version>3.0.6</easy-trans.version>
<redisson.version>3.41.0</redisson.version> <redisson.version>3.41.0</redisson.version>
<dm8.jdbc.version>8.1.3.140</dm8.jdbc.version> <dm8.jdbc.version>8.1.3.140</dm8.jdbc.version>
@@ -54,7 +54,7 @@
<mapstruct.version>1.6.3</mapstruct.version> <mapstruct.version>1.6.3</mapstruct.version>
<hutool-5.version>5.8.35</hutool-5.version> <hutool-5.version>5.8.35</hutool-5.version>
<hutool-6.version>6.0.0-M19</hutool-6.version> <hutool-6.version>6.0.0-M19</hutool-6.version>
<easyexcel.version>4.0.3</easyexcel.version> <fastexcel.version>1.2.0</fastexcel.version>
<velocity.version>2.4.1</velocity.version> <velocity.version>2.4.1</velocity.version>
<fastjson.version>1.2.83</fastjson.version> <fastjson.version>1.2.83</fastjson.version>
<guava.version>33.4.8-jre</guava.version> <guava.version>33.4.8-jre</guava.version>
@@ -69,12 +69,11 @@
<pf4j-spring.version>0.9.0</pf4j-spring.version> <pf4j-spring.version>0.9.0</pf4j-spring.version>
<vertx.version>4.5.13</vertx.version> <vertx.version>4.5.13</vertx.version>
<!-- 三方云服务相关 --> <!-- 三方云服务相关 -->
<commons-io.version>2.17.0</commons-io.version>
<commons-compress.version>1.27.1</commons-compress.version>
<awssdk.version>2.30.14</awssdk.version> <awssdk.version>2.30.14</awssdk.version>
<justauth.version>1.16.7</justauth.version> <justauth.version>1.16.7</justauth.version>
<justauth-starter.version>1.4.0</justauth-starter.version> <justauth-starter.version>1.4.0</justauth-starter.version>
<jimureport.version>1.9.4</jimureport.version> <jimureport.version>2.0.0</jimureport.version>
<jimubi.version>1.9.5</jimubi.version>
<weixin-java.version>4.7.5.B</weixin-java.version> <weixin-java.version>4.7.5.B</weixin-java.version>
</properties> </properties>
@@ -479,20 +478,11 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>cn.idev.excel</groupId>
<artifactId>easyexcel</artifactId> <artifactId>fastexcel</artifactId>
<version>${easyexcel.version}</version> <version>${fastexcel.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>${commons-compress.version}</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.tika</groupId> <groupId>org.apache.tika</groupId>
<artifactId>tika-core</artifactId> <!-- 文件类型的识别 --> <artifactId>tika-core</artifactId> <!-- 文件类型的识别 -->
@@ -603,7 +593,7 @@
<dependency> <dependency>
<groupId>org.jeecgframework.jimureport</groupId> <groupId>org.jeecgframework.jimureport</groupId>
<artifactId>jimubi-spring-boot3-starter</artifactId> <artifactId>jimubi-spring-boot3-starter</artifactId>
<version>${jimureport.version}</version> <version>${jimubi.version}</version>
<exclusions> <exclusions>
<exclusion> <exclusion>
<groupId>com.github.jsqlparser</groupId> <groupId>com.github.jsqlparser</groupId>

View File

@@ -14,6 +14,13 @@ import java.util.concurrent.Executors;
*/ */
public class CacheUtils { public class CacheUtils {
/**
* 异步刷新的 LoadingCache 最大缓存数量
*
* @see <a href="">本地缓存 CacheUtils 工具类建议</a>
*/
private static final Integer CACHE_MAX_SIZE = 10000;
/** /**
* 构建异步刷新的 LoadingCache 对象 * 构建异步刷新的 LoadingCache 对象
* *
@@ -29,6 +36,7 @@ public class CacheUtils {
*/ */
public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) { public static <K, V> LoadingCache<K, V> buildAsyncReloadingCache(Duration duration, CacheLoader<K, V> loader) {
return CacheBuilder.newBuilder() return CacheBuilder.newBuilder()
.maximumSize(CACHE_MAX_SIZE)
// 只阻塞当前数据加载线程,其他线程返回旧值 // 只阻塞当前数据加载线程,其他线程返回旧值
.refreshAfterWrite(duration) .refreshAfterWrite(duration)
// 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程
@@ -43,7 +51,11 @@ public class CacheUtils {
* @return LoadingCache 对象 * @return LoadingCache 对象
*/ */
public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) { public static <K, V> LoadingCache<K, V> buildCache(Duration duration, CacheLoader<K, V> loader) {
return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); return CacheBuilder.newBuilder()
.maximumSize(CACHE_MAX_SIZE)
// 只阻塞当前数据加载线程,其他线程返回旧值
.refreshAfterWrite(duration)
.build(loader);
} }
} }

View File

@@ -74,30 +74,30 @@ public class DateUtils {
* 创建指定时间 * 创建指定时间
* *
* @param year 年 * @param year 年
* @param mouth 月 * @param month 月
* @param day 日 * @param day 日
* @return 指定时间 * @return 指定时间
*/ */
public static Date buildTime(int year, int mouth, int day) { public static Date buildTime(int year, int month, int day) {
return buildTime(year, mouth, day, 0, 0, 0); return buildTime(year, month, day, 0, 0, 0);
} }
/** /**
* 创建指定时间 * 创建指定时间
* *
* @param year 年 * @param year 年
* @param mouth 月 * @param month 月
* @param day 日 * @param day 日
* @param hour 小时 * @param hour 小时
* @param minute 分钟 * @param minute 分钟
* @param second 秒 * @param second 秒
* @return 指定时间 * @return 指定时间
*/ */
public static Date buildTime(int year, int mouth, int day, public static Date buildTime(int year, int month, int day,
int hour, int minute, int second) { int hour, int minute, int second) {
Calendar calendar = Calendar.getInstance(); Calendar calendar = Calendar.getInstance();
calendar.set(Calendar.YEAR, year); calendar.set(Calendar.YEAR, year);
calendar.set(Calendar.MONTH, mouth - 1); calendar.set(Calendar.MONTH, month - 1);
calendar.set(Calendar.DAY_OF_MONTH, day); calendar.set(Calendar.DAY_OF_MONTH, day);
calendar.set(Calendar.HOUR_OF_DAY, hour); calendar.set(Calendar.HOUR_OF_DAY, hour);
calendar.set(Calendar.MINUTE, minute); calendar.set(Calendar.MINUTE, minute);

View File

@@ -69,17 +69,17 @@ public class LocalDateTimeUtils {
* 创建指定时间 * 创建指定时间
* *
* @param year 年 * @param year 年
* @param mouth 月 * @param month 月
* @param day 日 * @param day 日
* @return 指定时间 * @return 指定时间
*/ */
public static LocalDateTime buildTime(int year, int mouth, int day) { public static LocalDateTime buildTime(int year, int month, int day) {
return LocalDateTime.of(year, mouth, day, 0, 0, 0); return LocalDateTime.of(year, month, day, 0, 0, 0);
} }
public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1,
int year2, int mouth2, int day2) { int year2, int month2, int day2) {
return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)};
} }
/** /**

View File

@@ -18,7 +18,7 @@ import java.util.List;
*/ */
@AutoConfiguration @AutoConfiguration
@ConditionalOnClass(LoginUser.class) @ConditionalOnClass(LoginUser.class)
@ConditionalOnBean(value = {PermissionCommonApi.class, DeptDataPermissionRuleCustomizer.class}) @ConditionalOnBean(value = {DeptDataPermissionRuleCustomizer.class})
public class YudaoDeptDataPermissionAutoConfiguration { public class YudaoDeptDataPermissionAutoConfiguration {
@Bean @Bean

View File

@@ -42,8 +42,14 @@
<!-- 工具类相关 --> <!-- 工具类相关 -->
<dependency> <dependency>
<groupId>com.alibaba</groupId> <groupId>cn.idev.excel</groupId>
<artifactId>easyexcel</artifactId> <artifactId>fastexcel</artifactId>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<scope>provided</scope>
</dependency> </dependency>
<dependency> <dependency>
@@ -51,11 +57,6 @@
<artifactId>guava</artifactId> <artifactId>guava</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId> <!-- 解决 https://github.com/alibaba/easyexcel/issues/3954 问题 -->
</dependency>
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-biz-ip</artifactId> <artifactId>yudao-spring-boot-starter-biz-ip</artifactId>

View File

@@ -76,4 +76,9 @@ public class DictFrameworkUtils {
return dictData!= null ? dictData.getValue(): null; return dictData!= null ? dictData.getValue(): null;
} }
@SneakyThrows
public static List<String> getDictDataValueList(String dictType) {
List<DictDataRespDTO> dictDatas = GET_DICT_DATA_CACHE.get(dictType);
return convertList(dictDatas, DictDataRespDTO::getValue);
}
} }

View File

@@ -0,0 +1,38 @@
package cn.iocoder.yudao.framework.dict.validation;
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({
ElementType.METHOD,
ElementType.FIELD,
ElementType.ANNOTATION_TYPE,
ElementType.CONSTRUCTOR,
ElementType.PARAMETER,
ElementType.TYPE_USE
})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(
validatedBy = {InDictValidator.class, InDictCollectionValidator.class}
)
public @interface InDict {
/**
* 数据字典 type
*/
String type();
String message() default "必须在指定范围 {value}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

View File

@@ -0,0 +1,43 @@
package cn.iocoder.yudao.framework.dict.validation;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.Collection;
import java.util.List;
public class InDictCollectionValidator implements ConstraintValidator<InDict, Collection<?>> {
private String dictType;
@Override
public void initialize(InDict annotation) {
this.dictType = annotation.type();
}
@Override
public boolean isValid(Collection<?> list, ConstraintValidatorContext context) {
// 为空时,默认不校验,即认为通过
if (CollUtil.isEmpty(list)) {
return true;
}
// 校验全部通过
List<String> dbValues = DictFrameworkUtils.getDictDataValueList(dictType);
boolean match = list.stream().allMatch(v -> dbValues.stream()
.anyMatch(dbValue -> dbValue.equalsIgnoreCase(v.toString())));
if (match) {
return true;
}
// 校验不通过,自定义提示语句
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(
context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}", dbValues.toString())
).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
}

View File

@@ -0,0 +1,41 @@
package cn.iocoder.yudao.framework.dict.validation;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import java.util.List;
public class InDictValidator implements ConstraintValidator<InDict, Object> {
private String dictType;
@Override
public void initialize(InDict annotation) {
this.dictType = annotation.type();
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
// 为空时,默认不校验,即认为通过
if (value == null) {
return true;
}
// 校验通过
final List<String> values = DictFrameworkUtils.getDictDataValueList(dictType);
boolean match = values.stream().anyMatch(v -> StrUtil.equalsIgnoreCase(v, value.toString()));
if (match) {
return true;
}
// 校验不通过,自定义提示语句
context.disableDefaultConstraintViolation(); // 禁用默认的 message 的值
context.buildConstraintViolationWithTemplate(
context.getDefaultConstraintMessageTemplate().replaceAll("\\{value}", values.toString())
).addConstraintViolation(); // 重新添加错误提示语句
return false;
}
}

View File

@@ -3,11 +3,11 @@ package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.Area;
import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils;
import com.alibaba.excel.converters.Converter; import cn.idev.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum; import cn.idev.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration; import cn.idev.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData; import cn.idev.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty; import cn.idev.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**

View File

@@ -3,12 +3,12 @@ package cn.iocoder.yudao.framework.excel.core.convert;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import com.alibaba.excel.converters.Converter; import cn.idev.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum; import cn.idev.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration; import cn.idev.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.ReadCellData; import cn.idev.excel.metadata.data.ReadCellData;
import com.alibaba.excel.metadata.data.WriteCellData; import cn.idev.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty; import cn.idev.excel.metadata.property.ExcelContentProperty;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
/** /**

View File

@@ -1,11 +1,11 @@
package cn.iocoder.yudao.framework.excel.core.convert; package cn.iocoder.yudao.framework.excel.core.convert;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import com.alibaba.excel.converters.Converter; import cn.idev.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum; import cn.idev.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration; import cn.idev.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData; import cn.idev.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty; import cn.idev.excel.metadata.property.ExcelContentProperty;
/** /**
* Excel Json 转换器 * Excel Json 转换器

View File

@@ -1,10 +1,10 @@
package cn.iocoder.yudao.framework.excel.core.convert; package cn.iocoder.yudao.framework.excel.core.convert;
import com.alibaba.excel.converters.Converter; import cn.idev.excel.converters.Converter;
import com.alibaba.excel.enums.CellDataTypeEnum; import cn.idev.excel.enums.CellDataTypeEnum;
import com.alibaba.excel.metadata.GlobalConfiguration; import cn.idev.excel.metadata.GlobalConfiguration;
import com.alibaba.excel.metadata.data.WriteCellData; import cn.idev.excel.metadata.data.WriteCellData;
import com.alibaba.excel.metadata.property.ExcelContentProperty; import cn.idev.excel.metadata.property.ExcelContentProperty;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.math.RoundingMode; import java.math.RoundingMode;

View File

@@ -0,0 +1,78 @@
package cn.iocoder.yudao.framework.excel.core.handler;
import cn.hutool.core.collection.CollUtil;
import cn.idev.excel.enums.CellDataTypeEnum;
import cn.idev.excel.metadata.Head;
import cn.idev.excel.metadata.data.WriteCellData;
import cn.idev.excel.util.MapUtils;
import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import cn.idev.excel.write.style.column.AbstractColumnWidthStyleStrategy;
import cn.idev.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import org.apache.poi.ss.usermodel.Cell;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Excel 自适应列宽处理器
*
* 相比 {@link LongestMatchColumnWidthStyleStrategy} 来说,额外处理了 DATE 类型!
*
* @see <a href="https://github.com/YunaiV/yudao-cloud/pull/196/">添加自适应列宽处理器,并替换默认列宽策略</a>
* @author hmb
*/
public class ColumnWidthMatchStyleStrategy extends AbstractColumnWidthStyleStrategy {
private static final int MAX_COLUMN_WIDTH = 255;
private final Map<Integer, Map<Integer, Integer>> cache = MapUtils.newHashMapWithExpectedSize(8);
@Override
protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List<WriteCellData<?>> cellDataList, Cell cell,
Head head, Integer relativeRowIndex, Boolean isHead) {
boolean needSetWidth = isHead || CollUtil.isNotEmpty(cellDataList);
if (!needSetWidth) {
return;
}
Map<Integer, Integer> maxColumnWidthMap = cache.computeIfAbsent(writeSheetHolder.getSheetNo(),
key -> new HashMap<>(16));
Integer columnWidth = dataLength(cellDataList, cell, isHead);
if (columnWidth < 0) {
return;
}
if (columnWidth > MAX_COLUMN_WIDTH) {
columnWidth = MAX_COLUMN_WIDTH;
}
Integer maxColumnWidth = maxColumnWidthMap.get(cell.getColumnIndex());
if (maxColumnWidth == null || columnWidth > maxColumnWidth) {
maxColumnWidthMap.put(cell.getColumnIndex(), columnWidth);
writeSheetHolder.getSheet().setColumnWidth(cell.getColumnIndex(), columnWidth * 256);
}
}
@SuppressWarnings("EnhancedSwitchMigration")
private Integer dataLength(List<WriteCellData<?>> cellDataList, Cell cell, Boolean isHead) {
if (isHead) {
return cell.getStringCellValue().getBytes().length;
}
WriteCellData<?> cellData = cellDataList.get(0);
CellDataTypeEnum type = cellData.getType();
if (type == null) {
return -1;
}
switch (type) {
case STRING:
return cellData.getStringValue().getBytes().length;
case BOOLEAN:
return cellData.getBooleanValue().toString().getBytes().length;
case NUMBER:
return cellData.getNumberValue().toString().getBytes().length;
case DATE:
return cellData.getDateValue().toString().getBytes().length;
default:
return -1;
}
}
}

View File

@@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.excel.core.handler;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.poi.excel.ExcelUtil;
@@ -11,12 +10,12 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils;
import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect; import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction; import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction;
import com.alibaba.excel.annotation.ExcelIgnore; import cn.idev.excel.annotation.ExcelIgnore;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import com.alibaba.excel.write.handler.SheetWriteHandler; import cn.idev.excel.write.handler.SheetWriteHandler;
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; import cn.idev.excel.write.metadata.holder.WriteSheetHolder;
import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.poi.hssf.usermodel.HSSFDataValidation; import org.apache.poi.hssf.usermodel.HSSFDataValidation;
import org.apache.poi.ss.usermodel.*; import org.apache.poi.ss.usermodel.*;
@@ -87,7 +86,7 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
/** /**
* 判断字段是否是静态的、最终的、 transient 的 * 判断字段是否是静态的、最终的、 transient 的
* 原因:EasyExcel 默认是忽略 static final 或 transient 的字段,所以需要判断 * 原因:FastExcel 默认是忽略 static final 或 transient 的字段,所以需要判断
* *
* @param field 字段 * @param field 字段
* @return 是否是静态的、最终的、transient 的 * @return 是否是静态的、最终的、transient 的
@@ -185,4 +184,4 @@ public class SelectSheetWriteHandler implements SheetWriteHandler {
writeSheetHolder.getSheet().addValidationData(validation); writeSheetHolder.getSheet().addValidationData(validation);
} }
} }

View File

@@ -1,10 +1,10 @@
package cn.iocoder.yudao.framework.excel.core.util; package cn.iocoder.yudao.framework.excel.core.util;
import cn.idev.excel.FastExcelFactory;
import cn.idev.excel.converters.longconverter.LongStringConverter;
import cn.iocoder.yudao.framework.common.util.http.HttpUtils; import cn.iocoder.yudao.framework.common.util.http.HttpUtils;
import cn.iocoder.yudao.framework.excel.core.handler.ColumnWidthMatchStyleStrategy;
import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler; import cn.iocoder.yudao.framework.excel.core.handler.SelectSheetWriteHandler;
import com.alibaba.excel.EasyExcel;
import com.alibaba.excel.converters.longconverter.LongStringConverter;
import com.alibaba.excel.write.style.column.LongestMatchColumnWidthStyleStrategy;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -32,9 +32,9 @@ public class ExcelUtils {
public static <T> void write(HttpServletResponse response, String filename, String sheetName, public static <T> void write(HttpServletResponse response, String filename, String sheetName,
Class<T> head, List<T> data) throws IOException { Class<T> head, List<T> data) throws IOException {
// 输出 Excel // 输出 Excel
EasyExcel.write(response.getOutputStream(), head) FastExcelFactory.write(response.getOutputStream(), head)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度
.registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框
.registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度
.sheet(sheetName).doWrite(data); .sheet(sheetName).doWrite(data);
@@ -44,7 +44,7 @@ public class ExcelUtils {
} }
public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException { public static <T> List<T> read(MultipartFile file, Class<T> head) throws IOException {
return EasyExcel.read(file.getInputStream(), head, null) return FastExcelFactory.read(file.getInputStream(), head, null)
.autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理
.doReadAllSync(); .doReadAllSync();
} }

View File

@@ -1,4 +1,4 @@
/** /**
* 基于 EasyExcel 实现 Excel 相关的操作 * 基于 FastExcel 实现 Excel 相关的操作
*/ */
package cn.iocoder.yudao.framework.excel; package cn.iocoder.yudao.framework.excel;

View File

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

View File

@@ -44,29 +44,35 @@
<dependency> <dependency>
<groupId>io.opentracing</groupId> <groupId>io.opentracing</groupId>
<artifactId>opentracing-util</artifactId> <artifactId>opentracing-util</artifactId>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.skywalking</groupId> <groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-trace</artifactId> <artifactId>apm-toolkit-trace</artifactId>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.skywalking</groupId> <groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-logback-1.x</artifactId> <artifactId>apm-toolkit-logback-1.x</artifactId>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.apache.skywalking</groupId> <groupId>org.apache.skywalking</groupId>
<artifactId>apm-toolkit-opentracing</artifactId> <artifactId>apm-toolkit-opentracing</artifactId>
<optional>true</optional>
</dependency> </dependency>
<!-- Micrometer 对 Prometheus 的支持 --> <!-- Micrometer 对 Prometheus 的支持 -->
<dependency> <dependency>
<groupId>io.micrometer</groupId> <groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId> <artifactId>micrometer-registry-prometheus</artifactId>
<optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>de.codecentric</groupId> <groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Server 服务--> <artifactId>spring-boot-admin-starter-client</artifactId> <!-- 实现 Spring Boot Admin Client 客户-->
<optional>true</optional>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@@ -3,6 +3,9 @@ package cn.iocoder.yudao.framework.tracer.config;
import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect; import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect;
import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter; import cn.iocoder.yudao.framework.tracer.core.filter.TraceFilter;
import io.opentracing.Tracer;
import io.opentracing.util.GlobalTracer;
import org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -16,30 +19,32 @@ import org.springframework.context.annotation.Bean;
* @author mashu * @author mashu
*/ */
@AutoConfiguration @AutoConfiguration
@ConditionalOnClass({BizTraceAspect.class}) @ConditionalOnClass(name = {
"org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer",
"io.opentracing.Tracer"
})
@EnableConfigurationProperties(TracerProperties.class) @EnableConfigurationProperties(TracerProperties.class)
@ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) @ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true)
public class YudaoTracerAutoConfiguration { public class YudaoTracerAutoConfiguration {
// TODO @芋艿:重要。目前 opentracing 版本存在冲突,要么保证 skywalking要么保证阿里云短信 sdk @Bean
// @Bean public TracerProperties bizTracerProperties() {
// public TracerProperties bizTracerProperties() { return new TracerProperties();
// return new TracerProperties(); }
// }
// @Bean
// @Bean public BizTraceAspect bizTracingAop() {
// public BizTraceAspect bizTracingAop() { return new BizTraceAspect(tracer());
// return new BizTraceAspect(tracer()); }
// }
// @Bean
// @Bean public Tracer tracer() {
// public Tracer tracer() { // 创建 SkywalkingTracer 对象
// // 创建 SkywalkingTracer 对象 SkywalkingTracer tracer = new SkywalkingTracer();
// SkywalkingTracer tracer = new SkywalkingTracer(); // 设置为 GlobalTracer 的追踪器
// // 设置为 GlobalTracer 的追踪器 GlobalTracer.registerIfAbsent(tracer);
// GlobalTracer.register(tracer); return tracer;
// return tracer; }
// }
/** /**
* 创建 TraceFilter 过滤器,响应 header 设置 traceId * 创建 TraceFilter 过滤器,响应 header 设置 traceId

View File

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

View File

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

View File

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

View File

@@ -118,7 +118,6 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
@Override @Override
public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) { public <X> MPJLambdaWrapperX<T> orderByDesc(SFunction<X, ?> column) {
//noinspection unchecked
super.orderByDesc(true, column); super.orderByDesc(true, column);
return this; return this;
} }
@@ -208,7 +207,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
} }
@Override @Override
public <S, X> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) { public <S> MPJLambdaWrapperX<T> selectCount(SFunction<S, ?> column, String alias) {
super.selectCount(column, alias); super.selectCount(column, alias);
return this; return this;
} }
@@ -226,7 +225,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
} }
@Override @Override
public <S, X> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) { public <S> MPJLambdaWrapperX<T> selectSum(SFunction<S, ?> column, String alias) {
super.selectSum(column, alias); super.selectSum(column, alias);
return this; return this;
} }
@@ -244,7 +243,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
} }
@Override @Override
public <S, X> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) { public <S> MPJLambdaWrapperX<T> selectMax(SFunction<S, ?> column, String alias) {
super.selectMax(column, alias); super.selectMax(column, alias);
return this; return this;
} }
@@ -262,7 +261,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
} }
@Override @Override
public <S, X> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) { public <S> MPJLambdaWrapperX<T> selectMin(SFunction<S, ?> column, String alias) {
super.selectMin(column, alias); super.selectMin(column, alias);
return this; return this;
} }
@@ -280,7 +279,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
} }
@Override @Override
public <S, X> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) { public <S> MPJLambdaWrapperX<T> selectAvg(SFunction<S, ?> column, String alias) {
super.selectAvg(column, alias); super.selectAvg(column, alias);
return this; return this;
} }
@@ -298,7 +297,7 @@ public class MPJLambdaWrapperX<T> extends MPJLambdaWrapper<T> {
} }
@Override @Override
public <S, X> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) { public <S> MPJLambdaWrapperX<T> selectLen(SFunction<S, ?> column, String alias) {
super.selectLen(column, alias); super.selectLen(column, alias);
return this; return this;
} }

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.web.core.filter; package cn.iocoder.yudao.framework.web.core.filter;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
@@ -16,6 +17,14 @@ import java.io.IOException;
*/ */
public class CacheRequestBodyFilter extends OncePerRequestFilter { public class CacheRequestBodyFilter extends OncePerRequestFilter {
/**
* 需要排除的 URI
*
* 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。
* 例如说:<a href="https://github.com/YunaiV/ruoyi-vue-pro/issues/795">795 ISSUE</a>
*/
private static final String[] IGNORE_URIS = {"/admin/", "/actuator/"};
@Override @Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws IOException, ServletException { throws IOException, ServletException {
@@ -24,7 +33,13 @@ public class CacheRequestBodyFilter extends OncePerRequestFilter {
@Override @Override
protected boolean shouldNotFilter(HttpServletRequest request) { protected boolean shouldNotFilter(HttpServletRequest request) {
// 只处理 json 请求内容 // 1. 校验是否为排除的 URL
String requestURI = request.getRequestURI();
if (StrUtil.startWithAny(requestURI, IGNORE_URIS)) {
return true;
}
// 2. 只处理 json 请求内容
return !ServletUtils.isJsonRequest(request); return !ServletUtils.isJsonRequest(request);
} }

View File

@@ -6,6 +6,8 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil; import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
@@ -14,8 +16,6 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils; import cn.iocoder.yudao.framework.common.util.monitor.TracerUtils;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.framework.common.biz.infra.logger.ApiErrorLogCommonApi;
import cn.iocoder.yudao.framework.common.biz.infra.logger.dto.ApiErrorLogCreateReqDTO;
import com.fasterxml.jackson.databind.exc.InvalidFormatException; import com.fasterxml.jackson.databind.exc.InvalidFormatException;
import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
@@ -29,6 +29,7 @@ import org.springframework.util.Assert;
import org.springframework.validation.BindException; import org.springframework.validation.BindException;
import org.springframework.validation.FieldError; import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError; import org.springframework.validation.ObjectError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.MissingServletRequestParameterException;
@@ -101,6 +102,9 @@ public class GlobalExceptionHandler {
if (ex instanceof HttpRequestMethodNotSupportedException) { if (ex instanceof HttpRequestMethodNotSupportedException) {
return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex);
} }
if (ex instanceof HttpMediaTypeNotSupportedException) {
return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex);
}
if (ex instanceof ServiceException) { if (ex instanceof ServiceException) {
return serviceExceptionHandler((ServiceException) ex); return serviceExceptionHandler((ServiceException) ex);
} }
@@ -171,17 +175,19 @@ public class GlobalExceptionHandler {
/** /**
* 处理 SpringMVC 请求参数类型错误 * 处理 SpringMVC 请求参数类型错误
* *
* 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer结果传递 xx 参数类型为 String * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer结果传递 xx 参数类型为 String
*/ */
@ExceptionHandler(HttpMessageNotReadableException.class) @ExceptionHandler(HttpMessageNotReadableException.class)
public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { public CommonResult<?> methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) {
log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex);
if(ex.getCause() instanceof InvalidFormatException) { if (ex.getCause() instanceof InvalidFormatException) {
InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause();
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue())); return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求参数类型错误:%s", invalidFormatException.getValue()));
} else {
return defaultExceptionHandler(ServletUtils.getRequest(), ex);
} }
if (StrUtil.startWith(ex.getMessage(), "Required request body is missing")) {
return CommonResult.error(BAD_REQUEST.getCode(), "请求参数类型错误: request body 缺失");
}
return defaultExceptionHandler(ServletUtils.getRequest(), ex);
} }
/** /**
@@ -237,6 +243,17 @@ public class GlobalExceptionHandler {
return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage())); return CommonResult.error(METHOD_NOT_ALLOWED.getCode(), String.format("请求方法不正确:%s", ex.getMessage()));
} }
/**
* 处理 SpringMVC 请求的 Content-Type 不正确
*
* 例如说A 接口的 Content-Type 为 application/json结果请求的 Content-Type 为 application/octet-stream导致不匹配
*/
@ExceptionHandler(HttpMediaTypeNotSupportedException.class)
public CommonResult<?> httpMediaTypeNotSupportedExceptionHandler(HttpMediaTypeNotSupportedException ex) {
log.warn("[httpMediaTypeNotSupportedExceptionHandler]", ex);
return CommonResult.error(BAD_REQUEST.getCode(), String.format("请求类型不正确:%s", ex.getMessage()));
}
/** /**
* 处理 Spring Security 权限不足的异常 * 处理 Spring Security 权限不足的异常
* *

View File

@@ -5,7 +5,6 @@ import cn.iocoder.yudao.framework.xss.core.clean.JsoupXssCleaner;
import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner; import cn.iocoder.yudao.framework.xss.core.clean.XssCleaner;
import cn.iocoder.yudao.framework.xss.core.filter.XssFilter; import cn.iocoder.yudao.framework.xss.core.filter.XssFilter;
import cn.iocoder.yudao.framework.xss.core.json.XssStringJsonDeserializer; import cn.iocoder.yudao.framework.xss.core.json.XssStringJsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -42,13 +41,13 @@ public class YudaoXssAutoConfiguration implements WebMvcConfigurer {
*/ */
@Bean @Bean
@ConditionalOnMissingBean(name = "xssJacksonCustomizer") @ConditionalOnMissingBean(name = "xssJacksonCustomizer")
@ConditionalOnBean(ObjectMapper.class)
@ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true") @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true")
public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties, public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties,
PathMatcher pathMatcher, PathMatcher pathMatcher,
XssCleaner xssCleaner) { XssCleaner xssCleaner) {
// 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer在序列化时进行处理 // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer在序列化时进行处理
return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); return builder ->
builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner));
} }
/** /**

View File

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

View File

@@ -12,6 +12,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.chat.vo.conversation.AiChatCo
import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO; import cn.iocoder.yudao.module.ai.dal.dataobject.chat.AiChatConversationDO;
import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService; import cn.iocoder.yudao.module.ai.service.chat.AiChatConversationService;
import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService; import cn.iocoder.yudao.module.ai.service.chat.AiChatMessageService;
import com.fhs.core.trans.anno.TransMethodResult;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -54,6 +55,7 @@ public class AiChatConversationController {
@GetMapping("/my-list") @GetMapping("/my-list")
@Operation(summary = "获得【我的】聊天对话列表") @Operation(summary = "获得【我的】聊天对话列表")
@TransMethodResult
public CommonResult<List<AiChatConversationRespVO>> getChatConversationMyList() { public CommonResult<List<AiChatConversationRespVO>> getChatConversationMyList() {
List<AiChatConversationDO> list = chatConversationService.getChatConversationListByUserId(getLoginUserId()); List<AiChatConversationDO> list = chatConversationService.getChatConversationListByUserId(getLoginUserId());
return success(BeanUtils.toBean(list, AiChatConversationRespVO.class)); return success(BeanUtils.toBean(list, AiChatConversationRespVO.class));
@@ -62,6 +64,7 @@ public class AiChatConversationController {
@GetMapping("/get-my") @GetMapping("/get-my")
@Operation(summary = "获得【我的】聊天对话") @Operation(summary = "获得【我的】聊天对话")
@Parameter(name = "id", required = true, description = "对话编号", example = "1024") @Parameter(name = "id", required = true, description = "对话编号", example = "1024")
@TransMethodResult
public CommonResult<AiChatConversationRespVO> getChatConversationMy(@RequestParam("id") Long id) { public CommonResult<AiChatConversationRespVO> getChatConversationMy(@RequestParam("id") Long id) {
AiChatConversationDO conversation = chatConversationService.getChatConversation(id); AiChatConversationDO conversation = chatConversationService.getChatConversation(id);
if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) {
@@ -90,6 +93,7 @@ public class AiChatConversationController {
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获得对话分页", description = "用于【对话管理】菜单") @Operation(summary = "获得对话分页", description = "用于【对话管理】菜单")
@PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')")
@TransMethodResult
public CommonResult<PageResult<AiChatConversationRespVO>> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) { public CommonResult<PageResult<AiChatConversationRespVO>> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) {
PageResult<AiChatConversationDO> pageResult = chatConversationService.getChatConversationPage(pageReqVO); PageResult<AiChatConversationDO> pageResult = chatConversationService.getChatConversationPage(pageReqVO);
if (CollUtil.isEmpty(pageResult.getList())) { if (CollUtil.isEmpty(pageResult.getList())) {

View File

@@ -10,6 +10,7 @@ import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleS
import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO; import cn.iocoder.yudao.module.ai.controller.admin.model.vo.chatRole.AiChatRoleSaveReqVO;
import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiChatRoleDO;
import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService; import cn.iocoder.yudao.module.ai.service.model.AiChatRoleService;
import com.fhs.core.trans.anno.TransMethodResult;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -35,6 +36,7 @@ public class AiChatRoleController {
@GetMapping("/my-page") @GetMapping("/my-page")
@Operation(summary = "获得【我的】聊天角色分页") @Operation(summary = "获得【我的】聊天角色分页")
@TransMethodResult
public CommonResult<PageResult<AiChatRoleRespVO>> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) { public CommonResult<PageResult<AiChatRoleRespVO>> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) {
PageResult<AiChatRoleDO> pageResult = chatRoleService.getChatRoleMyPage(pageReqVO, getLoginUserId()); PageResult<AiChatRoleDO> pageResult = chatRoleService.getChatRoleMyPage(pageReqVO, getLoginUserId());
return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class)); return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class));
@@ -43,6 +45,7 @@ public class AiChatRoleController {
@GetMapping("/get-my") @GetMapping("/get-my")
@Operation(summary = "获得【我的】聊天角色") @Operation(summary = "获得【我的】聊天角色")
@Parameter(name = "id", description = "编号", required = true, example = "1024") @Parameter(name = "id", description = "编号", required = true, example = "1024")
@TransMethodResult
public CommonResult<AiChatRoleRespVO> getChatRoleMy(@RequestParam("id") Long id) { public CommonResult<AiChatRoleRespVO> getChatRoleMy(@RequestParam("id") Long id) {
AiChatRoleDO chatRole = chatRoleService.getChatRole(id); AiChatRoleDO chatRole = chatRoleService.getChatRole(id);
if (ObjUtil.notEqual(chatRole.getUserId(), getLoginUserId())) { if (ObjUtil.notEqual(chatRole.getUserId(), getLoginUserId())) {
@@ -108,6 +111,7 @@ public class AiChatRoleController {
@Operation(summary = "获得聊天角色") @Operation(summary = "获得聊天角色")
@Parameter(name = "id", description = "编号", required = true, example = "1024") @Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('ai:chat-role:query')") @PreAuthorize("@ss.hasPermission('ai:chat-role:query')")
@TransMethodResult
public CommonResult<AiChatRoleRespVO> getChatRole(@RequestParam("id") Long id) { public CommonResult<AiChatRoleRespVO> getChatRole(@RequestParam("id") Long id) {
AiChatRoleDO chatRole = chatRoleService.getChatRole(id); AiChatRoleDO chatRole = chatRoleService.getChatRole(id);
return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class)); return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class));

View File

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

View File

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

View File

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

View File

@@ -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<ChatResponse> stream(Prompt prompt) {
return openAiChatModel.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return openAiChatModel.getDefaultOptions();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -23,7 +23,9 @@ import java.util.List;
public class OllamaChatModelTests { public class OllamaChatModelTests {
private final OllamaChatModel chatModel = OllamaChatModel.builder() 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() .defaultOptions(OllamaOptions.builder()
// .model("qwen") // 模型https://ollama.com/library/qwen // .model("qwen") // 模型https://ollama.com/library/qwen
.model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1 .model("deepseek-r1") // 模型https://ollama.com/library/deepseek-r1

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression; package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.expression;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -27,4 +27,4 @@ public class BpmProcessExpressionRespVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime; private LocalDateTime createTime;
} }

View File

@@ -71,6 +71,9 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "是否填写审批意见", example = "false") @Schema(description = "是否填写审批意见", example = "false")
private Boolean reasonRequire; private Boolean reasonRequire;
@Schema(description = "跳过表达式", example = "{true}")
private String skipExpression; // 用于审批节点
/** /**
* 审批节点拒绝处理 * 审批节点拒绝处理
*/ */

View File

@@ -73,7 +73,7 @@ public class BpmApprovalDetailRespVO {
private List<UserSimpleBaseVO> candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表 private List<UserSimpleBaseVO> candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表
@Schema(description = "流程编号", example = "8761d8e0-0922-11f0-bd37-00ff1db677bf") @Schema(description = "流程编号", example = "8761d8e0-0922-11f0-bd37-00ff1db677bf")
private String processInstanceId; // 当且仅当该节点是子流程节点时才会有值CallActivity 的 processInstanceId 字段) private String processInstanceId; // 当且仅当该节点是子流程节点时才会有值CallActivity 的 calledProcessInstanceId 字段)
} }

View File

@@ -40,7 +40,7 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在"); ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在");
ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程"); ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消"); ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败"); ErrorCode PROCESS_INSTANCE_HTTP_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 请求调用失败");
ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置"); ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置");
ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消"); ErrorCode PROCESS_INSTANCE_CANCEL_CHILD_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_008, "子流程取消失败,子流程不允许取消");
@@ -58,7 +58,6 @@ public interface ErrorCodeConstants {
ErrorCode TASK_SIGN_DELETE_NO_PARENT = new ErrorCode(1_009_005_012, "任务减签失败,被减签的任务必须是通过加签生成的任务"); ErrorCode TASK_SIGN_DELETE_NO_PARENT = new ErrorCode(1_009_005_012, "任务减签失败,被减签的任务必须是通过加签生成的任务");
ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人"); ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人");
ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在"); ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在");
ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!");
ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!");
ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!"); ErrorCode TASK_REASON_REQUIRE = new ErrorCode(1_009_005_016, "审批意见不能为空!");
@@ -74,6 +73,7 @@ public interface ErrorCodeConstants {
ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1_009_012_000, "流程分类不存在"); ErrorCode CATEGORY_NOT_EXISTS = new ErrorCode(1_009_012_000, "流程分类不存在");
ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(1_009_012_001, "流程分类名字【{}】重复"); ErrorCode CATEGORY_NAME_DUPLICATE = new ErrorCode(1_009_012_001, "流程分类名字【{}】重复");
ErrorCode CATEGORY_CODE_DUPLICATE = new ErrorCode(1_009_012_002, "流程分类编码【{}】重复"); ErrorCode CATEGORY_CODE_DUPLICATE = new ErrorCode(1_009_012_002, "流程分类编码【{}】重复");
ErrorCode CATEGORY_DELETE_FAIL_MODEL_USED = new ErrorCode(1_009_012_003, "删除失败,流程分类【{}】已被流程模型使用,请先删除对应的流程模型");
// ========== BPM 流程监听器 1-009-013-000 ========== // ========== BPM 流程监听器 1-009-013-000 ==========
ErrorCode PROCESS_LISTENER_NOT_EXISTS = new ErrorCode(1_009_013_000, "流程监听器不存在"); ErrorCode PROCESS_LISTENER_NOT_EXISTS = new ErrorCode(1_009_013_000, "流程监听器不存在");

View File

@@ -35,7 +35,7 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable<Integer> {
// 50 ~ 条件分支 // 50 ~ 条件分支
CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),
PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), PARALLEL_BRANCH_NODE(52, "并行分支", "inclusiveGateway"), // 并行分支使用包容网关实现,条件表达式结果设置为 true
INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"),
ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway") ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway")
; ;

View File

@@ -14,6 +14,7 @@ import lombok.Getter;
@AllArgsConstructor @AllArgsConstructor
public enum BpmTaskStatusEnum { public enum BpmTaskStatusEnum {
SKIP(-2, "跳过"),
NOT_START(-1, "未开始"), NOT_START(-1, "未开始"),
RUNNING(1, "审批中"), RUNNING(1, "审批中"),
APPROVE(2, "审批通过"), APPROVE(2, "审批通过"),

View File

@@ -18,10 +18,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.CallActivity;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
@@ -132,7 +129,7 @@ public class BpmTaskCandidateInvoker {
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) { Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
// 如果是 CallActivity 子流程,不进行计算候选人 // 如果是 CallActivity 子流程,不进行计算候选人
FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId);
if (flowElement instanceof CallActivity) { if (flowElement instanceof CallActivity || flowElement instanceof SubProcess) {
return new HashSet<>(); return new HashSet<>();
} }
// 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过

View File

@@ -24,6 +24,7 @@ import static java.util.Collections.emptySet;
* @author 芋道源码 * @author 芋道源码
*/ */
@Component @Component
@Deprecated // 仅仅是表达式的示例,建议使用 BpmTaskCandidateStartUserDeptLeaderStrategy 替代
public class BpmTaskAssignLeaderExpression { public class BpmTaskAssignLeaderExpression {
@Resource @Resource

View File

@@ -16,6 +16,7 @@ import java.util.Set;
* @author 芋道源码 * @author 芋道源码
*/ */
@Component @Component
@Deprecated // 仅仅是表达式的示例,建议使用 BpmTaskCandidateStartUserStrategy 替代
public class BpmTaskAssignStartUserExpression { public class BpmTaskAssignStartUserExpression {
@Resource @Resource

View File

@@ -8,6 +8,7 @@ import com.google.common.collect.Sets;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.api.FlowableException; import org.flowable.common.engine.api.FlowableException;
import org.flowable.common.engine.impl.javax.el.PropertyNotFoundException;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -48,10 +49,12 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
Object result = FlowableUtils.getExpressionValue(variables, param); Object result = FlowableUtils.getExpressionValue(variables, param);
return CollectionUtils.toLinkedHashSet(Long.class, result); return CollectionUtils.toLinkedHashSet(Long.class, result);
} catch (FlowableException ex) { } catch (FlowableException ex) {
// 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常, // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常,此时忽略该异常!相当于说,不做流程预测!!!
log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); if (ex.getCause() != null && ex.getCause() instanceof PropertyNotFoundException) {
// 不能预测候选人,返回空列表, 避免流程无法进行 return Sets.newHashSet();
return Sets.newHashSet(); }
log.error("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex);
throw ex;
} }
} }

View File

@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -37,18 +38,26 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
@Override @Override
protected void processCreated(FlowableEngineEntityEvent event) { protected void processCreated(FlowableEngineEntityEvent event) {
processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity()); ProcessInstance processInstance = (ProcessInstance) event.getEntity();
FlowableUtils.execute(processInstance.getTenantId(),
() -> processInstanceService.processProcessInstanceCreated(processInstance));
} }
@Override @Override
protected void processCompleted(FlowableEngineEntityEvent event) { protected void processCompleted(FlowableEngineEntityEvent event) {
processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); ProcessInstance processInstance = (ProcessInstance) event.getEntity();
FlowableUtils.execute(processInstance.getTenantId(),
() -> processInstanceService.processProcessInstanceCompleted(processInstance));
} }
@Override // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法 @Override
protected void processCancelled(FlowableCancelledEvent event) { protected void processCancelled(FlowableCancelledEvent event) {
// 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法
ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId()); ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId());
processInstanceService.processProcessInstanceCompleted(processInstance); if (processInstance != null) {
FlowableUtils.execute(processInstance.getTenantId(),
() -> processInstanceService.processProcessInstanceCompleted(processInstance));
}
} }
} }

View File

@@ -3,10 +3,12 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService; import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
@@ -58,17 +60,20 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
@Override @Override
protected void taskCreated(FlowableEngineEntityEvent event) { protected void taskCreated(FlowableEngineEntityEvent event) {
taskService.processTaskCreated((Task) event.getEntity()); Task entity = (Task) event.getEntity();
FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskCreated(entity));
} }
@Override @Override
protected void taskAssigned(FlowableEngineEntityEvent event) { protected void taskAssigned(FlowableEngineEntityEvent event) {
taskService.processTaskAssigned((Task) event.getEntity()); Task entity = (Task) event.getEntity();
FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskAssigned(entity));
} }
@Override @Override
protected void taskCompleted(FlowableEngineEntityEvent event) { protected void taskCompleted(FlowableEngineEntityEvent event) {
taskService.processTaskCompleted((Task) event.getEntity()); Task entity = (Task) event.getEntity();
FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskCompleted(entity));
} }
@Override @Override
@@ -94,6 +99,23 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
String processDefinitionId = event.getProcessDefinitionId(); String processDefinitionId = event.getProcessDefinitionId();
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
Job entity = (Job) event.getEntity(); Job entity = (Job) event.getEntity();
// 特殊 from https://t.zsxq.com/h6oWr :当 elementId 为空时,尝试从 JobHandlerConfiguration 中解析 JSON 获取
String elementId = entity.getElementId();
if (elementId == null && entity.getJobHandlerConfiguration() != null) {
try {
String handlerConfig = entity.getJobHandlerConfiguration();
if (handlerConfig.startsWith("{") && handlerConfig.contains("activityId")) {
elementId = new JSONObject(handlerConfig).getStr("activityId");
}
} catch (Exception e) {
log.error("[timerFired][解析 entity({}) 失败]", entity, e);
return;
}
}
if (elementId == null) {
log.error("[timerFired][解析 entity({}) elementId 为空,跳过处理]", entity);
return;
}
FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId()); FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId());
if (!(element instanceof BoundaryEvent)) { if (!(element instanceof BoundaryEvent)) {
return; return;

View File

@@ -6,15 +6,15 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; import cn.iocoder.yudao.framework.common.util.spring.SpringUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.bpm.api.event.BpmProcessInstanceStatusEvent;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.http.HttpEntity; import org.springframework.http.*;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestClientException;
@@ -26,7 +26,7 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID; import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_CALL_ERROR;
/** /**
* 工作流发起 HTTP 请求工具类 * 工作流发起 HTTP 请求工具类
@@ -42,7 +42,6 @@ public class BpmHttpRequestUtils {
List<BpmSimpleModelNodeVO.HttpRequestParam> bodyParams, List<BpmSimpleModelNodeVO.HttpRequestParam> bodyParams,
Boolean handleResponse, Boolean handleResponse,
List<KeyValue<String, String>> response) { List<KeyValue<String, String>> response) {
RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class);
BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class);
// 1.1 设置请求头 // 1.1 设置请求头
@@ -51,6 +50,7 @@ public class BpmHttpRequestUtils {
MultiValueMap<String, String> body = buildHttpBody(processInstance, bodyParams); MultiValueMap<String, String> body = buildHttpBody(processInstance, bodyParams);
// 2. 发起请求 // 2. 发起请求
RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class);
ResponseEntity<String> responseEntity = sendHttpRequest(url, headers, body, restTemplate); ResponseEntity<String> responseEntity = sendHttpRequest(url, headers, body, restTemplate);
// 3. 处理返回 // 3. 处理返回
@@ -78,27 +78,55 @@ public class BpmHttpRequestUtils {
} }
} }
public static void executeBpmHttpRequest(BpmProcessInstanceStatusEvent event,
String url) {
// 1.1 设置请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
if (TenantContextHolder.getTenantId() != null) {
headers.add(HEADER_TENANT_ID, String.valueOf(TenantContextHolder.getTenantId()));
} else {
BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class);
ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getId());
if (processInstance != null) {
headers.add(HEADER_TENANT_ID, String.valueOf(TenantContextHolder.getTenantId()));
}
}
// 1.2 设置请求体
// MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
// body.add("id", event.getId());
// body.add("processDefinitionKey", event.getProcessDefinitionKey());
// body.add("status", event.getStatus().toString());
// if (StrUtil.isNotEmpty(event.getBusinessKey())) {
// body.add("businessKey", event.getBusinessKey());
// }
// 2. 发起请求
RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class);
sendHttpRequest(url, headers, event, restTemplate);
}
public static ResponseEntity<String> sendHttpRequest(String url, public static ResponseEntity<String> sendHttpRequest(String url,
MultiValueMap<String, String> headers, MultiValueMap<String, String> headers,
MultiValueMap<String, String> body, Object body,
RestTemplate restTemplate) { RestTemplate restTemplate) {
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers); HttpEntity<Object> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<String> responseEntity; ResponseEntity<String> responseEntity;
try { try {
responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class); responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
log.info("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity); log.info("[sendHttpRequest][HTTP 请求,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity);
} catch (RestClientException e) { } catch (RestClientException e) {
log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); log.error("[sendHttpRequest][HTTP 请求,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage());
throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR); throw exception(PROCESS_INSTANCE_HTTP_CALL_ERROR);
} }
return responseEntity; return responseEntity;
} }
public static MultiValueMap<String, String> buildHttpHeaders(ProcessInstance processInstance, public static MultiValueMap<String, String> buildHttpHeaders(ProcessInstance processInstance,
List<BpmSimpleModelNodeVO.HttpRequestParam> headerSettings) { List<BpmSimpleModelNodeVO.HttpRequestParam> headerSettings) {
Map<String, Object> processVariables = processInstance.getProcessVariables();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>(); MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); headers.add(HEADER_TENANT_ID, processInstance.getTenantId());
Map<String, Object> processVariables = processInstance.getProcessVariables();
addHttpRequestParam(headers, headerSettings, processVariables); addHttpRequestParam(headers, headerSettings, processVariables);
return headers; return headers;
} }
@@ -108,7 +136,9 @@ public class BpmHttpRequestUtils {
Map<String, Object> processVariables = processInstance.getProcessVariables(); Map<String, Object> processVariables = processInstance.getProcessVariables();
MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
addHttpRequestParam(body, bodySettings, processVariables); addHttpRequestParam(body, bodySettings, processVariables);
body.add("processInstanceId", processInstance.getId()); if (!body.containsKey("processInstanceId")) { // 避免重复添加
body.add("processInstanceId", processInstance.getId());
}
return body; return body;
} }

View File

@@ -478,7 +478,11 @@ public class BpmnModelUtils {
*/ */
public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) {
Process process = model.getMainProcess(); Process process = model.getMainProcess();
return process.getFlowElement(flowElementId); FlowElement flowElement = process.getFlowElement(flowElementId);
if (flowElement != null) {
return flowElement;
}
return model.getFlowElement(flowElementId);
} }
/** /**
@@ -796,9 +800,10 @@ public class BpmnModelUtils {
|| currentElement instanceof EndEvent || currentElement instanceof EndEvent
|| currentElement instanceof UserTask || currentElement instanceof UserTask
|| currentElement instanceof ServiceTask) { || currentElement instanceof ServiceTask) {
// 添加元素 // 添加节点
FlowNode flowNode = (FlowNode) currentElement; FlowNode flowNode = (FlowNode) currentElement;
resultElements.add(flowNode); resultElements.add(flowNode);
// 遍历子节点 // 遍历子节点
flowNode.getOutgoingFlows().forEach( flowNode.getOutgoingFlows().forEach(
nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements));
@@ -831,6 +836,31 @@ public class BpmnModelUtils {
} }
} }
/**
* 判断是否跳过此节点
*
* @param flowNode 节点
* @param variables 流程变量
*/
public static boolean isSkipNode(FlowElement flowNode, Map<String, Object> variables) {
// 1. 检查节点是否有跳过表达式(支持多种任务节点类型)
String skipExpression = null;
if (flowNode instanceof UserTask) {
skipExpression = ((UserTask) flowNode).getSkipExpression();
} else if (flowNode instanceof ServiceTask) {
skipExpression = ((ServiceTask) flowNode).getSkipExpression();
} else if (flowNode instanceof ScriptTask) {
skipExpression = ((ScriptTask) flowNode).getSkipExpression();
}
if (StrUtil.isEmpty(skipExpression)) {
return false;
}
// 2. 计算跳过表达式的值
return evalConditionExpress(variables, skipExpression);
}
/** /**
* 根据当前节点,获取下一个节点 * 根据当前节点,获取下一个节点
* *
@@ -993,7 +1023,7 @@ public class BpmnModelUtils {
* @return 是否满足条件 * @return 是否满足条件
*/ */
public static boolean evalConditionExpress(Map<String, Object> variables, String expression) { public static boolean evalConditionExpress(Map<String, Object> variables, String expression) {
if (expression == null) { if (StrUtil.isEmpty(expression)) {
return Boolean.FALSE; return Boolean.FALSE;
} }
// 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables

View File

@@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*; import cn.hutool.core.util.*;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups;
@@ -17,13 +16,14 @@ import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener
import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener; import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener;
import org.flowable.bpmn.BpmnAutoLayout; import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.constants.BpmnXMLConstants;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*; import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.Process;
import org.flowable.engine.delegate.ExecutionListener; import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.delegate.TaskListener;
import java.util.*; import java.util.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
@@ -239,13 +239,13 @@ public class SimpleModelUtils {
// 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况 // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况
if (isValidNode(conditionChildNode)) { if (isValidNode(conditionChildNode)) {
// 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线 // 3.1.1 建立与后续的节点的连线。例如说,建立 A->B 的连线
SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), item); SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), conditionChildNode.getId(), nodeType, item);
process.addFlowElement(sequenceFlow); process.addFlowElement(sequenceFlow);
// 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线 // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线
traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId); traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId);
} else { } else {
// 3.2 分支没有后续节点。例如说,建立 A->D 的连线 // 3.2 分支没有后续节点。例如说,建立 A->D 的连线
SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item); SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, nodeType, item);
process.addFlowElement(sequenceFlow); process.addFlowElement(sequenceFlow);
} }
} }
@@ -464,6 +464,10 @@ public class SimpleModelUtils {
addReasonRequire(node.getReasonRequire(), userTask); addReasonRequire(node.getReasonRequire(), userTask);
// 节点类型 // 节点类型
addNodeType(node.getType(), userTask); addNodeType(node.getType(), userTask);
// 添加跳过表达式
if (StrUtil.isNotEmpty(node.getSkipExpression())) {
userTask.setSkipExpression(node.getSkipExpression());
}
return userTask; return userTask;
} }
@@ -591,17 +595,23 @@ public class SimpleModelUtils {
private static class ParallelBranchNodeConvert implements NodeConvert { private static class ParallelBranchNodeConvert implements NodeConvert {
/**
* 并行分支使用包容网关。需要设置所有出口条件表达式的值为 true 。原因是,解决 https://t.zsxq.com/m6GXh 反馈问题
*
* @see {@link ConditionNodeConvert#buildSequenceFlow}
*/
@Override @Override
public List<ParallelGateway> convertList(BpmSimpleModelNodeVO node) { public List<InclusiveGateway> convertList(BpmSimpleModelNodeVO node) {
ParallelGateway parallelGateway = new ParallelGateway();
parallelGateway.setId(node.getId()); InclusiveGateway inclusiveGateway = new InclusiveGateway();
inclusiveGateway.setId(node.getId());
// TODO @jasonsetName // TODO @jasonsetName
// 并行聚合网关由程序创建,前端不需要传入 // 合网关 由程序创建,前端不需要传入
ParallelGateway joinParallelGateway = new ParallelGateway(); InclusiveGateway joinParallelGateway = new InclusiveGateway();
joinParallelGateway.setId(buildGatewayJoinId(node.getId())); joinParallelGateway.setId(buildGatewayJoinId(node.getId()));
// TODO @jasonsetName // TODO @jasonsetName
return CollUtil.newArrayList(parallelGateway, joinParallelGateway); return CollUtil.newArrayList(inclusiveGateway, joinParallelGateway);
} }
@Override @Override
@@ -652,8 +662,14 @@ public class SimpleModelUtils {
} }
public static SequenceFlow buildSequenceFlow(String sourceId, String targetId, public static SequenceFlow buildSequenceFlow(String sourceId, String targetId,
BpmSimpleModelNodeVO node) { BpmSimpleModelNodeTypeEnum nodeType, BpmSimpleModelNodeVO node) {
String conditionExpression = buildConditionExpression(node.getConditionSetting()); String conditionExpression;
// 并行分支,使用包容网关实现,强制设置条件表达式为 true
if (BpmSimpleModelNodeTypeEnum.PARALLEL_BRANCH_NODE == nodeType) {
conditionExpression ="${true}";
} else {
conditionExpression = buildConditionExpression(node.getConditionSetting());
}
return buildBpmnSequenceFlow(sourceId, targetId, node.getId(), node.getName(), conditionExpression); return buildBpmnSequenceFlow(sourceId, targetId, node.getId(), node.getName(), conditionExpression);
} }
} }
@@ -662,7 +678,6 @@ public class SimpleModelUtils {
* 构造条件表达式 * 构造条件表达式
*/ */
public static String buildConditionExpression(BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { public static String buildConditionExpression(BpmSimpleModelNodeVO.ConditionSetting conditionSetting) {
// 并行网关不需要设置条件
if (conditionSetting == null) { if (conditionSetting == null) {
return null; return null;
} }
@@ -684,15 +699,18 @@ public class SimpleModelUtils {
if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) { if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) {
return null; return null;
} }
List<String> strConditionGroups = CollectionUtils.convertList(conditionGroups.getConditions(), item -> { List<String> strConditionGroups = convertList(conditionGroups.getConditions(), item -> {
if (CollUtil.isEmpty(item.getRules())) { if (CollUtil.isEmpty(item.getRules())) {
return ""; return "";
} }
// 构造规则表达式 // 构造规则表达式
List<String> list = CollectionUtils.convertList(item.getRules(), (rule) -> { List<String> list = convertList(item.getRules(), (rule) -> {
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide()
: "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号 : "\"" + rule.getRightSide() + "\""; // 如果非数值类型加引号
return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide); return String.format(" vars:getOrDefault(%s, null) %s var:convertByType(%s,%s) ",
rule.getLeftSide(), // 左侧:读取变量
rule.getOpCode(), // 中间:操作符,比较
rule.getLeftSide(), rightSide); // 右侧转换变量VariableConvertByTypeExpressionFunction
}); });
// 构造条件组的表达式 // 构造条件组的表达式
Boolean and = item.getAnd(); Boolean and = item.getAnd();
@@ -954,7 +972,7 @@ public class SimpleModelUtils {
|| nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE
|| nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS
|| nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) {
// 添加元素 // 添加此节点
resultNodes.add(currentNode); resultNodes.add(currentNode);
} }
@@ -1000,6 +1018,16 @@ public class SimpleModelUtils {
simulateNextNode(currentNode.getChildNode(), variables, resultNodes); simulateNextNode(currentNode.getChildNode(), variables, resultNodes);
} }
/**
* 根据跳过表达式,判断是否跳过此节点
*/
public static boolean isSkipNode(BpmSimpleModelNodeVO currentNode, Map<String, Object> variables) {
if (StrUtil.isEmpty(currentNode.getSkipExpression())) {
return false;
}
return BpmnModelUtils.evalConditionExpress(variables, currentNode.getSkipExpression());
}
public static boolean evalConditionExpress(Map<String, Object> variables, BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { public static boolean evalConditionExpress(Map<String, Object> variables, BpmSimpleModelNodeVO.ConditionSetting conditionSetting) {
return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting));
} }

View File

@@ -34,6 +34,9 @@ public class BpmCategoryServiceImpl implements BpmCategoryService {
@Resource @Resource
private BpmCategoryMapper bpmCategoryMapper; private BpmCategoryMapper bpmCategoryMapper;
@Resource
private BpmModelService modelService;
@Override @Override
public Long createCategory(BpmCategorySaveReqVO createReqVO) { public Long createCategory(BpmCategorySaveReqVO createReqVO) {
// 校验唯一 // 校验唯一
@@ -77,15 +80,22 @@ public class BpmCategoryServiceImpl implements BpmCategoryService {
@Override @Override
public void deleteCategory(Long id) { public void deleteCategory(Long id) {
// 校验存在 // 校验存在
validateCategoryExists(id); BpmCategoryDO category = validateCategoryExists(id);
// 校验是否被流程模型使用
Long count = modelService.getModelCountByCategory(category.getCode());
if (count > 0) {
throw exception(CATEGORY_DELETE_FAIL_MODEL_USED, category.getName());
}
// 删除 // 删除
bpmCategoryMapper.deleteById(id); bpmCategoryMapper.deleteById(id);
} }
private void validateCategoryExists(Long id) { private BpmCategoryDO validateCategoryExists(Long id) {
if (bpmCategoryMapper.selectById(id) == null) { BpmCategoryDO category = bpmCategoryMapper.selectById(id);
if (category == null) {
throw exception(CATEGORY_NOT_EXISTS); throw exception(CATEGORY_NOT_EXISTS);
} }
return category;
} }
@Override @Override

View File

@@ -24,6 +24,14 @@ public interface BpmModelService {
*/ */
List<Model> getModelList(String name); List<Model> getModelList(String name);
/**
* 根据分类编码获得流程模型数量
*
* @param category 分类编码
* @return 流程模型数量
*/
Long getModelCountByCategory(String category);
/** /**
* 创建流程模型 * 创建流程模型
* *

View File

@@ -88,6 +88,14 @@ public class BpmModelServiceImpl implements BpmModelService {
return modelQuery.list(); return modelQuery.list();
} }
@Override
public Long getModelCountByCategory(String category) {
return repositoryService.createModelQuery()
.modelCategory(category)
.modelTenantId(FlowableUtils.getTenantId())
.count();
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public String createModel(@Valid BpmModelSaveReqVO createReqVO) { public String createModel(@Valid BpmModelSaveReqVO createReqVO) {

View File

@@ -4,11 +4,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil; import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.*;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils; import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils; import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
@@ -61,12 +59,15 @@ import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.util.*; import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
@@ -264,7 +265,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 3. 获取下一个将要执行的节点集合 // 3. 获取下一个将要执行的节点集合
FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey()); FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
List<FlowNode> nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); List<FlowNode> nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables);
List<ActivityNode> nextActivityNodes = convertList(nextFlowNodes, node -> new ActivityNode().setId(node.getId()) // 仅仅获取 UserTask 节点 TODO add from jason如果网关节点和网关节点相连获取下个 UserTask. 貌似有点不准。
List<FlowNode> nextUserTaskList = CollectionUtils.filterList(nextFlowNodes, node -> node instanceof UserTask);
List<ActivityNode> nextActivityNodes = convertList(nextUserTaskList, node -> new ActivityNode().setId(node.getId())
.setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) .setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
.setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node))
@@ -395,7 +398,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
: ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别 : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别
BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
.setStatus(FlowableUtils.getTaskStatus(task)) .setStatus(getEndActivityNodeStatus(task))
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
.setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
.setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task))); .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)));
@@ -449,7 +452,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
.setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus) .setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus)
.setStartTime(DateUtils.of(activity.getStartTime())) .setStartTime(DateUtils.of(activity.getStartTime()))
.setEndTime(DateUtils.of(activity.getEndTime())) .setEndTime(DateUtils.of(activity.getEndTime()))
.setProcessInstanceId(activity.getProcessInstanceId()); .setProcessInstanceId(activity.getCalledProcessInstanceId());
approvalNodes.add(callActivity); approvalNodes.add(callActivity);
} }
}); });
@@ -459,6 +462,18 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
return approvalNodes; return approvalNodes;
} }
/**
* 获取结束节点的状态
*/
private Integer getEndActivityNodeStatus(HistoricTaskInstance task) {
Integer status = FlowableUtils.getTaskStatus(task);
if (status != null) {
return status;
}
// 结束节点未获取到状态,为跳过状态。可见 bpmn 或者 simple 的 skipExpression
return BpmTaskStatusEnum.SKIP.getStatus();
}
/** /**
* 获得【进行中】的活动节点们 * 获得【进行中】的活动节点们
*/ */
@@ -521,7 +536,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size())); activityNode.setCandidateUserIds(CollUtil.sub(candidateUserIds, index + 1, candidateUserIds.size()));
} }
if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) { if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) {
activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId()); activityNode.setProcessInstanceId(firstActivity.getCalledProcessInstanceId());
} }
return activityNode; return activityNode;
}); });
@@ -562,10 +577,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (runActivityIds.contains(node.getId())) { if (runActivityIds.contains(node.getId())) {
return null; return null;
} }
Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
// 如果节点被跳过。设置状态为跳过
if (SimpleModelUtils.isSkipNode(node, processVariables)) {
status = BpmTaskStatusEnum.SKIP.getStatus();
}
ActivityNode activityNode = new ActivityNode().setId(node.getId()).setName(node.getName()) ActivityNode activityNode = new ActivityNode().setId(node.getId()).setName(node.getName())
.setNodeType(node.getType()).setCandidateStrategy(node.getCandidateStrategy()) .setNodeType(node.getType()).setCandidateStrategy(node.getCandidateStrategy())
.setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); .setStatus(status);
// 1. 开始节点/审批节点 // 1. 开始节点/审批节点
if (ObjectUtils.equalsAny(node.getType(), if (ObjectUtils.equalsAny(node.getType(),
@@ -605,8 +624,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
if (runActivityIds.contains(node.getId())) { if (runActivityIds.contains(node.getId())) {
return null; return null;
} }
Integer status = BpmTaskStatusEnum.NOT_START.getStatus();
// 如果节点被跳过,状态设置为跳过
if(BpmnModelUtils.isSkipNode(node, processVariables)){
status = BpmTaskStatusEnum.SKIP.getStatus();
}
ActivityNode activityNode = new ActivityNode().setId(node.getId()) ActivityNode activityNode = new ActivityNode().setId(node.getId())
.setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); .setStatus(status);
// 1. 开始节点 // 1. 开始节点
if (node instanceof StartEvent) { if (node instanceof StartEvent) {
@@ -771,17 +795,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule)); processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule));
} }
// 3.2 流程名称 // 3.2 流程名称
BpmModelMetaInfoVO.TitleSetting titleSetting = processDefinitionInfo.getTitleSetting(); processInstanceBuilder.name(generateProcessInstanceName(userId, definition, processDefinitionInfo, variables));
if (titleSetting != null && Boolean.TRUE.equals(titleSetting.getEnable())) {
AdminUserRespDTO user = adminUserApi.getUser(userId);
Map<String, Object> cloneVariables = new HashMap<>(variables);
cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname());
cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now());
cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim());
processInstanceBuilder.name(StrUtil.format(titleSetting.getTitle(), cloneVariables));
} else {
processInstanceBuilder.name(definition.getName().trim());
}
// 3.3 发起流程实例 // 3.3 发起流程实例
ProcessInstance instance = processInstanceBuilder.start(); ProcessInstance instance = processInstanceBuilder.start();
return instance.getId(); return instance.getId();
@@ -817,6 +831,25 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
}); });
} }
private String generateProcessInstanceName(Long userId,
ProcessDefinition definition,
BpmProcessDefinitionInfoDO definitionInfo,
Map<String, Object> variables) {
if (definition == null || definitionInfo == null) {
return null;
}
BpmModelMetaInfoVO.TitleSetting titleSetting = definitionInfo.getTitleSetting();
if (titleSetting == null || !BooleanUtil.isTrue(titleSetting.getEnable())) {
return definition.getName();
}
AdminUserRespDTO user = adminUserApi.getUser(userId);
Map<String, Object> cloneVariables = new HashMap<>(variables);
cloneVariables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, user.getNickname());
cloneVariables.put(BpmnVariableConstants.PROCESS_START_TIME, DateUtil.now());
cloneVariables.put(BpmnVariableConstants.PROCESS_DEFINITION_NAME, definition.getName().trim());
return StrUtil.format(definitionInfo.getTitleSetting().getTitle(), cloneVariables);
}
@Override @Override
public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) {
// 1.1 校验流程实例存在 // 1.1 校验流程实例存在
@@ -833,7 +866,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
.getProcessDefinitionInfo(instance.getProcessDefinitionId()); .getProcessDefinitionInfo(instance.getProcessDefinitionId());
Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo); Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo);
if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消 if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消
&& Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) { && BooleanUtil.isFalse(processDefinitionInfo.getAllowCancelRunningProcess())) {
throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW); throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW);
} }
// 1.4 子流程不允许取消 // 1.4 子流程不允许取消
@@ -900,64 +933,77 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
@Override @Override
public void processProcessInstanceCompleted(ProcessInstance instance) { public void processProcessInstanceCompleted(ProcessInstance instance) {
// 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 // 1.1 获取当前状态
FlowableUtils.execute(instance.getTenantId(), () -> { Integer status = (Integer) instance.getProcessVariables()
// 1.1 获取当前状态 .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
Integer status = (Integer) instance.getProcessVariables() String reason = (String) instance.getProcessVariables()
.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON);
String reason = (String) instance.getProcessVariables() // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态
.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了
// 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) {
// 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 status = BpmProcessInstanceStatusEnum.APPROVE.getStatus();
if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS,
status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); status);
runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, }
status);
// 2. 发送对应的消息通知
if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
messageService.sendMessageWhenProcessInstanceApprove(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance));
} else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) {
messageService.sendMessageWhenProcessInstanceReject(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason));
}
// 3. 发送流程实例的状态事件
processInstanceEventPublisher.sendProcessInstanceResultEvent(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status));
// 4. 流程后置通知
if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
getProcessDefinitionInfo(instance.getProcessDefinitionId());
if (ObjUtil.isNotNull(processDefinitionInfo) &&
ObjUtil.isNotNull(processDefinitionInfo.getProcessAfterTriggerSetting())) {
BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessAfterTriggerSetting();
BpmHttpRequestUtils.executeBpmHttpRequest(instance,
setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse());
} }
}
// 2. 发送对应的消息通知
if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
messageService.sendMessageWhenProcessInstanceApprove(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceApproveMessage(instance));
} else if (Objects.equals(status, BpmProcessInstanceStatusEnum.REJECT.getStatus())) {
messageService.sendMessageWhenProcessInstanceReject(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceRejectMessage(instance, reason));
}
// 3. 发送流程实例的状态事件
processInstanceEventPublisher.sendProcessInstanceResultEvent(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status));
// 4. 流程后置通知
if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
getProcessDefinitionInfo(instance.getProcessDefinitionId());
if (ObjUtil.isNotNull(processDefinitionInfo) &&
ObjUtil.isNotNull(processDefinitionInfo.getProcessAfterTriggerSetting())) {
BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessAfterTriggerSetting();
BpmHttpRequestUtils.executeBpmHttpRequest(instance,
setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse());
}
}
});
} }
@Override @Override
public void processProcessInstanceCreated(ProcessInstance instance) { public void processProcessInstanceCreated(ProcessInstance instance) {
// 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
FlowableUtils.execute(instance.getTenantId(), () -> { getProcessDefinitionInfo(instance.getProcessDefinitionId());
// 流程前置通知 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(instance.getProcessDefinitionId());
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. if (processDefinition == null || processDefinitionInfo == null) {
getProcessDefinitionInfo(instance.getProcessDefinitionId()); return;
if (ObjUtil.isNull(processDefinitionInfo) || }
ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) {
return; // 自定义标题。目的:主要处理子流程的标题无法处理
// 注意:必须使用 TransactionSynchronizationManager 事务提交后,否则不生效!!!
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
String name = generateProcessInstanceName(Long.valueOf(instance.getStartUserId()),
processDefinition, processDefinitionInfo, instance.getProcessVariables());
if (ObjUtil.notEqual(instance.getName(), name)) {
runtimeService.setProcessInstanceName(instance.getProcessInstanceId(), name);
}
} }
BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessBeforeTriggerSetting();
BpmHttpRequestUtils.executeBpmHttpRequest(instance,
setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse());
}); });
// 流程前置通知
if (ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) {
return;
}
BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getProcessBeforeTriggerSetting();
BpmHttpRequestUtils.executeBpmHttpRequest(instance,
setting.getUrl(), setting.getHeader(), setting.getBody(), true, setting.getResponse());
} }
} }

View File

@@ -230,10 +230,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (StrUtil.isNotBlank(pageVO.getName())) { if (StrUtil.isNotBlank(pageVO.getName())) {
taskQuery.taskNameLike("%" + pageVO.getName() + "%"); taskQuery.taskNameLike("%" + pageVO.getName() + "%");
} }
if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { // if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); // taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); // taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
} // }
// 执行查询 // 执行查询
long count = taskQuery.count(); long count = taskQuery.count();
if (count == 0) { if (count == 0) {
@@ -244,6 +244,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 特殊:强制移除自动完成的“发起人”节点 // 特殊:强制移除自动完成的“发起人”节点
// 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤
tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID));
// TODO @芋艿https://t.zsxq.com/MNzqp 【flowable bug】taskCreatedAfter、taskCreatedBefore 拼接的是 OR
if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
tasks.removeIf(task -> task.getCreateTime() == null
|| task.getCreateTime().before(DateUtils.of(pageVO.getCreateTime()[0]))
|| task.getCreateTime().after(DateUtils.of(pageVO.getCreateTime()[1])));
}
return new PageResult<>(tasks, count); return new PageResult<>(tasks, count);
} }
@@ -259,16 +265,22 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (StrUtil.isNotEmpty(pageVO.getCategory())) { if (StrUtil.isNotEmpty(pageVO.getCategory())) {
taskQuery.taskCategory(pageVO.getCategory()); taskQuery.taskCategory(pageVO.getCategory());
} }
if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { // if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); // taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); // taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
} // }
// 执行查询 // 执行查询
long count = taskQuery.count(); long count = taskQuery.count();
if (count == 0) { if (count == 0) {
return PageResult.empty(); return PageResult.empty();
} }
List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
// TODO @芋艿https://t.zsxq.com/MNzqp 【flowable bug】taskCreatedAfter、taskCreatedBefore 拼接的是 OR
if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
tasks.removeIf(task -> task.getCreateTime() == null
|| task.getCreateTime().before(DateUtils.of(pageVO.getCreateTime()[0]))
|| task.getCreateTime().after(DateUtils.of(pageVO.getCreateTime()[1])));
}
return new PageResult<>(tasks, count); return new PageResult<>(tasks, count);
} }
@@ -886,7 +898,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) {
return; return;
} }
runExecutionIds.add(task.getExecutionId()); if (task.getExecutionId() != null) {
runExecutionIds.add(task.getExecutionId());
}
// 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务
if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记
@@ -933,7 +947,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason()));
// 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee)
taskService.setOwner(taskId, task.getAssignee()); // 特殊如果已经被委派owner 非空),则不需要更新 ownerhttps://gitee.com/zhijiantianya/yudao-cloud/issues/ICJ153
if (StrUtil.isEmpty(task.getOwner())) {
taskService.setOwner(taskId, task.getAssignee());
}
// 3.2 执行委派,将任务委派给 delegateUser // 3.2 执行委派,将任务委派给 delegateUser
taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString()); taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString());
// 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中 // 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中
@@ -959,7 +976,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason())); BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason()));
// 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee)
taskService.setOwner(taskId, task.getAssignee()); // 特殊如果已经被转派owner 非空),则不需要更新 ownerhttps://gitee.com/zhijiantianya/yudao-cloud/issues/ICJ153
if (StrUtil.isEmpty(task.getOwner())) {
taskService.setOwner(taskId, task.getAssignee());
}
// 3.2 执行转派(审批人),将任务转派给 assigneeUser // 3.2 执行转派(审批人),将任务转派给 assigneeUser
// 委托( delegate和转派transfer的差别就在这块的调用 // 委托( delegate和转派transfer的差别就在这块的调用
taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString()); taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString());
@@ -1367,7 +1387,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID) if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核
|| Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));

View File

@@ -85,10 +85,15 @@ public class BpmCallActivityListener implements ExecutionListener {
// 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人 // 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人
try { try {
FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue)); FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue));
} catch (Exception e) { } catch (NumberFormatException ex) {
log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", try {
DELEGATE_EXPRESSION, formFieldValue); List<Long> formFieldValues = JsonUtils.parseArray(formFieldValue, Long.class);
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); FlowableUtils.setAuthenticatedUserId(formFieldValues.get(0));
} catch (Exception e) {
log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]",
DELEGATE_EXPRESSION, formFieldValue);
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
}
} }
} }
} }

View File

@@ -8,9 +8,11 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCa
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO; import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper; import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime; import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
@@ -32,6 +34,9 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest {
@Resource @Resource
private BpmCategoryServiceImpl categoryService; private BpmCategoryServiceImpl categoryService;
@MockitoBean
private BpmModelService modelService;
@Resource @Resource
private BpmCategoryMapper categoryMapper; private BpmCategoryMapper categoryMapper;

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business; package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;

View File

@@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.clue.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.ToString; import lombok.ToString;

View File

@@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.contact.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
import lombok.ToString; import lombok.ToString;

View File

@@ -1,7 +1,7 @@
package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract; package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;

View File

@@ -5,7 +5,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.ExcelColumnSelect;
import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert; import cn.iocoder.yudao.framework.excel.core.convert.AreaConvert;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.crm.framework.excel.core.AreaExcelColumnSelectFunction; import cn.iocoder.yudao.module.crm.framework.excel.core.AreaExcelColumnSelectFunction;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Builder; import lombok.Builder;
import lombok.Data; import lombok.Data;

View File

@@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.customer.vo.customer;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;

View File

@@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO; import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;
@@ -61,4 +61,4 @@ public class CrmFollowUpRecordRespVO {
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime; private LocalDateTime createTime;
} }

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo; package cn.iocoder.yudao.module.crm.controller.admin.operatelog.vo;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;

View File

@@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductDO;
import cn.iocoder.yudao.module.crm.enums.product.CrmProductStatusEnum; import cn.iocoder.yudao.module.crm.enums.product.CrmProductStatusEnum;
import cn.iocoder.yudao.module.crm.service.product.CrmProductService; import cn.iocoder.yudao.module.crm.service.product.CrmProductService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import com.fhs.core.trans.anno.TransMethodResult;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
@@ -70,6 +71,7 @@ public class CrmProductController {
@Operation(summary = "获得产品") @Operation(summary = "获得产品")
@Parameter(name = "id", description = "编号", required = true, example = "1024") @Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('crm:product:query')") @PreAuthorize("@ss.hasPermission('crm:product:query')")
@TransMethodResult
public CommonResult<CrmProductRespVO> getProduct(@RequestParam("id") Long id) { public CommonResult<CrmProductRespVO> getProduct(@RequestParam("id") Long id) {
CrmProductDO product = productService.getProduct(id); CrmProductDO product = productService.getProduct(id);
return success(BeanUtils.toBean(product, CrmProductRespVO.class)); return success(BeanUtils.toBean(product, CrmProductRespVO.class));
@@ -86,6 +88,7 @@ public class CrmProductController {
@GetMapping("/page") @GetMapping("/page")
@Operation(summary = "获得产品分页") @Operation(summary = "获得产品分页")
@PreAuthorize("@ss.hasPermission('crm:product:query')") @PreAuthorize("@ss.hasPermission('crm:product:query')")
@TransMethodResult
public CommonResult<PageResult<CrmProductRespVO>> getProductPage(@Valid CrmProductPageReqVO pageVO) { public CommonResult<PageResult<CrmProductRespVO>> getProductPage(@Valid CrmProductPageReqVO pageVO) {
PageResult<CrmProductDO> pageResult = productService.getProductPage(pageVO); PageResult<CrmProductDO> pageResult = productService.getProductPage(pageVO);
return success(BeanUtils.toBean(pageResult, CrmProductRespVO.class)); return success(BeanUtils.toBean(pageResult, CrmProductRespVO.class));

View File

@@ -1,6 +1,6 @@
package cn.iocoder.yudao.module.crm.controller.admin.product.vo.category; package cn.iocoder.yudao.module.crm.controller.admin.product.vo.category;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;

View File

@@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO; import cn.iocoder.yudao.module.crm.dal.dataobject.product.CrmProductCategoryDO;
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants; import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.anno.Trans;
import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.constant.TransType;
import com.fhs.core.trans.vo.VO; import com.fhs.core.trans.vo.VO;

View File

@@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan; package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan;
import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO; import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;

View File

@@ -4,8 +4,8 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO; import cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO;
import cn.iocoder.yudao.module.crm.enums.DictTypeConstants; import cn.iocoder.yudao.module.crm.enums.DictTypeConstants;
import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import com.alibaba.excel.annotation.ExcelProperty; import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data; import lombok.Data;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.crm.util;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.biz.system.permission.PermissionCommonApi;
import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO; import cn.iocoder.yudao.module.crm.dal.dataobject.permission.CrmPermissionDO;
import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum; import cn.iocoder.yudao.module.crm.enums.common.CrmBizTypeEnum;
import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum; import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum;
import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum; import cn.iocoder.yudao.module.crm.enums.permission.CrmPermissionLevelEnum;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum;
@@ -32,7 +32,7 @@ public class CrmPermissionUtils {
* @return 是/否 * @return 是/否
*/ */
public static boolean isCrmAdmin() { public static boolean isCrmAdmin() {
PermissionApi permissionApi = SpringUtil.getBean(PermissionApi.class); PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class);
return permissionApi.hasAnyRoles(getLoginUserId(), RoleCodeEnum.CRM_ADMIN.getCode()); return permissionApi.hasAnyRoles(getLoginUserId(), RoleCodeEnum.CRM_ADMIN.getCode());
} }

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