diff --git a/pom.xml b/pom.xml index e85946ce91..fdb62e5464 100644 --- a/pom.xml +++ b/pom.xml @@ -32,7 +32,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.0-SNAPSHOT + 2025.08-SNAPSHOT 17 ${java.version} @@ -87,6 +87,13 @@ lombok ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + org.mapstruct mapstruct-processor diff --git a/sql/postgresql/quartz.sql b/sql/postgresql/quartz.sql index 4ec390c527..46bb938431 100644 --- a/sql/postgresql/quartz.sql +++ b/sql/postgresql/quartz.sql @@ -1,253 +1,208 @@ --- ---------------------------- --- qrtz_blob_triggers --- ---------------------------- -CREATE TABLE qrtz_blob_triggers +-- https://github.com/quartz-scheduler/quartz/blob/main/quartz/src/main/resources/org/quartz/impl/jdbcjobstore/tables_postgres.sql +-- Thanks to Patrick Lightbody for submitting this... +-- +-- In your Quartz properties file, you'll need to set +-- org.quartz.jobStore.driverDelegateClass = org.quartz.impl.jdbcjobstore.PostgreSQLDelegate + +DROP TABLE IF EXISTS QRTZ_FIRED_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_PAUSED_TRIGGER_GRPS; +DROP TABLE IF EXISTS QRTZ_SCHEDULER_STATE; +DROP TABLE IF EXISTS QRTZ_LOCKS; +DROP TABLE IF EXISTS QRTZ_SIMPLE_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_CRON_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_SIMPROP_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_BLOB_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_TRIGGERS; +DROP TABLE IF EXISTS QRTZ_JOB_DETAILS; +DROP TABLE IF EXISTS QRTZ_CALENDARS; + +CREATE TABLE QRTZ_JOB_DETAILS ( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - blob_data bytea NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) + SCHED_NAME VARCHAR(120) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + JOB_CLASS_NAME VARCHAR(250) NOT NULL, + IS_DURABLE BOOL NOT NULL, + IS_NONCONCURRENT BOOL NOT NULL, + IS_UPDATE_DATA BOOL NOT NULL, + REQUESTS_RECOVERY BOOL NOT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) ); -CREATE INDEX idx_qrtz_blob_triggers_sched_name ON qrtz_blob_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- qrtz_calendars --- ---------------------------- -CREATE TABLE qrtz_calendars +CREATE TABLE QRTZ_TRIGGERS ( - sched_name varchar(120) NOT NULL, - calendar_name varchar(190) NOT NULL, - calendar bytea NOT NULL, - PRIMARY KEY (sched_name, calendar_name) + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + JOB_NAME VARCHAR(200) NOT NULL, + JOB_GROUP VARCHAR(200) NOT NULL, + DESCRIPTION VARCHAR(250) NULL, + NEXT_FIRE_TIME BIGINT NULL, + PREV_FIRE_TIME BIGINT NULL, + PRIORITY INTEGER NULL, + TRIGGER_STATE VARCHAR(16) NOT NULL, + TRIGGER_TYPE VARCHAR(8) NOT NULL, + START_TIME BIGINT NOT NULL, + END_TIME BIGINT NULL, + CALENDAR_NAME VARCHAR(200) NULL, + MISFIRE_INSTR SMALLINT NULL, + JOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, JOB_NAME, JOB_GROUP) + REFERENCES QRTZ_JOB_DETAILS (SCHED_NAME, JOB_NAME, JOB_GROUP) +); + +CREATE TABLE QRTZ_SIMPLE_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + REPEAT_COUNT BIGINT NOT NULL, + REPEAT_INTERVAL BIGINT NOT NULL, + TIMES_TRIGGERED BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CRON_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + CRON_EXPRESSION VARCHAR(120) NOT NULL, + TIME_ZONE_ID VARCHAR(80), + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_SIMPROP_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + STR_PROP_1 VARCHAR(512) NULL, + STR_PROP_2 VARCHAR(512) NULL, + STR_PROP_3 VARCHAR(512) NULL, + INT_PROP_1 INT NULL, + INT_PROP_2 INT NULL, + LONG_PROP_1 BIGINT NULL, + LONG_PROP_2 BIGINT NULL, + DEC_PROP_1 NUMERIC(13, 4) NULL, + DEC_PROP_2 NUMERIC(13, 4) NULL, + BOOL_PROP_1 BOOL NULL, + BOOL_PROP_2 BOOL NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_BLOB_TRIGGERS +( + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + BLOB_DATA BYTEA NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP), + FOREIGN KEY (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) + REFERENCES QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP) +); + +CREATE TABLE QRTZ_CALENDARS +( + SCHED_NAME VARCHAR(120) NOT NULL, + CALENDAR_NAME VARCHAR(200) NOT NULL, + CALENDAR BYTEA NOT NULL, + PRIMARY KEY (SCHED_NAME, CALENDAR_NAME) ); --- ---------------------------- --- qrtz_cron_triggers --- ---------------------------- -CREATE TABLE qrtz_cron_triggers +CREATE TABLE QRTZ_PAUSED_TRIGGER_GRPS ( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - cron_expression varchar(120) NOT NULL, - time_zone_id varchar(80) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) + SCHED_NAME VARCHAR(120) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + PRIMARY KEY (SCHED_NAME, TRIGGER_GROUP) ); --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_fired_triggers --- ---------------------------- -CREATE TABLE qrtz_fired_triggers +CREATE TABLE QRTZ_FIRED_TRIGGERS ( - sched_name varchar(120) NOT NULL, - entry_id varchar(95) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - instance_name varchar(190) NOT NULL, - fired_time int8 NOT NULL, - sched_time int8 NOT NULL, - priority int4 NOT NULL, - state varchar(16) NOT NULL, - job_name varchar(190) NULL DEFAULT NULL, - job_group varchar(190) NULL DEFAULT NULL, - is_nonconcurrent varchar(1) NULL DEFAULT NULL, - requests_recovery varchar(1) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, entry_id) + SCHED_NAME VARCHAR(120) NOT NULL, + ENTRY_ID VARCHAR(95) NOT NULL, + TRIGGER_NAME VARCHAR(200) NOT NULL, + TRIGGER_GROUP VARCHAR(200) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + FIRED_TIME BIGINT NOT NULL, + SCHED_TIME BIGINT NOT NULL, + PRIORITY INTEGER NOT NULL, + STATE VARCHAR(16) NOT NULL, + JOB_NAME VARCHAR(200) NULL, + JOB_GROUP VARCHAR(200) NULL, + IS_NONCONCURRENT BOOL NULL, + REQUESTS_RECOVERY BOOL NULL, + PRIMARY KEY (SCHED_NAME, ENTRY_ID) ); -CREATE INDEX idx_qrtz_ft_trig_inst_name ON qrtz_fired_triggers (sched_name, instance_name); -CREATE INDEX idx_qrtz_ft_inst_job_req_rcvry ON qrtz_fired_triggers (sched_name, instance_name, requests_recovery); -CREATE INDEX idx_qrtz_ft_j_g ON qrtz_fired_triggers (sched_name, job_name, job_group); -CREATE INDEX idx_qrtz_ft_jg ON qrtz_fired_triggers (sched_name, job_group); -CREATE INDEX idx_qrtz_ft_t_g ON qrtz_fired_triggers (sched_name, trigger_name, trigger_group); -CREATE INDEX idx_qrtz_ft_tg ON qrtz_fired_triggers (sched_name, trigger_group); - --- ---------------------------- --- qrtz_job_details --- ---------------------------- -CREATE TABLE qrtz_job_details +CREATE TABLE QRTZ_SCHEDULER_STATE ( - sched_name varchar(120) NOT NULL, - job_name varchar(190) NOT NULL, - job_group varchar(190) NOT NULL, - description varchar(250) NULL DEFAULT NULL, - job_class_name varchar(250) NOT NULL, - is_durable varchar(1) NOT NULL, - is_nonconcurrent varchar(1) NOT NULL, - is_update_data varchar(1) NOT NULL, - requests_recovery varchar(1) NOT NULL, - job_data bytea NULL, - PRIMARY KEY (sched_name, job_name, job_group) + SCHED_NAME VARCHAR(120) NOT NULL, + INSTANCE_NAME VARCHAR(200) NOT NULL, + LAST_CHECKIN_TIME BIGINT NOT NULL, + CHECKIN_INTERVAL BIGINT NOT NULL, + PRIMARY KEY (SCHED_NAME, INSTANCE_NAME) ); -CREATE INDEX idx_qrtz_j_req_recovery ON qrtz_job_details (sched_name, requests_recovery); -CREATE INDEX idx_qrtz_j_grp ON qrtz_job_details (sched_name, job_group); - --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_locks --- ---------------------------- -CREATE TABLE qrtz_locks +CREATE TABLE QRTZ_LOCKS ( - sched_name varchar(120) NOT NULL, - lock_name varchar(40) NOT NULL, - PRIMARY KEY (sched_name, lock_name) + SCHED_NAME VARCHAR(120) NOT NULL, + LOCK_NAME VARCHAR(40) NOT NULL, + PRIMARY KEY (SCHED_NAME, LOCK_NAME) ); --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on +CREATE INDEX IDX_QRTZ_J_REQ_RECOVERY + ON QRTZ_JOB_DETAILS (SCHED_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_J_GRP + ON QRTZ_JOB_DETAILS (SCHED_NAME, JOB_GROUP); --- ---------------------------- --- qrtz_paused_trigger_grps --- ---------------------------- -CREATE TABLE qrtz_paused_trigger_grps -( - sched_name varchar(120) NOT NULL, - trigger_group varchar(190) NOT NULL, - PRIMARY KEY (sched_name, trigger_group) -); +CREATE INDEX IDX_QRTZ_T_J + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_JG + ON QRTZ_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_T_C + ON QRTZ_TRIGGERS (SCHED_NAME, CALENDAR_NAME); +CREATE INDEX IDX_QRTZ_T_G + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_T_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_N_G_STATE + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_GROUP, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NEXT_FIRE_TIME + ON QRTZ_TRIGGERS (SCHED_NAME, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST + ON QRTZ_TRIGGERS (SCHED_NAME, TRIGGER_STATE, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_STATE); +CREATE INDEX IDX_QRTZ_T_NFT_ST_MISFIRE_GRP + ON QRTZ_TRIGGERS (SCHED_NAME, MISFIRE_INSTR, NEXT_FIRE_TIME, TRIGGER_GROUP, TRIGGER_STATE); --- ---------------------------- --- qrtz_scheduler_state --- ---------------------------- -CREATE TABLE qrtz_scheduler_state -( - sched_name varchar(120) NOT NULL, - instance_name varchar(190) NOT NULL, - last_checkin_time int8 NOT NULL, - checkin_interval int8 NOT NULL, - PRIMARY KEY (sched_name, instance_name) -); - --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on - --- ---------------------------- --- qrtz_simple_triggers --- ---------------------------- -CREATE TABLE qrtz_simple_triggers -( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - repeat_count int8 NOT NULL, - repeat_interval int8 NOT NULL, - times_triggered int8 NOT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) -); - --- ---------------------------- --- qrtz_simprop_triggers --- ---------------------------- -CREATE TABLE qrtz_simprop_triggers -( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - str_prop_1 varchar(512) NULL DEFAULT NULL, - str_prop_2 varchar(512) NULL DEFAULT NULL, - str_prop_3 varchar(512) NULL DEFAULT NULL, - int_prop_1 int4 NULL DEFAULT NULL, - int_prop_2 int4 NULL DEFAULT NULL, - long_prop_1 int8 NULL DEFAULT NULL, - long_prop_2 int8 NULL DEFAULT NULL, - dec_prop_1 numeric(13, 4) NULL DEFAULT NULL, - dec_prop_2 numeric(13, 4) NULL DEFAULT NULL, - bool_prop_1 varchar(1) NULL DEFAULT NULL, - bool_prop_2 varchar(1) NULL DEFAULT NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) -); - --- ---------------------------- --- qrtz_triggers --- ---------------------------- -CREATE TABLE qrtz_triggers -( - sched_name varchar(120) NOT NULL, - trigger_name varchar(190) NOT NULL, - trigger_group varchar(190) NOT NULL, - job_name varchar(190) NOT NULL, - job_group varchar(190) NOT NULL, - description varchar(250) NULL DEFAULT NULL, - next_fire_time int8 NULL DEFAULT NULL, - prev_fire_time int8 NULL DEFAULT NULL, - priority int4 NULL DEFAULT NULL, - trigger_state varchar(16) NOT NULL, - trigger_type varchar(8) NOT NULL, - start_time int8 NOT NULL, - end_time int8 NULL DEFAULT NULL, - calendar_name varchar(190) NULL DEFAULT NULL, - misfire_instr int2 NULL DEFAULT NULL, - job_data bytea NULL, - PRIMARY KEY (sched_name, trigger_name, trigger_group) -); - -CREATE INDEX idx_qrtz_t_j ON qrtz_triggers (sched_name, job_name, job_group); -CREATE INDEX idx_qrtz_t_jg ON qrtz_triggers (sched_name, job_group); -CREATE INDEX idx_qrtz_t_c ON qrtz_triggers (sched_name, calendar_name); -CREATE INDEX idx_qrtz_t_g ON qrtz_triggers (sched_name, trigger_group); -CREATE INDEX idx_qrtz_t_state ON qrtz_triggers (sched_name, trigger_state); -CREATE INDEX idx_qrtz_t_n_state ON qrtz_triggers (sched_name, trigger_name, trigger_group, trigger_state); -CREATE INDEX idx_qrtz_t_n_g_state ON qrtz_triggers (sched_name, trigger_group, trigger_state); -CREATE INDEX idx_qrtz_t_next_fire_time ON qrtz_triggers (sched_name, next_fire_time); -CREATE INDEX idx_qrtz_t_nft_st ON qrtz_triggers (sched_name, trigger_state, next_fire_time); -CREATE INDEX idx_qrtz_t_nft_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time); -CREATE INDEX idx_qrtz_t_nft_st_misfire ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_state); -CREATE INDEX idx_qrtz_t_nft_st_misfire_grp ON qrtz_triggers (sched_name, misfire_instr, next_fire_time, trigger_group, - trigger_state); - --- @formatter:off -BEGIN; -COMMIT; --- @formatter:on +CREATE INDEX IDX_QRTZ_FT_TRIG_INST_NAME + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME); +CREATE INDEX IDX_QRTZ_FT_INST_JOB_REQ_RCVRY + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, INSTANCE_NAME, REQUESTS_RECOVERY); +CREATE INDEX IDX_QRTZ_FT_J_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_JG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, JOB_GROUP); +CREATE INDEX IDX_QRTZ_FT_T_G + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_NAME, TRIGGER_GROUP); +CREATE INDEX IDX_QRTZ_FT_TG + ON QRTZ_FIRED_TRIGGERS (SCHED_NAME, TRIGGER_GROUP); --- ---------------------------- --- FK: qrtz_blob_triggers --- ---------------------------- -ALTER TABLE qrtz_blob_triggers - ADD CONSTRAINT qrtz_blob_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, - trigger_name, - trigger_group); - --- ---------------------------- --- FK: qrtz_cron_triggers --- ---------------------------- -ALTER TABLE qrtz_cron_triggers - ADD CONSTRAINT qrtz_cron_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- FK: qrtz_simple_triggers --- ---------------------------- -ALTER TABLE qrtz_simple_triggers - ADD CONSTRAINT qrtz_simple_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, - trigger_name, - trigger_group); - --- ---------------------------- --- FK: qrtz_simprop_triggers --- ---------------------------- -ALTER TABLE qrtz_simprop_triggers - ADD CONSTRAINT qrtz_simprop_triggers_ibfk_1 FOREIGN KEY (sched_name, trigger_name, trigger_group) REFERENCES qrtz_triggers (sched_name, trigger_name, trigger_group); - --- ---------------------------- --- FK: qrtz_triggers --- ---------------------------- -ALTER TABLE qrtz_triggers - ADD CONSTRAINT qrtz_triggers_ibfk_1 FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details (sched_name, job_name, job_group); +COMMIT; \ No newline at end of file diff --git a/sql/tools/convertor.py b/sql/tools/convertor.py index 7ab8ad1ef3..d286d3b238 100644 --- a/sql/tools/convertor.py +++ b/sql/tools/convertor.py @@ -17,6 +17,7 @@ uv run --with simple-ddl-parser convertor.py dm8 > ../dm/ruoyi-vue-pro-dm8.sql import argparse import pathlib import re +import sys import time from abc import ABC, abstractmethod from typing import Dict, Generator, Optional, Tuple, Union @@ -293,8 +294,10 @@ class Convertor(ABC): # 将parse失败的脚本打印出来 if error_scripts: + print("!!! 以下内容无法正常解析", file=sys.stderr) for script in error_scripts: - print(script) + # print to stderr + print(script, file=sys.stderr) class PostgreSQLConvertor(Convertor): diff --git a/yudao-dependencies/pom.xml b/yudao-dependencies/pom.xml index 94c3e31aa8..668249863a 100644 --- a/yudao-dependencies/pom.xml +++ b/yudao-dependencies/pom.xml @@ -14,7 +14,7 @@ https://github.com/YunaiV/ruoyi-vue-pro - 2.6.0-SNAPSHOT + 2025.08-SNAPSHOT 1.6.0 3.4.5 @@ -24,9 +24,9 @@ 1.2.24 3.5.19 - 3.5.10.1 + 3.5.12 + 1.5.4 4.3.1 - 1.4.13 3.0.6 3.41.0 8.1.3.140 @@ -54,7 +54,7 @@ 1.6.3 5.8.35 6.0.0-M19 - 4.0.3 + 1.2.0 2.4.1 1.2.83 33.4.8-jre @@ -69,12 +69,11 @@ 0.9.0 4.5.13 - 2.17.0 - 1.27.1 2.30.14 1.16.7 1.4.0 - 1.9.4 + 2.0.0 + 1.9.5 4.7.5.B @@ -479,20 +478,11 @@ - com.alibaba - easyexcel - ${easyexcel.version} - - - commons-io - commons-io - ${commons-io.version} - - - org.apache.commons - commons-compress - ${commons-compress.version} + cn.idev.excel + fastexcel + ${fastexcel.version} + org.apache.tika tika-core @@ -603,7 +593,7 @@ org.jeecgframework.jimureport jimubi-spring-boot3-starter - ${jimureport.version} + ${jimubi.version} com.github.jsqlparser diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java index 12a6e17246..4d9168ebdb 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/cache/CacheUtils.java @@ -14,6 +14,13 @@ import java.util.concurrent.Executors; */ public class CacheUtils { + /** + * 异步刷新的 LoadingCache 最大缓存数量 + * + * @see 本地缓存 CacheUtils 工具类建议 + */ + private static final Integer CACHE_MAX_SIZE = 10000; + /** * 构建异步刷新的 LoadingCache 对象 * @@ -29,6 +36,7 @@ public class CacheUtils { */ public static LoadingCache buildAsyncReloadingCache(Duration duration, CacheLoader loader) { return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) // 只阻塞当前数据加载线程,其他线程返回旧值 .refreshAfterWrite(duration) // 通过 asyncReloading 实现全异步加载,包括 refreshAfterWrite 被阻塞的加载线程 @@ -43,7 +51,11 @@ public class CacheUtils { * @return LoadingCache 对象 */ public static LoadingCache buildCache(Duration duration, CacheLoader loader) { - return CacheBuilder.newBuilder().refreshAfterWrite(duration).build(loader); + return CacheBuilder.newBuilder() + .maximumSize(CACHE_MAX_SIZE) + // 只阻塞当前数据加载线程,其他线程返回旧值 + .refreshAfterWrite(duration) + .build(loader); } } diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java index b51a838c69..d6051e85fe 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/DateUtils.java @@ -74,30 +74,30 @@ public class DateUtils { * 创建指定时间 * * @param year 年 - * @param mouth 月 + * @param month 月 * @param day 日 * @return 指定时间 */ - public static Date buildTime(int year, int mouth, int day) { - return buildTime(year, mouth, day, 0, 0, 0); + public static Date buildTime(int year, int month, int day) { + return buildTime(year, month, day, 0, 0, 0); } /** * 创建指定时间 * * @param year 年 - * @param mouth 月 + * @param month 月 * @param day 日 * @param hour 小时 * @param minute 分钟 * @param second 秒 * @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) { Calendar calendar = Calendar.getInstance(); 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.HOUR_OF_DAY, hour); calendar.set(Calendar.MINUTE, minute); diff --git a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java index a2c5241e1d..4cbd4b6183 100644 --- a/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java +++ b/yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/date/LocalDateTimeUtils.java @@ -69,17 +69,17 @@ public class LocalDateTimeUtils { * 创建指定时间 * * @param year 年 - * @param mouth 月 + * @param month 月 * @param day 日 * @return 指定时间 */ - public static LocalDateTime buildTime(int year, int mouth, int day) { - return LocalDateTime.of(year, mouth, day, 0, 0, 0); + public static LocalDateTime buildTime(int year, int month, int day) { + return LocalDateTime.of(year, month, day, 0, 0, 0); } - public static LocalDateTime[] buildBetweenTime(int year1, int mouth1, int day1, - int year2, int mouth2, int day2) { - return new LocalDateTime[]{buildTime(year1, mouth1, day1), buildTime(year2, mouth2, day2)}; + public static LocalDateTime[] buildBetweenTime(int year1, int month1, int day1, + int year2, int month2, int day2) { + return new LocalDateTime[]{buildTime(year1, month1, day1), buildTime(year2, month2, day2)}; } /** diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java index ab5b3bc7ec..46b6ca7c59 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoDeptDataPermissionAutoConfiguration.java @@ -18,7 +18,7 @@ import java.util.List; */ @AutoConfiguration @ConditionalOnClass(LoginUser.class) -@ConditionalOnBean(value = {PermissionCommonApi.class, DeptDataPermissionRuleCustomizer.class}) +@ConditionalOnBean(value = {DeptDataPermissionRuleCustomizer.class}) public class YudaoDeptDataPermissionAutoConfiguration { @Bean diff --git a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml index 0413986a64..b1218183cb 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-excel/pom.xml @@ -42,8 +42,14 @@ - com.alibaba - easyexcel + cn.idev.excel + fastexcel + + + + jakarta.validation + jakarta.validation-api + provided @@ -51,11 +57,6 @@ guava - - org.apache.commons - commons-compress - - cn.iocoder.boot yudao-spring-boot-starter-biz-ip diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java index 9fc67bfe7f..2a2350f02b 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/core/DictFrameworkUtils.java @@ -76,4 +76,9 @@ public class DictFrameworkUtils { return dictData!= null ? dictData.getValue(): null; } + @SneakyThrows + public static List getDictDataValueList(String dictType) { + List dictDatas = GET_DICT_DATA_CACHE.get(dictType); + return convertList(dictDatas, DictDataRespDTO::getValue); + } } diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java new file mode 100644 index 0000000000..de7498775c --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDict.java @@ -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[] payload() default {}; + +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java new file mode 100644 index 0000000000..a7184b066a --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictCollectionValidator.java @@ -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> { + + 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 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; + } + +} + diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java new file mode 100644 index 0000000000..b67f017750 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/dict/validation/InDictValidator.java @@ -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 { + + 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 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; + } + +} + diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java index 9778b17aea..b5ca863173 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/AreaConvert.java @@ -3,11 +3,11 @@ package cn.iocoder.yudao.framework.excel.core.convert; import cn.hutool.core.convert.Convert; import cn.iocoder.yudao.framework.ip.core.Area; import cn.iocoder.yudao.framework.ip.core.utils.AreaUtils; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.ReadCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; /** diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java index e393195ed1..b9e0dcb735 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/DictConvert.java @@ -3,12 +3,12 @@ package cn.iocoder.yudao.framework.excel.core.convert; import cn.hutool.core.convert.Convert; import cn.iocoder.yudao.framework.dict.core.DictFrameworkUtils; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.ReadCellData; -import com.alibaba.excel.metadata.data.WriteCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.ReadCellData; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; import lombok.extern.slf4j.Slf4j; /** diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java index 0d4794e5fa..6958c32e0c 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/JsonConvert.java @@ -1,11 +1,11 @@ package cn.iocoder.yudao.framework.excel.core.convert; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.WriteCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; /** * Excel Json 转换器 diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java index ee66fe7dec..9ed0bd581f 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/convert/MoneyConvert.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.framework.excel.core.convert; -import com.alibaba.excel.converters.Converter; -import com.alibaba.excel.enums.CellDataTypeEnum; -import com.alibaba.excel.metadata.GlobalConfiguration; -import com.alibaba.excel.metadata.data.WriteCellData; -import com.alibaba.excel.metadata.property.ExcelContentProperty; +import cn.idev.excel.converters.Converter; +import cn.idev.excel.enums.CellDataTypeEnum; +import cn.idev.excel.metadata.GlobalConfiguration; +import cn.idev.excel.metadata.data.WriteCellData; +import cn.idev.excel.metadata.property.ExcelContentProperty; import java.math.BigDecimal; import java.math.RoundingMode; diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java new file mode 100644 index 0000000000..49a5b31572 --- /dev/null +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/ColumnWidthMatchStyleStrategy.java @@ -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 添加自适应列宽处理器,并替换默认列宽策略 + * @author hmb + */ +public class ColumnWidthMatchStyleStrategy extends AbstractColumnWidthStyleStrategy { + + private static final int MAX_COLUMN_WIDTH = 255; + + private final Map> cache = MapUtils.newHashMapWithExpectedSize(8); + + @Override + protected void setColumnWidth(WriteSheetHolder writeSheetHolder, List> cellDataList, Cell cell, + Head head, Integer relativeRowIndex, Boolean isHead) { + boolean needSetWidth = isHead || CollUtil.isNotEmpty(cellDataList); + if (!needSetWidth) { + return; + } + Map 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> 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; + } + } + +} \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java index 8e3e28eb44..c55e1210a2 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/handler/SelectSheetWriteHandler.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.framework.excel.core.handler; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; 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.excel.core.annotations.ExcelColumnSelect; import cn.iocoder.yudao.framework.excel.core.function.ExcelColumnSelectFunction; -import com.alibaba.excel.annotation.ExcelIgnore; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; -import com.alibaba.excel.write.handler.SheetWriteHandler; -import com.alibaba.excel.write.metadata.holder.WriteSheetHolder; -import com.alibaba.excel.write.metadata.holder.WriteWorkbookHolder; +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.write.handler.SheetWriteHandler; +import cn.idev.excel.write.metadata.holder.WriteSheetHolder; +import cn.idev.excel.write.metadata.holder.WriteWorkbookHolder; import lombok.extern.slf4j.Slf4j; import org.apache.poi.hssf.usermodel.HSSFDataValidation; import org.apache.poi.ss.usermodel.*; @@ -87,7 +86,7 @@ public class SelectSheetWriteHandler implements SheetWriteHandler { /** * 判断字段是否是静态的、最终的、 transient 的 - * 原因:EasyExcel 默认是忽略 static final 或 transient 的字段,所以需要判断 + * 原因:FastExcel 默认是忽略 static final 或 transient 的字段,所以需要判断 * * @param field 字段 * @return 是否是静态的、最终的、transient 的 @@ -185,4 +184,4 @@ public class SelectSheetWriteHandler implements SheetWriteHandler { writeSheetHolder.getSheet().addValidationData(validation); } -} \ No newline at end of file +} diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java index eb037d9e17..f05d3e51e5 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/core/util/ExcelUtils.java @@ -1,10 +1,10 @@ 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.excel.core.handler.ColumnWidthMatchStyleStrategy; 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 org.springframework.web.multipart.MultipartFile; @@ -32,9 +32,9 @@ public class ExcelUtils { public static void write(HttpServletResponse response, String filename, String sheetName, Class head, List data) throws IOException { // 输出 Excel - EasyExcel.write(response.getOutputStream(), head) + FastExcelFactory.write(response.getOutputStream(), head) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 - .registerWriteHandler(new LongestMatchColumnWidthStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 + .registerWriteHandler(new ColumnWidthMatchStyleStrategy()) // 基于 column 长度,自动适配。最大 255 宽度 .registerWriteHandler(new SelectSheetWriteHandler(head)) // 基于固定 sheet 实现下拉框 .registerConverter(new LongStringConverter()) // 避免 Long 类型丢失精度 .sheet(sheetName).doWrite(data); @@ -44,7 +44,7 @@ public class ExcelUtils { } public static List read(MultipartFile file, Class head) throws IOException { - return EasyExcel.read(file.getInputStream(), head, null) + return FastExcelFactory.read(file.getInputStream(), head, null) .autoCloseStream(false) // 不要自动关闭,交给 Servlet 自己处理 .doReadAllSync(); } diff --git a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java index 53bc5c01bf..72c3ac42e1 100644 --- a/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java +++ b/yudao-framework/yudao-spring-boot-starter-excel/src/main/java/cn/iocoder/yudao/framework/excel/package-info.java @@ -1,4 +1,4 @@ /** - * 基于 EasyExcel 实现 Excel 相关的操作 + * 基于 FastExcel 实现 Excel 相关的操作 */ package cn.iocoder.yudao.framework.excel; diff --git a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java index 6d517e5e40..4b08210971 100644 --- a/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-job/src/main/java/cn/iocoder/yudao/framework/quartz/config/YudaoAsyncAutoConfiguration.java @@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableAsync; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.core.task.SimpleAsyncTaskExecutor; /** * 异步任务 Configuration @@ -21,13 +22,20 @@ public class YudaoAsyncAutoConfiguration { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { - if (!(bean instanceof ThreadPoolTaskExecutor)) { - return bean; + // 处理 ThreadPoolTaskExecutor + if (bean instanceof ThreadPoolTaskExecutor) { + ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; } - // 修改提交的任务,接入 TransmittableThreadLocal - ThreadPoolTaskExecutor executor = (ThreadPoolTaskExecutor) bean; - executor.setTaskDecorator(TtlRunnable::get); - return executor; + // 处理 SimpleAsyncTaskExecutor + // 参考 https://t.zsxq.com/CBoks 增加 + if (bean instanceof SimpleAsyncTaskExecutor) { + SimpleAsyncTaskExecutor executor = (SimpleAsyncTaskExecutor) bean; + executor.setTaskDecorator(TtlRunnable::get); + return executor; + } + return bean; } }; diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml index ebd1210c03..6b6970fbef 100644 --- a/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-monitor/pom.xml @@ -44,29 +44,35 @@ io.opentracing opentracing-util + true org.apache.skywalking apm-toolkit-trace + true org.apache.skywalking apm-toolkit-logback-1.x + true org.apache.skywalking apm-toolkit-opentracing + true io.micrometer micrometer-registry-prometheus + true de.codecentric - spring-boot-admin-starter-client + spring-boot-admin-starter-client + true diff --git a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java index c7d9e2c0db..7876742620 100644 --- a/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-monitor/src/main/java/cn/iocoder/yudao/framework/tracer/config/YudaoTracerAutoConfiguration.java @@ -3,6 +3,9 @@ package cn.iocoder.yudao.framework.tracer.config; import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum; import cn.iocoder.yudao.framework.tracer.core.aop.BizTraceAspect; 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.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -16,30 +19,32 @@ import org.springframework.context.annotation.Bean; * @author mashu */ @AutoConfiguration -@ConditionalOnClass({BizTraceAspect.class}) +@ConditionalOnClass(name = { + "org.apache.skywalking.apm.toolkit.opentracing.SkywalkingTracer", + "io.opentracing.Tracer" +}) @EnableConfigurationProperties(TracerProperties.class) @ConditionalOnProperty(prefix = "yudao.tracer", value = "enable", matchIfMissing = true) public class YudaoTracerAutoConfiguration { - // TODO @芋艿:重要。目前 opentracing 版本存在冲突,要么保证 skywalking,要么保证阿里云短信 sdk -// @Bean -// public TracerProperties bizTracerProperties() { -// return new TracerProperties(); -// } -// -// @Bean -// public BizTraceAspect bizTracingAop() { -// return new BizTraceAspect(tracer()); -// } -// -// @Bean -// public Tracer tracer() { -// // 创建 SkywalkingTracer 对象 -// SkywalkingTracer tracer = new SkywalkingTracer(); -// // 设置为 GlobalTracer 的追踪器 -// GlobalTracer.register(tracer); -// return tracer; -// } + @Bean + public TracerProperties bizTracerProperties() { + return new TracerProperties(); + } + + @Bean + public BizTraceAspect bizTracingAop() { + return new BizTraceAspect(tracer()); + } + + @Bean + public Tracer tracer() { + // 创建 SkywalkingTracer 对象 + SkywalkingTracer tracer = new SkywalkingTracer(); + // 设置为 GlobalTracer 的追踪器 + GlobalTracer.registerIfAbsent(tracer); + return tracer; + } /** * 创建 TraceFilter 过滤器,响应 header 设置 traceId diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml index 5a619c5118..3272059d85 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/pom.xml @@ -24,8 +24,8 @@ cn.iocoder.boot - yudao-spring-boot-starter-web - provided + yudao-spring-boot-starter-security + provided diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java index 94dada129b..3c35999109 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/handler/DefaultDBFieldHandler.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.framework.mybatis.core.handler; 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 org.apache.ibatis.reflection.MetaObject; @@ -32,7 +32,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler { baseDO.setUpdateTime(current); } - Long userId = WebFrameworkUtils.getLoginUserId(); + Long userId = SecurityFrameworkUtils.getLoginUserId(); // 当前登录用户不为空,创建人为空,则当前登录用户为创建人 if (Objects.nonNull(userId) && Objects.isNull(baseDO.getCreator())) { baseDO.setCreator(userId.toString()); @@ -54,7 +54,7 @@ public class DefaultDBFieldHandler implements MetaObjectHandler { // 当前登录用户不为空,更新人为空,则当前登录用户为更新人 Object modifier = getFieldValByName("updater", metaObject); - Long userId = WebFrameworkUtils.getLoginUserId(); + Long userId = SecurityFrameworkUtils.getLoginUserId(); if (Objects.nonNull(userId) && Objects.isNull(modifier)) { setFieldValByName("updater", userId.toString(), metaObject); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java index d7ad5fad8f..52ca947cc5 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java @@ -42,6 +42,7 @@ public interface BaseMapperX extends MPJBaseMapper { default PageResult selectPage(PageParam pageParam, Collection sortingFields, @Param("ew") Wrapper queryWrapper) { // 特殊:不分页,直接查询全部 if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageSize())) { + MyBatisUtils.addOrder(queryWrapper, sortingFields); List list = selectList(queryWrapper); return new PageResult<>(list, (long) list.size()); } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java index 48e901d624..8b5a0fcfc8 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/query/MPJLambdaWrapperX.java @@ -118,7 +118,6 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { @Override public MPJLambdaWrapperX orderByDesc(SFunction column) { - //noinspection unchecked super.orderByDesc(true, column); return this; } @@ -208,7 +207,7 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { } @Override - public MPJLambdaWrapperX selectCount(SFunction column, String alias) { + public MPJLambdaWrapperX selectCount(SFunction column, String alias) { super.selectCount(column, alias); return this; } @@ -226,7 +225,7 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { } @Override - public MPJLambdaWrapperX selectSum(SFunction column, String alias) { + public MPJLambdaWrapperX selectSum(SFunction column, String alias) { super.selectSum(column, alias); return this; } @@ -244,7 +243,7 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { } @Override - public MPJLambdaWrapperX selectMax(SFunction column, String alias) { + public MPJLambdaWrapperX selectMax(SFunction column, String alias) { super.selectMax(column, alias); return this; } @@ -262,7 +261,7 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { } @Override - public MPJLambdaWrapperX selectMin(SFunction column, String alias) { + public MPJLambdaWrapperX selectMin(SFunction column, String alias) { super.selectMin(column, alias); return this; } @@ -280,7 +279,7 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { } @Override - public MPJLambdaWrapperX selectAvg(SFunction column, String alias) { + public MPJLambdaWrapperX selectAvg(SFunction column, String alias) { super.selectAvg(column, alias); return this; } @@ -298,7 +297,7 @@ public class MPJLambdaWrapperX extends MPJLambdaWrapper { } @Override - public MPJLambdaWrapperX selectLen(SFunction column, String alias) { + public MPJLambdaWrapperX selectLen(SFunction column, String alias) { super.selectLen(column, alias); return this; } diff --git a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java index 784f699e44..ac33ba8eff 100644 --- a/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/util/MyBatisUtils.java @@ -1,6 +1,6 @@ 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.LambdaUtil; 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.mybatis.core.enums.DbTypeEnum; 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.toolkit.StringPool; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; @@ -20,7 +22,6 @@ import net.sf.jsqlparser.schema.Table; import java.util.ArrayList; import java.util.Collection; import java.util.List; -import java.util.stream.Collectors; /** * MyBatis 工具类 @@ -37,15 +38,27 @@ public class MyBatisUtils { // 页码 + 数量 Page page = new Page<>(pageParam.getPageNo(), pageParam.getPageSize()); // 排序字段 - if (!CollectionUtil.isEmpty(sortingFields)) { - page.addOrder(sortingFields.stream().map(sortingField -> SortingField.ORDER_ASC.equals(sortingField.getOrder()) - ? OrderItem.asc(StrUtil.toUnderlineCase(sortingField.getField())) - : OrderItem.desc(StrUtil.toUnderlineCase(sortingField.getField()))) - .collect(Collectors.toList())); + if (CollUtil.isNotEmpty(sortingFields)) { + for (SortingField sortingField : sortingFields) { + page.addOrder(new OrderItem().setAsc(SortingField.ORDER_ASC.equals(sortingField.getOrder())) + .setColumn(StrUtil.toUnderlineCase(sortingField.getField()))); + } } return page; } + public static void addOrder(Wrapper wrapper, Collection sortingFields) { + if (CollUtil.isEmpty(sortingFields)) { + return; + } + QueryWrapper query = (QueryWrapper) wrapper; + for (SortingField sortingField : sortingFields) { + query.orderBy(true, + SortingField.ORDER_ASC.equals(sortingField.getOrder()), + StrUtil.toUnderlineCase(sortingField.getField())); + } + } + /** * 将拦截器添加到链中 * 由于 MybatisPlusInterceptor 不支持添加拦截器,所以只能全量设置 diff --git a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java index 98dc7afb8d..4f8024474b 100644 --- a/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java +++ b/yudao-framework/yudao-spring-boot-starter-security/src/main/java/cn/iocoder/yudao/framework/security/core/util/SecurityFrameworkUtils.java @@ -126,8 +126,10 @@ public class SecurityFrameworkUtils { // 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号; // 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息 - WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); - WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + if (request != null) { + WebFrameworkUtils.setLoginUserId(request, loginUser.getId()); + WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType()); + } } private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) { diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java index 9071998f91..49958fc096 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/filter/CacheRequestBodyFilter.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.framework.web.core.filter; +import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import org.springframework.web.filter.OncePerRequestFilter; @@ -16,6 +17,14 @@ import java.io.IOException; */ public class CacheRequestBodyFilter extends OncePerRequestFilter { + /** + * 需要排除的 URI + * + * 1. 排除 Spring Boot Admin 相关请求,避免客户端连接中断导致的异常。 + * 例如说:795 ISSUE + */ + private static final String[] IGNORE_URIS = {"/admin/", "/actuator/"}; + @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { @@ -24,7 +33,13 @@ public class CacheRequestBodyFilter extends OncePerRequestFilter { @Override 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); } diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java index 11ef9a8ef6..627a5ea784 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/web/core/handler/GlobalExceptionHandler.java @@ -6,6 +6,8 @@ import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; 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.util.ServiceExceptionUtil; 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.servlet.ServletUtils; 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 jakarta.servlet.http.HttpServletRequest; import jakarta.validation.ConstraintViolation; @@ -29,6 +29,7 @@ import org.springframework.util.Assert; import org.springframework.validation.BindException; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; +import org.springframework.web.HttpMediaTypeNotSupportedException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; @@ -101,6 +102,9 @@ public class GlobalExceptionHandler { if (ex instanceof HttpRequestMethodNotSupportedException) { return httpRequestMethodNotSupportedExceptionHandler((HttpRequestMethodNotSupportedException) ex); } + if (ex instanceof HttpMediaTypeNotSupportedException) { + return httpMediaTypeNotSupportedExceptionHandler((HttpMediaTypeNotSupportedException) ex); + } if (ex instanceof ServiceException) { return serviceExceptionHandler((ServiceException) ex); } @@ -171,17 +175,19 @@ public class GlobalExceptionHandler { /** * 处理 SpringMVC 请求参数类型错误 * - * 例如说,接口上设置了 @RequestBody实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String + * 例如说,接口上设置了 @RequestBody 实体中 xx 属性类型为 Integer,结果传递 xx 参数类型为 String */ @ExceptionHandler(HttpMessageNotReadableException.class) public CommonResult methodArgumentTypeInvalidFormatExceptionHandler(HttpMessageNotReadableException ex) { log.warn("[methodArgumentTypeInvalidFormatExceptionHandler]", ex); - if(ex.getCause() instanceof InvalidFormatException) { + if (ex.getCause() instanceof InvalidFormatException) { InvalidFormatException invalidFormatException = (InvalidFormatException) ex.getCause(); 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())); } + /** + * 处理 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 权限不足的异常 * diff --git a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java index 99b6a448f3..16f87dae1f 100644 --- a/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/xss/config/YudaoXssAutoConfiguration.java @@ -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.filter.XssFilter; 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.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -42,13 +41,13 @@ public class YudaoXssAutoConfiguration implements WebMvcConfigurer { */ @Bean @ConditionalOnMissingBean(name = "xssJacksonCustomizer") - @ConditionalOnBean(ObjectMapper.class) @ConditionalOnProperty(value = "yudao.xss.enable", havingValue = "true") public Jackson2ObjectMapperBuilderCustomizer xssJacksonCustomizer(XssProperties properties, PathMatcher pathMatcher, XssCleaner xssCleaner) { // 在反序列化时进行 xss 过滤,可以替换使用 XssStringJsonSerializer,在序列化时进行处理 - return builder -> builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); + return builder -> + builder.deserializerByType(String.class, new XssStringJsonDeserializer(properties, pathMatcher, xssCleaner)); } /** diff --git a/yudao-module-ai/pom.xml b/yudao-module-ai/pom.xml index 4b02a7e00f..4a2323bcfa 100644 --- a/yudao-module-ai/pom.xml +++ b/yudao-module-ai/pom.xml @@ -19,7 +19,8 @@ 国外:OpenAI、Ollama、Midjourney、StableDiffusion、Suno - 1.0.0-M6 + 1.0.0 + 1.0.0.2 1.0.2 @@ -75,65 +76,73 @@ org.springframework.ai - spring-ai-openai-spring-boot-starter + spring-ai-starter-model-openai ${spring-ai.version} org.springframework.ai - spring-ai-azure-openai-spring-boot-starter + spring-ai-starter-model-azure-openai ${spring-ai.version} org.springframework.ai - spring-ai-ollama-spring-boot-starter + spring-ai-starter-model-deepseek ${spring-ai.version} org.springframework.ai - spring-ai-stability-ai-spring-boot-starter + spring-ai-starter-model-ollama ${spring-ai.version} - - com.alibaba.cloud.ai - spring-ai-alibaba-starter - ${spring-ai.version}.1 - - - org.springframework.ai - spring-ai-qianfan-spring-boot-starter + spring-ai-starter-model-stability-ai ${spring-ai.version} org.springframework.ai - spring-ai-zhipuai-spring-boot-starter + spring-ai-starter-model-zhipuai ${spring-ai.version} org.springframework.ai - spring-ai-minimax-spring-boot-starter + spring-ai-starter-model-minimax ${spring-ai.version} + - org.springframework.ai - spring-ai-moonshot-spring-boot-starter - ${spring-ai.version} + + com.alibaba.cloud.ai + spring-ai-alibaba-starter-dashscope + ${alibaba-ai.version} + + + + + org.springaicommunity + qianfan-spring-boot-starter + 1.0.0 + + + + org.springaicommunity + moonshot-spring-boot-starter + 1.0.0 org.springframework.ai - spring-ai-qdrant-store + spring-ai-starter-vector-store-qdrant ${spring-ai.version} org.springframework.ai - spring-ai-redis-store + spring-ai-starter-vector-store-redis ${spring-ai.version} @@ -144,7 +153,7 @@ org.springframework.ai - spring-ai-milvus-store + spring-ai-starter-vector-store-milvus ${spring-ai.version} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java index 5142cde443..ddd426d283 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/chat/AiChatConversationController.java @@ -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.service.chat.AiChatConversationService; 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.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -54,6 +55,7 @@ public class AiChatConversationController { @GetMapping("/my-list") @Operation(summary = "获得【我的】聊天对话列表") + @TransMethodResult public CommonResult> getChatConversationMyList() { List list = chatConversationService.getChatConversationListByUserId(getLoginUserId()); return success(BeanUtils.toBean(list, AiChatConversationRespVO.class)); @@ -62,6 +64,7 @@ public class AiChatConversationController { @GetMapping("/get-my") @Operation(summary = "获得【我的】聊天对话") @Parameter(name = "id", required = true, description = "对话编号", example = "1024") + @TransMethodResult public CommonResult getChatConversationMy(@RequestParam("id") Long id) { AiChatConversationDO conversation = chatConversationService.getChatConversation(id); if (conversation != null && ObjUtil.notEqual(conversation.getUserId(), getLoginUserId())) { @@ -90,6 +93,7 @@ public class AiChatConversationController { @GetMapping("/page") @Operation(summary = "获得对话分页", description = "用于【对话管理】菜单") @PreAuthorize("@ss.hasPermission('ai:chat-conversation:query')") + @TransMethodResult public CommonResult> getChatConversationPage(AiChatConversationPageReqVO pageReqVO) { PageResult pageResult = chatConversationService.getChatConversationPage(pageReqVO); if (CollUtil.isEmpty(pageResult.getList())) { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java index 5714c5fedd..804e211527 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/controller/admin/model/AiChatRoleController.java @@ -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.dal.dataobject.model.AiChatRoleDO; 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.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -35,6 +36,7 @@ public class AiChatRoleController { @GetMapping("/my-page") @Operation(summary = "获得【我的】聊天角色分页") + @TransMethodResult public CommonResult> getChatRoleMyPage(@Valid AiChatRolePageReqVO pageReqVO) { PageResult pageResult = chatRoleService.getChatRoleMyPage(pageReqVO, getLoginUserId()); return success(BeanUtils.toBean(pageResult, AiChatRoleRespVO.class)); @@ -43,6 +45,7 @@ public class AiChatRoleController { @GetMapping("/get-my") @Operation(summary = "获得【我的】聊天角色") @Parameter(name = "id", description = "编号", required = true, example = "1024") + @TransMethodResult public CommonResult getChatRoleMy(@RequestParam("id") Long id) { AiChatRoleDO chatRole = chatRoleService.getChatRole(id); if (ObjUtil.notEqual(chatRole.getUserId(), getLoginUserId())) { @@ -108,6 +111,7 @@ public class AiChatRoleController { @Operation(summary = "获得聊天角色") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('ai:chat-role:query')") + @TransMethodResult public CommonResult getChatRole(@RequestParam("id") Long id) { AiChatRoleDO chatRole = chatRoleService.getChatRole(id); return success(BeanUtils.toBean(chatRole, AiChatRoleRespVO.class)); diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java index a28d726b90..4ff7c9e4dc 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/AiAutoConfiguration.java @@ -5,7 +5,6 @@ import cn.hutool.extra.spring.SpringUtil; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactory; import cn.iocoder.yudao.module.ai.framework.ai.core.AiModelFactoryImpl; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -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.xinghuo.XingHuoChatModel; import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.TokenCountBatchingStrategy; import org.springframework.ai.model.tool.ToolCallingManager; @@ -26,6 +21,10 @@ import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.tokenizer.JTokkitTokenCountEstimator; import org.springframework.ai.tokenizer.TokenCountEstimator; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; +import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; @@ -52,33 +51,6 @@ public class AiAutoConfiguration { // ========== 各种 AI Client 创建 ========== - @Bean - @ConditionalOnProperty(value = "yudao.ai.deepseek.enable", havingValue = "true") - public DeepSeekChatModel deepSeekChatModel(YudaoAiProperties yudaoAiProperties) { - YudaoAiProperties.DeepSeekProperties properties = yudaoAiProperties.getDeepseek(); - return buildDeepSeekChatModel(properties); - } - - public DeepSeekChatModel buildDeepSeekChatModel(YudaoAiProperties.DeepSeekProperties properties) { - if (StrUtil.isEmpty(properties.getModel())) { - properties.setModel(DeepSeekChatModel.MODEL_DEFAULT); - } - OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() - .baseUrl(DeepSeekChatModel.BASE_URL) - .apiKey(properties.getApiKey()) - .build()) - .defaultOptions(OpenAiChatOptions.builder() - .model(properties.getModel()) - .temperature(properties.getTemperature()) - .maxTokens(properties.getMaxTokens()) - .topP(properties.getTopP()) - .build()) - .toolCallingManager(getToolCallingManager()) - .build(); - return new DeepSeekChatModel(openAiChatModel); - } - @Bean @ConditionalOnProperty(value = "yudao.ai.doubao.enable", havingValue = "true") public DouBaoChatModel douBaoChatClient(YudaoAiProperties yudaoAiProperties) { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java index 7f8046768a..7c26aa89ca 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/config/YudaoAiProperties.java @@ -13,12 +13,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties; @Data public class YudaoAiProperties { - /** - * DeepSeek - */ - @SuppressWarnings("SpellCheckingInspection") - private DeepSeekProperties deepseek; - /** * 字节豆包 */ @@ -60,19 +54,6 @@ public class YudaoAiProperties { @SuppressWarnings("SpellCheckingInspection") private SunoProperties suno; - @Data - public static class DeepSeekProperties { - - private String enable; - private String apiKey; - - private String model; - private Double temperature; - private Integer maxTokens; - private Double topP; - - } - @Data public static class DouBaoProperties { diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java index f258ffaf1b..f7b42e30ae 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/AiModelFactoryImpl.java @@ -8,11 +8,11 @@ import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.RuntimeUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; +import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.framework.ai.config.AiAutoConfiguration; import cn.iocoder.yudao.module.ai.framework.ai.config.YudaoAiProperties; -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.doubao.DouBaoChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.hunyuan.HunYuanChatModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; @@ -22,8 +22,9 @@ import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlo import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageModel; import cn.iocoder.yudao.module.ai.framework.ai.core.model.suno.api.SunoApi; import cn.iocoder.yudao.module.ai.framework.ai.core.model.xinghuo.XingHuoChatModel; -import cn.iocoder.yudao.framework.common.util.spring.SpringUtils; -import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeChatAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeEmbeddingAutoConfiguration; +import com.alibaba.cloud.ai.autoconfigure.dashscope.DashScopeImageAutoConfiguration; import com.alibaba.cloud.ai.dashscope.api.DashScopeApi; import com.alibaba.cloud.ai.dashscope.api.DashScopeImageApi; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel; @@ -32,47 +33,55 @@ import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingModel; import com.alibaba.cloud.ai.dashscope.embedding.DashScopeEmbeddingOptions; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageModel; import com.azure.ai.openai.OpenAIClientBuilder; +import com.azure.core.credential.KeyCredential; import io.micrometer.observation.ObservationRegistry; import io.milvus.client.MilvusServiceClient; import io.qdrant.client.QdrantClient; import io.qdrant.client.QdrantGrpcClient; import lombok.SneakyThrows; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiConnectionProperties; -import org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiEmbeddingProperties; -import org.springframework.ai.autoconfigure.minimax.MiniMaxAutoConfiguration; -import org.springframework.ai.autoconfigure.moonshot.MoonshotAutoConfiguration; -import org.springframework.ai.autoconfigure.ollama.OllamaAutoConfiguration; -import org.springframework.ai.autoconfigure.openai.OpenAiAutoConfiguration; -import org.springframework.ai.autoconfigure.qianfan.QianFanAutoConfiguration; -import org.springframework.ai.autoconfigure.stabilityai.StabilityAiImageAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientConnectionDetails; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusServiceClientProperties; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreProperties; -import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreAutoConfiguration; -import org.springframework.ai.autoconfigure.vectorstore.redis.RedisVectorStoreProperties; -import org.springframework.ai.autoconfigure.zhipuai.ZhiPuAiAutoConfiguration; +import org.springaicommunity.moonshot.MoonshotChatModel; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.moonshot.api.MoonshotApi; +import org.springaicommunity.moonshot.autoconfigure.MoonshotChatAutoConfiguration; +import org.springaicommunity.qianfan.QianFanChatModel; +import org.springaicommunity.qianfan.QianFanEmbeddingModel; +import org.springaicommunity.qianfan.QianFanEmbeddingOptions; +import org.springaicommunity.qianfan.QianFanImageModel; +import org.springaicommunity.qianfan.api.QianFanApi; +import org.springaicommunity.qianfan.api.QianFanImageApi; +import org.springaicommunity.qianfan.autoconfigure.QianFanChatAutoConfiguration; +import org.springaicommunity.qianfan.autoconfigure.QianFanEmbeddingAutoConfiguration; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; import org.springframework.ai.azure.openai.AzureOpenAiEmbeddingModel; import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import org.springframework.ai.document.MetadataMode; import org.springframework.ai.embedding.BatchingStrategy; import org.springframework.ai.embedding.EmbeddingModel; +import org.springframework.ai.embedding.observation.EmbeddingModelObservationConvention; import org.springframework.ai.image.ImageModel; import org.springframework.ai.minimax.MiniMaxChatModel; import org.springframework.ai.minimax.MiniMaxChatOptions; import org.springframework.ai.minimax.MiniMaxEmbeddingModel; import org.springframework.ai.minimax.MiniMaxEmbeddingOptions; import org.springframework.ai.minimax.api.MiniMaxApi; -import org.springframework.ai.model.function.FunctionCallbackResolver; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatAutoConfiguration; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiEmbeddingProperties; +import org.springframework.ai.model.deepseek.autoconfigure.DeepSeekChatAutoConfiguration; +import org.springframework.ai.model.minimax.autoconfigure.MiniMaxChatAutoConfiguration; +import org.springframework.ai.model.minimax.autoconfigure.MiniMaxEmbeddingAutoConfiguration; +import org.springframework.ai.model.ollama.autoconfigure.OllamaChatAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiChatAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.openai.autoconfigure.OpenAiImageAutoConfiguration; +import org.springframework.ai.model.stabilityai.autoconfigure.StabilityAiImageAutoConfiguration; import org.springframework.ai.model.tool.ToolCallingManager; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiChatAutoConfiguration; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiEmbeddingAutoConfiguration; +import org.springframework.ai.model.zhipuai.autoconfigure.ZhiPuAiImageAutoConfiguration; import org.springframework.ai.ollama.OllamaChatModel; import org.springframework.ai.ollama.OllamaEmbeddingModel; import org.springframework.ai.ollama.api.OllamaApi; @@ -84,21 +93,23 @@ import org.springframework.ai.openai.OpenAiImageModel; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.ai.openai.api.OpenAiImageApi; import org.springframework.ai.openai.api.common.OpenAiApiConstants; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanEmbeddingModel; -import org.springframework.ai.qianfan.QianFanEmbeddingOptions; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.ai.qianfan.api.QianFanApi; -import org.springframework.ai.qianfan.api.QianFanImageApi; import org.springframework.ai.stabilityai.StabilityAiImageModel; import org.springframework.ai.stabilityai.api.StabilityAiApi; import org.springframework.ai.vectorstore.SimpleVectorStore; import org.springframework.ai.vectorstore.VectorStore; import org.springframework.ai.vectorstore.milvus.MilvusVectorStore; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientConnectionDetails; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusServiceClientProperties; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreProperties; import org.springframework.ai.vectorstore.observation.DefaultVectorStoreObservationConvention; import org.springframework.ai.vectorstore.observation.VectorStoreObservationConvention; import org.springframework.ai.vectorstore.qdrant.QdrantVectorStore; +import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreProperties; import org.springframework.ai.vectorstore.redis.RedisVectorStore; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreAutoConfiguration; +import org.springframework.ai.vectorstore.redis.autoconfigure.RedisVectorStoreProperties; import org.springframework.ai.zhipuai.*; import org.springframework.ai.zhipuai.api.ZhiPuAiApi; import org.springframework.ai.zhipuai.api.ZhiPuAiImageApi; @@ -190,7 +201,7 @@ public class AiModelFactoryImpl implements AiModelFactory { case XING_HUO: return SpringUtil.getBean(XingHuoChatModel.class); case BAI_CHUAN: - return SpringUtil.getBean(AzureOpenAiChatModel.class); + return SpringUtil.getBean(BaiChuanChatModel.class); case OPENAI: return SpringUtil.getBean(OpenAiChatModel.class); case AZURE_OPENAI: @@ -319,27 +330,34 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 spring-ai 客户端的方法 ========== /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeChatModel 方法 + * 可参考 {@link DashScopeChatAutoConfiguration} 的 dashscopeChatModel 方法 */ private static DashScopeChatModel buildTongYiChatModel(String key) { - DashScopeApi dashScopeApi = new DashScopeApi(key); + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(key).build(); DashScopeChatOptions options = DashScopeChatOptions.builder().withModel(DashScopeApi.DEFAULT_CHAT_MODEL) .withTemperature(0.7).build(); - return new DashScopeChatModel(dashScopeApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return DashScopeChatModel.builder() + .dashScopeApi(dashScopeApi) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashScopeImageModel 方法 + * 可参考 {@link DashScopeImageAutoConfiguration} 的 dashScopeImageModel 方法 */ private static DashScopeImageModel buildTongYiImagesModel(String key) { DashScopeImageApi dashScopeImageApi = new DashScopeImageApi(key); - return new DashScopeImageModel(dashScopeImageApi); + return DashScopeImageModel.builder() + .dashScopeApi(dashScopeImageApi) + .build(); } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanChatModel 方法 + * 可参考 {@link QianFanChatAutoConfiguration} 的 qianFanChatModel 方法 */ private static QianFanChatModel buildYiYanChatModel(String key) { + // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -349,9 +367,10 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanImageModel 方法 + * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanImageModel 方法 */ private QianFanImageModel buildQianFanImageModel(String key) { + // TODO spring ai qianfan 有 bug,无法使用 https://github.com/spring-ai-community/qianfan/issues/6 List keys = StrUtil.split(key, '|'); Assert.equals(keys.size(), 2, "YiYanChatClient 的密钥需要 (appKey|secretKey) 格式"); String appKey = keys.get(0); @@ -361,12 +380,17 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link AiAutoConfiguration#deepSeekChatModel(YudaoAiProperties)} + * 可参考 {@link DeepSeekChatAutoConfiguration} 的 deepSeekChatModel 方法 */ private static DeepSeekChatModel buildDeepSeekChatModel(String apiKey) { - YudaoAiProperties.DeepSeekProperties properties = new YudaoAiProperties.DeepSeekProperties() - .setApiKey(apiKey); - return new AiAutoConfiguration().buildDeepSeekChatModel(properties); + DeepSeekApi deepSeekApi = DeepSeekApi.builder().apiKey(apiKey).build(); + DeepSeekChatOptions options = DeepSeekChatOptions.builder().model(DeepSeekApi.DEFAULT_CHAT_MODEL) + .temperature(0.7).build(); + return DeepSeekChatModel.builder() + .deepSeekApi(deepSeekApi) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -397,17 +421,18 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiChatModel 方法 + * 可参考 {@link ZhiPuAiChatAutoConfiguration} 的 zhiPuAiChatModel 方法 */ private ZhiPuAiChatModel buildZhiPuChatModel(String apiKey, String url) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) : new ZhiPuAiApi(url, apiKey); ZhiPuAiChatOptions options = ZhiPuAiChatOptions.builder().model(ZhiPuAiApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new ZhiPuAiChatModel(zhiPuAiApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return new ZhiPuAiChatModel(zhiPuAiApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE, + getObservationRegistry().getIfAvailable()); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiImageModel 方法 + * 可参考 {@link ZhiPuAiImageAutoConfiguration} 的 zhiPuAiImageModel 方法 */ private ZhiPuAiImageModel buildZhiPuAiImageModel(String apiKey, String url) { ZhiPuAiImageApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiImageApi(apiKey) @@ -416,23 +441,30 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxChatModel 方法 + * 可参考 {@link MiniMaxChatAutoConfiguration} 的 miniMaxChatModel 方法 */ private MiniMaxChatModel buildMiniMaxChatModel(String apiKey, String url) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url) ? new MiniMaxApi(apiKey) : new MiniMaxApi(url, apiKey); MiniMaxChatOptions options = MiniMaxChatOptions.builder().model(MiniMaxApi.DEFAULT_CHAT_MODEL).temperature(0.7).build(); - return new MiniMaxChatModel(miniMaxApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return new MiniMaxChatModel(miniMaxApi, options, getToolCallingManager(), DEFAULT_RETRY_TEMPLATE); } /** - * 可参考 {@link MoonshotAutoConfiguration} 的 moonshotChatModel 方法 + * 可参考 {@link MoonshotChatAutoConfiguration} 的 moonshotChatModel 方法 */ private MoonshotChatModel buildMoonshotChatModel(String apiKey, String url) { - MoonshotApi moonshotApi = StrUtil.isEmpty(url)? new MoonshotApi(apiKey) - : new MoonshotApi(url, apiKey); + MoonshotApi.Builder moonshotApiBuilder = MoonshotApi.builder() + .apiKey(apiKey); + if (StrUtil.isNotEmpty(url)) { + moonshotApiBuilder.baseUrl(url); + } MoonshotChatOptions options = MoonshotChatOptions.builder().model(MoonshotApi.DEFAULT_CHAT_MODEL).build(); - return new MoonshotChatModel(moonshotApi, options, getFunctionCallbackResolver(), DEFAULT_RETRY_TEMPLATE); + return MoonshotChatModel.builder() + .moonshotApi(moonshotApiBuilder.build()) + .defaultOptions(options) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -456,33 +488,32 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiChatModel 方法 + * 可参考 {@link OpenAiChatAutoConfiguration} 的 openAiChatModel 方法 */ private static OpenAiChatModel buildOpenAiChatModel(String openAiToken, String url) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(url).apiKey(openAiToken).build(); - return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build(); + return OpenAiChatModel.builder() + .openAiApi(openAiApi) + .toolCallingManager(getToolCallingManager()) + .build(); } - // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** - * 可参考 {@link AzureOpenAiAutoConfiguration} + * 可参考 {@link AzureOpenAiChatAutoConfiguration} */ private static AzureOpenAiChatModel buildAzureOpenAiChatModel(String apiKey, String url) { - AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); - // 创建 OpenAIClient 对象 - AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - connectionProperties.setEndpoint(url); - OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); - // 获取 AzureOpenAiChatProperties 对象 - AzureOpenAiChatProperties chatProperties = SpringUtil.getBean(AzureOpenAiChatProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiChatModel(openAIClient, chatProperties, - getToolCallingManager(), null, null); + // TODO @芋艿:使用前,请测试,暂时没密钥!!! + OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() + .endpoint(url).credential(new KeyCredential(apiKey)); + return AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAIClientBuilder) + .toolCallingManager(getToolCallingManager()) + .build(); } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiImageModel 方法 + * 可参考 {@link OpenAiImageAutoConfiguration} 的 openAiImageModel 方法 */ private OpenAiImageModel buildOpenAiImageModel(String openAiToken, String url) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); @@ -500,11 +531,14 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link OllamaAutoConfiguration} 的 ollamaApi 方法 + * 可参考 {@link OllamaChatAutoConfiguration} 的 ollamaChatModel 方法 */ private static OllamaChatModel buildOllamaChatModel(String url) { - OllamaApi ollamaApi = new OllamaApi(url); - return OllamaChatModel.builder().ollamaApi(ollamaApi).toolCallingManager(getToolCallingManager()).build(); + OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); + return OllamaChatModel.builder() + .ollamaApi(ollamaApi) + .toolCallingManager(getToolCallingManager()) + .build(); } /** @@ -519,16 +553,16 @@ public class AiModelFactoryImpl implements AiModelFactory { // ========== 各种创建 EmbeddingModel 的方法 ========== /** - * 可参考 {@link DashScopeAutoConfiguration} 的 dashscopeEmbeddingModel 方法 + * 可参考 {@link DashScopeEmbeddingAutoConfiguration} 的 dashscopeEmbeddingModel 方法 */ private DashScopeEmbeddingModel buildTongYiEmbeddingModel(String apiKey, String model) { - DashScopeApi dashScopeApi = new DashScopeApi(apiKey); + DashScopeApi dashScopeApi = DashScopeApi.builder().apiKey(apiKey).build(); DashScopeEmbeddingOptions dashScopeEmbeddingOptions = DashScopeEmbeddingOptions.builder().withModel(model).build(); return new DashScopeEmbeddingModel(dashScopeApi, MetadataMode.EMBED, dashScopeEmbeddingOptions); } /** - * 可参考 {@link ZhiPuAiAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 + * 可参考 {@link ZhiPuAiEmbeddingAutoConfiguration} 的 zhiPuAiEmbeddingModel 方法 */ private ZhiPuAiEmbeddingModel buildZhiPuEmbeddingModel(String apiKey, String url, String model) { ZhiPuAiApi zhiPuAiApi = StrUtil.isEmpty(url) ? new ZhiPuAiApi(apiKey) @@ -538,7 +572,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link MiniMaxAutoConfiguration} 的 miniMaxEmbeddingModel 方法 + * 可参考 {@link MiniMaxEmbeddingAutoConfiguration} 的 miniMaxEmbeddingModel 方法 */ private EmbeddingModel buildMiniMaxEmbeddingModel(String apiKey, String url, String model) { MiniMaxApi miniMaxApi = StrUtil.isEmpty(url)? new MiniMaxApi(apiKey) @@ -548,7 +582,7 @@ public class AiModelFactoryImpl implements AiModelFactory { } /** - * 可参考 {@link QianFanAutoConfiguration} 的 qianFanEmbeddingModel 方法 + * 可参考 {@link QianFanEmbeddingAutoConfiguration} 的 qianFanEmbeddingModel 方法 */ private QianFanEmbeddingModel buildYiYanEmbeddingModel(String key, String model) { List keys = StrUtil.split(key, '|'); @@ -561,13 +595,16 @@ public class AiModelFactoryImpl implements AiModelFactory { } private OllamaEmbeddingModel buildOllamaEmbeddingModel(String url, String model) { - OllamaApi ollamaApi = new OllamaApi(url); + OllamaApi ollamaApi = OllamaApi.builder().baseUrl(url).build(); OllamaOptions ollamaOptions = OllamaOptions.builder().model(model).build(); - return OllamaEmbeddingModel.builder().ollamaApi(ollamaApi).defaultOptions(ollamaOptions).build(); + return OllamaEmbeddingModel.builder() + .ollamaApi(ollamaApi) + .defaultOptions(ollamaOptions) + .build(); } /** - * 可参考 {@link OpenAiAutoConfiguration} 的 openAiEmbeddingModel 方法 + * 可参考 {@link OpenAiEmbeddingAutoConfiguration} 的 openAiEmbeddingModel 方法 */ private OpenAiEmbeddingModel buildOpenAiEmbeddingModel(String openAiToken, String url, String model) { url = StrUtil.blankToDefault(url, OpenAiApiConstants.DEFAULT_BASE_URL); @@ -576,21 +613,19 @@ public class AiModelFactoryImpl implements AiModelFactory { return new OpenAiEmbeddingModel(openAiApi, MetadataMode.EMBED, openAiEmbeddingProperties); } - // TODO @芋艿:手头暂时没密钥,使用建议再测试下 /** - * 可参考 {@link AzureOpenAiAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 + * 可参考 {@link AzureOpenAiEmbeddingAutoConfiguration} 的 azureOpenAiEmbeddingModel 方法 */ private AzureOpenAiEmbeddingModel buildAzureOpenAiEmbeddingModel(String apiKey, String url, String model) { - AzureOpenAiAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiAutoConfiguration(); - // 创建 OpenAIClient 对象 - AzureOpenAiConnectionProperties connectionProperties = new AzureOpenAiConnectionProperties(); - connectionProperties.setApiKey(apiKey); - connectionProperties.setEndpoint(url); - OpenAIClientBuilder openAIClient = azureOpenAiAutoConfiguration.openAIClientBuilder(connectionProperties, null); + // TODO @芋艿:手头暂时没密钥,使用建议再测试下 + AzureOpenAiEmbeddingAutoConfiguration azureOpenAiAutoConfiguration = new AzureOpenAiEmbeddingAutoConfiguration(); + // 创建 OpenAIClientBuilder 对象 + OpenAIClientBuilder openAIClientBuilder = new OpenAIClientBuilder() + .endpoint(url).credential(new KeyCredential(apiKey)); // 获取 AzureOpenAiChatProperties 对象 AzureOpenAiEmbeddingProperties embeddingProperties = SpringUtil.getBean(AzureOpenAiEmbeddingProperties.class); - return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClient, embeddingProperties, - null, null); + return azureOpenAiAutoConfiguration.azureOpenAiEmbeddingModel(openAIClientBuilder, embeddingProperties, + getObservationRegistry(), getEmbeddingModelObservationConvention()); } // ========== 各种创建 VectorStore 的方法 ========== @@ -655,12 +690,12 @@ public class AiModelFactoryImpl implements AiModelFactory { Map> metadataFields) { // 创建 JedisPooled 对象 RedisProperties redisProperties = SpringUtils.getBean(RedisProperties.class); - JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort()); + JedisPooled jedisPooled = new JedisPooled(redisProperties.getHost(), redisProperties.getPort(), + redisProperties.getUsername(), redisProperties.getPassword()); // 创建 RedisVectorStoreProperties 对象 - RedisVectorStoreAutoConfiguration configuration = new RedisVectorStoreAutoConfiguration(); RedisVectorStoreProperties properties = SpringUtil.getBean(RedisVectorStoreProperties.class); RedisVectorStore redisVectorStore = RedisVectorStore.builder(jedisPooled, embeddingModel) - .indexName(properties.getIndex()).prefix(properties.getPrefix()) + .indexName(properties.getIndexName()).prefix(properties.getPrefix()) .initializeSchema(properties.isInitializeSchema()) .metadataFields(convertList(metadataFields.entrySet(), entry -> { String fieldName = entry.getKey(); @@ -730,10 +765,12 @@ public class AiModelFactoryImpl implements AiModelFactory { private static ObjectProvider getCustomObservationConvention() { return new ObjectProvider<>() { + @Override public VectorStoreObservationConvention getObject() throws BeansException { return new DefaultVectorStoreObservationConvention(); } + }; } @@ -745,8 +782,15 @@ public class AiModelFactoryImpl implements AiModelFactory { return SpringUtil.getBean(ToolCallingManager.class); } - private static FunctionCallbackResolver getFunctionCallbackResolver() { - return SpringUtil.getBean(FunctionCallbackResolver.class); + private static ObjectProvider getEmbeddingModelObservationConvention() { + return new ObjectProvider<>() { + + @Override + public EmbeddingModelObservationConvention getObject() throws BeansException { + return SpringUtil.getBean(EmbeddingModelObservationConvention.class); + } + + }; } } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java deleted file mode 100644 index d603abf6b0..0000000000 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/deepseek/DeepSeekChatModel.java +++ /dev/null @@ -1,45 +0,0 @@ -package cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.ai.chat.model.ChatModel; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.ChatOptions; -import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import reactor.core.publisher.Flux; - -/** - * DeepSeek {@link ChatModel} 实现类 - * - * @author fansili - */ -@Slf4j -@RequiredArgsConstructor -public class DeepSeekChatModel implements ChatModel { - - public static final String BASE_URL = "https://api.deepseek.com"; - - public static final String MODEL_DEFAULT = "deepseek-chat"; - - /** - * 兼容 OpenAI 接口,进行复用 - */ - private final OpenAiChatModel openAiChatModel; - - @Override - public ChatResponse call(Prompt prompt) { - return openAiChatModel.call(prompt); - } - - @Override - public Flux stream(Prompt prompt) { - return openAiChatModel.stream(prompt); - } - - @Override - public ChatOptions getDefaultOptions() { - return openAiChatModel.getDefaultOptions(); - } - -} diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java index 43f8ad2168..44a652309e 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/siliconflow/SiliconFlowImageModel.java @@ -89,7 +89,7 @@ public class SiliconFlowImageModel implements ImageModel { var observationContext = ImageModelObservationContext.builder() .imagePrompt(imagePrompt) .provider(SiliconFlowApiConstants.PROVIDER_NAME) - .requestOptions(imagePrompt.getOptions()) + .imagePrompt(imagePrompt) .build(); return ImageModelObservationDocumentation.IMAGE_MODEL_OPERATION diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java index 671098a704..79214a0325 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/image/AiImageServiceImpl.java @@ -9,9 +9,6 @@ import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.http.HttpUtil; -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.ai.controller.admin.image.vo.AiImageDrawReqVO; @@ -24,17 +21,20 @@ import cn.iocoder.yudao.module.ai.dal.dataobject.image.AiImageDO; import cn.iocoder.yudao.module.ai.dal.dataobject.model.AiModelDO; import cn.iocoder.yudao.module.ai.dal.mysql.image.AiImageMapper; import cn.iocoder.yudao.module.ai.enums.image.AiImageStatusEnum; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.midjourney.api.MidjourneyApi; +import cn.iocoder.yudao.module.ai.framework.ai.core.model.siliconflow.SiliconFlowImageOptions; import cn.iocoder.yudao.module.ai.service.model.AiModelService; import cn.iocoder.yudao.module.infra.api.file.FileApi; import com.alibaba.cloud.ai.dashscope.image.DashScopeImageOptions; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; +import org.springaicommunity.qianfan.QianFanImageOptions; import org.springframework.ai.image.ImageModel; import org.springframework.ai.image.ImageOptions; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; import org.springframework.ai.openai.OpenAiImageOptions; -import org.springframework.ai.qianfan.QianFanImageOptions; import org.springframework.ai.stabilityai.api.StabilityAiImageOptions; import org.springframework.ai.zhipuai.ZhiPuAiImageOptions; import org.springframework.scheduling.annotation.Async; @@ -140,10 +140,10 @@ public class AiImageServiceImpl implements AiImageService { private static ImageOptions buildImageOptions(AiImageDrawReqVO draw, AiModelDO model) { if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.OPENAI.getPlatform())) { // https://platform.openai.com/docs/api-reference/images/create - return OpenAiImageOptions.builder().withModel(model.getModel()) - .withHeight(draw.getHeight()).withWidth(draw.getWidth()) - .withStyle(MapUtil.getStr(draw.getOptions(), "style")) // 风格 - .withResponseFormat("b64_json") + return OpenAiImageOptions.builder().model(model.getModel()) + .height(draw.getHeight()).width(draw.getWidth()) + .style(MapUtil.getStr(draw.getOptions(), "style")) // 风格 + .responseFormat("b64_json") .build(); } else if (ObjUtil.equal(model.getPlatform(), AiPlatformEnum.SILICON_FLOW.getPlatform())) { // https://docs.siliconflow.cn/cn/api-reference/images/images-generations diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java index 6e0ba51c9e..5656d39292 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/service/model/tool/UserProfileQueryToolFunction.java @@ -7,6 +7,8 @@ import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.fasterxml.jackson.annotation.JsonClassDescription; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; import jakarta.annotation.Resource; import lombok.AllArgsConstructor; import lombok.Data; @@ -17,7 +19,7 @@ import org.springframework.stereotype.Component; import java.util.function.BiFunction; /** - * 工具:当前用户信息查询 + * 工具:用户信息查询 * * 同时,也是展示 ToolContext 上下文的使用 * @@ -31,8 +33,17 @@ public class UserProfileQueryToolFunction private AdminUserApi adminUserApi; @Data - @JsonClassDescription("当前用户信息查询") - public static class Request { } + @JsonClassDescription("用户信息查询") + public static class Request { + + /** + * 用户编号 + */ + @JsonProperty(value = "id") + @JsonPropertyDescription("用户编号,例如说:1。如果查询自己,则 id 为空") + private Long id; + + } @Data @AllArgsConstructor @@ -61,13 +72,19 @@ public class UserProfileQueryToolFunction @Override public Response apply(Request request, ToolContext toolContext) { - LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); Long tenantId = (Long) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_TENANT_ID); - if (loginUser == null | tenantId == null) { - return null; + if (tenantId == null) { + return new Response(); + } + if (request.getId() == null) { + LoginUser loginUser = (LoginUser) toolContext.getContext().get(AiUtils.TOOL_CONTEXT_LOGIN_USER); + if (loginUser == null) { + return new Response(); + } + request.setId(loginUser.getId()); } return TenantUtils.execute(tenantId, () -> { - AdminUserRespDTO user = adminUserApi.getUser(loginUser.getId()); + AdminUserRespDTO user = adminUserApi.getUser(request.getId()); return BeanUtils.toBean(user, Response.class); }); } diff --git a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java index ac3ff39a49..0744ff6307 100644 --- a/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java +++ b/yudao-module-ai/src/main/java/cn/iocoder/yudao/module/ai/util/AiUtils.java @@ -2,18 +2,18 @@ package cn.iocoder.yudao.module.ai.util; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; -import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; +import cn.iocoder.yudao.module.ai.enums.model.AiPlatformEnum; import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.qianfan.QianFanChatOptions; import org.springframework.ai.azure.openai.AzureOpenAiChatOptions; import org.springframework.ai.chat.messages.*; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.minimax.MiniMaxChatOptions; -import org.springframework.ai.moonshot.MoonshotChatOptions; import org.springframework.ai.ollama.api.OllamaOptions; import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.qianfan.QianFanChatOptions; import org.springframework.ai.zhipuai.ZhiPuAiChatOptions; import java.util.Collections; @@ -43,18 +43,18 @@ public class AiUtils { switch (platform) { case TONG_YI: return DashScopeChatOptions.builder().withModel(model).withTemperature(temperature).withMaxToken(maxTokens) - .withFunctions(toolNames).withToolContext(toolContext).build(); + .withToolNames(toolNames).withToolContext(toolContext).build(); case YI_YAN: return QianFanChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens).build(); case ZHI_PU: return ZhiPuAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case MINI_MAX: return MiniMaxChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case MOONSHOT: return MoonshotChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens) - .functions(toolNames).toolContext(toolContext).build(); + .toolNames(toolNames).toolContext(toolContext).build(); case OPENAI: case DEEP_SEEK: // 复用 OpenAI 客户端 case DOU_BAO: // 复用 OpenAI 客户端 diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java index 5c924a5823..69776d8e68 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/AzureOpenAIChatModelTests.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import com.azure.ai.openai.OpenAIClientBuilder; import com.azure.core.credential.AzureKeyCredential; -import com.azure.core.util.ClientOptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.azure.openai.AzureOpenAiChatModel; @@ -17,7 +16,7 @@ import reactor.core.publisher.Flux; import java.util.ArrayList; import java.util.List; -import static org.springframework.ai.autoconfigure.azure.openai.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; +import static org.springframework.ai.model.azure.openai.autoconfigure.AzureOpenAiChatProperties.DEFAULT_DEPLOYMENT_NAME; /** * {@link AzureOpenAiChatModel} 集成测试 @@ -29,10 +28,13 @@ public class AzureOpenAIChatModelTests { // TODO @芋艿:晚点在调整 private final OpenAIClientBuilder openAiApi = new OpenAIClientBuilder() .endpoint("https://eastusprejade.openai.azure.com") - .credential(new AzureKeyCredential("xxx")) - .clientOptions((new ClientOptions()).setApplicationId("spring-ai")); - private final AzureOpenAiChatModel chatModel = new AzureOpenAiChatModel(openAiApi, - AzureOpenAiChatOptions.builder().deploymentName(DEFAULT_DEPLOYMENT_NAME).build()); + .credential(new AzureKeyCredential("xxx")); + private final AzureOpenAiChatModel chatModel = AzureOpenAiChatModel.builder() + .openAIClientBuilder(openAiApi) + .defaultOptions(AzureOpenAiChatOptions.builder() + .deploymentName(DEFAULT_DEPLOYMENT_NAME) + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java index d1cc381fb9..06b0b25653 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/BaiChuanChatModelTests.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import cn.iocoder.yudao.module.ai.framework.ai.core.model.baichuan.BaiChuanChatModel; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -35,7 +34,7 @@ public class BaiChuanChatModelTests { .build()) .build(); - private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); + private final BaiChuanChatModel chatModel = new BaiChuanChatModel(openAiChatModel); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java index d20a1761f6..7b51df1662 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/DeepSeekChatModelTests.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; -import cn.iocoder.yudao.module.ai.framework.ai.core.model.deepseek.DeepSeekChatModel; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.ai.chat.messages.Message; @@ -8,9 +7,9 @@ import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.openai.OpenAiChatModel; -import org.springframework.ai.openai.OpenAiChatOptions; -import org.springframework.ai.openai.api.OpenAiApi; +import org.springframework.ai.deepseek.DeepSeekChatModel; +import org.springframework.ai.deepseek.DeepSeekChatOptions; +import org.springframework.ai.deepseek.api.DeepSeekApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -23,19 +22,16 @@ import java.util.List; */ public class DeepSeekChatModelTests { - private final OpenAiChatModel openAiChatModel = OpenAiChatModel.builder() - .openAiApi(OpenAiApi.builder() - .baseUrl(DeepSeekChatModel.BASE_URL) - .apiKey("sk-e52047409b144d97b791a6a46a2d") // apiKey + private final DeepSeekChatModel chatModel = DeepSeekChatModel.builder() + .deepSeekApi(DeepSeekApi.builder() + .apiKey("sk-eaf4172a057344dd9bc64b1f806b6axx") // apiKey .build()) - .defaultOptions(OpenAiChatOptions.builder() + .defaultOptions(DeepSeekChatOptions.builder() .model("deepseek-chat") // 模型 .temperature(0.7) .build()) .build(); - private final DeepSeekChatModel chatModel = new DeepSeekChatModel(openAiChatModel); - @Test @Disabled public void testCall() { diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java index 153342d44c..69e2c1daaa 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/LlamaChatModelTests.java @@ -1,20 +1,6 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; -import org.junit.jupiter.api.Disabled; -import org.junit.jupiter.api.Test; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.UserMessage; -import org.springframework.ai.chat.model.ChatResponse; -import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.ollama.OllamaChatModel; -import org.springframework.ai.ollama.api.OllamaApi; -import org.springframework.ai.ollama.api.OllamaModel; -import org.springframework.ai.ollama.api.OllamaOptions; -import reactor.core.publisher.Flux; - -import java.util.ArrayList; -import java.util.List; /** * {@link OllamaChatModel} 集成测试 @@ -23,43 +9,43 @@ import java.util.List; */ public class LlamaChatModelTests { - private final OllamaChatModel chatModel = OllamaChatModel.builder() - .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 - .defaultOptions(OllamaOptions.builder() - .model(OllamaModel.LLAMA3.getName()) // 模型 - .build()) - .build(); - - @Test - @Disabled - public void testCall() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - ChatResponse response = chatModel.call(new Prompt(messages)); - // 打印结果 - System.out.println(response); - System.out.println(response.getResult().getOutput()); - } - - @Test - @Disabled - public void testStream() { - // 准备参数 - List messages = new ArrayList<>(); - messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); - messages.add(new UserMessage("1 + 1 = ?")); - - // 调用 - Flux flux = chatModel.stream(new Prompt(messages)); - // 打印结果 - flux.doOnNext(response -> { -// System.out.println(response); - System.out.println(response.getResult().getOutput()); - }).then().block(); - } +// private final OllamaChatModel chatModel = OllamaChatModel.builder() +// .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 +// .defaultOptions(OllamaOptions.builder() +// .model(OllamaModel.LLAMA3.getName()) // 模型 +// .build()) +// .build(); +// +// @Test +// @Disabled +// public void testCall() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// ChatResponse response = chatModel.call(new Prompt(messages)); +// // 打印结果 +// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// } +// +// @Test +// @Disabled +// public void testStream() { +// // 准备参数 +// List messages = new ArrayList<>(); +// messages.add(new SystemMessage("你是一个优质的文言文作者,用文言文描述着各城市的人文风景。")); +// messages.add(new UserMessage("1 + 1 = ?")); +// +// // 调用 +// Flux flux = chatModel.stream(new Prompt(messages)); +// // 打印结果 +// flux.doOnNext(response -> { +//// System.out.println(response); +// System.out.println(response.getResult().getOutput()); +// }).then().block(); +// } } diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java index 7de7fd709c..992334b4d9 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/MoonshotChatModelTests.java @@ -2,14 +2,14 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springaicommunity.moonshot.MoonshotChatModel; +import org.springaicommunity.moonshot.MoonshotChatOptions; +import org.springaicommunity.moonshot.api.MoonshotApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.SystemMessage; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.moonshot.MoonshotChatModel; -import org.springframework.ai.moonshot.MoonshotChatOptions; -import org.springframework.ai.moonshot.api.MoonshotApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -22,11 +22,15 @@ import java.util.List; */ public class MoonshotChatModelTests { - private final MoonshotChatModel chatModel = new MoonshotChatModel( - new MoonshotApi("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA"), // 密钥 - MoonshotChatOptions.builder() - .model("moonshot-v1-8k") // 模型 - .build()); + private final MoonshotChatModel chatModel = MoonshotChatModel.builder() + .moonshotApi(MoonshotApi.builder() + .apiKey("sk-aHYYV1SARscItye5QQRRNbXij4fy65Ee7pNZlC9gsSQnUKXA") // 密钥 + .build()) + .defaultOptions(MoonshotChatOptions.builder() + .model("kimi-k2-0711-preview") // 模型 + .build()) + .build(); + @Test @Disabled public void testCall() { diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java index f86e67a667..d2bf68812b 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OllamaChatModelTests.java @@ -23,7 +23,9 @@ import java.util.List; public class OllamaChatModelTests { private final OllamaChatModel chatModel = OllamaChatModel.builder() - .ollamaApi(new OllamaApi("http://127.0.0.1:11434")) // Ollama 服务地址 + .ollamaApi(OllamaApi.builder() + .baseUrl("http://127.0.0.1:11434") // Ollama 服务地址 + .build()) .defaultOptions(OllamaOptions.builder() // .model("qwen") // 模型(https://ollama.com/library/qwen) .model("deepseek-r1") // 模型(https://ollama.com/library/deepseek-r1) diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java index ff866fe40b..c650fd0420 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/OpenAIChatModelTests.java @@ -25,10 +25,10 @@ public class OpenAIChatModelTests { private final OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.holdai.top") - .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") // apiKey + .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") // apiKey .build()) .defaultOptions(OpenAiChatOptions.builder() - .model(OpenAiApi.ChatModel.GPT_4_O) // 模型 + .model(OpenAiApi.ChatModel.GPT_4_1_NANO) // 模型 .temperature(0.7) .build()) .build(); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java index 4f0efdb20c..4f2e27edd2 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/TongYiChatModelTests.java @@ -22,14 +22,17 @@ import java.util.List; */ public class TongYiChatModelTests { - private final DashScopeChatModel chatModel = new DashScopeChatModel( - new DashScopeApi("sk-7d903764249848cfa912733146da12d1"), - DashScopeChatOptions.builder() + private final DashScopeChatModel chatModel = DashScopeChatModel.builder() + .dashScopeApi(DashScopeApi.builder() + .apiKey("sk-47aa124781be4bfb95244cc62f63f7d0") + .build()) + .defaultOptions( DashScopeChatOptions.builder() .withModel("qwen1.5-72b-chat") // 模型 // .withModel("deepseek-r1") // 模型(deepseek-r1) // .withModel("deepseek-v3") // 模型(deepseek-v3) // .withModel("deepseek-r1-distill-qwen-1.5b") // 模型(deepseek-r1-distill-qwen-1.5b) - .build()); + .build()) + .build(); @Test @Disabled diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java index ab6f642437..cb7be2a296 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/chat/YiYanChatModelTests.java @@ -2,13 +2,13 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.chat; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springaicommunity.qianfan.QianFanChatModel; +import org.springaicommunity.qianfan.QianFanChatOptions; +import org.springaicommunity.qianfan.api.QianFanApi; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; -import org.springframework.ai.qianfan.QianFanChatModel; -import org.springframework.ai.qianfan.QianFanChatOptions; -import org.springframework.ai.qianfan.api.QianFanApi; import reactor.core.publisher.Flux; import java.util.ArrayList; @@ -23,9 +23,9 @@ import java.util.List; public class YiYanChatModelTests { private final QianFanChatModel chatModel = new QianFanChatModel( - new QianFanApi("qS8k8dYr2nXunagK4SSU8Xjj", "pHGbx51ql2f0hOyabQvSZezahVC3hh3e"), // 密钥 + new QianFanApi("DGnyzREuaY7av7c38bOM9Ji2", "9aR8myflEOPDrEeLhoXv0FdqANOAyIZW"), // 密钥 QianFanChatOptions.builder() - .model(QianFanApi.ChatModel.ERNIE_4_0_8K_Preview.getValue()) + .model("ERNIE-4.5-8K-Preview") .build() ); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java index 49015b9b9e..1b124529de 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/OpenAiImageModelTests.java @@ -18,7 +18,7 @@ public class OpenAiImageModelTests { private final OpenAiImageModel imageModel = new OpenAiImageModel(OpenAiImageApi.builder() .baseUrl("https://api.holdai.top") // apiKey - .apiKey("sk-aN6nWn3fILjrgLFT0fC4Aa60B72e4253826c77B29dC94f17") + .apiKey("sk-PytRecQlmjEteoa2RRN6cGnwslo72UUPLQVNEMS6K9yjbmpD") .build()); @Test @@ -26,8 +26,8 @@ public class OpenAiImageModelTests { public void testCall() { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .withModel(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 - .withHeight(256).withWidth(256) + .model(OpenAiImageApi.ImageModel.DALL_E_2.getValue()) // 这个模型比较便宜 + .height(256).width(256) .build(); ImagePrompt prompt = new ImagePrompt("中国长城!", options); diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java index 8f44ab9ad1..156360f255 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/QianFanImageTests.java @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.ai.framework.ai.core.model.image; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springaicommunity.qianfan.QianFanImageModel; +import org.springaicommunity.qianfan.QianFanImageOptions; +import org.springaicommunity.qianfan.api.QianFanImageApi; import org.springframework.ai.image.ImagePrompt; import org.springframework.ai.image.ImageResponse; -import org.springframework.ai.qianfan.QianFanImageModel; -import org.springframework.ai.qianfan.QianFanImageOptions; -import org.springframework.ai.qianfan.api.QianFanImageApi; import static cn.iocoder.yudao.module.ai.framework.ai.core.model.image.StabilityAiImageModelTests.viewImage; diff --git a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java index b58e6df00e..8cf556d9f8 100644 --- a/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java +++ b/yudao-module-ai/src/test/java/cn/iocoder/yudao/module/ai/framework/ai/core/model/image/StabilityAiImageModelTests.java @@ -31,8 +31,8 @@ public class StabilityAiImageModelTests { public void testCall() { // 准备参数 ImageOptions options = OpenAiImageOptions.builder() - .withModel("stable-diffusion-v1-6") - .withHeight(320).withWidth(320) + .model("stable-diffusion-v1-6") + .height(320).width(320) .build(); ImagePrompt prompt = new ImagePrompt("great wall", options); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java index d877f60a88..7ce45b3350 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/expression/BpmProcessExpressionRespVO.java @@ -1,6 +1,6 @@ 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 lombok.Data; @@ -27,4 +27,4 @@ public class BpmProcessExpressionRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java index 1cbebe06e4..4d34df8308 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java @@ -71,6 +71,9 @@ public class BpmSimpleModelNodeVO { @Schema(description = "是否填写审批意见", example = "false") private Boolean reasonRequire; + @Schema(description = "跳过表达式", example = "{true}") + private String skipExpression; // 用于审批节点 + /** * 审批节点拒绝处理 */ diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java index 38c2bc1013..0a785b10f4 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailRespVO.java @@ -73,7 +73,7 @@ public class BpmApprovalDetailRespVO { private List candidateUsers; // 只包含未生成 ApprovalTaskInfo 的用户列表 @Schema(description = "流程编号", example = "8761d8e0-0922-11f0-bd37-00ff1db677bf") - private String processInstanceId; // 当且仅当,该节点是子流程节点时,才会有值(CallActivity 的 processInstanceId 字段) + private String processInstanceId; // 当且仅当,该节点是子流程节点时,才会有值(CallActivity 的 calledProcessInstanceId 字段) } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index d5d6fa77c4..0e3b1b9208 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -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_CAN_START = 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_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_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_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!"); ErrorCode TASK_SIGNATURE_NOT_EXISTS = new ErrorCode(1_009_005_015, "签名不能为空!"); 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_NAME_DUPLICATE = new ErrorCode(1_009_012_001, "流程分类名字【{}】重复"); 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 ========== ErrorCode PROCESS_LISTENER_NOT_EXISTS = new ErrorCode(1_009_013_000, "流程监听器不存在"); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java index e5ffa12025..d97c145c3f 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java @@ -35,7 +35,7 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable { // 50 ~ 条件分支 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), - PARALLEL_BRANCH_NODE(52, "并行分支", "parallelGateway"), + PARALLEL_BRANCH_NODE(52, "并行分支", "inclusiveGateway"), // 并行分支使用包容网关实现,条件表达式结果设置为 true INCLUSIVE_BRANCH_NODE(53, "包容分支", "inclusiveGateway"), ROUTER_BRANCH_NODE(54, "路由分支", "exclusiveGateway") ; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java index a19f122bd8..9ba3b5cb3a 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmTaskStatusEnum.java @@ -14,6 +14,7 @@ import lombok.Getter; @AllArgsConstructor public enum BpmTaskStatusEnum { + SKIP(-2, "跳过"), NOT_START(-1, "未开始"), RUNNING(1, "审批中"), APPROVE(2, "审批通过"), diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java index 7f66b29d3b..df8b0d5fd5 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -18,10 +18,7 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.CallActivity; -import org.flowable.bpmn.model.FlowElement; -import org.flowable.bpmn.model.UserTask; +import org.flowable.bpmn.model.*; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; @@ -132,7 +129,7 @@ public class BpmTaskCandidateInvoker { Long startUserId, String processDefinitionId, Map processVariables) { // 如果是 CallActivity 子流程,不进行计算候选人 FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); - if (flowElement instanceof CallActivity) { + if (flowElement instanceof CallActivity || flowElement instanceof SubProcess) { return new HashSet<>(); } // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java index 7c1950f8ce..e9180c8695 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignLeaderExpression.java @@ -24,6 +24,7 @@ import static java.util.Collections.emptySet; * @author 芋道源码 */ @Component +@Deprecated // 仅仅是表达式的示例,建议使用 BpmTaskCandidateStartUserDeptLeaderStrategy 替代 public class BpmTaskAssignLeaderExpression { @Resource diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java index ac243c0f43..f22dc508cb 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/expression/BpmTaskAssignStartUserExpression.java @@ -16,6 +16,7 @@ import java.util.Set; * @author 芋道源码 */ @Component +@Deprecated // 仅仅是表达式的示例,建议使用 BpmTaskCandidateStartUserStrategy 替代 public class BpmTaskAssignStartUserExpression { @Resource diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java index 64ca9e8538..86c137a4b7 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/other/BpmTaskCandidateExpressionStrategy.java @@ -8,6 +8,7 @@ import com.google.common.collect.Sets; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; import org.flowable.common.engine.api.FlowableException; +import org.flowable.common.engine.impl.javax.el.PropertyNotFoundException; import org.flowable.engine.delegate.DelegateExecution; import org.springframework.stereotype.Component; @@ -48,10 +49,12 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat Object result = FlowableUtils.getExpressionValue(variables, param); return CollectionUtils.toLinkedHashSet(Long.class, result); } catch (FlowableException ex) { - // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常, - log.warn("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); - // 不能预测候选人,返回空列表, 避免流程无法进行 - return Sets.newHashSet(); + // 预测未运行的节点时候,表达式如果包含 execution 或者不存在的流程变量会抛异常,此时忽略该异常!相当于说,不做流程预测!!! + if (ex.getCause() != null && ex.getCause() instanceof PropertyNotFoundException) { + return Sets.newHashSet(); + } + log.error("[calculateUsersByActivity][表达式({}) 变量({}) 解析报错", param, variables, ex); + throw ex; } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java index ba2aaa6bcb..3deb545fa8 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -1,5 +1,6 @@ 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 com.google.common.collect.ImmutableSet; import jakarta.annotation.Resource; @@ -37,18 +38,26 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent @Override protected void processCreated(FlowableEngineEntityEvent event) { - processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity()); + ProcessInstance processInstance = (ProcessInstance) event.getEntity(); + FlowableUtils.execute(processInstance.getTenantId(), + () -> processInstanceService.processProcessInstanceCreated(processInstance)); } @Override 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) { + // 特殊情况:当跳转到 EndEvent 流程实例未结束, 会执行 deleteProcessInstance 方法 ProcessInstance processInstance = processInstanceService.getProcessInstance(event.getProcessInstanceId()); - processInstanceService.processProcessInstanceCompleted(processInstance); + if (processInstance != null) { + FlowableUtils.execute(processInstance.getTenantId(), + () -> processInstanceService.processProcessInstanceCompleted(processInstance)); + } } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index e50df0bcf0..6e2e48cbec 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -3,10 +3,12 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONObject; import cn.iocoder.yudao.framework.common.util.number.NumberUtils; 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.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.task.BpmTaskService; import com.google.common.collect.ImmutableSet; @@ -58,17 +60,20 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { @Override protected void taskCreated(FlowableEngineEntityEvent event) { - taskService.processTaskCreated((Task) event.getEntity()); + Task entity = (Task) event.getEntity(); + FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskCreated(entity)); } @Override protected void taskAssigned(FlowableEngineEntityEvent event) { - taskService.processTaskAssigned((Task) event.getEntity()); + Task entity = (Task) event.getEntity(); + FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskAssigned(entity)); } @Override protected void taskCompleted(FlowableEngineEntityEvent event) { - taskService.processTaskCompleted((Task) event.getEntity()); + Task entity = (Task) event.getEntity(); + FlowableUtils.execute(entity.getTenantId(), () -> taskService.processTaskCompleted(entity)); } @Override @@ -94,6 +99,23 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { String processDefinitionId = event.getProcessDefinitionId(); BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId); 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()); if (!(element instanceof BoundaryEvent)) { return; diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java index 2503c0fff9..358ee9f4dc 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -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.util.json.JsonUtils; 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.enums.definition.BpmHttpRequestParamTypeEnum; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import com.fasterxml.jackson.core.type.TypeReference; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.runtime.ProcessInstance; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; +import org.springframework.http.*; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; 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.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 请求工具类 @@ -42,7 +42,6 @@ public class BpmHttpRequestUtils { List bodyParams, Boolean handleResponse, List> response) { - RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); BpmProcessInstanceService processInstanceService = SpringUtils.getBean(BpmProcessInstanceService.class); // 1.1 设置请求头 @@ -51,6 +50,7 @@ public class BpmHttpRequestUtils { MultiValueMap body = buildHttpBody(processInstance, bodyParams); // 2. 发起请求 + RestTemplate restTemplate = SpringUtils.getBean(RestTemplate.class); ResponseEntity responseEntity = sendHttpRequest(url, headers, body, restTemplate); // 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 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 sendHttpRequest(String url, MultiValueMap headers, - MultiValueMap body, + Object body, RestTemplate restTemplate) { - HttpEntity> requestEntity = new HttpEntity<>(body, headers); + HttpEntity requestEntity = new HttpEntity<>(body, headers); ResponseEntity responseEntity; try { 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) { - log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); - throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR); + log.error("[sendHttpRequest][HTTP 请求,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage()); + throw exception(PROCESS_INSTANCE_HTTP_CALL_ERROR); } return responseEntity; } public static MultiValueMap buildHttpHeaders(ProcessInstance processInstance, List headerSettings) { - Map processVariables = processInstance.getProcessVariables(); MultiValueMap headers = new LinkedMultiValueMap<>(); headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); + Map processVariables = processInstance.getProcessVariables(); addHttpRequestParam(headers, headerSettings, processVariables); return headers; } @@ -108,7 +136,9 @@ public class BpmHttpRequestUtils { Map processVariables = processInstance.getProcessVariables(); MultiValueMap body = new LinkedMultiValueMap<>(); addHttpRequestParam(body, bodySettings, processVariables); - body.add("processInstanceId", processInstance.getId()); + if (!body.containsKey("processInstanceId")) { // 避免重复添加 + body.add("processInstanceId", processInstance.getId()); + } return body; } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 1cccf18f04..1bafa578db 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -478,7 +478,11 @@ public class BpmnModelUtils { */ public static FlowElement getFlowElementById(BpmnModel model, String flowElementId) { 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 UserTask || currentElement instanceof ServiceTask) { - // 添加元素 + // 添加节点 FlowNode flowNode = (FlowNode) currentElement; resultElements.add(flowNode); + // 遍历子节点 flowNode.getOutgoingFlows().forEach( 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 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 是否满足条件 */ public static boolean evalConditionExpress(Map variables, String expression) { - if (expression == null) { + if (StrUtil.isEmpty(expression)) { return Boolean.FALSE; } // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index bd427e32f2..2582399a81 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -4,7 +4,6 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; 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.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO; 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 org.flowable.bpmn.BpmnAutoLayout; import org.flowable.bpmn.constants.BpmnXMLConstants; -import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.bpmn.model.Process; import org.flowable.engine.delegate.ExecutionListener; import org.flowable.engine.delegate.TaskListener; 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.util.BpmnModelUtils.*; import static java.util.Arrays.asList; @@ -239,13 +239,13 @@ public class SimpleModelUtils { // 3.1 分支有后续节点。即分支 1: A->B->C->D 的情况 if (isValidNode(conditionChildNode)) { // 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); // 3.1.2 递归调用后续节点连线。例如说,建立 B->C->D 的连线 traverseNodeToBuildSequenceFlow(process, conditionChildNode, branchEndNodeId); } else { // 3.2 分支没有后续节点。例如说,建立 A->D 的连线 - SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, item); + SequenceFlow sequenceFlow = ConditionNodeConvert.buildSequenceFlow(node.getId(), branchEndNodeId, nodeType, item); process.addFlowElement(sequenceFlow); } } @@ -464,6 +464,10 @@ public class SimpleModelUtils { addReasonRequire(node.getReasonRequire(), userTask); // 节点类型 addNodeType(node.getType(), userTask); + // 添加跳过表达式 + if (StrUtil.isNotEmpty(node.getSkipExpression())) { + userTask.setSkipExpression(node.getSkipExpression()); + } return userTask; } @@ -591,17 +595,23 @@ public class SimpleModelUtils { private static class ParallelBranchNodeConvert implements NodeConvert { + /** + * 并行分支使用包容网关。需要设置所有出口条件表达式的值为 true 。原因是,解决 https://t.zsxq.com/m6GXh 反馈问题 + * + * @see {@link ConditionNodeConvert#buildSequenceFlow} + */ @Override - public List convertList(BpmSimpleModelNodeVO node) { - ParallelGateway parallelGateway = new ParallelGateway(); - parallelGateway.setId(node.getId()); + public List convertList(BpmSimpleModelNodeVO node) { + + InclusiveGateway inclusiveGateway = new InclusiveGateway(); + inclusiveGateway.setId(node.getId()); // TODO @jason:setName - // 并行聚合网关由程序创建,前端不需要传入 - ParallelGateway joinParallelGateway = new ParallelGateway(); + // 合并网关 由程序创建,前端不需要传入 + InclusiveGateway joinParallelGateway = new InclusiveGateway(); joinParallelGateway.setId(buildGatewayJoinId(node.getId())); // TODO @jason:setName - return CollUtil.newArrayList(parallelGateway, joinParallelGateway); + return CollUtil.newArrayList(inclusiveGateway, joinParallelGateway); } @Override @@ -652,8 +662,14 @@ public class SimpleModelUtils { } public static SequenceFlow buildSequenceFlow(String sourceId, String targetId, - BpmSimpleModelNodeVO node) { - String conditionExpression = buildConditionExpression(node.getConditionSetting()); + BpmSimpleModelNodeTypeEnum nodeType, BpmSimpleModelNodeVO node) { + 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); } } @@ -662,7 +678,6 @@ public class SimpleModelUtils { * 构造条件表达式 */ public static String buildConditionExpression(BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { - // 并行网关不需要设置条件 if (conditionSetting == null) { return null; } @@ -684,15 +699,18 @@ public class SimpleModelUtils { if (conditionGroups == null || CollUtil.isEmpty(conditionGroups.getConditions())) { return null; } - List strConditionGroups = CollectionUtils.convertList(conditionGroups.getConditions(), item -> { + List strConditionGroups = convertList(conditionGroups.getConditions(), item -> { if (CollUtil.isEmpty(item.getRules())) { return ""; } // 构造规则表达式 - List list = CollectionUtils.convertList(item.getRules(), (rule) -> { + List list = convertList(item.getRules(), (rule) -> { String rightSide = NumberUtil.isNumber(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(); @@ -954,7 +972,7 @@ public class SimpleModelUtils { || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { - // 添加元素 + // 添加此节点 resultNodes.add(currentNode); } @@ -1000,6 +1018,16 @@ public class SimpleModelUtils { simulateNextNode(currentNode.getChildNode(), variables, resultNodes); } + /** + * 根据跳过表达式,判断是否跳过此节点 + */ + public static boolean isSkipNode(BpmSimpleModelNodeVO currentNode, Map variables) { + if (StrUtil.isEmpty(currentNode.getSkipExpression())) { + return false; + } + return BpmnModelUtils.evalConditionExpress(variables, currentNode.getSkipExpression()); + } + public static boolean evalConditionExpress(Map variables, BpmSimpleModelNodeVO.ConditionSetting conditionSetting) { return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java index 8a48da15a4..42d399acd5 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmCategoryServiceImpl.java @@ -34,6 +34,9 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { @Resource private BpmCategoryMapper bpmCategoryMapper; + @Resource + private BpmModelService modelService; + @Override public Long createCategory(BpmCategorySaveReqVO createReqVO) { // 校验唯一 @@ -77,15 +80,22 @@ public class BpmCategoryServiceImpl implements BpmCategoryService { @Override 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); } - private void validateCategoryExists(Long id) { - if (bpmCategoryMapper.selectById(id) == null) { + private BpmCategoryDO validateCategoryExists(Long id) { + BpmCategoryDO category = bpmCategoryMapper.selectById(id); + if (category == null) { throw exception(CATEGORY_NOT_EXISTS); } + return category; } @Override diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java index 5601bcda31..3273d24cbe 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java @@ -24,6 +24,14 @@ public interface BpmModelService { */ List getModelList(String name); + /** + * 根据分类编码获得流程模型数量 + * + * @param category 分类编码 + * @return 流程模型数量 + */ + Long getModelCountByCategory(String category); + /** * 创建流程模型 * diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index e8e90006f8..c23ea9ce8f 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -88,6 +88,14 @@ public class BpmModelServiceImpl implements BpmModelService { return modelQuery.list(); } + @Override + public Long getModelCountByCategory(String category) { + return repositoryService.createModelQuery() + .modelCategory(category) + .modelTenantId(FlowableUtils.getTenantId()) + .count(); + } + @Override @Transactional(rollbackFor = Exception.class) public String createModel(@Valid BpmModelSaveReqVO createReqVO) { diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index a42a3ecd4b..30adda6341 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -4,11 +4,9 @@ import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.util.ObjUtil; -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; +import cn.hutool.core.util.*; 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.json.JsonUtils; 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.stereotype.Service; 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 java.util.*; 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.convertList; 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.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; @@ -264,7 +265,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 获取下一个将要执行的节点集合 FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey()); List nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); - List nextActivityNodes = convertList(nextFlowNodes, node -> new ActivityNode().setId(node.getId()) + // 仅仅获取 UserTask 节点 TODO add from jason:如果网关节点和网关节点相连,获取下个 UserTask. 貌似有点不准。 + List nextUserTaskList = CollectionUtils.filterList(nextFlowNodes, node -> node instanceof UserTask); + List nextActivityNodes = convertList(nextUserTaskList, node -> new ActivityNode().setId(node.getId()) .setName(node.getName()).setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node)) @@ -395,7 +398,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别 BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) - .setStatus(FlowableUtils.getTaskStatus(task)) + .setStatus(getEndActivityNodeStatus(task)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) .setTasks(singletonList(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task))); @@ -449,7 +452,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .setNodeType(BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType()).setStatus(processInstanceStatus) .setStartTime(DateUtils.of(activity.getStartTime())) .setEndTime(DateUtils.of(activity.getEndTime())) - .setProcessInstanceId(activity.getProcessInstanceId()); + .setProcessInstanceId(activity.getCalledProcessInstanceId()); approvalNodes.add(callActivity); } }); @@ -459,6 +462,18 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService 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())); } if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(activityNode.getNodeType())) { - activityNode.setProcessInstanceId(firstActivity.getProcessInstanceId()); + activityNode.setProcessInstanceId(firstActivity.getCalledProcessInstanceId()); } return activityNode; }); @@ -562,10 +577,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (runActivityIds.contains(node.getId())) { 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()) .setNodeType(node.getType()).setCandidateStrategy(node.getCandidateStrategy()) - .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); + .setStatus(status); // 1. 开始节点/审批节点 if (ObjectUtils.equalsAny(node.getType(), @@ -605,8 +624,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService if (runActivityIds.contains(node.getId())) { return null; } + Integer status = BpmTaskStatusEnum.NOT_START.getStatus(); + // 如果节点被跳过,状态设置为跳过 + if(BpmnModelUtils.isSkipNode(node, processVariables)){ + status = BpmTaskStatusEnum.SKIP.getStatus(); + } ActivityNode activityNode = new ActivityNode().setId(node.getId()) - .setStatus(BpmTaskStatusEnum.NOT_START.getStatus()); + .setStatus(status); // 1. 开始节点 if (node instanceof StartEvent) { @@ -771,17 +795,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService processInstanceBuilder.predefineProcessInstanceId(processIdRedisDAO.generate(processIdRule)); } // 3.2 流程名称 - BpmModelMetaInfoVO.TitleSetting titleSetting = processDefinitionInfo.getTitleSetting(); - if (titleSetting != null && Boolean.TRUE.equals(titleSetting.getEnable())) { - AdminUserRespDTO user = adminUserApi.getUser(userId); - Map 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()); - } + processInstanceBuilder.name(generateProcessInstanceName(userId, definition, processDefinitionInfo, variables)); // 3.3 发起流程实例 ProcessInstance instance = processInstanceBuilder.start(); return instance.getId(); @@ -817,6 +831,25 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService }); } + private String generateProcessInstanceName(Long userId, + ProcessDefinition definition, + BpmProcessDefinitionInfoDO definitionInfo, + Map 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 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 public void cancelProcessInstanceByStartUser(Long userId, @Valid BpmProcessInstanceCancelReqVO cancelReqVO) { // 1.1 校验流程实例存在 @@ -833,7 +866,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService .getProcessDefinitionInfo(instance.getProcessDefinitionId()); Assert.notNull(processDefinitionInfo, "流程定义({})不存在", processDefinitionInfo); if (processDefinitionInfo.getAllowCancelRunningProcess() != null // 防止未配置 AllowCancelRunningProcess , 默认为可取消 - && Boolean.FALSE.equals(processDefinitionInfo.getAllowCancelRunningProcess())) { + && BooleanUtil.isFalse(processDefinitionInfo.getAllowCancelRunningProcess())) { throw exception(PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW); } // 1.4 子流程不允许取消 @@ -900,64 +933,77 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @Override public void processProcessInstanceCompleted(ProcessInstance instance) { - // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 - FlowableUtils.execute(instance.getTenantId(), () -> { - // 1.1 获取当前状态 - Integer status = (Integer) instance.getProcessVariables() - .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); - String reason = (String) instance.getProcessVariables() - .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); - // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 - // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 - if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { - status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); - runtimeService.setVariable(instance.getId(), BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, - status); + // 1.1 获取当前状态 + Integer status = (Integer) instance.getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS); + String reason = (String) instance.getProcessVariables() + .get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_REASON); + // 1.2 当流程状态还是审批状态中,说明审批通过了,则变更下它的状态 + // 为什么这么处理?因为流程完成,并且完成了,说明审批通过了 + if (Objects.equals(status, BpmProcessInstanceStatusEnum.RUNNING.getStatus())) { + status = BpmProcessInstanceStatusEnum.APPROVE.getStatus(); + 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 public void processProcessInstanceCreated(ProcessInstance instance) { - // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 - FlowableUtils.execute(instance.getTenantId(), () -> { - // 流程前置通知 - BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. - getProcessDefinitionInfo(instance.getProcessDefinitionId()); - if (ObjUtil.isNull(processDefinitionInfo) || - ObjUtil.isNull(processDefinitionInfo.getProcessBeforeTriggerSetting())) { - return; + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(instance.getProcessDefinitionId()); + if (processDefinition == null || processDefinitionInfo == null) { + 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()); } } diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 1164f4da72..8ea607a773 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -230,10 +230,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (StrUtil.isNotBlank(pageVO.getName())) { taskQuery.taskNameLike("%" + pageVO.getName() + "%"); } - if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); - } +// if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { +// taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); +// taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); +// } // 执行查询 long count = taskQuery.count(); if (count == 0) { @@ -244,6 +244,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 特殊:强制移除自动完成的“发起人”节点 // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 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); } @@ -259,16 +265,22 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (StrUtil.isNotEmpty(pageVO.getCategory())) { taskQuery.taskCategory(pageVO.getCategory()); } - if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { - taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); - taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); - } +// if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { +// taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); +// taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); +// } // 执行查询 long count = taskQuery.count(); if (count == 0) { return PageResult.empty(); } List 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); } @@ -886,7 +898,9 @@ public class BpmTaskServiceImpl implements BpmTaskService { if (!returnTaskKeyList.contains(task.getTaskDefinitionKey())) { return; } - runExecutionIds.add(task.getExecutionId()); + if (task.getExecutionId() != null) { + runExecutionIds.add(task.getExecutionId()); + } // 判断是否分配给自己任务,因为会签任务,一个节点会有多个任务 if (isAssignUserTask(userId, task)) { // 情况一:自己的任务,进行 RETURN 标记 @@ -933,7 +947,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmCommentTypeEnum.DELEGATE_START.formatComment(currentUser.getNickname(), delegateUser.getNickname(), reqVO.getReason())); // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) - taskService.setOwner(taskId, task.getAssignee()); + // 特殊:如果已经被委派(owner 非空),则不需要更新 owner:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICJ153 + if (StrUtil.isEmpty(task.getOwner())) { + taskService.setOwner(taskId, task.getAssignee()); + } // 3.2 执行委派,将任务委派给 delegateUser taskService.delegateTask(taskId, reqVO.getDelegateUserId().toString()); // 补充说明:委托不单独设置状态。如果需要,可通过 Task 的 DelegationState 字段,判断是否为 DelegationState.PENDING 委托中 @@ -959,7 +976,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { BpmCommentTypeEnum.TRANSFER.formatComment(currentUser.getNickname(), assigneeUser.getNickname(), reqVO.getReason())); // 3.1 设置任务所有人 (owner) 为原任务的处理人 (assignee) - taskService.setOwner(taskId, task.getAssignee()); + // 特殊:如果已经被转派(owner 非空),则不需要更新 owner:https://gitee.com/zhijiantianya/yudao-cloud/issues/ICJ153 + if (StrUtil.isEmpty(task.getOwner())) { + taskService.setOwner(taskId, task.getAssignee()); + } // 3.2 执行转派(审批人),将任务转派给 assigneeUser // 委托( delegate)和转派(transfer)的差别,就在这块的调用!!!! taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString()); @@ -1367,7 +1387,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class)); if (userTaskElement.getId().equals(START_USER_NODE_ID) && (skipStartUserNodeFlag == null // 目的:一般是“主流程”,发起人节点,自动通过审核 - || Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 + || BooleanUtil.isTrue(skipStartUserNodeFlag)) // 目的:一般是“子流程”,发起人节点,按配置自动通过审核 && ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) { getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId()) .setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason())); diff --git a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java index 40313a9663..da148a8562 100644 --- a/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java +++ b/yudao-module-bpm/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java @@ -85,10 +85,15 @@ public class BpmCallActivityListener implements ExecutionListener { // 2.2 使用表单值,并兜底字符串转 Long 失败时使用主流程发起人 try { FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue)); - } catch (Exception e) { - log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", - DELEGATE_EXPRESSION, formFieldValue); - FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + } catch (NumberFormatException ex) { + try { + List formFieldValues = JsonUtils.parseArray(formFieldValue, Long.class); + FlowableUtils.setAuthenticatedUserId(formFieldValues.get(0)); + } catch (Exception e) { + log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]", + DELEGATE_EXPRESSION, formFieldValue); + FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId())); + } } } } diff --git a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java index f71b8aae50..19199e5616 100644 --- a/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java +++ b/yudao-module-bpm/src/test/java/cn/iocoder/yudao/module/bpm/service/category/BpmCategoryServiceImplTest.java @@ -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.mysql.category.BpmCategoryMapper; import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl; +import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService; import jakarta.annotation.Resource; import org.junit.jupiter.api.Test; 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.buildTime; @@ -32,6 +34,9 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest { @Resource private BpmCategoryServiceImpl categoryService; + @MockitoBean + private BpmModelService modelService; + @Resource private BpmCategoryMapper categoryMapper; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java index 49cdcb80be..d94c856210 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/business/vo/business/CrmBusinessRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.business.vo.business; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java index 56e5c25612..911f48d130 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/clue/vo/CrmClueRespVO.java @@ -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.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java index b2b1e83848..e198531179 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contact/vo/CrmContactRespVO.java @@ -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.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.ToString; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java index a01bc110b9..307a7472fb 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/contract/vo/contract/CrmContractRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.crm.controller.admin.contract.vo.contract; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.AllArgsConstructor; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java index a45e9115fe..07d86a4f03 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerImportExcelVO.java @@ -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.DictConvert; 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.Builder; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java index 236129918c..1b337282ac 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/customer/vo/customer/CrmCustomerRespVO.java @@ -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.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java index 1ce10b73e3..075c3b360f 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java @@ -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.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -61,4 +61,4 @@ public class CrmFollowUpRecordRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java index 8e458a8a0f..a9c2a7a701 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/operatelog/vo/CrmOperateLogRespVO.java @@ -1,6 +1,6 @@ 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 lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java index bf98a80606..adefc77556 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/CrmProductController.java @@ -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.service.product.CrmProductService; 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.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; @@ -70,6 +71,7 @@ public class CrmProductController { @Operation(summary = "获得产品") @Parameter(name = "id", description = "编号", required = true, example = "1024") @PreAuthorize("@ss.hasPermission('crm:product:query')") + @TransMethodResult public CommonResult getProduct(@RequestParam("id") Long id) { CrmProductDO product = productService.getProduct(id); return success(BeanUtils.toBean(product, CrmProductRespVO.class)); @@ -86,6 +88,7 @@ public class CrmProductController { @GetMapping("/page") @Operation(summary = "获得产品分页") @PreAuthorize("@ss.hasPermission('crm:product:query')") + @TransMethodResult public CommonResult> getProductPage(@Valid CrmProductPageReqVO pageVO) { PageResult pageResult = productService.getProductPage(pageVO); return success(BeanUtils.toBean(pageResult, CrmProductRespVO.class)); diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java index 6144c95c4d..c8e181e341 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/category/CrmProductCategoryListReqVO.java @@ -1,6 +1,6 @@ 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 lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java index aa955a9f4e..5ef01a37f5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/product/vo/product/CrmProductRespVO.java @@ -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.module.crm.dal.dataobject.product.CrmProductCategoryDO; import cn.iocoder.yudao.module.crm.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java index ad1ce3a7b7..2208887db5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/plan/CrmReceivablePlanRespVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.plan; import cn.iocoder.yudao.module.crm.controller.admin.receivable.vo.receivable.CrmReceivableRespVO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java index 6d712e3a54..75827db68f 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/receivable/vo/receivable/CrmReceivableRespVO.java @@ -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.module.crm.controller.admin.contract.vo.contract.CrmContractRespVO; import cn.iocoder.yudao.module.crm.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java index 6e75f23daa..71b1884cc2 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/business/CrmBusinessServiceImpl.java @@ -142,6 +142,7 @@ public class CrmBusinessServiceImpl implements CrmBusinessService { updateBusinessProduct(updateObj.getId(), businessProducts); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldBusiness.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldBusiness, CrmBusinessSaveReqVO.class)); LogRecordContext.putVariable("businessName", oldBusiness.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java index c8c850ab48..0250fe14f5 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/clue/CrmClueServiceImpl.java @@ -92,19 +92,20 @@ public class CrmClueServiceImpl implements CrmClueService { @Transactional(rollbackFor = Exception.class) @LogRecord(type = CRM_CLUE_TYPE, subType = CRM_CLUE_UPDATE_SUB_TYPE, bizNo = "{{#updateReqVO.id}}", success = CRM_CLUE_UPDATE_SUCCESS) - @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReq.id", level = CrmPermissionLevelEnum.OWNER) - public void updateClue(CrmClueSaveReqVO updateReq) { - Assert.notNull(updateReq.getId(), "线索编号不能为空"); + @CrmPermission(bizType = CrmBizTypeEnum.CRM_CLUE, bizId = "#updateReqVO.id", level = CrmPermissionLevelEnum.OWNER) + public void updateClue(CrmClueSaveReqVO updateReqVO) { + Assert.notNull(updateReqVO.getId(), "线索编号不能为空"); // 1.1 校验线索是否存在 - CrmClueDO oldClue = validateClueExists(updateReq.getId()); + CrmClueDO oldClue = validateClueExists(updateReqVO.getId()); // 1.2 校验关联数据 - validateRelationDataExists(updateReq); + validateRelationDataExists(updateReqVO); // 2. 更新线索 - CrmClueDO updateObj = BeanUtils.toBean(updateReq, CrmClueDO.class); + CrmClueDO updateObj = BeanUtils.toBean(updateReqVO, CrmClueDO.class); clueMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldClue.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldClue, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable("clueName", oldClue.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java index 2819c528cd..958fc7520d 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contact/CrmContactServiceImpl.java @@ -114,6 +114,7 @@ public class CrmContactServiceImpl implements CrmContactService { contactMapper.updateById(updateObj); // 3. 记录操作日志 + updateReqVO.setOwnerUserId(oldContact.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContact, CrmContactSaveReqVO.class)); LogRecordContext.putVariable("contactName", oldContact.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java index 12236bf5bc..d25d7c0046 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/contract/CrmContractServiceImpl.java @@ -140,9 +140,9 @@ public class CrmContractServiceImpl implements CrmContractService { Assert.notNull(updateReqVO.getId(), "合同编号不能为空"); updateReqVO.setOwnerUserId(null); // 不允许更新的字段 // 1.1 校验存在 - CrmContractDO contract = validateContractExists(updateReqVO.getId()); + CrmContractDO oldContract = validateContractExists(updateReqVO.getId()); // 1.2 只有草稿、审批中,可以编辑; - if (!ObjectUtils.equalsAny(contract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), + if (!ObjectUtils.equalsAny(oldContract.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(CONTRACT_UPDATE_FAIL_NOT_DRAFT); } @@ -159,8 +159,9 @@ public class CrmContractServiceImpl implements CrmContractService { updateContractProduct(updateReqVO.getId(), contractProducts); // 3. 记录操作日志上下文 - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(contract, CrmContractSaveReqVO.class)); - LogRecordContext.putVariable("contractName", contract.getName()); + updateReqVO.setOwnerUserId(oldContract.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldContract, CrmContractSaveReqVO.class)); + LogRecordContext.putVariable("contractName", oldContract.getName()); } private void updateContractProduct(Long id, List newList) { diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java index 184369c791..11ac107d98 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/customer/CrmCustomerServiceImpl.java @@ -137,6 +137,7 @@ public class CrmCustomerServiceImpl implements CrmCustomerService { customerMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldCustomer.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldCustomer, CrmCustomerSaveReqVO.class)); LogRecordContext.putVariable("customerName", oldCustomer.getName()); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java index b479443390..6e2c37c89a 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/permission/CrmPermissionServiceImpl.java @@ -210,12 +210,12 @@ public class CrmPermissionServiceImpl implements CrmPermissionService { CrmPermissionDO oldPermission = permissionMapper.selectByBizTypeAndBizIdByUserId( transferReqBO.getBizType(), transferReqBO.getBizId(), transferReqBO.getUserId()); String bizTypeName = CrmBizTypeEnum.getNameByType(transferReqBO.getBizType()); - if (oldPermission == null // 不是拥有者,并且不是超管 - || (!isOwner(oldPermission.getLevel()) && !CrmPermissionUtils.isCrmAdmin())) { + if ((oldPermission == null || !isOwner(oldPermission.getLevel())) + && !CrmPermissionUtils.isCrmAdmin()) { // 并且不是超管 throw exception(CRM_PERMISSION_DENIED, bizTypeName); } // 1.1 校验转移对象是否已经是该负责人 - if (ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { + if (oldPermission != null && ObjUtil.equal(transferReqBO.getNewOwnerUserId(), oldPermission.getUserId())) { throw exception(CRM_PERMISSION_MODEL_TRANSFER_FAIL_OWNER_USER_EXISTS, bizTypeName); } // 1.2 校验新负责人是否存在 diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java index b18c1a0c4f..5005508bbb 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivablePlanServiceImpl.java @@ -104,6 +104,7 @@ public class CrmReceivablePlanServiceImpl implements CrmReceivablePlanService { receivablePlanMapper.updateById(updateObj); // 3. 记录操作日志上下文 + updateReqVO.setOwnerUserId(oldReceivablePlan.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivablePlan, CrmReceivablePlanSaveReqVO.class)); LogRecordContext.putVariable("receivablePlan", oldReceivablePlan); } diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java index c1ba3a7c43..642c8e6872 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/receivable/CrmReceivableServiceImpl.java @@ -162,14 +162,14 @@ public class CrmReceivableServiceImpl implements CrmReceivableService { Assert.notNull(updateReqVO.getId(), "回款编号不能为空"); updateReqVO.setOwnerUserId(null).setCustomerId(null).setContractId(null).setPlanId(null); // 不允许修改的字段 // 1.1 校验存在 - CrmReceivableDO receivable = validateReceivableExists(updateReqVO.getId()); - updateReqVO.setOwnerUserId(receivable.getOwnerUserId()).setCustomerId(receivable.getCustomerId()) - .setContractId(receivable.getContractId()).setPlanId(receivable.getPlanId()); // 设置已存在的值 + CrmReceivableDO oldReceivable = validateReceivableExists(updateReqVO.getId()); + updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()).setCustomerId(oldReceivable.getCustomerId()) + .setContractId(oldReceivable.getContractId()).setPlanId(oldReceivable.getPlanId()); // 设置已存在的值 // 1.2 校验可回款金额超过上限 validateReceivablePriceExceedsLimit(updateReqVO); // 1.3 只有草稿、审批中,可以编辑; - if (!ObjectUtils.equalsAny(receivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), + if (!ObjectUtils.equalsAny(oldReceivable.getAuditStatus(), CrmAuditStatusEnum.DRAFT.getStatus(), CrmAuditStatusEnum.PROCESS.getStatus())) { throw exception(RECEIVABLE_UPDATE_FAIL_EDITING_PROHIBITED); } @@ -179,9 +179,10 @@ public class CrmReceivableServiceImpl implements CrmReceivableService { receivableMapper.updateById(updateObj); // 3. 记录操作日志上下文 - LogRecordContext.putVariable("receivable", receivable); - LogRecordContext.putVariable("period", getReceivablePeriod(receivable.getPlanId())); - LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(receivable, CrmReceivableSaveReqVO.class)); + updateReqVO.setOwnerUserId(oldReceivable.getOwnerUserId()); // 避免操作日志出现“删除负责人”的情况 + LogRecordContext.putVariable("oldReceivable", oldReceivable); + LogRecordContext.putVariable("period", getReceivablePeriod(oldReceivable.getPlanId())); + LogRecordContext.putVariable(DiffParseFunction.OLD_OBJECT, BeanUtils.toBean(oldReceivable, CrmReceivableSaveReqVO.class)); } private Integer getReceivablePeriod(Long planId) { diff --git a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java index d2818cd072..0563af0ad3 100644 --- a/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java +++ b/yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/util/CrmPermissionUtils.java @@ -2,11 +2,11 @@ package cn.iocoder.yudao.module.crm.util; import cn.hutool.core.collection.CollUtil; 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.enums.common.CrmBizTypeEnum; import cn.iocoder.yudao.module.crm.enums.common.CrmSceneTypeEnum; 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.dto.AdminUserRespDTO; import cn.iocoder.yudao.module.system.enums.permission.RoleCodeEnum; @@ -32,7 +32,7 @@ public class CrmPermissionUtils { * @return 是/否 */ public static boolean isCrmAdmin() { - PermissionApi permissionApi = SpringUtil.getBean(PermissionApi.class); + PermissionCommonApi permissionApi = SpringUtil.getBean(PermissionCommonApi.class); return permissionApi.hasAnyRoles(getLoginUserId(), RoleCodeEnum.CRM_ADMIN.getCode()); } diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java index a1c2e954db..3e03169689 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/account/ErpAccountRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.account; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -47,4 +47,4 @@ public class ErpAccountRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java index 12b5a7df8e..df89465f00 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/payment/ErpFinancePaymentRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.payment; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -96,4 +96,4 @@ public class ErpFinancePaymentRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java index ec82875957..2398e065ca 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/finance/vo/receipt/ErpFinanceReceiptRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.finance.vo.receipt; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -96,4 +96,4 @@ public class ErpFinanceReceiptRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java index 23d7d9e8ff..29d4c2fd6e 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/category/ErpProductCategoryRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.category; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -44,4 +44,4 @@ public class ErpProductCategoryRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java index 9be9bc2559..8dcc9f89ba 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/product/ErpProductRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.product; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -73,4 +73,4 @@ public class ErpProductRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java index 06f604920c..3044d5aef1 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/product/vo/unit/ErpProductUnitRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.product.vo.unit; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -31,4 +31,4 @@ public class ErpProductUnitRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java index beeeab8692..03c446ed1c 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/in/ErpPurchaseInRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.in; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -142,4 +142,4 @@ public class ErpPurchaseInRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java index bc76720eed..e6a6aa4f64 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.order; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -149,4 +149,4 @@ public class ErpPurchaseOrderRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java index 223b9327e9..676241949c 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/returns/ErpPurchaseReturnRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.returns; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -142,4 +142,4 @@ public class ErpPurchaseReturnRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java index 5ba5892c1b..538a3b1e60 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/supplier/ErpSupplierRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.purchase.vo.supplier; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -81,4 +81,4 @@ public class ErpSupplierRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java index f1a58a03d5..c75a99a086 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/customer/ErpCustomerRespVO.java @@ -2,12 +2,9 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.customer; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; -import java.util.*; -import java.util.*; import java.math.BigDecimal; -import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -81,4 +78,4 @@ public class ErpCustomerRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java index e5958a841f..b1374e8248 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.order; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -152,4 +152,4 @@ public class ErpSaleOrderRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java index bc15a13398..a004ed3889 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/out/ErpSaleOutRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.out; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -145,4 +145,4 @@ public class ErpSaleOutRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java index ba52f4f80d..6a1ade0b33 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/returns/ErpSaleReturnRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.sale.vo.returns; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -145,4 +145,4 @@ public class ErpSaleReturnRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java index af53e3c726..6f9b8da0f8 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/check/ErpStockCheckRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.check; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; @@ -108,4 +108,4 @@ public class ErpStockCheckRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java index 077b9dd1b6..1d23f3692c 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/in/ErpStockInRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.in; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -107,4 +107,4 @@ public class ErpStockInRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java index 799ddc3f16..f5f7650aee 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/move/ErpStockMoveRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.move; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -104,4 +104,4 @@ public class ErpStockMoveRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java index 22a88e7c93..a1d0f51016 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/out/ErpStockOutRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.out; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -107,4 +107,4 @@ public class ErpStockOutRespVO { } -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java index ff4b3e12a1..47e1261831 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/record/ErpStockRecordRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.record; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.erp.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -84,4 +84,4 @@ public class ErpStockRecordRespVO { @ExcelProperty("创建人") private String creatorName; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java index 06366a0dd4..8d84789901 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/stock/ErpStockRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.stock; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -46,4 +46,4 @@ public class ErpStockRespVO { @ExcelProperty("仓库名称") private String warehouseName; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java index 188d426997..06825b848b 100644 --- a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java +++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/stock/vo/warehouse/ErpWarehouseRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.erp.controller.admin.stock.vo.warehouse; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -61,4 +61,4 @@ public class ErpWarehouseRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml b/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml index 699286b5da..a88fb3a88c 100644 --- a/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml +++ b/yudao-module-erp/src/main/resources/mapper/statistics/ErpPurchaseStatisticsMapper.xml @@ -10,7 +10,9 @@ AND in_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) - (SELECT IFNULL(SUM(total_price), 0) FROM erp_purchase_return @@ -18,7 +20,9 @@ AND return_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) diff --git a/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml b/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml index 324cbd432b..3e04ee706e 100644 --- a/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml +++ b/yudao-module-erp/src/main/resources/mapper/statistics/ErpSaleStatisticsMapper.xml @@ -10,7 +10,9 @@ AND out_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) - (SELECT IFNULL(SUM(total_price), 0) FROM erp_sale_return @@ -18,7 +20,9 @@ AND return_time < #{endTime} - AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getRequiredTenantId()} + + AND tenant_id = ${@cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder@getTenantId()} + AND deleted = 0) diff --git a/yudao-module-infra/pom.xml b/yudao-module-infra/pom.xml index 8b983cfe4b..be053b5cae 100644 --- a/yudao-module-infra/pom.xml +++ b/yudao-module-infra/pom.xml @@ -92,6 +92,7 @@ de.codecentric spring-boot-admin-starter-server + true diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java index cc060aafa1..52950537c3 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/api/websocket/WebSocketSenderApiImpl.java @@ -1,10 +1,9 @@ package cn.iocoder.yudao.module.infra.api.websocket; import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; - /** * WebSocket 发送器的 API 实现类 * @@ -13,7 +12,8 @@ import jakarta.annotation.Resource; @Component public class WebSocketSenderApiImpl implements WebSocketSenderApi { - @Resource + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 yudao.websocket.enable 配置项,可以关闭 WebSocket 的功能,所以这里只能不强制注入 private WebSocketMessageSender webSocketMessageSender; @Override diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java index b4f642f697..622bb04269 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/config/vo/ConfigRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.config.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java index 17ee9fef84..df0ef60401 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -43,4 +43,4 @@ public class Demo01ContactRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java index 1f2efd46a7..bac533bca6 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -28,4 +28,4 @@ public class Demo02CategoryRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java index 84dfe6e037..30944843f5 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/erp/vo/Demo03StudentErpRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.erp.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +39,4 @@ public class Demo03StudentErpRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java index 48d5e4889a..3db315b3ea 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/inner/vo/Demo03StudentInnerRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.inner.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +39,4 @@ public class Demo03StudentInnerRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java index e36a7965c1..f100c80dce 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/normal/vo/Demo03StudentNormalRespVO.java @@ -2,8 +2,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.normal.vo; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -39,4 +39,4 @@ public class Demo03StudentNormalRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java index 4096f477e3..44e8b65d76 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/file/vo/file/FileUploadReqVO.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.infra.controller.admin.file.vo.file; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @@ -16,4 +19,10 @@ public class FileUploadReqVO { @Schema(description = "文件目录", example = "XXX/YYY") private String directory; + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java index aee0d9bcf1..9a4be2eafe 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/job/JobRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.job.vo.job; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java index 543339d695..3574e98d59 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/job/vo/log/JobLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.job.vo.log; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java index d9e65c403c..45fc4df130 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apiaccesslog/ApiAccessLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apiaccesslog; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java index a3f6f0e84c..7097924e96 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/logger/vo/apierrorlog/ApiErrorLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.infra.controller.admin.logger.vo.apierrorlog; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.infra.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java index fde120a067..d10a21cc49 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/controller/app/file/vo/AppFileUploadReqVO.java @@ -1,6 +1,9 @@ package cn.iocoder.yudao.module.infra.controller.app.file.vo; +import cn.hutool.core.util.StrUtil; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotNull; import lombok.Data; import org.springframework.web.multipart.MultipartFile; @@ -16,4 +19,10 @@ public class AppFileUploadReqVO { @Schema(description = "文件目录", example = "XXX/YYY") private String directory; + @AssertTrue(message = "文件目录不正确") + @JsonIgnore + public boolean isDirectoryValid() { + return !StrUtil.containsAny(directory, "..", "/", "\\"); + } + } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java index e2c607842f..9cc4175884 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/framework/file/core/utils/FileTypeUtils.java @@ -3,7 +3,6 @@ package cn.iocoder.yudao.module.infra.framework.file.core.utils; import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.util.http.HttpUtils; -import com.alibaba.ttl.TransmittableThreadLocal; import jakarta.servlet.http.HttpServletResponse; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -86,8 +85,8 @@ public class FileTypeUtils { response.setContentType(contentType); // 针对 video 的特殊处理,解决视频地址在移动端播放的兼容性问题 if (StrUtil.containsIgnoreCase(contentType, "video")) { - response.setHeader("Content-Length", String.valueOf(content.length - 1)); - response.setHeader("Content-Range", String.valueOf(content.length - 1)); + response.setHeader("Content-Length", String.valueOf(content.length)); + response.setHeader("Content-Range", "bytes 0-" + (content.length - 1) + "/" + content.length); response.setHeader("Accept-Ranges", "bytes"); } // 输出附件 diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index b69f0c6254..a0ce062cdc 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -139,85 +139,85 @@ public class CodegenEngine { vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // VUE3_VBEN2_ANTD_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN2_ANTD_SCHEMA.getType(), vue3VbenTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) // VUE3_VBEN5_ANTD_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_SCHEMA.getType(), vue3Vben5AntdSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) // VUE3_VBEN5_ANTD .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_ANTD_GENERAL.getType(), vue3Vben5AntdGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) // VUE3_VBEN5_EP_SCHEMA .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/data.ts"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/data.ts")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/data.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_SCHEMA.getType(), vue3Vben5EpSchemaTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) // VUE3_VBEN5_EP .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/index.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/index.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/form.vue"), - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("api/api.ts"), - vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) + vue3VbenFilePath("api/${table.moduleName}/${table.businessName}/index.ts")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_normal.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/form_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-form.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/list_sub_inner.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .put(CodegenFrontTypeEnum.VUE3_VBEN5_EP_GENERAL.getType(), vue3Vben5EpGeneralTemplatePath("views/modules/list_sub_erp.vue"), // 特殊:主子表专属逻辑 - vue3FilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) + vue3VbenFilePath("views/${table.moduleName}/${table.businessName}/modules/${subSimpleClassName_strikeCase}-list.vue")) .build(); @Resource @@ -614,6 +614,11 @@ public class CodegenEngine { "src/" + path; } + private static String vue3VbenFilePath(String path) { + return "yudao-ui-${sceneEnum.basePackage}-vben/" + // 顶级目录 + "src/" + path; + } + private static String vue3VbenTemplatePath(String path) { return "codegen/vue3_vben/" + path + ".vm"; } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java index e91c098941..536b265f93 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -40,11 +40,16 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService { ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); - if (TenantContextHolder.getTenantId() != null) { - apiErrorLogMapper.insert(apiErrorLog); - } else { - // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败! - TenantUtils.executeIgnore(() -> apiErrorLogMapper.insert(apiErrorLog)); + try { + if (TenantContextHolder.getTenantId() != null) { + apiErrorLogMapper.insert(apiErrorLog); + } else { + // 极端情况下,上下文中没有租户时,此时忽略租户上下文,避免插入失败! + TenantUtils.executeIgnore(() -> apiErrorLogMapper.insert(apiErrorLog)); + } + } catch (Exception ex) { + // 兜底处理,目前只有 yudao-cloud 会发生:https://gitee.com/yudaocode/yudao-cloud-mini/issues/IC1O0A + log.error("[createApiErrorLog][记录时({}) 发生异常]", createDTO, ex); } } diff --git a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java index a3b71505bc..f0bdc69325 100644 --- a/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java +++ b/yudao-module-infra/src/main/java/cn/iocoder/yudao/module/infra/websocket/DemoWebSocketMessageListener.java @@ -6,11 +6,10 @@ import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; import cn.iocoder.yudao.framework.websocket.core.util.WebSocketFrameworkUtils; import cn.iocoder.yudao.module.infra.websocket.message.DemoReceiveMessage; import cn.iocoder.yudao.module.infra.websocket.message.DemoSendMessage; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.socket.WebSocketSession; -import jakarta.annotation.Resource; - /** * WebSocket 示例:单发消息 * @@ -19,7 +18,8 @@ import jakarta.annotation.Resource; @Component public class DemoWebSocketMessageListener implements WebSocketMessageListener { - @Resource + @SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection") + @Autowired(required = false) // 由于 yudao.websocket.enable 配置项,可以关闭 WebSocket 的功能,所以这里只能不强制注入 private WebSocketMessageSender webSocketMessageSender; @Override diff --git a/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm index 24c3519451..eae083a893 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/controller/vo/respVO.vm @@ -19,7 +19,7 @@ import java.time.LocalDateTime; #end #end ## 处理 Excel 导出 -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; #foreach ($column in $columns) #if ("$!column.dictType" != "")## 有设置数据字典 import ${DictFormatClassName}; @@ -50,4 +50,4 @@ public class ${sceneEnum.prefixClass}${table.className}RespVO { #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm b/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm index baf53f5986..f15ada4309 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/dal/do.vm @@ -15,7 +15,7 @@ import ${BaseDOClassName}; ## 处理 Excel 导出 + Schema 注解(仅 DO 模式) #if ($voType == 20) import io.swagger.v3.oas.annotations.media.Schema; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; #foreach ($column in $columns) #if ("$!column.dictType" != "")## 有设置数据字典 import ${DictFormatClassName}; @@ -100,4 +100,4 @@ public class ${table.className}DO extends BaseDO { #end #end -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm b/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm index ee9fc49f97..42afdf90ae 100644 --- a/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm +++ b/yudao-module-infra/src/main/resources/codegen/java/service/serviceImpl.vm @@ -316,7 +316,7 @@ public class ${table.className}ServiceImpl implements ${table.className}Service } // 插入 #end - ${subClassNameVar}.clean() // 清理掉创建、更新时间等相关属性值 + ${subClassNameVar}.clean(); // 清理掉创建、更新时间等相关属性值 ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar}); return ${subClassNameVar}.getId(); } diff --git a/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm index c0dddc0767..f9fbb9787b 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm @@ -138,6 +138,7 @@ watch( () => props.${subJoinColumn.javaField}, (val: number) => { if (!val) { + list.value = [] // 清空列表 return } queryParams.${subJoinColumn.javaField} = val diff --git a/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm index f6ce0e50c7..851bc2b5e4 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3/views/index.vue.vm @@ -353,6 +353,7 @@ const handleDelete = async (id: number) => { // 发起删除 await ${simpleClassName}Api.delete${simpleClassName}(id) message.success(t('common.delSuccess')) + currentRow.value = {} // 刷新列表 await getList() } catch {} diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm index fdac956ff7..635e12ac24 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/index.vue.vm @@ -180,7 +180,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ #end }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>, diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm index 97404b0211..4001ed3992 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_antd/schema/views/modules/list_sub_erp.vue.vm @@ -131,7 +131,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ enabled: true, }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, #else diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm index 3bb2b6a4a1..f9232d6b50 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/index.vue.vm @@ -174,7 +174,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ #end }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, } as VxeTableGridOptions<${simpleClassName}Api.${simpleClassName}>, diff --git a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm index 4dd5a6f1b4..5afb9c7a0d 100644 --- a/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm +++ b/yudao-module-infra/src/main/resources/codegen/vue3_vben5_ele/schema/views/modules/list_sub_erp.vue.vm @@ -125,7 +125,7 @@ const [Grid, gridApi] = useVbenVxeGrid({ enabled: true, }, toolbarConfig: { - refresh: { code: 'query' }, + refresh: true, search: true, }, #else diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_erp/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_inner/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_master_normal/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_one/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO index 6325d866c5..335881c4b7 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue2_tree/java/InfraCategoryRespVO @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.util.*; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; @Schema(description = "管理后台 - 分类 Response VO") @Data @@ -23,4 +23,4 @@ public class InfraCategoryRespVO { @ExcelProperty("父编号") private Long parentId; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_erp/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_inner/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_master_normal/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO index c41a5501fe..f86f6791c4 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_one/java/InfraStudentRespVO @@ -6,7 +6,7 @@ import java.util.*; import java.util.*; import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; @@ -57,4 +57,4 @@ public class InfraStudentRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO index 6325d866c5..335881c4b7 100644 --- a/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO +++ b/yudao-module-infra/src/test/resources/codegen/windows10/vue3_tree/java/InfraCategoryRespVO @@ -4,7 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.util.*; import java.util.*; -import com.alibaba.excel.annotation.*; +import cn.idev.excel.annotation.*; @Schema(description = "管理后台 - 分类 Response VO") @Data @@ -23,4 +23,4 @@ public class InfraCategoryRespVO { @ExcelProperty("父编号") private Long parentId; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java index 710e74263d..74585be565 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceImportExcelVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.AllArgsConstructor; @@ -34,4 +34,4 @@ public class IotDeviceImportExcelVO { @ExcelProperty("设备分组") private String groupNames; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java index 7b4e498802..ecb8f81c45 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/device/vo/device/IotDeviceRespVO.java @@ -1,10 +1,10 @@ package cn.iocoder.yudao.module.iot.controller.admin.device.vo.device; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java index 8c8f013215..99effda1d1 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/controller/admin/product/vo/product/IotProductRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.iot.controller.admin.product.vo.product; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.iot.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -75,4 +75,4 @@ public class IotProductRespVO { @ExcelProperty("创建时间") private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java index ccb0a680b1..01d1c45eee 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/message/IotDeviceMessageServiceImpl.java @@ -236,7 +236,8 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { @Override public Long getDeviceMessageCount(LocalDateTime createTime) { - return deviceMessageMapper.selectCountByCreateTime(createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); + return deviceMessageMapper.selectCountByCreateTime( + createTime != null ? LocalDateTimeUtil.toEpochMilli(createTime) : null); } @Override @@ -244,10 +245,12 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { IotStatisticsDeviceMessageReqVO reqVO) { // 1. 按小时统计,获取分项统计数据 List> countList = deviceMessageMapper.selectDeviceMessageCountGroupByDate( - LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[0]), + LocalDateTimeUtil.toEpochMilli(reqVO.getTimes()[1])); // 2. 按照日期间隔,合并数据 - List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], reqVO.getInterval()); + List timeRanges = LocalDateTimeUtils.getDateRangeList(reqVO.getTimes()[0], reqVO.getTimes()[1], + reqVO.getInterval()); return convertList(timeRanges, times -> { Integer upstreamCount = countList.stream() .filter(vo -> LocalDateTimeUtils.isBetween(times[0], times[1], (Timestamp) vo.get("time"))) diff --git a/yudao-module-iot/yudao-module-iot-gateway/pom.xml b/yudao-module-iot/yudao-module-iot-gateway/pom.xml index d156d38c35..3c2b1fc642 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/pom.xml +++ b/yudao-module-iot/yudao-module-iot-gateway/pom.xml @@ -47,6 +47,13 @@ io.vertx vertx-mqtt + + + + cn.iocoder.boot + yudao-spring-boot-starter-test + test + diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java index 300b2e48ec..9086480d3f 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/alink/IotAlinkDeviceMessageCodec.java @@ -18,6 +18,8 @@ import org.springframework.stereotype.Component; @Component public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { + private static final String TYPE = "Alink"; + @Data @NoArgsConstructor @AllArgsConstructor @@ -62,6 +64,11 @@ public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { } + @Override + public String type() { + return TYPE; + } + @Override public byte[] encode(IotDeviceMessage message) { AlinkMessage alinkMessage = new AlinkMessage(message.getRequestId(), AlinkMessage.VERSION_1, @@ -79,9 +86,4 @@ public class IotAlinkDeviceMessageCodec implements IotDeviceMessageCodec { alinkMessage.getData(), alinkMessage.getCode(), alinkMessage.getMsg()); } - @Override - public String type() { - return "Alink"; - } - } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java new file mode 100644 index 0000000000..8279ca2471 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpBinaryDeviceMessageCodec.java @@ -0,0 +1,351 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import io.vertx.core.buffer.Buffer; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; + +/** + * TCP 二进制格式 {@link IotDeviceMessage} 编解码器 + *

+ * 二进制协议格式(所有数值使用大端序): + * + *

+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * | 魔术字 | 版本号 | 消息类型| 消息标志|         消息长度(4 字节)          |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |           消息 ID 长度(2 字节)        |      消息 ID (变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |           方法名长度(2 字节)        |      方法名(变长字符串)         |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * |                        消息体数据(变长)                              |
+ * +--------+--------+--------+--------+--------+--------+--------+--------+
+ * 
+ *

+ * 消息体格式: + * - 请求消息:params 数据(JSON) + * - 响应消息:code (4字节) + msg 长度(2字节) + msg 字符串 + data 数据(JSON) + *

+ * 注意:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpBinaryDeviceMessageCodec implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP_BINARY"; + + // ==================== 协议常量 ==================== + + /** + * 协议魔术字,用于协议识别 + */ + private static final byte MAGIC_NUMBER = (byte) 0x7E; + + /** + * 协议版本号 + */ + private static final byte PROTOCOL_VERSION = (byte) 0x01; + + // TODO @haohao:这个要不直接静态枚举,不用 MessageType + /** + * 消息类型常量 + */ + public static class MessageType { + + /** + * 请求消息 + */ + public static final byte REQUEST = 0x01; + /** + * 响应消息 + */ + public static final byte RESPONSE = 0x02; + + } + + /** + * 协议头部固定长度(魔术字 + 版本号 + 消息类型 + 消息标志 + 消息长度) + */ + private static final int HEADER_FIXED_LENGTH = 8; + + /** + * 最小消息长度(头部 + 消息ID长度 + 方法名长度) + */ + private static final int MIN_MESSAGE_LENGTH = HEADER_FIXED_LENGTH + 4; + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + Assert.notNull(message, "消息不能为空"); + Assert.notBlank(message.getMethod(), "消息方法不能为空"); + try { + // 1. 确定消息类型 + byte messageType = determineMessageType(message); + // 2. 构建消息体 + byte[] bodyData = buildMessageBody(message, messageType); + // 3. 构建完整消息(不包含deviceId,由连接上下文管理) + return buildCompleteMessage(message, messageType, bodyData); + } catch (Exception e) { + log.error("[encode][TCP 二进制消息编码失败,消息: {}]", message, e); + throw new RuntimeException("TCP 二进制消息编码失败: " + e.getMessage(), e); + } + } + + @Override + public IotDeviceMessage decode(byte[] bytes) { + Assert.notNull(bytes, "待解码数据不能为空"); + Assert.isTrue(bytes.length >= MIN_MESSAGE_LENGTH, "数据包长度不足"); + try { + Buffer buffer = Buffer.buffer(bytes); + // 1. 解析协议头部 + ProtocolHeader header = parseProtocolHeader(buffer); + // 2. 解析消息内容(不包含deviceId,由上层连接管理器设置) + return parseMessageContent(buffer, header); + } catch (Exception e) { + log.error("[decode][TCP 二进制消息解码失败,数据长度: {}]", bytes.length, e); + throw new RuntimeException("TCP 二进制消息解码失败: " + e.getMessage(), e); + } + } + + // ==================== 编码相关方法 ==================== + + /** + * 确定消息类型 + * 优化后的判断逻辑:有响应字段就是响应消息,否则就是请求消息 + */ + private byte determineMessageType(IotDeviceMessage message) { + // 判断是否为响应消息:有响应码或响应消息时为响应 + // TODO @haohao:感觉只判断 code 更稳妥点?msg 有可能空。。。 + if (message.getCode() != null || StrUtil.isNotBlank(message.getMsg())) { + return MessageType.RESPONSE; + } + // 默认为请求消息 + return MessageType.REQUEST; + } + + /** + * 构建消息体 + */ + private byte[] buildMessageBody(IotDeviceMessage message, byte messageType) { + Buffer bodyBuffer = Buffer.buffer(); + if (messageType == MessageType.RESPONSE) { + // code + bodyBuffer.appendInt(message.getCode() != null ? message.getCode() : 0); + // msg + String msg = message.getMsg() != null ? message.getMsg() : ""; + byte[] msgBytes = msg.getBytes(StandardCharsets.UTF_8); + bodyBuffer.appendShort((short) msgBytes.length); + bodyBuffer.appendBytes(msgBytes); + // data + if (message.getData() != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(message.getData())); + } + } else { + // params + // TODO @haohao:请求是不是只处理 message.getParams() 哈? + Object payload = message.getParams() != null ? message.getParams() : message.getData(); + if (payload != null) { + bodyBuffer.appendBytes(JsonUtils.toJsonByte(payload)); + } + } + return bodyBuffer.getBytes(); + } + + /** + * 构建完整消息 + */ + private byte[] buildCompleteMessage(IotDeviceMessage message, byte messageType, byte[] bodyData) { + Buffer buffer = Buffer.buffer(); + // 1. 写入协议头部 + buffer.appendByte(MAGIC_NUMBER); + buffer.appendByte(PROTOCOL_VERSION); + buffer.appendByte(messageType); + buffer.appendByte((byte) 0x00); // 消息标志,预留字段 TODO @haohao:这个标识的作用是啥呀? + // 2. 预留消息长度位置(在 6. 更新消息长度) + int lengthPosition = buffer.length(); + buffer.appendInt(0); + // 3. 写入消息 ID + String messageId = StrUtil.isNotBlank(message.getRequestId()) ? message.getRequestId() + // TODO @haohao:复用 IotDeviceMessageUtils 的 generateMessageId 哇? + : generateMessageId(message.getMethod()); + // TODO @haohao:StrUtil.utf8Bytes() + byte[] messageIdBytes = messageId.getBytes(StandardCharsets.UTF_8); + buffer.appendShort((short) messageIdBytes.length); + buffer.appendBytes(messageIdBytes); + // 4. 写入方法名 + byte[] methodBytes = message.getMethod().getBytes(StandardCharsets.UTF_8); + buffer.appendShort((short) methodBytes.length); + buffer.appendBytes(methodBytes); + // 5. 写入消息体 + buffer.appendBytes(bodyData); + // 6. 更新消息长度 + buffer.setInt(lengthPosition, buffer.length()); + return buffer.getBytes(); + } + + /** + * 生成消息 ID + */ + private String generateMessageId(String method) { + return method + "_" + System.currentTimeMillis() + "_" + (int) (Math.random() * 1000); + } + + // ==================== 解码相关方法 ==================== + + // TODO @haohao:是不是把 parseProtocolHeader、parseMessageContent 合并? + /** + * 解析协议头部 + */ + private ProtocolHeader parseProtocolHeader(Buffer buffer) { + int index = 0; + // 1. 验证魔术字 + byte magic = buffer.getByte(index++); + Assert.isTrue(magic == MAGIC_NUMBER, "无效的协议魔术字: " + magic); + // 2. 验证版本号 + byte version = buffer.getByte(index++); + Assert.isTrue(version == PROTOCOL_VERSION, "不支持的协议版本: " + version); + + // 3. 读取消息类型 + byte messageType = buffer.getByte(index++); + Assert.isTrue(isValidMessageType(messageType), "无效的消息类型: " + messageType); + // 4. 读取消息标志(暂时跳过) + byte messageFlags = buffer.getByte(index++); + + // 5. 读取消息长度 + int messageLength = buffer.getInt(index); + index += 4; + + Assert.isTrue(messageLength == buffer.length(), + "消息长度不匹配,期望: " + messageLength + ", 实际: " + buffer.length()); + + return new ProtocolHeader(magic, version, messageType, messageFlags, messageLength, index); + } + + /** + * 解析消息内容 + */ + private IotDeviceMessage parseMessageContent(Buffer buffer, ProtocolHeader header) { + int index = header.getNextIndex(); + + // 1. 读取消息 ID + short messageIdLength = buffer.getShort(index); + index += 2; + String messageId = buffer.getString(index, index + messageIdLength, StandardCharsets.UTF_8.name()); + index += messageIdLength; + + // 2. 读取方法名 + short methodLength = buffer.getShort(index); + index += 2; + String method = buffer.getString(index, index + methodLength, StandardCharsets.UTF_8.name()); + index += methodLength; + + // 3. 解析消息体 + return parseMessageBody(buffer, index, header.getMessageType(), messageId, method); + } + + /** + * 解析消息体 + */ + private IotDeviceMessage parseMessageBody(Buffer buffer, int startIndex, byte messageType, + String messageId, String method) { + if (startIndex >= buffer.length()) { + // 空消息体 + return IotDeviceMessage.of(messageId, method, null, null, null, null); + } + + if (messageType == MessageType.RESPONSE) { + // 响应消息:解析 code + msg + data + return parseResponseMessage(buffer, startIndex, messageId, method); + } else { + // 请求消息:解析 payload(可能是 params 或 data) + Object payload = parseJsonData(buffer, startIndex, buffer.length()); + return IotDeviceMessage.of(messageId, method, payload, null, null, null); + } + } + + /** + * 解析响应消息 + */ + private IotDeviceMessage parseResponseMessage(Buffer buffer, int startIndex, String messageId, String method) { + int index = startIndex; + + // 1. 读取响应码 + Integer code = buffer.getInt(index); + index += 4; + + // 2. 读取响应消息 + short msgLength = buffer.getShort(index); + index += 2; + String msg = msgLength > 0 ? buffer.getString(index, index + msgLength, StandardCharsets.UTF_8.name()) : null; + index += msgLength; + + // 3. 读取响应数据 + Object data = null; + if (index < buffer.length()) { + data = parseJsonData(buffer, index, buffer.length()); + } + + return IotDeviceMessage.of(messageId, method, null, data, code, msg); + } + + /** + * 解析JSON数据 + */ + private Object parseJsonData(Buffer buffer, int startIndex, int endIndex) { + if (startIndex >= endIndex) { + return null; + } + try { + String jsonStr = buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + return JsonUtils.parseObject(jsonStr, Object.class); + } catch (Exception e) { + log.warn("[parseJsonData][JSON 解析失败,返回原始字符串]", e); + return buffer.getString(startIndex, endIndex, StandardCharsets.UTF_8.name()); + } + } + + // ==================== 辅助方法 ==================== + + // TODO @haohao:这个貌似只用一次,可以考虑不抽小方法哈; + /** + * 验证消息类型是否有效 + */ + private boolean isValidMessageType(byte messageType) { + return messageType == MessageType.REQUEST || messageType == MessageType.RESPONSE; + } + + // ==================== 内部类 ==================== + + /** + * 协议头部信息 + */ + @Data + @AllArgsConstructor + private static class ProtocolHeader { + + private byte magic; + private byte version; + private byte messageType; + private byte messageFlags; + private int messageLength; + /** + * 指向消息内容开始位置 + */ + private int nextIndex; + + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java deleted file mode 100644 index 0bcef2e0cb..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpDeviceMessageCodec.java +++ /dev/null @@ -1,390 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.codec.tcp; - -import cn.hutool.core.util.ObjectUtil; -import cn.hutool.core.util.StrUtil; -import cn.hutool.json.JSONException; -import cn.hutool.json.JSONObject; -import cn.hutool.json.JSONUtil; -import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.nio.charset.StandardCharsets; -import java.util.Map; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -/** - * TCP {@link IotDeviceMessage} 编解码器 - *

- * 参考 EMQX 设计理念: - * 1. 高性能编解码 - * 2. 容错机制 - * 3. 缓存优化 - * 4. 监控统计 - * 5. 资源管理 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class IotTcpDeviceMessageCodec implements IotDeviceMessageCodec { - - /** - * 编解码器类型 - */ - public static final String TYPE = "tcp"; - - // ==================== 方法映射 ==================== - - /** - * 消息方法到功能码的映射 - */ - private static final Map METHOD_TO_CODE_MAP = new ConcurrentHashMap<>(); - - /** - * 功能码到消息方法的映射 - */ - private static final Map CODE_TO_METHOD_MAP = new ConcurrentHashMap<>(); - - static { - // 初始化方法映射 - initializeMethodMappings(); - } - - // ==================== 缓存管理 ==================== - - /** - * JSON 缓存,提升编解码性能 - */ - private final Map jsonCache = new ConcurrentHashMap<>(); - - /** - * 缓存最大大小 - */ - private static final int MAX_CACHE_SIZE = 1000; - - // ==================== 常量定义 ==================== - - /** - * 负载字段名 - */ - public static class PayloadField { - public static final String TIMESTAMP = "timestamp"; - public static final String MESSAGE_ID = "msgId"; - public static final String DEVICE_ID = "deviceId"; - public static final String PARAMS = "params"; - public static final String DATA = "data"; - public static final String CODE = "code"; - public static final String MESSAGE = "message"; - } - - /** - * 消息方法映射 - */ - public static class MessageMethod { - public static final String PROPERTY_POST = "thing.property.post"; - public static final String PROPERTY_SET = "thing.property.set"; - public static final String PROPERTY_GET = "thing.property.get"; - public static final String EVENT_POST = "thing.event.post"; - public static final String SERVICE_INVOKE = "thing.service.invoke"; - public static final String CONFIG_PUSH = "thing.config.push"; - public static final String OTA_UPGRADE = "thing.ota.upgrade"; - public static final String STATE_ONLINE = "thing.state.online"; - public static final String STATE_OFFLINE = "thing.state.offline"; - } - - // ==================== 初始化方法 ==================== - - /** - * 初始化方法映射 - */ - private static void initializeMethodMappings() { - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_POST, TcpDataPackage.CODE_DATA_UP); - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_SET, TcpDataPackage.CODE_PROPERTY_SET); - METHOD_TO_CODE_MAP.put(MessageMethod.PROPERTY_GET, TcpDataPackage.CODE_PROPERTY_GET); - METHOD_TO_CODE_MAP.put(MessageMethod.EVENT_POST, TcpDataPackage.CODE_EVENT_UP); - METHOD_TO_CODE_MAP.put(MessageMethod.SERVICE_INVOKE, TcpDataPackage.CODE_SERVICE_INVOKE); - METHOD_TO_CODE_MAP.put(MessageMethod.CONFIG_PUSH, TcpDataPackage.CODE_DATA_DOWN); - METHOD_TO_CODE_MAP.put(MessageMethod.OTA_UPGRADE, TcpDataPackage.CODE_DATA_DOWN); - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_ONLINE, TcpDataPackage.CODE_HEARTBEAT); - METHOD_TO_CODE_MAP.put(MessageMethod.STATE_OFFLINE, TcpDataPackage.CODE_HEARTBEAT); - - // 反向映射 - METHOD_TO_CODE_MAP.forEach((method, code) -> CODE_TO_METHOD_MAP.put(code, method)); - } - - // ==================== 编解码方法 ==================== - - @Override - public byte[] encode(IotDeviceMessage message) { - validateEncodeParams(message); - - try { - if (log.isDebugEnabled()) { - log.debug("[encode][开始编码 TCP 消息] 方法: {}, 消息ID: {}", - message.getMethod(), message.getRequestId()); - } - - // 1. 获取功能码 - short code = getCodeByMethodSafely(message.getMethod()); - - // 2. 构建负载 - String payload = buildPayloadOptimized(message); - - // 3. 构建 TCP 数据包 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr("") // 地址在发送时由调用方设置 - .code(code) - .mid((short) 0) // 消息序号在发送时由调用方设置 - .payload(payload) - .build(); - - // 4. 编码为字节流 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] result = buffer.getBytes(); - - // 5. 统计信息 - if (log.isDebugEnabled()) { - log.debug("[encode][TCP 消息编码成功] 方法: {}, 数据长度: {}", - message.getMethod(), result.length); - } - - return result; - - } catch (Exception e) { - log.error("[encode][TCP 消息编码失败] 消息: {}", message, e); - throw new TcpCodecException("TCP 消息编码失败", e); - } - } - - @Override - public IotDeviceMessage decode(byte[] bytes) { - validateDecodeParams(bytes); - - try { - if (log.isDebugEnabled()) { - log.debug("[decode][开始解码 TCP 消息] 数据长度: {}", bytes.length); - } - - // 1. 解码 TCP 数据包 - Buffer buffer = Buffer.buffer(bytes); - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - - // 2. 获取消息方法 - String method = getMethodByCodeSafely(dataPackage.getCode()); - - // 3. 解析负载数据 - Object params = parsePayloadOptimized(dataPackage.getPayload()); - - // 4. 构建 IoT 设备消息 - IotDeviceMessage message = IotDeviceMessage.requestOf(method, params); - - // 5. 统计信息 - if (log.isDebugEnabled()) { - log.debug("[decode][TCP 消息解码成功] 方法: {}, 功能码: {}", - method, dataPackage.getCode()); - } - - return message; - - } catch (Exception e) { - log.error("[decode][TCP 消息解码失败] 数据长度: {}, 数据内容: {}", - bytes.length, truncateData(bytes, 100), e); - throw new TcpCodecException("TCP 消息解码失败", e); - } - } - - @Override - public String type() { - return TYPE; - } - - // ==================== 内部辅助方法 ==================== - - /** - * 验证编码参数 - */ - private void validateEncodeParams(IotDeviceMessage message) { - if (Objects.isNull(message)) { - throw new IllegalArgumentException("IoT 设备消息不能为空"); - } - if (StrUtil.isEmpty(message.getMethod())) { - throw new IllegalArgumentException("消息方法不能为空"); - } - } - - /** - * 验证解码参数 - */ - private void validateDecodeParams(byte[] bytes) { - if (Objects.isNull(bytes) || bytes.length == 0) { - throw new IllegalArgumentException("待解码数据不能为空"); - } - if (bytes.length > 1024 * 1024) { // 1MB 限制 - throw new IllegalArgumentException("数据包过大,超过1MB限制"); - } - } - - /** - * 安全获取功能码 - */ - private short getCodeByMethodSafely(String method) { - Short code = METHOD_TO_CODE_MAP.get(method); - if (code == null) { - log.warn("[getCodeByMethodSafely][未知的消息方法: {},使用默认功能码]", method); - return TcpDataPackage.CODE_DATA_UP; // 默认为数据上报 - } - return code; - } - - /** - * 安全获取消息方法 - */ - private String getMethodByCodeSafely(short code) { - String method = CODE_TO_METHOD_MAP.get(code); - if (method == null) { - log.warn("[getMethodByCodeSafely][未知的功能码: {},使用默认方法]", code); - return MessageMethod.PROPERTY_POST; // 默认为属性上报 - } - return method; - } - - /** - * 优化的负载构建 - */ - private String buildPayloadOptimized(IotDeviceMessage message) { - // 使用缓存键 - String cacheKey = message.getMethod() + "_" + message.getRequestId(); - JSONObject cachedPayload = jsonCache.get(cacheKey); - - if (cachedPayload != null) { - // 更新时间戳 - cachedPayload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - return cachedPayload.toString(); - } - - // 创建新的负载 - JSONObject payload = new JSONObject(); - - // 添加基础字段 - addToPayloadIfNotNull(payload, PayloadField.MESSAGE_ID, message.getRequestId()); - addToPayloadIfNotNull(payload, PayloadField.DEVICE_ID, message.getDeviceId()); - addToPayloadIfNotNull(payload, PayloadField.PARAMS, message.getParams()); - addToPayloadIfNotNull(payload, PayloadField.DATA, message.getData()); - addToPayloadIfNotNull(payload, PayloadField.CODE, message.getCode()); - addToPayloadIfNotEmpty(payload, PayloadField.MESSAGE, message.getMsg()); - - // 添加时间戳 - payload.set(PayloadField.TIMESTAMP, System.currentTimeMillis()); - - // 缓存管理 - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(cacheKey, payload); - } else { - cleanJsonCacheIfNeeded(); - } - - return payload.toString(); - } - - /** - * 优化的负载解析 - */ - private Object parsePayloadOptimized(String payload) { - if (StrUtil.isEmpty(payload)) { - return null; - } - - try { - // 尝试从缓存获取 - JSONObject cachedJson = jsonCache.get(payload); - if (cachedJson != null) { - return cachedJson.containsKey(PayloadField.PARAMS) ? cachedJson.get(PayloadField.PARAMS) : cachedJson; - } - - // 解析 JSON 对象 - JSONObject jsonObject = JSONUtil.parseObj(payload); - - // 缓存解析结果 - if (jsonCache.size() < MAX_CACHE_SIZE) { - jsonCache.put(payload, jsonObject); - } - - return jsonObject.containsKey(PayloadField.PARAMS) ? jsonObject.get(PayloadField.PARAMS) : jsonObject; - - } catch (JSONException e) { - log.warn("[parsePayloadOptimized][负载解析为JSON失败,返回原始字符串] 负载: {}", payload); - return payload; - } catch (Exception e) { - log.error("[parsePayloadOptimized][负载解析异常] 负载: {}", payload, e); - return payload; - } - } - - /** - * 添加非空值到负载 - */ - private void addToPayloadIfNotNull(JSONObject json, String key, Object value) { - if (ObjectUtil.isNotNull(value)) { - json.set(key, value); - } - } - - /** - * 添加非空字符串到负载 - */ - private void addToPayloadIfNotEmpty(JSONObject json, String key, String value) { - if (StrUtil.isNotEmpty(value)) { - json.set(key, value); - } - } - - /** - * 清理JSON缓存 - */ - private void cleanJsonCacheIfNeeded() { - if (jsonCache.size() > MAX_CACHE_SIZE) { - // 清理一半的缓存 - int clearCount = jsonCache.size() / 2; - jsonCache.entrySet().removeIf(entry -> clearCount > 0 && Math.random() < 0.5); - - if (log.isDebugEnabled()) { - log.debug("[cleanJsonCacheIfNeeded][JSON 缓存已清理] 当前缓存大小: {}", jsonCache.size()); - } - } - } - - /** - * 截断数据用于日志输出 - */ - private String truncateData(byte[] data, int maxLength) { - if (data.length <= maxLength) { - return new String(data, StandardCharsets.UTF_8); - } - - byte[] truncated = new byte[maxLength]; - System.arraycopy(data, 0, truncated, 0, maxLength); - return new String(truncated, StandardCharsets.UTF_8) + "...(截断)"; - } - - // ==================== 自定义异常 ==================== - - /** - * TCP 编解码异常 - */ - public static class TcpCodecException extends RuntimeException { - public TcpCodecException(String message) { - super(message); - } - - public TcpCodecException(String message, Throwable cause) { - super(message, cause); - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java new file mode 100644 index 0000000000..8f31305f17 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/codec/tcp/IotTcpJsonDeviceMessageCodec.java @@ -0,0 +1,108 @@ +package cn.iocoder.yudao.module.iot.gateway.codec.tcp; + +import cn.hutool.core.lang.Assert; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; +import cn.iocoder.yudao.module.iot.gateway.codec.IotDeviceMessageCodec; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * TCP JSON 格式 {@link IotDeviceMessage} 编解码器 + * + * 采用纯 JSON 格式传输,格式如下: + * { + * "id": "消息 ID", + * "method": "消息方法", + * "params": {...}, // 请求参数 + * "data": {...}, // 响应结果 + * "code": 200, // 响应错误码 + * "msg": "success", // 响应提示 + * "timestamp": 时间戳 + * } + * + * @author 芋道源码 + */ +@Component +public class IotTcpJsonDeviceMessageCodec implements IotDeviceMessageCodec { + + public static final String TYPE = "TCP_JSON"; + + @Data + @NoArgsConstructor + @AllArgsConstructor + private static class TcpJsonMessage { + + /** + * 消息 ID,且每个消息 ID 在当前设备具有唯一性 + */ + private String id; + + /** + * 请求方法 + */ + private String method; + + /** + * 请求参数 + */ + private Object params; + + /** + * 响应结果 + */ + private Object data; + + /** + * 响应错误码 + */ + private Integer code; + + /** + * 响应提示 + */ + private String msg; + + /** + * 时间戳 + */ + private Long timestamp; + + } + + @Override + public String type() { + return TYPE; + } + + @Override + public byte[] encode(IotDeviceMessage message) { + TcpJsonMessage tcpJsonMessage = new TcpJsonMessage( + message.getRequestId(), + message.getMethod(), + message.getParams(), + message.getData(), + message.getCode(), + message.getMsg(), + System.currentTimeMillis()); + return JsonUtils.toJsonByte(tcpJsonMessage); + } + + @Override + @SuppressWarnings("DataFlowIssue") + public IotDeviceMessage decode(byte[] bytes) { + TcpJsonMessage tcpJsonMessage = JsonUtils.parseObject(bytes, TcpJsonMessage.class); + Assert.notNull(tcpJsonMessage, "消息不能为空"); + Assert.notBlank(tcpJsonMessage.getMethod(), "消息方法不能为空"); + return IotDeviceMessage.of( + tcpJsonMessage.getId(), + tcpJsonMessage.getMethod(), + tcpJsonMessage.getParams(), + tcpJsonMessage.getData(), + tcpJsonMessage.getCode(), + tcpJsonMessage.getMsg()); + } + +} diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java index de5f3426be..51af9bd3ce 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/config/IotGatewayConfiguration.java @@ -1,6 +1,5 @@ package cn.iocoder.yudao.module.iot.gateway.config; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxAuthEventProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.emqx.IotEmqxDownstreamSubscriber; @@ -9,7 +8,7 @@ import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpDownstreamSubscr import cn.iocoder.yudao.module.iot.gateway.protocol.http.IotHttpUpstreamProtocol; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpDownstreamSubscriber; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Vertx; @@ -89,28 +88,24 @@ public class IotGatewayConfiguration { return Vertx.vertx(); } - @Bean - public TcpDeviceConnectionManager tcpDeviceConnectionManager() { - return new TcpDeviceConnectionManager(); - } - @Bean public IotTcpUpstreamProtocol iotTcpUpstreamProtocol(IotGatewayProperties gatewayProperties, - TcpDeviceConnectionManager connectionManager, IotDeviceService deviceService, IotDeviceMessageService messageService, - IotDeviceCommonApi deviceApi, + IotTcpConnectionManager connectionManager, Vertx tcpVertx) { - return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), connectionManager, - deviceService, messageService, deviceApi, tcpVertx); + return new IotTcpUpstreamProtocol(gatewayProperties.getProtocol().getTcp(), + deviceService, messageService, connectionManager, tcpVertx); } @Bean public IotTcpDownstreamSubscriber iotTcpDownstreamSubscriber(IotTcpUpstreamProtocol protocolHandler, - TcpDeviceConnectionManager connectionManager, IotDeviceMessageService messageService, + IotDeviceService deviceService, + IotTcpConnectionManager connectionManager, IotMessageBus messageBus) { - return new IotTcpDownstreamSubscriber(protocolHandler, connectionManager, messageService, messageBus); + return new IotTcpDownstreamSubscriber(protocolHandler, messageService, deviceService, connectionManager, + messageBus); } } diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java index d5c916295c..e4d46b3af6 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpDownstreamSubscriber.java @@ -4,160 +4,64 @@ import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageBus; import cn.iocoder.yudao.module.iot.core.messagebus.core.IotMessageSubscriber; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpDownstreamHandler; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import jakarta.annotation.PostConstruct; -import jakarta.annotation.PreDestroy; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicLong; - /** * IoT 网关 TCP 下游订阅者:接收下行给设备的消息 - *

- * 参考 EMQX 设计理念: - * 1. 高性能消息路由 - * 2. 容错机制 - * 3. 状态监控 - * 4. 资源管理 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j +@RequiredArgsConstructor public class IotTcpDownstreamSubscriber implements IotMessageSubscriber { - private final IotTcpUpstreamProtocol protocolHandler; - - private final TcpDeviceConnectionManager connectionManager; + private final IotTcpUpstreamProtocol protocol; private final IotDeviceMessageService messageService; + private final IotDeviceService deviceService; + + private final IotTcpConnectionManager connectionManager; + private final IotMessageBus messageBus; - private volatile IotTcpDownstreamHandler downstreamHandler; - - private final AtomicBoolean initialized = new AtomicBoolean(false); - - private final AtomicLong processedMessages = new AtomicLong(0); - - private final AtomicLong failedMessages = new AtomicLong(0); + private IotTcpDownstreamHandler downstreamHandler; @PostConstruct public void init() { - if (!initialized.compareAndSet(false, true)) { - log.warn("[init][TCP 下游消息订阅者已初始化,跳过重复初始化]"); - return; - } + // 初始化下游处理器 + this.downstreamHandler = new IotTcpDownstreamHandler(messageService, deviceService, connectionManager); - try { - // 初始化下游处理器 - downstreamHandler = new IotTcpDownstreamHandler(connectionManager, messageService); - - // 注册到消息总线 - messageBus.register(this); - - log.info("[init][TCP 下游消息订阅者初始化完成] Topic: {}, Group: {}", - getTopic(), getGroup()); - } catch (Exception e) { - initialized.set(false); - log.error("[init][TCP 下游消息订阅者初始化失败]", e); - throw new RuntimeException("TCP 下游消息订阅者初始化失败", e); - } - } - - @PreDestroy - public void destroy() { - if (!initialized.get()) { - return; - } - - try { - log.info("[destroy][TCP 下游消息订阅者已关闭] 处理消息数: {}, 失败消息数: {}", - processedMessages.get(), failedMessages.get()); - } catch (Exception e) { - log.error("[destroy][TCP 下游消息订阅者关闭失败]", e); - } finally { - initialized.set(false); - } + messageBus.register(this); + log.info("[init][TCP 下游订阅者初始化完成,服务器 ID: {},Topic: {}]", + protocol.getServerId(), getTopic()); } @Override public String getTopic() { - return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocolHandler.getServerId()); + return IotDeviceMessageUtils.buildMessageBusGatewayDeviceMessageTopic(protocol.getServerId()); } @Override public String getGroup() { - return "tcp-downstream-" + protocolHandler.getServerId(); + // 保证点对点消费,需要保证独立的 Group,所以使用 Topic 作为 Group + return getTopic(); } @Override public void onMessage(IotDeviceMessage message) { - if (!initialized.get()) { - log.warn("[onMessage][订阅者未初始化,跳过消息处理]"); - return; - } - - long startTime = System.currentTimeMillis(); - try { - processedMessages.incrementAndGet(); - - if (log.isDebugEnabled()) { - log.debug("[onMessage][收到下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", - message.getDeviceId(), message.getMethod(), message.getId()); - } - - // 参数校验 - if (message.getDeviceId() == null) { - log.warn("[onMessage][下行消息设备ID为空,跳过处理] 消息: {}", message); - return; - } - - // 检查连接状态 - if (connectionManager.getClientByDeviceId(message.getDeviceId()) == null) { - log.warn("[onMessage][设备({})离线,跳过下行消息] 方法: {}", - message.getDeviceId(), message.getMethod()); - return; - } - - // 处理下行消息 downstreamHandler.handle(message); - - // 性能监控 - long processTime = System.currentTimeMillis() - startTime; - if (processTime > 1000) { // 超过1秒的慢消息 - log.warn("[onMessage][慢消息处理] 设备ID: {}, 方法: {}, 耗时: {}ms", - message.getDeviceId(), message.getMethod(), processTime); - } - } catch (Exception e) { - failedMessages.incrementAndGet(); - log.error("[onMessage][处理下行消息失败] 设备ID: {}, 方法: {}, 消息: {}", - message.getDeviceId(), message.getMethod(), message, e); + log.error("[onMessage][处理下行消息失败,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId(), e); } } - /** - * 获取订阅者统计信息 - */ - public String getSubscriberStatistics() { - return String.format("TCP下游订阅者 - 已处理: %d, 失败: %d, 成功率: %.2f%%", - processedMessages.get(), - failedMessages.get(), - processedMessages.get() > 0 - ? (double) (processedMessages.get() - failedMessages.get()) / processedMessages.get() * 100 - : 0.0); - } - - /** - * 检查订阅者健康状态 - */ - public boolean isHealthy() { - return initialized.get() && downstreamHandler != null; - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java index c42fe19300..791c6cbfc2 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/IotTcpUpstreamProtocol.java @@ -1,9 +1,8 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp; -import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; import cn.iocoder.yudao.module.iot.core.util.IotDeviceMessageUtils; import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router.IotTcpUpstreamHandler; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; @@ -16,19 +15,8 @@ import jakarta.annotation.PreDestroy; import lombok.Getter; import lombok.extern.slf4j.Slf4j; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicReference; - /** * IoT 网关 TCP 协议:接收设备上行消息 - *

- * 负责接收设备上行消息,支持: - * 1. 设备注册 - * 2. 心跳保活 - * 3. 属性上报 - * 4. 事件上报 - * 5. 设备连接管理 * * @author 芋道源码 */ @@ -37,69 +25,41 @@ public class IotTcpUpstreamProtocol { private final IotGatewayProperties.TcpProperties tcpProperties; - private final TcpDeviceConnectionManager connectionManager; - private final IotDeviceService deviceService; private final IotDeviceMessageService messageService; - private final IotDeviceCommonApi deviceApi; + private final IotTcpConnectionManager connectionManager; private final Vertx vertx; @Getter private final String serverId; - private NetServer netServer; + private NetServer tcpServer; public IotTcpUpstreamProtocol(IotGatewayProperties.TcpProperties tcpProperties, - TcpDeviceConnectionManager connectionManager, IotDeviceService deviceService, IotDeviceMessageService messageService, - IotDeviceCommonApi deviceApi, + IotTcpConnectionManager connectionManager, Vertx vertx) { this.tcpProperties = tcpProperties; - this.connectionManager = connectionManager; this.deviceService = deviceService; this.messageService = messageService; - this.deviceApi = deviceApi; + this.connectionManager = connectionManager; this.vertx = vertx; this.serverId = IotDeviceMessageUtils.generateServerId(tcpProperties.getPort()); } @PostConstruct public void start() { - // 1. 启动 TCP 服务器 - try { - startTcpServer(); - log.info("[start][IoT 网关 TCP 协议处理器启动完成,服务器ID: {}]", serverId); - } catch (Exception e) { - log.error("[start][IoT 网关 TCP 协议处理器启动失败]", e); - // 抛出异常,中断 Spring 容器启动 - throw new RuntimeException("IoT 网关 TCP 协议处理器启动失败", e); - } - } - - @PreDestroy - public void stop() { - if (netServer != null) { - stopTcpServer(); - log.info("[stop][IoT 网关 TCP 协议处理器已停止]"); - } - } - - /** - * 启动 TCP 服务器 - */ - private void startTcpServer() { - // 1. 创建服务器选项 + // 创建服务器选项 NetServerOptions options = new NetServerOptions() .setPort(tcpProperties.getPort()) .setTcpKeepAlive(true) .setTcpNoDelay(true) .setReuseAddress(true); - - // 2. 配置 SSL(如果启用) + // 配置 SSL(如果启用) if (Boolean.TRUE.equals(tcpProperties.getSslEnabled())) { PemKeyCertOptions pemKeyCertOptions = new PemKeyCertOptions() .setKeyPath(tcpProperties.getSslKeyPath()) @@ -107,72 +67,33 @@ public class IotTcpUpstreamProtocol { options.setSsl(true).setKeyCertOptions(pemKeyCertOptions); } - // 3. 创建 TCP 服务器 - netServer = vertx.createNetServer(options); - - // 4. 设置连接处理器 - netServer.connectHandler(socket -> { - log.info("[startTcpServer][新设备连接: {}]", socket.remoteAddress()); - IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler( - tcpProperties, connectionManager, deviceService, messageService, deviceApi, serverId); + // 创建服务器并设置连接处理器 + tcpServer = vertx.createNetServer(options); + tcpServer.connectHandler(socket -> { + IotTcpUpstreamHandler handler = new IotTcpUpstreamHandler(this, messageService, deviceService, + connectionManager); handler.handle(socket); }); - // 5. 同步启动服务器,等待结果 - CountDownLatch latch = new CountDownLatch(1); - AtomicReference failure = new AtomicReference<>(); - netServer.listen(result -> { - if (result.succeeded()) { - log.info("[startTcpServer][TCP 服务器启动成功] 端口: {}, 服务器ID: {}", - result.result().actualPort(), serverId); - } else { - log.error("[startTcpServer][TCP 服务器启动失败]", result.cause()); - failure.set(result.cause()); - } - latch.countDown(); - }); - - // 6. 等待启动结果,设置超时 + // 启动服务器 try { - if (!latch.await(10, TimeUnit.SECONDS)) { - throw new RuntimeException("TCP 服务器启动超时"); - } - if (failure.get() != null) { - throw new RuntimeException("TCP 服务器启动失败", failure.get()); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new RuntimeException("TCP 服务器启动被中断", e); + tcpServer.listen().result(); + log.info("[start][IoT 网关 TCP 协议启动成功,端口:{}]", tcpProperties.getPort()); + } catch (Exception e) { + log.error("[start][IoT 网关 TCP 协议启动失败]", e); + throw e; } } - /** - * 停止 TCP 服务器 - */ - private void stopTcpServer() { - if (netServer == null) { - return; - } - log.info("[stopTcpServer][准备关闭 TCP 服务器]"); - CountDownLatch latch = new CountDownLatch(1); - // 异步关闭,并使用 Latch 等待结果 - netServer.close(result -> { - if (result.succeeded()) { - log.info("[stopTcpServer][IoT 网关 TCP 协议处理器已停止]"); - } else { - log.warn("[stopTcpServer][TCP 服务器关闭失败]", result.cause()); + @PreDestroy + public void stop() { + if (tcpServer != null) { + try { + tcpServer.close().result(); + log.info("[stop][IoT 网关 TCP 协议已停止]"); + } catch (Exception e) { + log.error("[stop][IoT 网关 TCP 协议停止失败]", e); } - latch.countDown(); - }); - - try { - // 等待关闭完成,设置超时 - if (!latch.await(10, TimeUnit.SECONDS)) { - log.warn("[stopTcpServer][关闭 TCP 服务器超时]"); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - log.warn("[stopTcpServer][等待 TCP 服务器关闭被中断]", e); } } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java deleted file mode 100644 index eb353a457a..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/client/TcpDeviceClient.java +++ /dev/null @@ -1,218 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client; - -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import io.vertx.core.parsetools.RecordParser; -import lombok.Getter; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.atomic.AtomicBoolean; - -/** - * TCP 设备客户端 - *

- * 封装设备连接的基本信息和操作。 - * 该类中的状态变更(如 authenticated, closed)使用 AtomicBoolean 保证原子性。 - * 对 socket 的操作应在 Vert.x Event Loop 线程中执行,以避免并发问题。 - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDeviceClient { - - @Getter - private final String clientId; - - @Getter - @Setter - private String deviceAddr; // 从 final 移除,因为在注册后才设置 - - @Getter - @Setter - private String productKey; - - @Getter - @Setter - private String deviceName; - - @Getter - @Setter - private Long deviceId; - - @Getter - private NetSocket socket; - - @Getter - @Setter - private RecordParser parser; - - @Getter - private final long keepAliveTimeoutMs; // 改为 final,通过构造函数注入 - - private volatile long lastKeepAliveTime; - - private final AtomicBoolean authenticated = new AtomicBoolean(false); - private final AtomicBoolean closed = new AtomicBoolean(false); - - /** - * 构造函数 - * - * @param clientId 客户端ID,全局唯一 - * @param keepAliveTimeoutMs 心跳超时时间(毫秒),从配置中读取 - */ - public TcpDeviceClient(String clientId, long keepAliveTimeoutMs) { - this.clientId = clientId; - this.keepAliveTimeoutMs = keepAliveTimeoutMs; - this.lastKeepAliveTime = System.currentTimeMillis(); - } - - /** - * 绑定网络套接字,并设置相关处理器。 - * 此方法应在 Vert.x Event Loop 线程中调用。 - * - * @param socket 网络套接字 - */ - public void setSocket(NetSocket socket) { - // 无需 synchronized,Vert.x 保证了同一个 socket 的事件在同一个 Event Loop 中处理 - if (this.socket != null && this.socket != socket) { - log.warn("[setSocket][客户端({})] 正在用新的 socket 替换旧的,旧 socket 将被关闭。", clientId); - this.socket.close(); - } - - this.socket = socket; - - if (socket != null) { - // 1. 设置关闭处理器 - socket.closeHandler(v -> { - log.info("[setSocket][设备客户端({})的连接已由远端关闭]", clientId); - shutdown(); // 统一调用 shutdown 进行资源清理 - }); - - // 2. 设置异常处理器 - socket.exceptionHandler(e -> { - log.error("[setSocket][设备客户端({})连接出现异常]", clientId, e); - shutdown(); // 出现异常时也关闭连接 - }); - - // 3. 设置数据处理器 - socket.handler(buffer -> { - // 任何数据往来都表示连接是活跃的 - keepAlive(); - - if (parser != null) { - parser.handle(buffer); - } else { - log.warn("[setSocket][设备客户端({})] 未设置解析器(parser),原始数据被忽略: {}", clientId, buffer.toString()); - } - }); - } - } - - /** - * 更新心跳时间,表示设备仍然活跃。 - */ - public void keepAlive() { - this.lastKeepAliveTime = System.currentTimeMillis(); - } - - /** - * 检查连接是否在线。 - * 判断标准:未被主动关闭、socket 存在、且在心跳超时时间内。 - * - * @return 是否在线 - */ - public boolean isOnline() { - if (closed.get() || socket == null) { - return false; - } - long idleTime = System.currentTimeMillis() - lastKeepAliveTime; - return idleTime < keepAliveTimeoutMs; - } - - public boolean isAuthenticated() { - return authenticated.get(); - } - - public void setAuthenticated(boolean authenticated) { - this.authenticated.set(authenticated); - } - - /** - * 向设备发送消息。 - * - * @param buffer 消息内容 - */ - public void sendMessage(Buffer buffer) { - if (closed.get() || socket == null) { - log.warn("[sendMessage][设备客户端({})连接已关闭,无法发送消息]", clientId); - return; - } - - // Vert.x 的 write 是异步的,不会阻塞 - socket.write(buffer, result -> { - if (result.succeeded()) { - log.debug("[sendMessage][设备客户端({})发送消息成功]", clientId); - // 发送成功也更新心跳,表示连接活跃 - keepAlive(); - } else { - log.error("[sendMessage][设备客户端({})发送消息失败]", clientId, result.cause()); - // 发送失败可能意味着连接已断开,主动关闭 - shutdown(); - } - }); - } - - /** - * 关闭客户端连接并清理资源。 - * 这是一个幂等操作,可以被多次安全调用。 - */ - public void shutdown() { - // 使用原子操作保证只执行一次关闭逻辑 - if (closed.getAndSet(true)) { - return; - } - - log.info("[shutdown][正在关闭设备客户端连接: {}]", clientId); - - // 先将 socket 引用置空,再关闭,避免并发问题 - NetSocket socketToClose = this.socket; - this.socket = null; - - if (socketToClose != null) { - try { - // close 是异步的,但我们在这里不关心其结果,因为我们已经将客户端标记为关闭 - socketToClose.close(); - } catch (Exception e) { - log.warn("[shutdown][关闭TCP连接时出现异常,可能已被关闭]", e); - } - } - - // 重置认证状态 - authenticated.set(false); - } - - public String getConnectionInfo() { - NetSocket currentSocket = this.socket; - if (currentSocket != null && currentSocket.remoteAddress() != null) { - return currentSocket.remoteAddress().toString(); - } - return "disconnected"; - } - - public long getLastKeepAliveTime() { - return lastKeepAliveTime; - } - - @Override - public String toString() { - return "TcpDeviceClient{" + - "clientId='" + clientId + '\'' + - ", deviceAddr='" + deviceAddr + '\'' + - ", deviceId=" + deviceId + - ", authenticated=" + authenticated.get() + - ", online=" + isOnline() + - ", connection=" + getConnectionInfo() + - '}'; - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java new file mode 100644 index 0000000000..520861e51e --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/IotTcpConnectionManager.java @@ -0,0 +1,188 @@ +package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; + +import io.vertx.core.net.NetSocket; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * IoT 网关 TCP 连接管理器 + *

+ * 统一管理 TCP 连接的认证状态、设备会话和消息发送功能: + * 1. 管理 TCP 连接的认证状态 + * 2. 管理设备会话和在线状态 + * 3. 管理消息发送到设备 + * + * @author 芋道源码 + */ +@Slf4j +@Component +public class IotTcpConnectionManager { + + /** + * 连接信息映射:NetSocket -> 连接信息 + */ + private final Map connectionMap = new ConcurrentHashMap<>(); + + /** + * 设备 ID -> NetSocket 的映射(用于快速查找) + */ + private final Map deviceSocketMap = new ConcurrentHashMap<>(); + + /** + * NetSocket -> 设备 ID 的映射(用于连接断开时清理) + */ + private final Map socketDeviceMap = new ConcurrentHashMap<>(); + + /** + * 注册设备连接(包含认证信息) + * + * @param socket TCP 连接 + * @param deviceId 设备 ID + * @param authInfo 认证信息 + */ + public void registerConnection(NetSocket socket, Long deviceId, AuthInfo authInfo) { + // 如果设备已有其他连接,先清理旧连接 + NetSocket oldSocket = deviceSocketMap.get(deviceId); + if (oldSocket != null && oldSocket != socket) { + log.info("[registerConnection][设备已有其他连接,断开旧连接,设备 ID: {},旧连接: {}]", + deviceId, oldSocket.remoteAddress()); + oldSocket.close(); + // 清理所有相关映射 + connectionMap.remove(oldSocket); + socketDeviceMap.remove(oldSocket); + } + + // 注册新连接 - 更新所有映射关系 + ConnectionInfo connectionInfo = new ConnectionInfo() + .setDeviceId(deviceId) + .setAuthInfo(authInfo) + .setAuthenticated(true); + connectionMap.put(socket, connectionInfo); + deviceSocketMap.put(deviceId, socket); + // TODO @haohao:socketDeviceMap 和 connectionMap 会重复哇?connectionMap.get(socket).getDeviceId + socketDeviceMap.put(socket, deviceId); + + log.info("[registerConnection][注册设备连接,设备 ID: {},连接: {},product key: {},device name: {}]", + deviceId, socket.remoteAddress(), authInfo.getProductKey(), authInfo.getDeviceName()); + } + + /** + * 注销设备连接 + * + * @param socket TCP 连接 + */ + public void unregisterConnection(NetSocket socket) { + ConnectionInfo connectionInfo = connectionMap.remove(socket); + Long deviceId = socketDeviceMap.remove(socket); + if (connectionInfo != null && deviceId != null) { + deviceSocketMap.remove(deviceId); + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", + deviceId, socket.remoteAddress()); + } + } + + // TODO @haohao:用不到,要不暂时清理哈。 + /** + * 注销设备连接(通过设备 ID) + * + * @param deviceId 设备 ID + */ + public void unregisterConnection(Long deviceId) { + NetSocket socket = deviceSocketMap.remove(deviceId); + if (socket != null) { + connectionMap.remove(socket); + socketDeviceMap.remove(socket); + log.info("[unregisterConnection][注销设备连接,设备 ID: {},连接: {}]", deviceId, socket.remoteAddress()); + } + } + + /** + * 检查连接是否已认证 + */ + public boolean isAuthenticated(NetSocket socket) { + ConnectionInfo info = connectionMap.get(socket); + return info != null && info.isAuthenticated(); + } + + /** + * 检查连接是否未认证 + */ + public boolean isNotAuthenticated(NetSocket socket) { + return !isAuthenticated(socket); + } + + /** + * 获取连接的认证信息 + */ + public AuthInfo getAuthInfo(NetSocket socket) { + ConnectionInfo info = connectionMap.get(socket); + return info != null ? info.getAuthInfo() : null; + } + + /** + * 检查设备是否在线 + */ + public boolean isDeviceOnline(Long deviceId) { + return deviceSocketMap.containsKey(deviceId); + } + + /** + * 检查设备是否离线 + */ + public boolean isDeviceOffline(Long deviceId) { + return !isDeviceOnline(deviceId); + } + + /** + * 发送消息到设备 + */ + public boolean sendToDevice(Long deviceId, byte[] data) { + NetSocket socket = deviceSocketMap.get(deviceId); + if (socket == null) { + log.warn("[sendToDevice][设备未连接,设备 ID: {}]", deviceId); + return false; + } + + try { + socket.write(io.vertx.core.buffer.Buffer.buffer(data)); + log.debug("[sendToDevice][发送消息成功,设备 ID: {},数据长度: {} 字节]", deviceId, data.length); + return true; + } catch (Exception e) { + log.error("[sendToDevice][发送消息失败,设备 ID: {}]", deviceId, e); + // 发送失败时清理连接 + unregisterConnection(socket); + return false; + } + } + + // TODO @haohao:ConnectionInfo 和 AuthInfo 是不是可以融合哈? + + /** + * 连接信息 + */ + @Data + public static class ConnectionInfo { + + private Long deviceId; + private AuthInfo authInfo; + private boolean authenticated; + + } + + /** + * 认证信息 + */ + @Data + public static class AuthInfo { + + private Long deviceId; + private String productKey; + private String deviceName; + private String clientId; + + } +} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java deleted file mode 100644 index ce7fe4aa5c..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/manager/TcpDeviceConnectionManager.java +++ /dev/null @@ -1,503 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager; - -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.net.NetSocket; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.util.concurrent.*; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.locks.ReentrantReadWriteLock; - -/** - * TCP 设备连接管理器 - *

- * 参考 EMQX 设计理念: - * 1. 高性能连接管理 - * 2. 连接池和资源管理 - * 3. 流量控制 - * 4. 监控统计 - * 5. 自动清理和容错 - * - * @author 芋道源码 - */ -@Component -@Slf4j -public class TcpDeviceConnectionManager { - - // ==================== 连接存储 ==================== - - /** - * 设备客户端映射 - * Key: 设备地址, Value: 设备客户端 - */ - private final ConcurrentMap clientMap = new ConcurrentHashMap<>(); - - /** - * 设备ID到设备地址的映射 - * Key: 设备ID, Value: 设备地址 - */ - private final ConcurrentMap deviceIdToAddrMap = new ConcurrentHashMap<>(); - - /** - * 套接字到客户端的映射,用于快速查找 - * Key: NetSocket, Value: 设备地址 - */ - private final ConcurrentMap socketToAddrMap = new ConcurrentHashMap<>(); - - // ==================== 读写锁 ==================== - - private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); - private final ReentrantReadWriteLock.ReadLock readLock = lock.readLock(); - private final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock(); - - // ==================== 定时任务 ==================== - - /** - * 定时任务执行器 - */ - private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3, r -> { - Thread t = new Thread(r, "tcp-connection-manager"); - t.setDaemon(true); - return t; - }); - - // ==================== 统计信息 ==================== - - private final AtomicLong totalConnections = new AtomicLong(0); - private final AtomicLong totalDisconnections = new AtomicLong(0); - private final AtomicLong totalMessages = new AtomicLong(0); - private final AtomicLong totalFailedMessages = new AtomicLong(0); - private final AtomicLong totalBytes = new AtomicLong(0); - - // ==================== 配置参数 ==================== - - private static final int MAX_CONNECTIONS = 10000; - private static final int HEARTBEAT_CHECK_INTERVAL = 30; // 秒 - private static final int CONNECTION_CLEANUP_INTERVAL = 60; // 秒 - private static final int STATS_LOG_INTERVAL = 300; // 秒 - - /** - * 构造函数,启动定时任务 - */ - public TcpDeviceConnectionManager() { - startScheduledTasks(); - } - - /** - * 启动定时任务 - */ - private void startScheduledTasks() { - // 心跳检查任务 - scheduler.scheduleAtFixedRate(this::checkHeartbeat, - HEARTBEAT_CHECK_INTERVAL, HEARTBEAT_CHECK_INTERVAL, TimeUnit.SECONDS); - - // 连接清理任务 - scheduler.scheduleAtFixedRate(this::cleanupConnections, - CONNECTION_CLEANUP_INTERVAL, CONNECTION_CLEANUP_INTERVAL, TimeUnit.SECONDS); - - // 统计日志任务 - scheduler.scheduleAtFixedRate(this::logStatistics, - STATS_LOG_INTERVAL, STATS_LOG_INTERVAL, TimeUnit.SECONDS); - } - - /** - * 添加设备客户端 - */ - public boolean addClient(String deviceAddr, TcpDeviceClient client) { - if (clientMap.size() >= MAX_CONNECTIONS) { - log.warn("[addClient][连接数已达上限({}),拒绝新连接: {}]", MAX_CONNECTIONS, deviceAddr); - return false; - } - - writeLock.lock(); - try { - log.info("[addClient][添加设备客户端: {}]", deviceAddr); - - // 关闭之前的连接(如果存在) - TcpDeviceClient existingClient = clientMap.get(deviceAddr); - if (existingClient != null) { - log.warn("[addClient][设备({})已存在连接,关闭旧连接]", deviceAddr); - removeClientInternal(deviceAddr, existingClient); - } - - // 添加新连接 - clientMap.put(deviceAddr, client); - - // 添加套接字映射 - if (client.getSocket() != null) { - socketToAddrMap.put(client.getSocket(), deviceAddr); - } - - // 如果客户端已设置设备ID,更新映射 - if (client.getDeviceId() != null) { - deviceIdToAddrMap.put(client.getDeviceId(), deviceAddr); - } - - totalConnections.incrementAndGet(); - return true; - - } finally { - writeLock.unlock(); - } - } - - /** - * 移除设备客户端 - */ - public void removeClient(String deviceAddr) { - writeLock.lock(); - try { - TcpDeviceClient client = clientMap.get(deviceAddr); - if (client != null) { - removeClientInternal(deviceAddr, client); - } - } finally { - writeLock.unlock(); - } - } - - /** - * 内部移除客户端方法(无锁) - */ - private void removeClientInternal(String deviceAddr, TcpDeviceClient client) { - log.info("[removeClient][移除设备客户端: {}]", deviceAddr); - - // 从映射中移除 - clientMap.remove(deviceAddr); - - // 移除套接字映射 - if (client.getSocket() != null) { - socketToAddrMap.remove(client.getSocket()); - } - - // 移除设备ID映射 - if (client.getDeviceId() != null) { - deviceIdToAddrMap.remove(client.getDeviceId()); - } - - // 关闭连接 - client.shutdown(); - - totalDisconnections.incrementAndGet(); - } - - /** - * 通过设备地址获取客户端 - */ - public TcpDeviceClient getClient(String deviceAddr) { - readLock.lock(); - try { - return clientMap.get(deviceAddr); - } finally { - readLock.unlock(); - } - } - - /** - * 通过设备ID获取客户端 - */ - public TcpDeviceClient getClientByDeviceId(Long deviceId) { - readLock.lock(); - try { - String deviceAddr = deviceIdToAddrMap.get(deviceId); - return deviceAddr != null ? clientMap.get(deviceAddr) : null; - } finally { - readLock.unlock(); - } - } - - /** - * 通过网络连接获取客户端 - */ - public TcpDeviceClient getClientBySocket(NetSocket socket) { - readLock.lock(); - try { - String deviceAddr = socketToAddrMap.get(socket); - return deviceAddr != null ? clientMap.get(deviceAddr) : null; - } finally { - readLock.unlock(); - } - } - - /** - * 检查设备是否在线 - */ - public boolean isDeviceOnline(Long deviceId) { - TcpDeviceClient client = getClientByDeviceId(deviceId); - return client != null && client.isOnline(); - } - - /** - * 设置设备ID映射 - */ - public void setDeviceIdMapping(String deviceAddr, Long deviceId) { - writeLock.lock(); - try { - TcpDeviceClient client = clientMap.get(deviceAddr); - if (client != null) { - client.setDeviceId(deviceId); - deviceIdToAddrMap.put(deviceId, deviceAddr); - log.debug("[setDeviceIdMapping][设置设备ID映射: {} -> {}]", deviceAddr, deviceId); - } - } finally { - writeLock.unlock(); - } - } - - /** - * 发送消息给设备 - */ - public boolean sendMessage(String deviceAddr, Buffer buffer) { - TcpDeviceClient client = getClient(deviceAddr); - if (client != null && client.isOnline()) { - try { - client.sendMessage(buffer); - totalMessages.incrementAndGet(); - totalBytes.addAndGet(buffer.length()); - return true; - } catch (Exception e) { - totalFailedMessages.incrementAndGet(); - log.error("[sendMessage][发送消息失败] 设备地址: {}", deviceAddr, e); - return false; - } - } - log.warn("[sendMessage][设备({})不在线,无法发送消息]", deviceAddr); - return false; - } - - /** - * 通过设备ID发送消息 - */ - public boolean sendMessageByDeviceId(Long deviceId, Buffer buffer) { - TcpDeviceClient client = getClientByDeviceId(deviceId); - if (client != null && client.isOnline()) { - try { - client.sendMessage(buffer); - totalMessages.incrementAndGet(); - totalBytes.addAndGet(buffer.length()); - return true; - } catch (Exception e) { - totalFailedMessages.incrementAndGet(); - log.error("[sendMessageByDeviceId][发送消息失败] 设备ID: {}", deviceId, e); - return false; - } - } - log.warn("[sendMessageByDeviceId][设备ID({})不在线,无法发送消息]", deviceId); - return false; - } - - /** - * 广播消息给所有在线设备 - */ - public int broadcastMessage(Buffer buffer) { - int successCount = 0; - readLock.lock(); - try { - for (TcpDeviceClient client : clientMap.values()) { - if (client.isOnline()) { - try { - client.sendMessage(buffer); - successCount++; - } catch (Exception e) { - log.error("[broadcastMessage][广播消息失败] 设备: {}", client.getDeviceAddr(), e); - } - } - } - } finally { - readLock.unlock(); - } - - totalMessages.addAndGet(successCount); - totalBytes.addAndGet((long) successCount * buffer.length()); - return successCount; - } - - /** - * 获取在线设备数量 - */ - public int getOnlineCount() { - readLock.lock(); - try { - return (int) clientMap.values().stream() - .filter(TcpDeviceClient::isOnline) - .count(); - } finally { - readLock.unlock(); - } - } - - /** - * 获取总连接数 - */ - public int getTotalCount() { - return clientMap.size(); - } - - /** - * 获取认证设备数量 - */ - public int getAuthenticatedCount() { - readLock.lock(); - try { - return (int) clientMap.values().stream() - .filter(TcpDeviceClient::isAuthenticated) - .count(); - } finally { - readLock.unlock(); - } - } - - /** - * 心跳检查任务 - */ - private void checkHeartbeat() { - try { - long currentTime = System.currentTimeMillis(); - int offlineCount = 0; - - readLock.lock(); - try { - for (TcpDeviceClient client : clientMap.values()) { - if (!client.isOnline()) { - offlineCount++; - } - } - } finally { - readLock.unlock(); - } - - if (offlineCount > 0) { - log.info("[checkHeartbeat][发现{}个离线设备,将在清理任务中处理]", offlineCount); - } - } catch (Exception e) { - log.error("[checkHeartbeat][心跳检查任务异常]", e); - } - } - - /** - * 连接清理任务 - */ - private void cleanupConnections() { - try { - int beforeSize = clientMap.size(); - - writeLock.lock(); - try { - clientMap.entrySet().removeIf(entry -> { - TcpDeviceClient client = entry.getValue(); - if (!client.isOnline()) { - log.debug("[cleanupConnections][清理离线连接: {}]", entry.getKey()); - - // 清理相关映射 - if (client.getSocket() != null) { - socketToAddrMap.remove(client.getSocket()); - } - if (client.getDeviceId() != null) { - deviceIdToAddrMap.remove(client.getDeviceId()); - } - - client.shutdown(); - totalDisconnections.incrementAndGet(); - return true; - } - return false; - }); - } finally { - writeLock.unlock(); - } - - int afterSize = clientMap.size(); - if (beforeSize != afterSize) { - log.info("[cleanupConnections][清理完成] 连接数: {} -> {}, 清理数: {}", - beforeSize, afterSize, beforeSize - afterSize); - } - } catch (Exception e) { - log.error("[cleanupConnections][连接清理任务异常]", e); - } - } - - /** - * 统计日志任务 - */ - private void logStatistics() { - try { - long totalConn = totalConnections.get(); - long totalDisconn = totalDisconnections.get(); - long totalMsg = totalMessages.get(); - long totalFailedMsg = totalFailedMessages.get(); - long totalBytesValue = totalBytes.get(); - - log.info("[logStatistics][连接统计] 总连接: {}, 总断开: {}, 当前在线: {}, 认证设备: {}, " + - "总消息: {}, 失败消息: {}, 总字节: {}", - totalConn, totalDisconn, getOnlineCount(), getAuthenticatedCount(), - totalMsg, totalFailedMsg, totalBytesValue); - } catch (Exception e) { - log.error("[logStatistics][统计日志任务异常]", e); - } - } - - /** - * 关闭连接管理器 - */ - public void shutdown() { - log.info("[shutdown][关闭TCP连接管理器]"); - - // 关闭定时任务 - scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(10, TimeUnit.SECONDS)) { - scheduler.shutdownNow(); - } - } catch (InterruptedException e) { - scheduler.shutdownNow(); - Thread.currentThread().interrupt(); - } - - // 关闭所有连接 - writeLock.lock(); - try { - clientMap.values().forEach(TcpDeviceClient::shutdown); - clientMap.clear(); - deviceIdToAddrMap.clear(); - socketToAddrMap.clear(); - } finally { - writeLock.unlock(); - } - } - - /** - * 获取连接状态信息 - */ - public String getConnectionStatus() { - return String.format("总连接数: %d, 在线设备: %d, 认证设备: %d, 成功率: %.2f%%", - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), - totalMessages.get() > 0 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 - : 0.0); - } - - /** - * 获取详细统计信息 - */ - public String getDetailedStatistics() { - return String.format( - "TCP连接管理器统计:\n" + - "- 当前连接数: %d\n" + - "- 在线设备数: %d\n" + - "- 认证设备数: %d\n" + - "- 历史总连接: %d\n" + - "- 历史总断开: %d\n" + - "- 总消息数: %d\n" + - "- 失败消息数: %d\n" + - "- 总字节数: %d\n" + - "- 消息成功率: %.2f%%", - getTotalCount(), getOnlineCount(), getAuthenticatedCount(), - totalConnections.get(), totalDisconnections.get(), - totalMessages.get(), totalFailedMessages.get(), totalBytes.get(), - totalMessages.get() > 0 - ? (double) (totalMessages.get() - totalFailedMessages.get()) / totalMessages.get() * 100 - : 0.0); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java deleted file mode 100644 index 8e7baa37d8..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataDecoder.java +++ /dev/null @@ -1,97 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; - -/** - * TCP 数据解码器 - *

- * 负责将字节流解码为 TcpDataPackage 对象 - *

- * 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataDecoder { - - /** - * 解码数据包 - * - * @param buffer 数据缓冲区 - * @return 解码后的数据包 - * @throws IllegalArgumentException 如果数据包格式不正确 - */ - public static TcpDataPackage decode(Buffer buffer) { - if (buffer == null || buffer.length() < 8) { - throw new IllegalArgumentException("数据包长度不足"); - } - - try { - int index = 0; - - // 1. 获取设备地址长度(2字节) - short addrLength = buffer.getShort(index); - index += 2; - - // 2. 校验数据包长度 - int expectedLength = 2 + addrLength + 2 + 2; // 地址长度 + 地址 + 功能码 + 消息序号 - if (buffer.length() < expectedLength) { - throw new IllegalArgumentException("数据包长度不足,期望至少 " + expectedLength + " 字节"); - } - - // 3. 获取设备地址 - String addr = buffer.getBuffer(index, index + addrLength).toString(); - index += addrLength; - - // 4. 获取功能码(2字节) - short code = buffer.getShort(index); - index += 2; - - // 5. 获取消息序号(2字节) - short mid = buffer.getShort(index); - index += 2; - - // 6. 获取包体数据 - String payload = ""; - if (index < buffer.length()) { - payload = buffer.getString(index, buffer.length()); - } - - // 7. 构建数据包对象 - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addrLength((int) addrLength) - .addr(addr) - .code(code) - .mid(mid) - .payload(payload) - .build(); - - log.debug("[decode][解码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 包体长度: {}", - addr, dataPackage.getCodeDescription(), mid, payload.length()); - - return dataPackage; - - } catch (Exception e) { - log.error("[decode][解码失败] 数据: {}", buffer.toString(), e); - throw new IllegalArgumentException("数据包解码失败: " + e.getMessage(), e); - } - } - - /** - * 校验数据包格式 - * - * @param buffer 数据缓冲区 - * @return 校验结果 - */ - public static boolean validate(Buffer buffer) { - try { - decode(buffer); - return true; - } catch (Exception e) { - log.warn("[validate][数据包格式校验失败] 数据: {}, 错误: {}", buffer.toString(), e.getMessage()); - return false; - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java deleted file mode 100644 index fb0a68c182..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataEncoder.java +++ /dev/null @@ -1,172 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.buffer.Buffer; -import lombok.extern.slf4j.Slf4j; - -/** - * TCP 数据编码器 - *

- * 负责将 TcpDataPackage 对象编码为字节流 - *

- * 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataEncoder { - - /** - * 编码数据包 - * - * @param dataPackage 数据包对象 - * @return 编码后的字节流 - * @throws IllegalArgumentException 如果数据包对象不正确 - */ - public static Buffer encode(TcpDataPackage dataPackage) { - if (dataPackage == null) { - throw new IllegalArgumentException("数据包对象不能为空"); - } - - if (dataPackage.getAddr() == null || dataPackage.getAddr().isEmpty()) { - throw new IllegalArgumentException("设备地址不能为空"); - } - - if (dataPackage.getPayload() == null) { - dataPackage.setPayload(""); - } - - try { - Buffer buffer = Buffer.buffer(); - - // 1. 计算包体长度(除了包头4字节) - int payloadLength = dataPackage.getPayload().getBytes().length; - int totalLength = 2 + dataPackage.getAddr().length() + 2 + 2 + payloadLength; - - // 2. 写入包头:总长度(4字节) - buffer.appendInt(totalLength); - - // 3. 写入设备地址长度(2字节) - buffer.appendShort((short) dataPackage.getAddr().length()); - - // 4. 写入设备地址(不定长) - buffer.appendBytes(dataPackage.getAddr().getBytes()); - - // 5. 写入功能码(2字节) - buffer.appendShort(dataPackage.getCode()); - - // 6. 写入消息序号(2字节) - buffer.appendShort(dataPackage.getMid()); - - // 7. 写入包体数据(不定长) - buffer.appendBytes(dataPackage.getPayload().getBytes()); - - log.debug("[encode][编码成功] 设备地址: {}, 功能码: {}, 消息序号: {}, 总长度: {}", - dataPackage.getAddr(), dataPackage.getCodeDescription(), - dataPackage.getMid(), buffer.length()); - - return buffer; - - } catch (Exception e) { - log.error("[encode][编码失败] 数据包: {}", dataPackage, e); - throw new IllegalArgumentException("数据包编码失败: " + e.getMessage(), e); - } - } - - /** - * 创建注册回复数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param success 是否成功 - * @return 编码后的数据包 - */ - public static Buffer createRegisterReply(String addr, short mid, boolean success) { - String payload = success ? "0" : "1"; // 0表示成功,1表示失败 - - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_REGISTER_REPLY) - .mid(mid) - .payload(payload) - .build(); - - return encode(dataPackage); - } - - /** - * 创建数据下发数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param data 下发数据 - * @return 编码后的数据包 - */ - public static Buffer createDataDownPackage(String addr, short mid, String data) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_DATA_DOWN) - .mid(mid) - .payload(data) - .build(); - - return encode(dataPackage); - } - - /** - * 创建服务调用数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param serviceData 服务数据 - * @return 编码后的数据包 - */ - public static Buffer createServiceInvokePackage(String addr, short mid, String serviceData) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_SERVICE_INVOKE) - .mid(mid) - .payload(serviceData) - .build(); - - return encode(dataPackage); - } - - /** - * 创建属性设置数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param propertyData 属性数据 - * @return 编码后的数据包 - */ - public static Buffer createPropertySetPackage(String addr, short mid, String propertyData) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_PROPERTY_SET) - .mid(mid) - .payload(propertyData) - .build(); - - return encode(dataPackage); - } - - /** - * 创建属性获取数据包 - * - * @param addr 设备地址 - * @param mid 消息序号 - * @param propertyNames 属性名称列表 - * @return 编码后的数据包 - */ - public static Buffer createPropertyGetPackage(String addr, short mid, String propertyNames) { - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(addr) - .code(TcpDataPackage.CODE_PROPERTY_GET) - .mid(mid) - .payload(propertyNames) - .build(); - - return encode(dataPackage); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java deleted file mode 100644 index 3b6f7df286..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataPackage.java +++ /dev/null @@ -1,153 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * TCP 数据包协议定义 - *

- * 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Data -@AllArgsConstructor -@NoArgsConstructor -@Builder -public class TcpDataPackage { - - // ==================== 功能码定义 ==================== - - /** - * 设备注册 - */ - public static final short CODE_REGISTER = 10; - /** - * 注册回复 - */ - public static final short CODE_REGISTER_REPLY = 11; - /** - * 心跳 - */ - public static final short CODE_HEARTBEAT = 20; - /** - * 数据上报 - */ - public static final short CODE_DATA_UP = 30; - /** - * 事件上报 - */ - public static final short CODE_EVENT_UP = 40; - /** - * 数据下发 - */ - public static final short CODE_DATA_DOWN = 50; - /** - * 服务调用 - */ - public static final short CODE_SERVICE_INVOKE = 60; - /** - * 属性设置 - */ - public static final short CODE_PROPERTY_SET = 70; - /** - * 属性获取 - */ - public static final short CODE_PROPERTY_GET = 80; - - // ==================== 数据包字段 ==================== - - /** - * 设备地址长度 - */ - private Integer addrLength; - - /** - * 设备地址 - */ - private String addr; - - /** - * 功能码 - */ - private short code; - - /** - * 消息序号 - */ - private short mid; - - /** - * 包体数据 - */ - private String payload; - - // ==================== 辅助方法 ==================== - - /** - * 是否为注册消息 - */ - public boolean isRegisterMessage() { - return code == CODE_REGISTER; - } - - /** - * 是否为心跳消息 - */ - public boolean isHeartbeatMessage() { - return code == CODE_HEARTBEAT; - } - - /** - * 是否为数据上报消息 - */ - public boolean isDataUpMessage() { - return code == CODE_DATA_UP; - } - - /** - * 是否为事件上报消息 - */ - public boolean isEventUpMessage() { - return code == CODE_EVENT_UP; - } - - /** - * 是否为下行消息 - */ - public boolean isDownstreamMessage() { - return code == CODE_DATA_DOWN || code == CODE_SERVICE_INVOKE || - code == CODE_PROPERTY_SET || code == CODE_PROPERTY_GET; - } - - /** - * 获取功能码描述 - */ - public String getCodeDescription() { - switch (code) { - case CODE_REGISTER: - return "设备注册"; - case CODE_REGISTER_REPLY: - return "注册回复"; - case CODE_HEARTBEAT: - return "心跳"; - case CODE_DATA_UP: - return "数据上报"; - case CODE_EVENT_UP: - return "事件上报"; - case CODE_DATA_DOWN: - return "数据下发"; - case CODE_SERVICE_INVOKE: - return "服务调用"; - case CODE_PROPERTY_SET: - return "属性设置"; - case CODE_PROPERTY_GET: - return "属性获取"; - default: - return "未知功能码"; - } - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java deleted file mode 100644 index f796389907..0000000000 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/protocol/TcpDataReader.java +++ /dev/null @@ -1,159 +0,0 @@ -package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol; - -import io.vertx.core.Handler; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.parsetools.RecordParser; -import lombok.extern.slf4j.Slf4j; - -import java.util.function.Consumer; - -/** - * TCP 数据读取器 - *

- * 负责从 TCP 流中读取完整的数据包 - *

- * 数据包格式: - * 包头(4字节长度) | 设备地址长度(2字节) | 设备地址(不定长) | 功能码(2字节) | 消息序号(2字节) | 包体(不定长) - * - * @author 芋道源码 - */ -@Slf4j -public class TcpDataReader { - - /** - * 创建数据包解析器 - * - * @param receiveHandler 接收处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createParser(Consumer receiveHandler) { - // 首先读取4字节的长度信息 - RecordParser parser = RecordParser.newFixed(4); - - // 设置处理器 - parser.setOutput(new Handler() { - // 当前数据包的长度,-1表示还没有读取到长度信息 - private int dataLength = -1; - - @Override - public void handle(Buffer buffer) { - try { - // 如果还没有读取到长度信息 - if (dataLength == -1) { - // 从包头中读取数据长度 - dataLength = buffer.getInt(0); - - // 校验数据长度 - if (dataLength <= 0 || dataLength > 1024 * 1024) { // 最大1MB - log.error("[handle][无效的数据包长度: {}]", dataLength); - reset(); - return; - } - - // 切换到读取数据模式 - parser.fixedSizeMode(dataLength); - - log.debug("[handle][读取到数据包长度: {}]", dataLength); - } else { - // 读取到完整的数据包 - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); - - // 处理数据包 - try { - receiveHandler.accept(buffer); - } catch (Exception e) { - log.error("[handle][处理数据包失败]", e); - } - - // 重置状态,准备读取下一个数据包 - reset(); - } - } catch (Exception e) { - log.error("[handle][数据包处理异常]", e); - reset(); - } - } - - /** - * 重置解析器状态 - */ - private void reset() { - dataLength = -1; - parser.fixedSizeMode(4); - } - }); - - return parser; - } - - /** - * 创建带异常处理的数据包解析器 - * - * @param receiveHandler 接收处理器 - * @param exceptionHandler 异常处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createParserWithExceptionHandler( - Consumer receiveHandler, - Consumer exceptionHandler) { - - RecordParser parser = RecordParser.newFixed(4); - - parser.setOutput(new Handler() { - private int dataLength = -1; - - @Override - public void handle(Buffer buffer) { - try { - if (dataLength == -1) { - dataLength = buffer.getInt(0); - - if (dataLength <= 0 || dataLength > 1024 * 1024) { - throw new IllegalArgumentException("无效的数据包长度: " + dataLength); - } - - parser.fixedSizeMode(dataLength); - log.debug("[handle][读取到数据包长度: {}]", dataLength); - } else { - log.debug("[handle][读取到完整数据包,长度: {}]", buffer.length()); - - try { - receiveHandler.accept(buffer); - } catch (Exception e) { - exceptionHandler.accept(e); - } - - reset(); - } - } catch (Exception e) { - exceptionHandler.accept(e); - reset(); - } - } - - private void reset() { - dataLength = -1; - parser.fixedSizeMode(4); - } - }); - - return parser; - } - - /** - * 创建简单的数据包解析器(用于测试) - * - * @param receiveHandler 接收处理器 - * @return RecordParser 解析器 - */ - public static RecordParser createSimpleParser(Consumer receiveHandler) { - return createParser(buffer -> { - try { - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - receiveHandler.accept(dataPackage); - } catch (Exception e) { - log.error("[createSimpleParser][解码数据包失败]", e); - } - }); - } -} \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java index 7c499fb974..3ee31d82e4 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpDownstreamHandler.java @@ -1,364 +1,63 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; -import cn.iocoder.yudao.module.iot.core.enums.IotDeviceMessageMethodEnum; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; +import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; -import com.alibaba.fastjson.JSON; -import io.vertx.core.buffer.Buffer; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; /** * IoT 网关 TCP 下行消息处理器 - *

- * 负责处理从业务系统发送到设备的下行消息,包括: - * 1. 属性设置 - * 2. 服务调用 - * 3. 属性获取 - * 4. 配置下发 - * 5. OTA 升级 * * @author 芋道源码 */ @Slf4j +@RequiredArgsConstructor public class IotTcpDownstreamHandler { - private final TcpDeviceConnectionManager connectionManager; + private final IotDeviceMessageService deviceMessageService; - private final IotDeviceMessageService messageService; + private final IotDeviceService deviceService; - public IotTcpDownstreamHandler(TcpDeviceConnectionManager connectionManager, - IotDeviceMessageService messageService) { - this.connectionManager = connectionManager; - this.messageService = messageService; - } + private final IotTcpConnectionManager connectionManager; /** * 处理下行消息 - * - * @param message 设备消息 */ public void handle(IotDeviceMessage message) { try { - log.info("[handle][处理下行消息] 设备ID: {}, 方法: {}, 消息ID: {}", + log.info("[handle][处理下行消息,设备 ID: {},方法: {},消息 ID: {}]", message.getDeviceId(), message.getMethod(), message.getId()); - // 1. 获取设备连接 - TcpDeviceClient client = connectionManager.getClientByDeviceId(message.getDeviceId()); - if (client == null || !client.isOnline()) { - log.error("[handle][设备({})不在线,无法发送下行消息]", message.getDeviceId()); + // 1.1 获取设备信息 + IotDeviceRespDTO deviceInfo = deviceService.getDeviceFromCache(message.getDeviceId()); + if (deviceInfo == null) { + log.error("[handle][设备不存在,设备 ID: {}]", message.getDeviceId()); + return; + } + // 1.2 检查设备是否在线 + if (connectionManager.isDeviceOffline(message.getDeviceId())) { + log.warn("[handle][设备不在线,设备 ID: {}]", message.getDeviceId()); return; } - // 2. 根据消息方法处理不同类型的下行消息 - switch (message.getMethod()) { - case "thing.property.set": - handlePropertySet(client, message); - break; - case "thing.property.get": - handlePropertyGet(client, message); - break; - case "thing.service.invoke": - handleServiceInvoke(client, message); - break; - case "thing.config.push": - handleConfigPush(client, message); - break; - case "thing.ota.upgrade": - handleOtaUpgrade(client, message); - break; - default: - log.warn("[handle][未知的下行消息方法: {}]", message.getMethod()); - break; + // 2. 根据产品 Key 和设备名称编码消息并发送到设备 + byte[] bytes = deviceMessageService.encodeDeviceMessage(message, deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); + boolean success = connectionManager.sendToDevice(message.getDeviceId(), bytes); + if (success) { + log.info("[handle][下行消息发送成功,设备 ID: {},方法: {},消息 ID: {},数据长度: {} 字节]", + message.getDeviceId(), message.getMethod(), message.getId(), bytes.length); + } else { + log.error("[handle][下行消息发送失败,设备 ID: {},方法: {},消息 ID: {}]", + message.getDeviceId(), message.getMethod(), message.getId()); } - } catch (Exception e) { - log.error("[handle][处理下行消息失败]", e); + log.error("[handle][处理下行消息失败,设备 ID: {},方法: {},消息内容: {}]", + message.getDeviceId(), message.getMethod(), message, e); } } - /** - * 处理属性设置 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handlePropertySet(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handlePropertySet][属性设置] 设备地址: {}, 属性: {}", - client.getDeviceAddr(), message.getParams()); - - // 使用编解码器发送消息,降级处理使用原始编码 - sendMessageWithCodec(client, message, "handlePropertySet", () -> { - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createPropertySetPackage( - client.getDeviceAddr(), mid, payload); - client.sendMessage(buffer); - - log.debug("[handlePropertySet][属性设置消息已发送(降级)] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - }); - - } catch (Exception e) { - log.error("[handlePropertySet][属性设置失败]", e); - } - } - - /** - * 处理属性获取 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handlePropertyGet(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handlePropertyGet][属性获取] 设备地址: {}, 属性列表: {}", - client.getDeviceAddr(), message.getParams()); - - // 使用编解码器发送消息,降级处理使用原始编码 - sendMessageWithCodec(client, message, "handlePropertyGet", () -> { - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createPropertyGetPackage( - client.getDeviceAddr(), mid, payload); - client.sendMessage(buffer); - - log.debug("[handlePropertyGet][属性获取消息已发送(降级)] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - }); - - } catch (Exception e) { - log.error("[handlePropertyGet][属性获取失败]", e); - } - } - - /** - * 处理服务调用 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleServiceInvoke(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleServiceInvoke][服务调用] 设备地址: {}, 服务参数: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建服务调用数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createServiceInvokePackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleServiceInvoke][服务调用消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - - } catch (Exception e) { - log.error("[handleServiceInvoke][服务调用失败]", e); - } - } - - /** - * 处理配置推送 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleConfigPush(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleConfigPush][配置推送] 设备地址: {}, 配置: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建配置推送数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createDataDownPackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleConfigPush][配置推送消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - - } catch (Exception e) { - log.error("[handleConfigPush][配置推送失败]", e); - } - } - - /** - * 处理 OTA 升级 - * - * @param client 设备客户端 - * @param message 设备消息 - */ - private void handleOtaUpgrade(TcpDeviceClient client, IotDeviceMessage message) { - try { - log.info("[handleOtaUpgrade][OTA升级] 设备地址: {}, 升级信息: {}", - client.getDeviceAddr(), message.getParams()); - - // 1. 构建 OTA 升级数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - Buffer buffer = TcpDataEncoder.createDataDownPackage( - client.getDeviceAddr(), mid, payload); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleOtaUpgrade][OTA升级消息已发送] 设备地址: {}, 消息序号: {}", - client.getDeviceAddr(), mid); - - } catch (Exception e) { - log.error("[handleOtaUpgrade][OTA升级失败]", e); - } - } - - /** - * 处理自定义下行消息 - * - * @param client 设备客户端 - * @param message 设备消息 - * @param code 功能码 - */ - private void handleCustomMessage(TcpDeviceClient client, IotDeviceMessage message, short code) { - try { - log.info("[handleCustomMessage][自定义消息] 设备地址: {}, 功能码: {}, 数据: {}", - client.getDeviceAddr(), code, message.getParams()); - - // 1. 构建自定义数据包 - String payload = JSON.toJSONString(message.getParams()); - short mid = generateMessageId(); - - TcpDataPackage dataPackage = TcpDataPackage.builder() - .addr(client.getDeviceAddr()) - .code(code) - .mid(mid) - .payload(payload) - .build(); - - Buffer buffer = TcpDataEncoder.encode(dataPackage); - - // 2. 发送消息 - client.sendMessage(buffer); - - log.debug("[handleCustomMessage][自定义消息已发送] 设备地址: {}, 功能码: {}, 消息序号: {}", - client.getDeviceAddr(), code, mid); - - } catch (Exception e) { - log.error("[handleCustomMessage][自定义消息发送失败]", e); - } - } - - /** - * 批量发送下行消息 - * - * @param deviceIds 设备ID列表 - * @param message 设备消息 - */ - public void broadcastMessage(Long[] deviceIds, IotDeviceMessage message) { - try { - log.info("[broadcastMessage][批量发送消息] 设备数量: {}, 方法: {}", - deviceIds.length, message.getMethod()); - - for (Long deviceId : deviceIds) { - // 创建副本消息(避免消息ID冲突) - IotDeviceMessage copyMessage = IotDeviceMessage.of( - message.getRequestId(), - message.getMethod(), - message.getParams(), - message.getData(), - message.getCode(), - message.getMsg()); - copyMessage.setDeviceId(deviceId); - - // 处理单个设备消息 - handle(copyMessage); - } - - } catch (Exception e) { - log.error("[broadcastMessage][批量发送消息失败]", e); - } - } - - /** - * 检查设备是否支持指定方法 - * - * @param client 设备客户端 - * @param method 消息方法 - * @return 是否支持 - */ - private boolean isMethodSupported(TcpDeviceClient client, String method) { - // TODO: 可以根据设备类型或产品信息判断是否支持特定方法 - return IotDeviceMessageMethodEnum.of(method) != null; - } - - /** - * 生成消息序号 - * - * @return 消息序号 - */ - private short generateMessageId() { - return (short) (System.currentTimeMillis() % Short.MAX_VALUE); - } - - /** - * 使用编解码器发送消息 - * - * @param client 设备客户端 - * @param message 设备消息 - * @param methodName 方法名称 - * @param fallbackAction 降级处理逻辑 - */ - private void sendMessageWithCodec(TcpDeviceClient client, IotDeviceMessage message, - String methodName, Runnable fallbackAction) { - try { - // 1. 使用编解码器编码消息 - byte[] messageBytes = messageService.encodeDeviceMessage( - message, client.getProductKey(), client.getDeviceName()); - - // 2. 解析编码后的数据包并设置设备地址和消息序号 - Buffer buffer = Buffer.buffer(messageBytes); - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - dataPackage.setAddr(client.getDeviceAddr()); - dataPackage.setMid(generateMessageId()); - - // 3. 重新编码并发送 - Buffer finalBuffer = TcpDataEncoder.encode(dataPackage); - client.sendMessage(finalBuffer); - - log.debug("[{}][消息已发送] 设备地址: {}, 消息序号: {}", - methodName, client.getDeviceAddr(), dataPackage.getMid()); - - } catch (Exception e) { - log.warn("[{}][使用编解码器编码失败,降级使用原始编码] 错误: {}", - methodName, e.getMessage()); - - // 执行降级处理 - if (fallbackAction != null) { - fallbackAction.run(); - } - } - } - - /** - * 获取连接统计信息 - * - * @return 连接统计信息 - */ - public String getHandlerStatistics() { - return String.format("TCP下游处理器 - %s", connectionManager.getConnectionStatus()); - } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java index 0067e72064..627daad680 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/protocol/tcp/router/IotTcpUpstreamHandler.java @@ -1,393 +1,450 @@ package cn.iocoder.yudao.module.iot.gateway.protocol.tcp.router; +import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONObject; import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.iot.core.biz.IotDeviceCommonApi; +import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceAuthReqDTO; import cn.iocoder.yudao.module.iot.core.biz.dto.IotDeviceRespDTO; import cn.iocoder.yudao.module.iot.core.mq.message.IotDeviceMessage; -import cn.iocoder.yudao.module.iot.gateway.config.IotGatewayProperties; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.client.TcpDeviceClient; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.TcpDeviceConnectionManager; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataDecoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataEncoder; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataPackage; -import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.protocol.TcpDataReader; +import cn.iocoder.yudao.module.iot.core.util.IotDeviceAuthUtils; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpBinaryDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.codec.tcp.IotTcpJsonDeviceMessageCodec; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.IotTcpUpstreamProtocol; +import cn.iocoder.yudao.module.iot.gateway.protocol.tcp.manager.IotTcpConnectionManager; import cn.iocoder.yudao.module.iot.gateway.service.device.IotDeviceService; import cn.iocoder.yudao.module.iot.gateway.service.device.message.IotDeviceMessageService; import io.vertx.core.Handler; import io.vertx.core.buffer.Buffer; import io.vertx.core.net.NetSocket; -import io.vertx.core.parsetools.RecordParser; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; +import lombok.Data; import lombok.extern.slf4j.Slf4j; +import java.nio.charset.StandardCharsets; + /** - * IoT 网关 TCP 上行消息处理器 - *

- * 核心负责: - * 1. 【设备注册】设备连接后发送注册消息,注册成功后可以进行通信 - * 2. 【心跳处理】定期接收设备心跳消息,维持连接状态 - * 3. 【数据上报】接收设备数据上报和事件上报 - * 4. 【连接管理】管理连接的建立、维护和清理 + * TCP 上行消息处理器 * * @author 芋道源码 */ -@RequiredArgsConstructor @Slf4j public class IotTcpUpstreamHandler implements Handler { - private final IotGatewayProperties.TcpProperties tcpConfig; + private static final String CODEC_TYPE_JSON = IotTcpJsonDeviceMessageCodec.TYPE; + private static final String CODEC_TYPE_BINARY = IotTcpBinaryDeviceMessageCodec.TYPE; - private final TcpDeviceConnectionManager connectionManager; + private static final String AUTH_METHOD = "auth"; + + private final IotDeviceMessageService deviceMessageService; private final IotDeviceService deviceService; - private final IotDeviceMessageService messageService; + private final IotTcpConnectionManager connectionManager; private final IotDeviceCommonApi deviceApi; private final String serverId; + public IotTcpUpstreamHandler(IotTcpUpstreamProtocol protocol, + IotDeviceMessageService deviceMessageService, + IotDeviceService deviceService, + IotTcpConnectionManager connectionManager) { + this.deviceMessageService = deviceMessageService; + this.deviceService = deviceService; + this.connectionManager = connectionManager; + this.deviceApi = SpringUtil.getBean(IotDeviceCommonApi.class); + this.serverId = protocol.getServerId(); + } + @Override public void handle(NetSocket socket) { - log.info("[handle][收到设备连接: {}]", socket.remoteAddress()); + String clientId = IdUtil.simpleUUID(); + log.debug("[handle][设备连接,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); - // 创建客户端ID和设备客户端 - String clientId = IdUtil.simpleUUID() + "_" + socket.remoteAddress(); - TcpDeviceClient client = new TcpDeviceClient(clientId, tcpConfig.getKeepAliveTimeoutMs()); + // 设置异常和关闭处理器 + socket.exceptionHandler(ex -> { + log.warn("[handle][连接异常,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + cleanupConnection(socket); + }); + socket.closeHandler(v -> { + log.debug("[handle][连接关闭,客户端 ID: {},地址: {}]", clientId, socket.remoteAddress()); + cleanupConnection(socket); + }); - try { - // 设置连接异常和关闭处理 - socket.exceptionHandler(ex -> { - log.error("[handle][连接({})异常]", socket.remoteAddress(), ex); - handleConnectionClose(client); - }); - - socket.closeHandler(v -> { - log.info("[handle][连接({})关闭]", socket.remoteAddress()); - handleConnectionClose(client); - }); - - // 设置网络连接 - client.setSocket(socket); - - // 创建数据解析器 - RecordParser parser = TcpDataReader.createParser(buffer -> { - try { - handleDataPackage(client, buffer); - } catch (Exception e) { - log.error("[handle][处理数据包异常]", e); - } - }); - - // 设置解析器 - client.setParser(parser); - - log.info("[handle][设备连接处理器初始化完成: {}]", clientId); - - } catch (Exception e) { - log.error("[handle][初始化连接处理器失败]", e); - client.shutdown(); - } + // 设置消息处理器 + socket.handler(buffer -> processMessage(clientId, buffer, socket)); } /** - * 处理数据包 - * - * @param client 设备客户端 - * @param buffer 数据缓冲区 + * 处理消息 */ - private void handleDataPackage(TcpDeviceClient client, io.vertx.core.buffer.Buffer buffer) { + private void processMessage(String clientId, Buffer buffer, NetSocket socket) { try { - // 解码数据包 - TcpDataPackage dataPackage = TcpDataDecoder.decode(buffer); - - log.info("[handleDataPackage][接收数据包] 设备地址: {}, 功能码: {}, 消息序号: {}", - dataPackage.getAddr(), dataPackage.getCodeDescription(), dataPackage.getMid()); - - // 根据功能码处理不同类型的消息 - switch (dataPackage.getCode()) { - case TcpDataPackage.CODE_REGISTER: - handleDeviceRegister(client, dataPackage); - break; - case TcpDataPackage.CODE_HEARTBEAT: - handleHeartbeat(client, dataPackage); - break; - case TcpDataPackage.CODE_DATA_UP: - handleDataUp(client, dataPackage); - break; - case TcpDataPackage.CODE_EVENT_UP: - handleEventUp(client, dataPackage); - break; - default: - log.warn("[handleDataPackage][未知功能码: {}]", dataPackage.getCode()); - break; + // 1.1 数据包基础检查 + if (buffer.length() == 0) { + return; + } + // 1.2 解码消息 + MessageInfo messageInfo = decodeMessage(buffer); + if (messageInfo == null) { + return; } + // 2. 根据消息类型路由处理 + if (isAuthRequest(messageInfo.message)) { + // 认证请求 + handleAuthenticationRequest(clientId, messageInfo, socket); + } else { + // 业务消息 + handleBusinessRequest(clientId, messageInfo, socket); + } } catch (Exception e) { - log.error("[handleDataPackage][处理数据包失败]", e); + log.error("[processMessage][处理消息失败,客户端 ID: {}]", clientId, e); } } /** - * 处理设备注册 - * - * @param client 设备客户端 - * @param dataPackage 数据包 + * 处理认证请求 */ - private void handleDeviceRegister(TcpDeviceClient client, TcpDataPackage dataPackage) { + private void handleAuthenticationRequest(String clientId, MessageInfo messageInfo, NetSocket socket) { try { - String deviceAddr = dataPackage.getAddr(); - String productKey = dataPackage.getPayload(); + // 1.1 解析认证参数 + IotDeviceMessage message = messageInfo.message; + AuthParams authParams = parseAuthParams(message.getParams()); + if (authParams == null) { + sendError(socket, message.getRequestId(), "认证参数不完整", messageInfo.codecType); + return; + } + // 1.2 执行认证 + if (!authenticateDevice(authParams)) { + log.warn("[handleAuthenticationRequest][认证失败,客户端 ID: {},username: {}]", + clientId, authParams.username); + sendError(socket, message.getRequestId(), "认证失败", messageInfo.codecType); + return; + } - log.info("[handleDeviceRegister][设备注册] 设备地址: {}, 产品密钥: {}", deviceAddr, productKey); - - // 获取设备信息 - IotDeviceRespDTO device = deviceService.getDeviceFromCache(productKey, deviceAddr); + // 2.1 解析设备信息 + IotDeviceAuthUtils.DeviceInfo deviceInfo = IotDeviceAuthUtils.parseUsername(authParams.username); + if (deviceInfo == null) { + sendError(socket, message.getRequestId(), "解析设备信息失败", messageInfo.codecType); + return; + } + // 2.2 获取设备信息 + IotDeviceRespDTO device = deviceService.getDeviceFromCache(deviceInfo.getProductKey(), + deviceInfo.getDeviceName()); if (device == null) { - log.error("[handleDeviceRegister][设备不存在: {} - {}]", productKey, deviceAddr); - sendRegisterReply(client, dataPackage, false); + sendError(socket, message.getRequestId(), "设备不存在", messageInfo.codecType); return; } - // 更新客户端信息 - client.setProductKey(productKey); - client.setDeviceName(deviceAddr); - client.setDeviceId(device.getId()); - client.setAuthenticated(true); + // 3. 注册连接并发送成功响应 + registerConnection(socket, device, deviceInfo, authParams.clientId); + sendOnlineMessage(deviceInfo); + sendSuccess(socket, message.getRequestId(), "认证成功", messageInfo.codecType); + log.info("[handleAuthenticationRequest][认证成功,设备 ID: {},设备名: {}]", + device.getId(), deviceInfo.getDeviceName()); + } catch (Exception e) { + log.error("[handleAuthenticationRequest][认证处理异常,客户端 ID: {}]", clientId, e); + sendError(socket, messageInfo.message.getRequestId(), "认证处理异常", messageInfo.codecType); + } + } - // 添加到连接管理器 - connectionManager.addClient(deviceAddr, client); - connectionManager.setDeviceIdMapping(deviceAddr, device.getId()); + /** + * 处理业务请求 + */ + private void handleBusinessRequest(String clientId, MessageInfo messageInfo, NetSocket socket) { + try { + // 1. 检查认证状态 + if (connectionManager.isNotAuthenticated(socket)) { + log.warn("[handleBusinessRequest][设备未认证,客户端 ID: {}]", clientId); + sendError(socket, messageInfo.message.getRequestId(), "请先进行认证", messageInfo.codecType); + return; + } - // 发送设备上线消息 + // 2. 获取认证信息并处理业务消息 + IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket); + processBusinessMessage(clientId, messageInfo.message, authInfo); + } catch (Exception e) { + log.error("[handleBusinessRequest][业务请求处理异常,客户端 ID: {}]", clientId, e); + } + } + + // TODO @haohao:processBusinessMessage 这个小方法,直接融合到 handleBusinessRequest 里?读起来更聚集点 + /** + * 处理业务消息 + */ + private void processBusinessMessage(String clientId, IotDeviceMessage message, + IotTcpConnectionManager.AuthInfo authInfo) { + try { + message.setDeviceId(authInfo.getDeviceId()); + message.setServerId(serverId); + // 发送到消息总线 + deviceMessageService.sendDeviceMessage(message, authInfo.getProductKey(), + authInfo.getDeviceName(), serverId); + } catch (Exception e) { + log.error("[processBusinessMessage][业务消息处理失败,客户端 ID: {},消息 ID: {}]", + clientId, message.getId(), e); + } + } + + /** + * 解码消息 + * + * @param buffer 消息 + */ + private MessageInfo decodeMessage(Buffer buffer) { + if (buffer == null || buffer.length() == 0) { + return null; + } + // 1. 快速检测消息格式类型 + // TODO @haohao:是不是进一步优化?socket 建立认证后,那条消息已经定义了所有消息的格式哈? + String codecType = detectMessageFormat(buffer); + try { + // 2. 使用检测到的格式进行解码 + IotDeviceMessage message = deviceMessageService.decodeDeviceMessage(buffer.getBytes(), codecType); + if (message == null) { + return null; + } + return new MessageInfo(message, codecType); + } catch (Exception e) { + log.warn("[decodeMessage][消息解码失败,格式: {},数据长度: {},错误: {}]", + codecType, buffer.length(), e.getMessage()); + // TODO @haohao:一般消息格式不对,应该抛出异常,断开连接居多? + return null; + } + } + + /** + * 检测消息格式类型 + * 优化性能:避免不必要的字符串转换 + */ + private String detectMessageFormat(Buffer buffer) { + // TODO @haohao:是不是 IotTcpBinaryDeviceMessageCodec 提供一个 isBinaryFormat 方法哈? + // 默认使用 JSON + if (buffer.length() == 0) { + return CODEC_TYPE_JSON; + } + + // 1. 优先检测二进制格式(检查魔术字节 0x7E) + if (isBinaryFormat(buffer)) { + return CODEC_TYPE_BINARY; + } + + // 2. 检测 JSON 格式(检查前几个有效字符) + // TODO @haohao:这个检测去掉?直接 return CODEC_TYPE_JSON 更简洁一点。 + if (isJsonFormat(buffer)) { + return CODEC_TYPE_JSON; + } + + // 3. 默认尝试 JSON 格式 + return CODEC_TYPE_JSON; + } + + /** + * 检测二进制格式 + * 通过检查魔术字节快速识别,避免完整字符串转换 + */ + private boolean isBinaryFormat(Buffer buffer) { + // 二进制协议最小长度检查 + if (buffer.length() < 8) { + return false; + } + + try { + // 检查魔术字节 0x7E(二进制协议的第一个字节) + byte firstByte = buffer.getByte(0); + return firstByte == (byte) 0x7E; + } catch (Exception e) { + return false; + } + } + + /** + * 检测 JSON 格式 + * 只检查前几个有效字符,避免完整字符串转换 + */ + private boolean isJsonFormat(Buffer buffer) { + try { + // 检查前 64 个字节或整个缓冲区(取较小值) + int checkLength = Math.min(buffer.length(), 64); + String prefix = buffer.getString(0, checkLength, StandardCharsets.UTF_8.name()); + + if (StrUtil.isBlank(prefix)) { + return false; + } + + String trimmed = prefix.trim(); + // JSON 格式必须以 { 或 [ 开头 + return trimmed.startsWith("{") || trimmed.startsWith("["); + + } catch (Exception e) { + return false; + } + } + + /** + * 注册连接信息 + */ + private void registerConnection(NetSocket socket, IotDeviceRespDTO device, + IotDeviceAuthUtils.DeviceInfo deviceInfo, String clientId) { + // TODO @haohao:AuthInfo 的创建,放在 connectionManager 里构建貌似会更收敛一点? + // 创建认证信息 + IotTcpConnectionManager.AuthInfo authInfo = new IotTcpConnectionManager.AuthInfo() + .setDeviceId(device.getId()) + .setProductKey(deviceInfo.getProductKey()) + .setDeviceName(deviceInfo.getDeviceName()) + .setClientId(clientId); + // 注册连接 + connectionManager.registerConnection(socket, device.getId(), authInfo); + } + + /** + * 发送设备上线消息 + */ + private void sendOnlineMessage(IotDeviceAuthUtils.DeviceInfo deviceInfo) { + try { IotDeviceMessage onlineMessage = IotDeviceMessage.buildStateUpdateOnline(); - messageService.sendDeviceMessage(onlineMessage, productKey, deviceAddr, serverId); - - // 发送注册成功回复 - sendRegisterReply(client, dataPackage, true); - - log.info("[handleDeviceRegister][设备注册成功] 设备地址: {}, 设备ID: {}", deviceAddr, device.getId()); - + deviceMessageService.sendDeviceMessage(onlineMessage, deviceInfo.getProductKey(), + deviceInfo.getDeviceName(), serverId); } catch (Exception e) { - log.error("[handleDeviceRegister][设备注册失败]", e); - sendRegisterReply(client, dataPackage, false); + log.error("[sendOnlineMessage][发送上线消息失败,设备: {}]", deviceInfo.getDeviceName(), e); } } /** - * 处理心跳 - * - * @param client 设备客户端 - * @param dataPackage 数据包 + * 清理连接 */ - private void handleHeartbeat(TcpDeviceClient client, TcpDataPackage dataPackage) { + private void cleanupConnection(NetSocket socket) { try { - String deviceAddr = dataPackage.getAddr(); - - log.debug("[handleHeartbeat][收到心跳] 设备地址: {}", deviceAddr); - - // 更新心跳时间 - client.keepAlive(); - - // 发送心跳回复(可选) - // sendHeartbeatReply(client, dataPackage); - - } catch (Exception e) { - log.error("[handleHeartbeat][处理心跳失败]", e); - } - } - - /** - * 处理数据上报 - * - * @param client 设备客户端 - * @param dataPackage 数据包 - */ - private void handleDataUp(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - String payload = dataPackage.getPayload(); - - log.info("[handleDataUp][数据上报] 设备地址: {}, 数据: {}", deviceAddr, payload); - - // 检查设备是否已认证 - if (!client.isAuthenticated()) { - log.warn("[handleDataUp][设备未认证,忽略数据上报: {}]", deviceAddr); - return; - } - - // 使用 IotDeviceMessageService 解码消息 - try { - // 1. 将 TCP 数据包重新编码为字节数组 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] messageBytes = buffer.getBytes(); - - // 2. 使用 messageService 解码消息 - IotDeviceMessage message = messageService.decodeDeviceMessage( - messageBytes, client.getProductKey(), client.getDeviceName()); - - // 3. 发送解码后的消息 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - - } catch (Exception e) { - log.warn("[handleDataUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); - - // 降级处理:使用原始方式解析数据 - JSONObject dataJson = JSONUtil.parseObj(payload); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.property.post", dataJson); - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } - - // 发送数据上报回复 - sendDataUpReply(client, dataPackage); - - } catch (Exception e) { - log.error("[handleDataUp][处理数据上报失败]", e); - } - } - - /** - * 处理事件上报 - * - * @param client 设备客户端 - * @param dataPackage 数据包 - */ - private void handleEventUp(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - String deviceAddr = dataPackage.getAddr(); - String payload = dataPackage.getPayload(); - - log.info("[handleEventUp][事件上报] 设备地址: {}, 数据: {}", deviceAddr, payload); - - // 检查设备是否已认证 - if (!client.isAuthenticated()) { - log.warn("[handleEventUp][设备未认证,忽略事件上报: {}]", deviceAddr); - return; - } - - // 使用 IotDeviceMessageService 解码消息 - try { - // 1. 将 TCP 数据包重新编码为字节数组 - Buffer buffer = TcpDataEncoder.encode(dataPackage); - byte[] messageBytes = buffer.getBytes(); - - // 2. 使用 messageService 解码消息 - IotDeviceMessage message = messageService.decodeDeviceMessage( - messageBytes, client.getProductKey(), client.getDeviceName()); - - // 3. 发送解码后的消息 - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - - } catch (Exception e) { - log.warn("[handleEventUp][使用编解码器解码失败,降级使用原始解析] 错误: {}", e.getMessage()); - - // 降级处理:使用原始方式解析数据 - JSONObject eventJson = JSONUtil.parseObj(payload); - IotDeviceMessage message = IotDeviceMessage.requestOf("thing.event.post", eventJson); - messageService.sendDeviceMessage(message, client.getProductKey(), client.getDeviceName(), serverId); - } - - // 发送事件上报回复 - sendEventUpReply(client, dataPackage); - - } catch (Exception e) { - log.error("[handleEventUp][处理事件上报失败]", e); - } - } - - /** - * 发送注册回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - * @param success 是否成功 - */ - private void sendRegisterReply(TcpDeviceClient client, TcpDataPackage dataPackage, boolean success) { - try { - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.createRegisterReply( - dataPackage.getAddr(), dataPackage.getMid(), success); - client.sendMessage(replyBuffer); - - log.debug("[sendRegisterReply][发送注册回复] 设备地址: {}, 结果: {}", - dataPackage.getAddr(), success ? "成功" : "失败"); - } catch (Exception e) { - log.error("[sendRegisterReply][发送注册回复失败]", e); - } - } - - /** - * 发送数据上报回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - */ - private void sendDataUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - TcpDataPackage replyPackage = TcpDataPackage.builder() - .addr(dataPackage.getAddr()) - .code(TcpDataPackage.CODE_DATA_UP) - .mid(dataPackage.getMid()) - .payload("0") // 0表示成功 - .build(); - - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); - client.sendMessage(replyBuffer); - - } catch (Exception e) { - log.error("[sendDataUpReply][发送数据上报回复失败]", e); - } - } - - /** - * 发送事件上报回复 - * - * @param client 设备客户端 - * @param dataPackage 原始数据包 - */ - private void sendEventUpReply(TcpDeviceClient client, TcpDataPackage dataPackage) { - try { - TcpDataPackage replyPackage = TcpDataPackage.builder() - .addr(dataPackage.getAddr()) - .code(TcpDataPackage.CODE_EVENT_UP) - .mid(dataPackage.getMid()) - .payload("0") // 0表示成功 - .build(); - - io.vertx.core.buffer.Buffer replyBuffer = TcpDataEncoder.encode(replyPackage); - client.sendMessage(replyBuffer); - - } catch (Exception e) { - log.error("[sendEventUpReply][发送事件上报回复失败]", e); - } - } - - /** - * 处理连接关闭 - * - * @param client 设备客户端 - */ - private void handleConnectionClose(TcpDeviceClient client) { - try { - String deviceAddr = client.getDeviceAddr(); - - // 发送设备离线消息 - if (client.isAuthenticated()) { + // 发送离线消息(如果已认证) + IotTcpConnectionManager.AuthInfo authInfo = connectionManager.getAuthInfo(socket); + if (authInfo != null) { IotDeviceMessage offlineMessage = IotDeviceMessage.buildStateOffline(); - messageService.sendDeviceMessage(offlineMessage, - client.getProductKey(), client.getDeviceName(), serverId); + deviceMessageService.sendDeviceMessage(offlineMessage, authInfo.getProductKey(), + authInfo.getDeviceName(), serverId); } - // 从连接管理器移除 - if (deviceAddr != null) { - connectionManager.removeClient(deviceAddr); - } + // 注销连接 + connectionManager.unregisterConnection(socket); + } catch (Exception e) { + log.error("[cleanupConnection][清理连接失败]", e); + } + } - log.info("[handleConnectionClose][处理连接关闭完成] 设备地址: {}", deviceAddr); + /** + * 发送响应消息 + */ + private void sendResponse(NetSocket socket, boolean success, String message, String requestId, String codecType) { + try { + Object responseData = MapUtil.builder() + .put("success", success) + .put("message", message) + .build(); + + IotDeviceMessage responseMessage = IotDeviceMessage.replyOf(requestId, AUTH_METHOD, responseData, + success ? 0 : 401, message); + + byte[] encodedData = deviceMessageService.encodeDeviceMessage(responseMessage, codecType); + socket.write(Buffer.buffer(encodedData)); } catch (Exception e) { - log.error("[handleConnectionClose][处理连接关闭失败]", e); + log.error("[sendResponse][发送响应失败,requestId: {}]", requestId, e); } } + + // ==================== 辅助方法 ==================== + + /** + * 判断是否为认证请求 + */ + private boolean isAuthRequest(IotDeviceMessage message) { + return AUTH_METHOD.equals(message.getMethod()); + } + + /** + * 解析认证参数 + */ + private AuthParams parseAuthParams(Object params) { + if (params == null) { + return null; + } + try { + JSONObject paramsJson = params instanceof JSONObject ? (JSONObject) params + : JSONUtil.parseObj(params.toString()); + String clientId = paramsJson.getStr("clientId"); + String username = paramsJson.getStr("username"); + String password = paramsJson.getStr("password"); + return StrUtil.hasBlank(clientId, username, password) ? null + : new AuthParams(clientId, username, password); + } catch (Exception e) { + log.warn("[parseAuthParams][解析认证参数失败]", e); + return null; + } + } + + /** + * 认证设备 + */ + private boolean authenticateDevice(AuthParams authParams) { + try { + CommonResult result = deviceApi.authDevice(new IotDeviceAuthReqDTO() + .setClientId(authParams.clientId) + .setUsername(authParams.username) + .setPassword(authParams.password)); + return result.isSuccess() && Boolean.TRUE.equals(result.getData()); + } catch (Exception e) { + log.error("[authenticateDevice][设备认证异常,username: {}]", authParams.username, e); + return false; + } + } + + // TODO @haohao:改成 sendErrorResponse sendSuccessResponse 更清晰点? + + /** + * 发送错误响应 + */ + private void sendError(NetSocket socket, String requestId, String errorMessage, String codecType) { + sendResponse(socket, false, errorMessage, requestId, codecType); + } + + /** + * 发送成功响应 + */ + private void sendSuccess(NetSocket socket, String requestId, String message, String codecType) { + sendResponse(socket, true, message, requestId, codecType); + } + + // ==================== 内部类 ==================== + + // TODO @haohao:IotDeviceAuthReqDTO 复用这个? + /** + * 认证参数 + */ + @Data + @AllArgsConstructor + private static class AuthParams { + + private final String clientId; + private final String username; + private final String password; + + } + + /** + * 消息信息 + */ + @Data + @AllArgsConstructor + private static class MessageInfo { + + private final IotDeviceMessage message; + + private final String codecType; + + } } \ No newline at end of file diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java index 24134ba94a..c86fc0983d 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageService.java @@ -20,6 +20,16 @@ public interface IotDeviceMessageService { byte[] encodeDeviceMessage(IotDeviceMessage message, String productKey, String deviceName); + /** + * 编码消息 + * + * @param message 消息 + * @param codecType 编解码器类型 + * @return 编码后的消息内容 + */ + byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType); + /** * 解码消息 * @@ -31,13 +41,22 @@ public interface IotDeviceMessageService { IotDeviceMessage decodeDeviceMessage(byte[] bytes, String productKey, String deviceName); + /** + * 解码消息 + * + * @param bytes 消息内容 + * @param codecType 编解码器类型 + * @return 解码后的消息内容 + */ + IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType); + /** * 发送消息 * - * @param message 消息 + * @param message 消息 * @param productKey 产品 Key * @param deviceName 设备名称 - * @param serverId 设备连接的 serverId + * @param serverId 设备连接的 serverId */ void sendDeviceMessage(IotDeviceMessage message, String productKey, String deviceName, String serverId); diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java index 6f1f731d29..014da9a5df 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/java/cn/iocoder/yudao/module/iot/gateway/service/device/message/IotDeviceMessageServiceImpl.java @@ -61,6 +61,19 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { return codec.encode(message); } + @Override + public byte[] encodeDeviceMessage(IotDeviceMessage message, + String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 编码消息 + return codec.encode(message); + } + @Override public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String productKey, String deviceName) { @@ -79,6 +92,18 @@ public class IotDeviceMessageServiceImpl implements IotDeviceMessageService { return codec.decode(bytes); } + @Override + public IotDeviceMessage decodeDeviceMessage(byte[] bytes, String codecType) { + // 1. 获取编解码器 + IotDeviceMessageCodec codec = codes.get(codecType); + if (codec == null) { + throw new IllegalArgumentException(StrUtil.format("编解码器({}) 不存在", codecType)); + } + + // 2. 解码消息 + return codec.decode(bytes); + } + @Override public void sendDeviceMessage(IotDeviceMessage message, String productKey, String deviceName, String serverId) { diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml index 26376b6669..b306f0588c 100644 --- a/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml +++ b/yudao-module-iot/yudao-module-iot-gateway/src/main/resources/application.yaml @@ -4,6 +4,15 @@ spring: profiles: active: local # 默认激活本地开发环境 + # Redis 配置 + data: + redis: + host: 127.0.0.1 # Redis 服务器地址 + port: 6379 # Redis 服务器端口 + database: 0 # Redis 数据库索引 + # password: # Redis 密码,如果有的话 + timeout: 30000ms # 连接超时时间 + --- #################### 消息队列相关 #################### # rocketmq 配置项,对应 RocketMQProperties 配置类 @@ -45,7 +54,7 @@ yudao: # 针对引入的 EMQX 组件的配置 # ==================================== emqx: - enabled: false + enabled: true http-port: 8090 # MQTT HTTP 服务端口 mqtt-host: 127.0.0.1 # MQTT Broker 地址 mqtt-port: 1883 # MQTT Broker 端口 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md new file mode 100644 index 0000000000..d85d347f70 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-binary-packet-examples.md @@ -0,0 +1,198 @@ +# TCP 二进制协议数据包格式说明 + +## 1. 协议概述 + +TCP 二进制协议是一种高效的自定义协议格式,采用紧凑的二进制格式传输数据,适用于对带宽和性能要求较高的 IoT 场景。 + +### 1.1 协议特点 + +- **高效传输**:完全二进制格式,减少数据传输量 +- **版本控制**:内置协议版本号,支持协议升级 +- **类型安全**:明确的消息类型标识 +- **扩展性**:预留标志位,支持未来功能扩展 +- **兼容性**:与现有 `IotDeviceMessage` 接口完全兼容 + +## 2. 协议格式 + +### 2.1 整体结构 + +``` ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 魔术字 | 版本号 | 消息类型| 消息标志| 消息长度(4字节) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 消息 ID 长度(2字节) | 消息 ID (变长字符串) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 方法名长度(2字节) | 方法名(变长字符串) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +| 消息体数据(变长) | ++--------+--------+--------+--------+--------+--------+--------+--------+ +``` + +### 2.2 字段详细说明 + +| 字段 | 长度 | 类型 | 说明 | +|------|------|------|------| +| 魔术字 | 1字节 | byte | `0x7E` - 协议识别标识,用于数据同步 | +| 版本号 | 1字节 | byte | `0x01` - 协议版本号,支持版本控制 | +| 消息类型 | 1字节 | byte | `0x01`=请求, `0x02`=响应 | +| 消息标志 | 1字节 | byte | 预留字段,用于未来扩展 | +| 消息长度 | 4字节 | int | 整个消息的总长度(包含头部) | +| 消息 ID 长度 | 2字节 | short | 消息 ID 字符串的字节长度 | +| 消息 ID | 变长 | string | 消息唯一标识符(UTF-8编码) | +| 方法名长度 | 2字节 | short | 方法名字符串的字节长度 | +| 方法名 | 变长 | string | 消息方法名(UTF-8编码) | +| 消息体 | 变长 | binary | 根据消息类型的不同数据格式 | + +**⚠️ 重要说明**:deviceId 不包含在协议中,由服务器根据连接上下文自动设置 + +### 2.3 协议常量定义 + +```java +// 协议标识 +private static final byte MAGIC_NUMBER = (byte) 0x7E; +private static final byte PROTOCOL_VERSION = (byte) 0x01; + +// 消息类型 +public static class MessageType { + public static final byte REQUEST = 0x01; // 请求消息 + public static final byte RESPONSE = 0x02; // 响应消息 +} + +// 协议长度 +private static final int HEADER_FIXED_LENGTH = 8; // 固定头部长度 +private static final int MIN_MESSAGE_LENGTH = 12; // 最小消息长度 +``` + +## 3. 消息类型和格式 + +### 3.1 请求消息 (REQUEST - 0x01) + +请求消息用于设备向服务器发送数据或请求。 + +#### 3.1.1 消息体格式 +``` +消息体 = params 数据(JSON格式) +``` + +#### 3.1.2 示例:设备认证请求 + +**消息内容:** +- 消息 ID: `auth_1704067200000_123` +- 方法名: `auth` +- 参数: `{"clientId":"device_001","username":"productKey_deviceName","password":"device_password"}` + +**二进制数据包结构:** +``` +7E // 魔术字 (0x7E) +01 // 版本号 (0x01) +01 // 消息类型 (REQUEST) +00 // 消息标志 (预留) +00 00 00 8A // 消息长度 (138字节) +00 19 // 消息 ID 长度 (25字节) +61 75 74 68 5F 31 37 30 34 30 // 消息 ID: "auth_1704067200000_123" +36 37 32 30 30 30 30 30 5F 31 +32 33 +00 04 // 方法名长度 (4字节) +61 75 74 68 // 方法名: "auth" +7B 22 63 6C 69 65 6E 74 49 64 // JSON参数数据 +22 3A 22 64 65 76 69 63 65 5F // {"clientId":"device_001", +30 30 31 22 2C 22 75 73 65 72 // "username":"productKey_deviceName", +6E 61 6D 65 22 3A 22 70 72 6F // "password":"device_password"} +64 75 63 74 4B 65 79 5F 64 65 +76 69 63 65 4E 61 6D 65 22 2C +22 70 61 73 73 77 6F 72 64 22 +3A 22 64 65 76 69 63 65 5F 70 +61 73 73 77 6F 72 64 22 7D +``` + +#### 3.1.3 示例:属性数据上报 + +**消息内容:** +- 消息 ID: `property_1704067200000_456` +- 方法名: `thing.property.post` +- 参数: `{"temperature":25.5,"humidity":60.2,"pressure":1013.25}` + +### 3.2 响应消息 (RESPONSE - 0x02) + +响应消息用于服务器向设备回复请求结果。 + +#### 3.2.1 消息体格式 +``` +消息体 = 响应码(4字节) + 响应消息长度(2字节) + 响应消息(UTF-8) + 响应数据(JSON) +``` + +#### 3.2.2 字段说明 + +| 字段 | 长度 | 类型 | 说明 | +|------|------|------|------| +| 响应码 | 4字节 | int | HTTP状态码风格,0=成功,其他=错误 | +| 响应消息长度 | 2字节 | short | 响应消息字符串的字节长度 | +| 响应消息 | 变长 | string | 响应提示信息(UTF-8编码) | +| 响应数据 | 变长 | binary | JSON格式的响应数据(可选) | + +#### 3.2.3 示例:认证成功响应 + +**消息内容:** +- 消息 ID: `auth_response_1704067200000_123` +- 方法名: `auth` +- 响应码: `0` +- 响应消息: `认证成功` +- 响应数据: `{"success":true,"message":"认证成功"}` + +**二进制数据包结构:** +``` +7E // 魔术字 (0x7E) +01 // 版本号 (0x01) +02 // 消息类型 (RESPONSE) +00 // 消息标志 (预留) +00 00 00 A5 // 消息长度 (165字节) +00 22 // 消息 ID 长度 (34字节) +61 75 74 68 5F 72 65 73 70 6F // 消息 ID: "auth_response_1704067200000_123" +6E 73 65 5F 31 37 30 34 30 36 +37 32 30 30 30 30 30 5F 31 32 +33 +00 04 // 方法名长度 (4字节) +61 75 74 68 // 方法名: "auth" +00 00 00 00 // 响应码 (0 = 成功) +00 0C // 响应消息长度 (12字节) +E8 AE A4 E8 AF 81 E6 88 90 E5 // 响应消息: "认证成功" (UTF-8) +8A 9F +7B 22 73 75 63 63 65 73 73 22 // JSON响应数据 +3A 74 72 75 65 2C 22 6D 65 73 // {"success":true,"message":"认证成功"} +73 61 67 65 22 3A 22 E8 AE A4 +E8 AF 81 E6 88 90 E5 8A 9F 22 +7D +``` + +## 4. 编解码器标识 + +```java +public static final String TYPE = "TCP_BINARY"; +``` + +## 5. 协议优势 + +- **数据紧凑**:二进制格式,相比 JSON 减少 30-50% 的数据量 +- **解析高效**:直接二进制操作,减少字符串转换开销 +- **类型安全**:明确的消息类型和字段定义 +- **扩展性强**:预留标志位支持未来功能扩展 +- **版本控制**:内置版本号支持协议升级 + +## 6. 与 JSON 协议对比 + +| 特性 | 二进制协议 | JSON协议 | +|------|------------|----------| +| 数据大小 | 小(节省30-50%) | 大 | +| 解析性能 | 高 | 中等 | +| 网络开销 | 低 | 高 | +| 可读性 | 差 | 优秀 | +| 调试难度 | 高 | 低 | +| 扩展性 | 良好(有预留位) | 优秀 | + +**推荐场景**: +- ✅ **高频数据传输**:传感器数据实时上报 +- ✅ **带宽受限环境**:移动网络、卫星通信 +- ✅ **性能要求高**:需要低延迟、高吞吐的场景 +- ✅ **设备资源有限**:嵌入式设备、低功耗设备 +- ❌ **开发调试阶段**:调试困难,建议使用 JSON 协议 +- ❌ **快速原型开发**:开发效率低 diff --git a/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md new file mode 100644 index 0000000000..09ef50cfe5 --- /dev/null +++ b/yudao-module-iot/yudao-module-iot-gateway/src/test/resources/tcp-json-packet-examples.md @@ -0,0 +1,191 @@ +# TCP JSON 格式协议说明 + +## 1. 协议概述 + +TCP JSON 格式协议采用纯 JSON 格式进行数据传输,具有以下特点: + +- **标准化**:使用标准 JSON 格式,易于解析和处理 +- **可读性**:人类可读,便于调试和维护 +- **扩展性**:可以轻松添加新字段,向后兼容 +- **跨平台**:JSON 格式支持所有主流编程语言 +- **安全优化**:移除冗余的 deviceId 字段,提高安全性 + +## 2. 消息格式 + +### 2.1 基础消息结构 + +```json +{ + "id": "消息唯一标识", + "method": "消息方法", + "params": { + // 请求参数 + }, + "data": { + // 响应数据 + }, + "code": 响应码, + "msg": "响应消息", + "timestamp": 时间戳 +} +``` + +**⚠️ 重要说明**: +- **不包含 deviceId 字段**:由服务器通过 TCP 连接上下文自动确定设备 ID +- **避免伪造攻击**:防止设备伪造其他设备的 ID 发送消息 + +### 2.2 字段详细说明 + +| 字段名 | 类型 | 必填 | 用途 | 说明 | +|--------|------|------|------|------| +| id | String | 是 | 所有消息 | 消息唯一标识 | +| method | String | 是 | 所有消息 | 消息方法,如 `auth`、`thing.property.post` | +| params | Object | 否 | 请求消息 | 请求参数,具体内容根据method而定 | +| data | Object | 否 | 响应消息 | 响应数据,服务器返回的结果数据 | +| code | Integer | 否 | 响应消息 | 响应码,0=成功,其他=错误 | +| msg | String | 否 | 响应消息 | 响应提示信息 | +| timestamp | Long | 是 | 所有消息 | 时间戳(毫秒),编码时自动生成 | + +### 2.3 消息分类 + +#### 2.3.1 请求消息(上行) +- **特征**:包含 `params` 字段,不包含 `code`、`msg` 字段 +- **方向**:设备 → 服务器 +- **用途**:设备认证、数据上报、状态更新等 + +#### 2.3.2 响应消息(下行) +- **特征**:包含 `code`、`msg` 字段,可能包含 `data` 字段 +- **方向**:服务器 → 设备 +- **用途**:认证结果、指令响应、错误提示等 + +## 3. 消息示例 + +### 3.1 设备认证 (auth) + +#### 认证请求格式 +**消息方向**:设备 → 服务器 + +```json +{ + "id": "auth_1704067200000_123", + "method": "auth", + "params": { + "clientId": "device_001", + "username": "productKey_deviceName", + "password": "设备密码" + }, + "timestamp": 1704067200000 +} +``` + +**认证参数说明:** + +| 字段名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| clientId | String | 是 | 客户端唯一标识,用于连接管理 | +| username | String | 是 | 设备用户名,格式为 `productKey_deviceName` | +| password | String | 是 | 设备密码,在设备管理平台配置 | + +#### 认证响应格式 +**消息方向**:服务器 → 设备 + +**认证成功响应:** +```json +{ + "id": "response_auth_1704067200000_123", + "method": "auth", + "data": { + "success": true, + "message": "认证成功" + }, + "code": 0, + "msg": "认证成功", + "timestamp": 1704067200001 +} +``` + +**认证失败响应:** +```json +{ + "id": "response_auth_1704067200000_123", + "method": "auth", + "data": { + "success": false, + "message": "认证失败:用户名或密码错误" + }, + "code": 401, + "msg": "认证失败", + "timestamp": 1704067200001 +} +``` + +### 3.2 属性数据上报 (thing.property.post) + +**消息方向**:设备 → 服务器 + +**示例:温度传感器数据上报** +```json +{ + "id": "property_1704067200000_456", + "method": "thing.property.post", + "params": { + "temperature": 25.5, + "humidity": 60.2, + "pressure": 1013.25, + "battery": 85, + "signal_strength": -65 + }, + "timestamp": 1704067200000 +} +``` + +### 3.3 设备状态更新 (thing.state.update) + +**消息方向**:设备 → 服务器 + +**示例:心跳请求** +```json +{ + "id": "heartbeat_1704067200000_321", + "method": "thing.state.update", + "params": { + "state": "online", + "uptime": 86400, + "memory_usage": 65.2, + "cpu_usage": 12.8 + }, + "timestamp": 1704067200000 +} +``` + +## 4. 编解码器标识 + +```java +public static final String TYPE = "TCP_JSON"; +``` + +## 5. 协议优势 + +- **开发效率高**:JSON 格式,开发和调试简单 +- **跨语言支持**:所有主流语言都支持 JSON +- **可读性优秀**:可以直接查看消息内容 +- **扩展性强**:可以轻松添加新字段 +- **安全性高**:移除 deviceId 字段,防止伪造攻击 + +## 6. 与二进制协议对比 + +| 特性 | JSON协议 | 二进制协议 | +|------|----------|------------| +| 开发难度 | 低 | 高 | +| 调试难度 | 低 | 高 | +| 可读性 | 优秀 | 差 | +| 数据大小 | 中等 | 小(节省30-50%) | +| 解析性能 | 中等 | 高 | +| 学习成本 | 低 | 高 | + +**推荐场景**: +- ✅ **开发调试阶段**:调试友好,开发效率高 +- ✅ **快速原型开发**:实现简单,快速迭代 +- ✅ **多语言集成**:广泛的语言支持 +- ❌ **高频数据传输**:建议使用二进制协议 +- ❌ **带宽受限环境**:建议使用二进制协议 \ No newline at end of file diff --git a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java index df8ae8c5bd..56ebc283fe 100644 --- a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java +++ b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/history/vo/ProductBrowseHistoryRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.product.controller.admin.history.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java index fbc75522a7..d962fe7469 100755 --- a/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java +++ b/yudao-module-mall/yudao-module-product/src/main/java/cn/iocoder/yudao/module/product/controller/admin/spu/vo/ProductSpuRespVO.java @@ -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.MoneyConvert; import cn.iocoder.yudao.module.product.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java index 248160dd9c..059d6af579 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/kefu/vo/message/KeFuMessageRespVO.java @@ -4,7 +4,6 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; -import com.alibaba.excel.annotation.*; @Schema(description = "管理后台 - 客服消息 Response VO") @Data @@ -42,4 +41,4 @@ public class KeFuMessageRespVO { @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED) private LocalDateTime createTime; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java index d81b3d6902..14f1ee4ccd 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/activity/PointActivityRespVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.activity; import cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product.PointProductRespVO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -69,4 +69,4 @@ public class PointActivityRespVO { @Schema(description = "兑换金额,单位:分", requiredMode = Schema.RequiredMode.REQUIRED, example = "15860") private Integer price; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java index 8e8250b387..638ea61fb8 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/admin/point/vo/product/PointProductRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.promotion.controller.admin.point.vo.product; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -36,4 +36,4 @@ public class PointProductRespVO { @Schema(description = "积分商城商品状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "2") private Integer activityStatus; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java index ddcc23b9cb..83e00c202a 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/controller/app/point/vo/AppPointActivityRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.promotion.controller.app.point.vo; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java index 91216bf184..59ee95fb12 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/dataobject/coupon/CouponTemplateDO.java @@ -31,9 +31,13 @@ import java.util.List; public class CouponTemplateDO extends BaseDO { /** - * 不限制领取数量 + * 领取数量 - 不限制 */ - public static final Integer TIME_LIMIT_COUNT_MAX = -1; + public static final Integer TAKE_LIMIT_COUNT_MAX = -1; + /** + * 发放数量 - 不限制 + */ + public static final Integer TOTAL_COUNT_MAX = -1; // ========== 基本信息 BEGIN ========== /** diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java index 3096a49f3c..84e98f3dbd 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/dal/mysql/coupon/CouponTemplateMapper.java @@ -40,10 +40,16 @@ public interface CouponTemplateMapper extends BaseMapperX { .orderByDesc(CouponTemplateDO::getId)); } - default void updateTakeCount(Long id, Integer incrCount) { - update(null, new LambdaUpdateWrapper() + default int updateTakeCount(Long id, Integer incrCount) { + LambdaUpdateWrapper updateWrapper = new LambdaUpdateWrapper() .eq(CouponTemplateDO::getId, id) - .setSql("take_count = take_count + " + incrCount)); + .setSql("take_count = take_count + " + incrCount); + // 增加已领取的数量(incrCount 为正数),需要考虑发放数量 totalCount 的限制 + if (incrCount > 0) { + updateWrapper.and(i -> i.apply("take_count < total_count") + .or().eq(CouponTemplateDO::getTotalCount, CouponTemplateDO.TOTAL_COUNT_MAX)); + } + return update(updateWrapper); } default List selectListByTakeType(Integer takeType) { diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java index 8e168f4f9f..f5b38a13cc 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/combination/CombinationActivityServiceImpl.java @@ -86,7 +86,7 @@ public class CombinationActivityServiceImpl implements CombinationActivityServic activityList.removeIf(item -> ObjectUtil.equal(item.getId(), activityId)); } // 查找是否有其它活动,选择了该产品 - List matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getId(), spuId)); + List matchActivityList = filterList(activityList, activity -> ObjectUtil.equal(activity.getSpuId(), spuId)); if (CollUtil.isNotEmpty(matchActivityList)) { throw exception(COMBINATION_ACTIVITY_SPU_CONFLICTS); } diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java index e6f82a69fc..e175807503 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponServiceImpl.java @@ -137,7 +137,6 @@ public class CouponServiceImpl implements CouponService { // 4. 增加优惠劵模板的领取数量 couponTemplateService.updateCouponTemplateTakeCount(template.getId(), userIds.size()); - return convertMultiMap(couponList, CouponDO::getUserId, CouponDO::getId); } @@ -281,7 +280,7 @@ public class CouponServiceImpl implements CouponService { } // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && couponTemplate.getTakeCount() + userIds.size() > couponTemplate.getTotalCount()) { throw exception(COUPON_TEMPLATE_NOT_ENOUGH); } diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java index 175e33b197..bdd8b32825 100755 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/coupon/CouponTemplateServiceImpl.java @@ -22,8 +22,7 @@ import java.util.List; import java.util.Objects; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_NOT_EXISTS; -import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL; +import static cn.iocoder.yudao.module.promotion.enums.ErrorCodeConstants.*; /** * 优惠劵模板 Service 实现类 @@ -60,7 +59,7 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { CouponTemplateDO couponTemplate = validateCouponTemplateExists(updateReqVO.getId()); // 校验发放数量不能过小(仅在 CouponTakeTypeEnum.USER 用户领取时) if (CouponTakeTypeEnum.isUser(couponTemplate.getTakeType()) - && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TIME_LIMIT_COUNT_MAX) // 非不限制 + && ObjUtil.notEqual(couponTemplate.getTakeLimitCount(), CouponTemplateDO.TAKE_LIMIT_COUNT_MAX) // 非不限制 && updateReqVO.getTotalCount() < couponTemplate.getTakeCount()) { throw exception(COUPON_TEMPLATE_TOTAL_COUNT_TOO_SMALL, couponTemplate.getTakeCount()); } @@ -116,7 +115,10 @@ public class CouponTemplateServiceImpl implements CouponTemplateService { @Override public void updateCouponTemplateTakeCount(Long id, int incrCount) { - couponTemplateMapper.updateTakeCount(id, incrCount); + int updateCount = couponTemplateMapper.updateTakeCount(id, incrCount); + if (updateCount == 0) { + throw exception(COUPON_TEMPLATE_NOT_ENOUGH); + } } @Override diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java index 582cadb0d4..0b945794ad 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/point/PointActivityServiceImpl.java @@ -222,6 +222,7 @@ public class PointActivityServiceImpl implements PointActivityService { if (spu == null) { throw exception(SPU_NOT_EXISTS); } + products.forEach(product -> product.setSpuId(spuId)); // 2. 校验商品 sku 都存在 List skus = productSkuApi.getSkuListBySpuId(singletonList(spuId)); diff --git a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java index dcc5596f25..3c84a172ab 100644 --- a/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java +++ b/yudao-module-mall/yudao-module-promotion/src/main/java/cn/iocoder/yudao/module/promotion/service/seckill/SeckillActivityServiceImpl.java @@ -303,7 +303,9 @@ public class SeckillActivityServiceImpl implements SeckillActivityService { throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR); } SeckillConfigDO config = seckillConfigService.getCurrentSeckillConfig(); - if (config == null || !CollectionUtil.contains(activity.getConfigIds(), config.getId())) { + if (config == null + || !CollectionUtil.contains(activity.getConfigIds(), config.getId()) + || !LocalDateTimeUtils.isBetween(config.getStartTime(), config.getEndTime())) { throw exception(SECKILL_JOIN_ACTIVITY_TIME_ERROR); } // 1.3 超过单次购买限制 diff --git a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java index 9d93142e31..86126ca705 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java +++ b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/product/vo/ProductStatisticsRespVO.java @@ -1,7 +1,7 @@ package cn.iocoder.yudao.module.statistics.controller.admin.product.vo; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -78,4 +78,4 @@ public class ProductStatisticsRespVO { @Schema(description = "访客支付转化率(百分比)", requiredMode = Schema.RequiredMode.REQUIRED, example = "15") private Integer browseConvertPercent; -} \ No newline at end of file +} diff --git a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java index 5b14fa1d4e..9e27c6f3a1 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java +++ b/yudao-module-mall/yudao-module-statistics/src/main/java/cn/iocoder/yudao/module/statistics/controller/admin/trade/vo/TradeTrendSummaryExcelVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.statistics.controller.admin.trade.vo; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; -import com.alibaba.excel.annotation.ExcelProperty; -import com.alibaba.excel.annotation.format.DateTimeFormat; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.format.DateTimeFormat; import lombok.Data; import java.time.LocalDate; diff --git a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml index 07d2f0d580..e47c85e9b4 100644 --- a/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml +++ b/yudao-module-mall/yudao-module-statistics/src/main/resources/mapper/trade/TradeOrderStatisticsMapper.xml @@ -76,8 +76,8 @@ - SELECT IFNULL(SUM(pay_price), 0) AS orderPayPrice, - COUNT(1) AS orderPayCount + SELECT IFNULL(SUM(pay_price), 0) AS order_pay_price, + COUNT(1) AS order_pay_count FROM trade_order WHERE pay_status = #{payStatus} AND pay_time BETWEEN #{beginTime} AND #{endTime} diff --git a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java index b2d72e4b01..8ec1159244 100644 --- a/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java +++ b/yudao-module-mall/yudao-module-trade-api/src/main/java/cn/iocoder/yudao/module/trade/enums/ErrorCodeConstants.java @@ -39,7 +39,8 @@ public interface ErrorCodeConstants { ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_NOT_FOUND = new ErrorCode(1_011_000_034, "交易订单更新支付订单退款状态失败,原因:退款单不存在"); ErrorCode ORDER_UPDATE_PAID_ORDER_REFUNDED_FAIL_REFUND_STATUS_NOT_SUCCESS = new ErrorCode(1_011_000_035, "交易订单更新支付订单退款状态失败,原因:退款单状态不是【退款成功】"); ErrorCode ORDER_PICK_UP_FAIL_NOT_VERIFY_USER = new ErrorCode(1_011_000_036, "交易订单自提失败,原因:你没有核销该门店订单的权限"); - ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_037, "交易订单创建失败,原因:用户积分不足"); + ErrorCode ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS = new ErrorCode(1_011_000_037, "交易订单自提失败,原因:商品拼团记录不是【成功】状态"); + ErrorCode ORDER_CREATE_FAIL_INSUFFICIENT_USER_POINTS = new ErrorCode(1_011_000_038, "交易订单创建失败,原因:用户积分不足"); // ========== After Sale 模块 1-011-000-100 ========== ErrorCode AFTER_SALE_NOT_FOUND = new ErrorCode(1_011_000_100, "售后单不存在"); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java index d2165cb868..2bc654b290 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/aftersale/AfterSaleController.java @@ -141,9 +141,8 @@ public class AfterSaleController { public CommonResult updateAfterSaleRefunded(@RequestBody PayRefundNotifyReqDTO notifyReqDTO) { log.info("[updateAfterRefund][notifyReqDTO({})]", notifyReqDTO); if (StrUtil.startWithAny(notifyReqDTO.getMerchantRefundId(), "order-")) { - tradeOrderUpdateService.updatePaidOrderRefunded( - Long.parseLong(notifyReqDTO.getMerchantRefundId()), - notifyReqDTO.getPayRefundId()); + Long orderId = Long.parseLong(StrUtil.subAfter(notifyReqDTO.getMerchantRefundId(), "order-", true)); + tradeOrderUpdateService.updatePaidOrderRefunded(orderId, notifyReqDTO.getPayRefundId()); } else { afterSaleService.updateAfterSaleRefunded( Long.parseLong(notifyReqDTO.getMerchantRefundId()), diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java index c84a3a1896..3cc312a913 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/controller/admin/delivery/vo/express/DeliveryExpressExcelVO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.trade.controller.admin.delivery.vo.express; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java index 54480f696b..0e508a4643 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/aftersale/AfterSaleConvert.java @@ -43,7 +43,7 @@ public interface AfterSaleConvert { @Mapping(source = "afterSale.id", target = "merchantRefundId"), @Mapping(source = "afterSale.applyReason", target = "reason"), @Mapping(source = "afterSale.refundPrice", target = "price"), - @Mapping(source = "orderProperties.payAppKey", target = "appKey") + @Mapping(source = "orderProperties.payAppKey", target = "appKey"), }) PayRefundCreateReqDTO convert(String userIp, AfterSaleDO afterSale, TradeOrderProperties orderProperties); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java index fa0b9c8163..beacddab87 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/convert/order/TradeOrderConvert.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.trade.convert.order; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.string.StrUtils; @@ -101,7 +102,8 @@ public interface TradeOrderConvert { default PayOrderCreateReqDTO convert(TradeOrderDO order, List orderItems, TradeOrderProperties orderProperties) { PayOrderCreateReqDTO createReqDTO = new PayOrderCreateReqDTO() - .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp()); + .setAppKey(orderProperties.getPayAppKey()).setUserIp(order.getUserIp()) + .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()); // 商户相关字段 createReqDTO.setMerchantOrderId(String.valueOf(order.getId())); String subject = orderItems.get(0).getSpuName(); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java index 9d33cac21b..d5442cbeeb 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/framework/delivery/core/client/dto/kd100/Kd100ExpressQueryRespDTO.java @@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.trade.framework.delivery.core.client.dto.kd100; import com.fasterxml.jackson.annotation.JsonFormat; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import lombok.Data; import java.time.LocalDateTime; @@ -59,8 +61,8 @@ public class Kd100ExpressQueryRespDTO { * 轨迹发生时间 */ @JsonFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND, timezone = TIME_ZONE_DEFAULT) + @JsonDeserialize(using = LocalDateTimeDeserializer.class) private LocalDateTime time; - /** * 轨迹描述 */ diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java index f0c63a032d..7be62fc78d 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/aftersale/AfterSaleServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.trade.service.aftersale; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.module.pay.api.refund.PayRefundApi; @@ -362,6 +363,7 @@ public class AfterSaleServiceImpl implements AfterSaleService { private void createPayRefund(String userIp, AfterSaleDO afterSale) { // 创建退款单 PayRefundCreateReqDTO createReqDTO = AfterSaleConvert.INSTANCE.convert(userIp, afterSale, tradeOrderProperties) + .setUserId(afterSale.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) .setReason(StrUtil.format("退款【{}】", afterSale.getSpuName())); Long payRefundId = payRefundApi.createRefund(createReqDTO); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java index a873927f6c..fcadac827a 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/brokerage/BrokerageWithdrawServiceImpl.java @@ -151,6 +151,7 @@ public class BrokerageWithdrawServiceImpl implements BrokerageWithdrawService { .setAppKey(tradeOrderProperties.getPayAppKey()).setChannelCode(channelCode) .setMerchantTransferId(withdraw.getId().toString()).setSubject("佣金提现").setPrice(withdraw.getPrice()) .setUserAccount(userAccount).setUserName(userName).setUserIp(getClientIP()) + .setUserId(withdraw.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) // 用户信息 .setChannelExtras(channelExtras); // 1.3 发起请求 PayTransferCreateRespDTO transferRespDTO = payTransferApi.createTransfer(transferReqDTO); diff --git a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java index 8dcc0ad75e..690f3de52c 100644 --- a/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java +++ b/yudao-module-mall/yudao-module-trade/src/main/java/cn/iocoder/yudao/module/trade/service/order/TradeOrderUpdateServiceImpl.java @@ -25,6 +25,9 @@ import cn.iocoder.yudao.module.pay.enums.order.PayOrderStatusEnum; import cn.iocoder.yudao.module.pay.enums.refund.PayRefundStatusEnum; import cn.iocoder.yudao.module.product.api.comment.ProductCommentApi; import cn.iocoder.yudao.module.product.api.comment.dto.ProductCommentCreateReqDTO; +import cn.iocoder.yudao.module.promotion.api.combination.CombinationRecordApi; +import cn.iocoder.yudao.module.promotion.api.combination.dto.CombinationRecordRespDTO; +import cn.iocoder.yudao.module.promotion.enums.combination.CombinationRecordStatusEnum; import cn.iocoder.yudao.module.system.api.social.SocialClientApi; import cn.iocoder.yudao.module.system.api.social.dto.SocialWxaSubscribeMessageSendReqDTO; import cn.iocoder.yudao.module.trade.controller.admin.order.vo.TradeOrderDeliveryReqVO; @@ -121,6 +124,8 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { public SocialClientApi socialClientApi; @Resource public PayRefundApi payRefundApi; + @Resource + private CombinationRecordApi combinationRecordApi; @Resource private TradeOrderProperties tradeOrderProperties; @@ -775,6 +780,14 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { if (ObjUtil.notEqual(DeliveryTypeEnum.PICK_UP.getType(), order.getDeliveryType())) { throw exception(ORDER_RECEIVE_FAIL_DELIVERY_TYPE_NOT_PICK_UP); } + // 情况一:如果是拼团订单,则校验拼团是否成功 + if (TradeOrderTypeEnum.isCombination(order.getType())) { + CombinationRecordRespDTO combinationRecord = combinationRecordApi.getCombinationRecordByOrderId( + order.getUserId(), order.getId()); + if (!CombinationRecordStatusEnum.isSuccess(combinationRecord.getStatus())) { + throw exception(ORDER_PICK_UP_FAIL_COMBINATION_NOT_SUCCESS); + } + } DeliveryPickUpStoreDO deliveryPickUpStore = pickUpStoreService.getDeliveryPickUpStore(order.getPickUpStoreId()); if (deliveryPickUpStore == null || !CollUtil.contains(deliveryPickUpStore.getVerifyUserIds(), userId)) { @@ -939,6 +952,7 @@ public class TradeOrderUpdateServiceImpl implements TradeOrderUpdateService { payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(tradeOrderProperties.getPayAppKey()) // 支付应用 .setUserIp(NetUtil.getLocalhostStr()) // 使用本机 IP,因为是服务器发起退款的 + .setUserId(order.getUserId()).setUserType(UserTypeEnum.MEMBER.getValue()) // 用户信息 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 // 特殊:因为订单支持 AfterSale 单个售后退款,也支持整单退款,所以需要通过 order- 进行下区分 // 具体可见 AfterSaleController 的 updateAfterSaleRefunded 方法 diff --git a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java index 14540836a7..0f68baf39e 100644 --- a/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java +++ b/yudao-module-mp/src/main/java/cn/iocoder/yudao/module/mp/service/handler/user/SubscribeHandler.java @@ -1,10 +1,13 @@ package cn.iocoder.yudao.module.mp.service.handler.user; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.module.mp.framework.mp.core.context.MpContextHolder; import cn.iocoder.yudao.module.mp.service.message.MpAutoReplyService; import cn.iocoder.yudao.module.mp.service.user.MpUserService; +import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import me.chanjar.weixin.common.error.WxErrorException; +import me.chanjar.weixin.common.error.WxMpErrorMsgEnum; import me.chanjar.weixin.common.session.WxSessionManager; import me.chanjar.weixin.mp.api.WxMpMessageHandler; import me.chanjar.weixin.mp.api.WxMpService; @@ -13,7 +16,6 @@ import me.chanjar.weixin.mp.bean.message.WxMpXmlOutMessage; import me.chanjar.weixin.mp.bean.result.WxMpUser; import org.springframework.stereotype.Component; -import jakarta.annotation.Resource; import java.util.Map; /** @@ -40,6 +42,13 @@ public class SubscribeHandler implements WxMpMessageHandler { wxMpUser = weixinService.getUserService().userInfo(wxMessage.getFromUser()); } catch (WxErrorException e) { log.error("[handle][粉丝({})] 获取粉丝信息失败!", wxMessage.getFromUser(), e); + // 特殊情况(个人账号,无接口权限):https://t.zsxq.com/cLFq5 + if (ObjUtil.equal(e.getError().getErrorCode(), WxMpErrorMsgEnum.CODE_48001)) { + wxMpUser = new WxMpUser(); + wxMpUser.setOpenId(wxMessage.getFromUser()); + wxMpUser.setSubscribe(true); + wxMpUser.setSubscribeTime(System.currentTimeMillis() / 1000L); + } } // 第二步,保存粉丝信息 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java index 3a7b181be3..a2f035cddd 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/order/dto/PayOrderCreateReqDTO.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.pay.api.order.dto; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import jakarta.validation.constraints.DecimalMin; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -22,12 +24,23 @@ public class PayOrderCreateReqDTO implements Serializable { */ @NotNull(message = "应用标识不能为空") private String appKey; + /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + // ========== 商户相关字段 ========== /** diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java index 6910fc2fe5..cc1743be64 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/refund/dto/PayRefundCreateReqDTO.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.pay.api.refund.dto; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; +import cn.iocoder.yudao.framework.common.validation.InEnum; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -19,12 +21,23 @@ public class PayRefundCreateReqDTO { */ @NotNull(message = "应用标识不能为空") private String appKey; + /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + // ========== 商户相关字段 ========== /** * 商户订单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java index 1ac5c04af6..86dd70cce5 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/api/transfer/dto/PayTransferCreateReqDTO.java @@ -1,7 +1,9 @@ package cn.iocoder.yudao.module.pay.api.transfer.dto; import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; +import cn.iocoder.yudao.framework.common.validation.InEnum; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; @@ -26,23 +28,23 @@ public class PayTransferCreateReqDTO { @NotNull(message = "应用标识不能为空") private String appKey; - /** - * 转账渠道 - */ - @NotEmpty(message = "转账渠道不能为空") - private String channelCode; - - /** - * 转账渠道的额外参数 - */ - private Map channelExtras; - /** * 用户 IP */ @NotEmpty(message = "用户 IP 不能为空") private String userIp; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + @InEnum(UserTypeEnum.class) + private Integer userType; + + // ========== 商户相关字段 ========== /** * 商户转账单编号 */ @@ -75,6 +77,17 @@ public class PayTransferCreateReqDTO { */ private String userName; + /** + * 转账渠道 + */ + @NotEmpty(message = "转账渠道不能为空") + private String channelCode; + + /** + * 转账渠道的额外参数 + */ + private Map channelExtras; + /** * 【微信】现金营销场景 * diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java index 033bc95d79..ef5e4a8a9c 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/demo/PayDemoWithdrawController.java @@ -19,6 +19,7 @@ import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 示例提现订单") // 目的:演示转账功能 @RestController @@ -40,7 +41,7 @@ public class PayDemoWithdrawController { @Operation(summary = "提现单转账") @Parameter(name = "id", required = true, description = "提现单编号", example = "1024") public CommonResult transferDemoWithdraw(@RequestParam("id") Long id) { - Long payTransferId = demoWithdrawService.transferDemoWithdraw(id); + Long payTransferId = demoWithdrawService.transferDemoWithdraw(id, getLoginUserId()); return success(payTransferId); } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java index 5dc17a0f0c..1cb362541e 100755 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/order/vo/PayOrderExcelVO.java @@ -4,7 +4,7 @@ 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.MoneyConvert; import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java index 758b6b6b09..8794086616 100755 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/refund/vo/PayRefundExcelVO.java @@ -4,7 +4,7 @@ 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.MoneyConvert; import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.Data; import java.time.LocalDateTime; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java index 9e98db1170..5c2ac3fa82 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/admin/transfer/vo/PayTransferRespVO.java @@ -5,8 +5,8 @@ import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.convert.MoneyConvert; import cn.iocoder.yudao.module.pay.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.*; import java.time.LocalDateTime; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java index 88eeb0960a..4f57163841 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/controller/app/order/AppPayOrderController.java @@ -1,12 +1,12 @@ package cn.iocoder.yudao.module.pay.controller.app.order; +import cn.hutool.core.util.ObjUtil; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderRespVO; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.PayOrderSubmitRespVO; import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitReqVO; import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; -import cn.iocoder.yudao.module.pay.convert.order.PayOrderConvert; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.wallet.PayWalletDO; import cn.iocoder.yudao.module.pay.enums.PayChannelEnum; @@ -52,6 +52,15 @@ public class AppPayOrderController { public CommonResult getOrder(@RequestParam("id") Long id, @RequestParam(value = "sync", required = false) Boolean sync) { PayOrderDO order = payOrderService.getOrder(id); + if (order== null) { + return success(null); + } + // 重要:校验订单是否是当前用户,避免越权 + if (order.getUserId() != null // 特殊:早期订单未存储 userId,所以忽略 + && ObjUtil.notEqual(order.getUserId(), getLoginUserId())) { + return success(null); + } + // sync 仅在等待支付 if (Boolean.TRUE.equals(sync) && PayOrderStatusEnum.isWaiting(order.getStatus())) { payOrderService.syncOrderQuietly(order.getId()); @@ -75,7 +84,7 @@ public class AppPayOrderController { // 2. 提交支付 PayOrderSubmitRespVO respVO = payOrderService.submitOrder(reqVO, getClientIP()); - return success(PayOrderConvert.INSTANCE.convert3(respVO)); + return success(BeanUtils.toBean(respVO, AppPayOrderSubmitRespVO.class)); } } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java index 8321de32ce..1813e2c2d7 100755 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/convert/order/PayOrderConvert.java @@ -3,14 +3,13 @@ package cn.iocoder.yudao.module.pay.convert.order; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.MapUtils; -import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderCreateReqDTO; import cn.iocoder.yudao.module.pay.api.order.dto.PayOrderRespDTO; import cn.iocoder.yudao.module.pay.controller.admin.order.vo.*; -import cn.iocoder.yudao.module.pay.controller.app.order.vo.AppPayOrderSubmitRespVO; import cn.iocoder.yudao.module.pay.dal.dataobject.app.PayAppDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderDO; import cn.iocoder.yudao.module.pay.dal.dataobject.order.PayOrderExtensionDO; +import cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderUnifiedReqDTO; import org.mapstruct.Mapper; import org.mapstruct.Mapping; import org.mapstruct.factory.Mappers; @@ -69,6 +68,4 @@ public interface PayOrderConvert { @Mapping(source = "order.status", target = "status") PayOrderSubmitRespVO convert(PayOrderDO order, cn.iocoder.yudao.module.pay.framework.pay.core.client.dto.order.PayOrderRespDTO respDTO); - AppPayOrderSubmitRespVO convert3(PayOrderSubmitRespVO bean); - } diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java index ac0ef34f2e..4eea1cc4ef 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/order/PayOrderDO.java @@ -49,6 +49,15 @@ public class PayOrderDO extends BaseDO { */ private String channelCode; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + // ========== 商户相关字段 ========== /** diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java index 607483f91a..0804926820 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/refund/PayRefundDO.java @@ -77,6 +77,15 @@ public class PayRefundDO extends BaseDO { */ private String orderNo; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + // ========== 商户相关字段 ========== /** * 商户订单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java index 94f8521dbd..d0f3a80211 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/dal/dataobject/transfer/PayTransferDO.java @@ -30,7 +30,6 @@ public class PayTransferDO extends BaseDO { */ @TableId private Long id; - /** * 转账单号 */ @@ -42,14 +41,12 @@ public class PayTransferDO extends BaseDO { * 关联 {@link PayAppDO#getId()} */ private Long appId; - /** * 转账渠道编号 * * 关联 {@link PayChannelDO#getId()} */ private Long channelId; - /** * 转账渠道编码 * @@ -57,6 +54,15 @@ public class PayTransferDO extends BaseDO { */ private String channelCode; + /** + * 用户编号 + */ + private Long userId; + /** + * 用户类型 + */ + private Integer userType; + // ========== 商户相关字段 ========== /** * 商户转账单编号 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java index 76571f355c..ac756f5763 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoOrderServiceImpl.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.pay.service.demo; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.module.pay.api.order.PayOrderApi; @@ -90,6 +91,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { // 2.1 创建支付单 Long payOrderId = payOrderApi.createOrder(new PayOrderCreateReqDTO() .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 .setMerchantOrderId(demoOrder.getId().toString()) // 业务的订单编号 .setSubject(spuName).setBody("").setPrice(price) // 价格信息 .setExpireTime(addTime(Duration.ofHours(2L)))); // 支付的过期时间 @@ -189,6 +191,7 @@ public class PayDemoOrderServiceImpl implements PayDemoOrderService { // 2.2 创建退款单 Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(PAY_APP_KEY).setUserIp(getClientIP()) // 支付应用 + .setUserId(order.getUserId()).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 .setMerchantOrderId(String.valueOf(order.getId())) // 支付单号 .setMerchantRefundId(refundId) .setReason("想退钱").setPrice(order.getPrice()));// 价格信息 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java index e8a5980ad8..6b55810257 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawService.java @@ -26,9 +26,10 @@ public interface PayDemoWithdrawService { * 提现单转账 * * @param id 提现单编号 + * @param userId 用户编号 * @return 转账编号 */ - Long transferDemoWithdraw(Long id); + Long transferDemoWithdraw(Long id, Long userId); /** * 获得示例提现单分页 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java index b1a3b91516..dddbceea0f 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/demo/PayDemoWithdrawServiceImpl.java @@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.pay.service.demo; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ObjectUtil; +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.pojo.PageParam; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.json.JsonUtils; @@ -60,7 +61,7 @@ public class PayDemoWithdrawServiceImpl implements PayDemoWithdrawService { } @Override - public Long transferDemoWithdraw(Long id) { + public Long transferDemoWithdraw(Long id, Long userId) { // 1.1 校验提现单 PayDemoWithdrawDO withdraw = validateDemoWithdrawCanTransfer(id); // 1.2 特殊:如果是转账失败的情况,需要充值下 @@ -76,6 +77,7 @@ public class PayDemoWithdrawServiceImpl implements PayDemoWithdrawService { // 2.1 创建支付单 PayTransferCreateReqDTO transferReqDTO = new PayTransferCreateReqDTO() .setAppKey(PAY_APP_KEY).setChannelCode(withdraw.getTransferChannelCode()).setUserIp(getClientIP()) // 支付应用 + .setUserId(userId).setUserType(UserTypeEnum.ADMIN.getValue()) // 用户信息 .setMerchantTransferId(String.valueOf(withdraw.getId())) // 业务的订单编号 .setSubject(withdraw.getSubject()).setPrice(withdraw.getPrice()) // 价格信息 .setUserAccount(withdraw.getUserAccount()).setUserName(withdraw.getUserName()); // 收款信息 diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java index dbd12c7986..8f123f8102 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/transfer/PayTransferServiceImpl.java @@ -264,9 +264,6 @@ public class PayTransferServiceImpl implements PayTransferService { } int count = 0; for (PayTransferDO transfer : list) { - if (!transfer.getId().equals(54L)) { - continue; - } count += syncTransfer(transfer) ? 1 : 0; } return count; diff --git a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java index f1253f4765..95539b28d5 100644 --- a/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java +++ b/yudao-module-pay/src/main/java/cn/iocoder/yudao/module/pay/service/wallet/PayWalletRechargeServiceImpl.java @@ -64,8 +64,6 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { private PayWalletService payWalletService; @Resource private PayOrderService payOrderService; -// @Resource -// private PayRefundService payRefundService; @Resource private PayWalletRechargePackageService payWalletRechargePackageService; @@ -99,6 +97,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { // 2.1 创建支付单 Long payOrderId = payOrderService.createOrder(new PayOrderCreateReqDTO() .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setUserId(userId).setUserType(userType) // 用户信息 .setMerchantOrderId(recharge.getId().toString()) // 业务的订单编号 .setSubject(WALLET_RECHARGE_ORDER_SUBJECT).setBody("") .setPrice(recharge.getPayPrice()) @@ -209,6 +208,7 @@ public class PayWalletRechargeServiceImpl implements PayWalletRechargeService { String refundId = walletRechargeId + "-refund"; Long payRefundId = payRefundApi.createRefund(new PayRefundCreateReqDTO() .setAppKey(payProperties.getWalletPayAppKey()).setUserIp(userIp) + .setUserId(wallet.getUserId()).setUserType(wallet.getUserType()) // 用户信息 .setMerchantOrderId(walletRechargeId) .setMerchantRefundId(refundId) .setReason("想退钱").setPrice(walletRecharge.getPayPrice())); diff --git a/yudao-module-pay/src/test/resources/sql/clean.sql b/yudao-module-pay/src/test/resources/sql/clean.sql index 91fff0caee..e7bf56bfa4 100644 --- a/yudao-module-pay/src/test/resources/sql/clean.sql +++ b/yudao-module-pay/src/test/resources/sql/clean.sql @@ -3,5 +3,6 @@ DELETE FROM pay_channel; DELETE FROM pay_order; DELETE FROM pay_order_extension; DELETE FROM pay_refund; +DELETE FROM pay_transfer; DELETE FROM pay_notify_task; DELETE FROM pay_notify_log; diff --git a/yudao-module-pay/src/test/resources/sql/create_tables.sql b/yudao-module-pay/src/test/resources/sql/create_tables.sql index 3f9f764179..6af612f018 100644 --- a/yudao-module-pay/src/test/resources/sql/create_tables.sql +++ b/yudao-module-pay/src/test/resources/sql/create_tables.sql @@ -45,6 +45,8 @@ CREATE TABLE IF NOT EXISTS `pay_order` ( `channel_fee_price` bigint(20) DEFAULT 0, `status` tinyint(4) NOT NULL, `user_ip` varchar(50) NOT NULL, + `user_id` bigint(20) DEFAULT NULL, + `user_type` tinyint(4) DEFAULT NULL, `expire_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `success_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, `notify_time` datetime(0) DEFAULT CURRENT_TIMESTAMP, @@ -97,6 +99,8 @@ CREATE TABLE IF NOT EXISTS `pay_refund` ( `refund_price` bigint(20) NOT NULL, `reason` varchar(256) NOT NULL, `user_ip` varchar(50) NULL DEFAULT NULL, + `user_id` bigint(20) NULL DEFAULT NULL, + `user_type` tinyint(4) NULL DEFAULT NULL, `channel_order_no` varchar(64) NOT NULL, `channel_refund_no` varchar(64) NULL DEFAULT NULL, `success_time` datetime(0) NULL DEFAULT NULL, @@ -145,3 +149,32 @@ CREATE TABLE IF NOT EXISTS `pay_notify_log` ( `deleted` bit(1) NOT NULL DEFAULT FALSE, PRIMARY KEY ("id") ) COMMENT = '支付通知日志'; + +CREATE TABLE IF NOT EXISTS `pay_transfer` ( + "id" number NOT NULL GENERATED BY DEFAULT AS IDENTITY, + `no` varchar(64) NOT NULL, + `app_id` bigint(20) NOT NULL, + `channel_id` bigint(20) NOT NULL, + `channel_code` varchar(32) NOT NULL, + `user_id` bigint(20) NULL DEFAULT NULL, + `user_type` tinyint(4) NULL DEFAULT NULL, + `merchant_transfer_id` varchar(64) NOT NULL, + `price` bigint(20) NOT NULL, + `subject` varchar(256) NOT NULL, + `user_account` varchar(256) NOT NULL, + `user_name` varchar(64) NULL DEFAULT NULL, + `status` tinyint(4) NOT NULL, + `notify_url` varchar(1024) NULL DEFAULT NULL, + `channel_transfer_no` varchar(64) NULL DEFAULT NULL, + `success_time` datetime(0) NULL DEFAULT NULL, + `channel_error_code` varchar(128) NULL DEFAULT NULL, + `channel_error_msg` varchar(256) NULL DEFAULT NULL, + `channel_notify_data` varchar(1024) NULL DEFAULT NULL, + `channel_extras` varchar(1024) NULL DEFAULT NULL, + `creator` varchar(64) NULL DEFAULT '', + `create_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updater` varchar(64) NULL DEFAULT '', + `update_time` datetime(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `deleted` bit(1) NOT NULL DEFAULT FALSE, + PRIMARY KEY ("id") +) COMMENT = '转账单'; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java index d9269470d0..e41aee576d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/auth/AuthController.java @@ -127,6 +127,8 @@ public class AuthController { @PostMapping("/sms-login") @PermitAll @Operation(summary = "使用短信验证码登录") + // 可按需开启限流:https://github.com/YunaiV/ruoyi-vue-pro/issues/851 + // @RateLimiter(time = 60, count = 6, keyResolver = ExpressionRateLimiterKeyResolver.class, keyArg = "#reqVO.mobile") public CommonResult smsLogin(@RequestBody @Valid AuthSmsLoginReqVO reqVO) { return success(authService.smsLogin(reqVO)); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java index dde6f95097..18ccb7ffad 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java index 72852a18e3..d019ad7235 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/vo/post/PostSimpleRespVO.java @@ -1,6 +1,6 @@ package cn.iocoder.yudao.module.system.controller.admin.dept.vo.post; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java index 8857a7059c..fce51e99c6 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/data/DictDataRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.dict.vo.data; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java index 6ddd47bcd8..2301a7526a 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dict/vo/type/DictTypeRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.dict.vo.type; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java index 71b45d346e..257477071a 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/OperateLogController.java @@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.Oper import cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog.OperateLogRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.logger.OperateLogDO; import cn.iocoder.yudao.module.system.service.logger.OperateLogService; +import com.fhs.core.trans.anno.TransMethodResult; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; @@ -40,6 +41,7 @@ public class OperateLogController { @GetMapping("/page") @Operation(summary = "查看操作日志分页列表") @PreAuthorize("@ss.hasPermission('system:operate-log:query')") + @TransMethodResult public CommonResult> pageOperateLog(@Valid OperateLogPageReqVO pageReqVO) { PageResult pageResult = operateLogService.getOperateLogPage(pageReqVO); return success(BeanUtils.toBean(pageResult, OperateLogRespVO.class)); diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java index 7b9cd165e2..9abe3aa53b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/loginlog/LoginLogRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.logger.vo.loginlog; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java index e017ba7cbe..436d209066 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/logger/vo/operatelog/OperateLogRespVO.java @@ -1,8 +1,8 @@ package cn.iocoder.yudao.module.system.controller.admin.logger.vo.operatelog; import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import com.fhs.core.trans.anno.Trans; import com.fhs.core.trans.constant.TransType; import com.fhs.core.trans.vo.VO; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java index 89f80c6724..c1c7cfea62 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/permission/vo/role/RoleRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.permission.vo.role; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java index 77409ed918..66a867fd0d 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/log/SmsLogRespVO.java @@ -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.JsonConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java index 6b8aecaaad..9caed5c49b 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/sms/vo/template/SmsTemplateRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.sms.vo.template; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java index 77a77daa1f..f26ca4d937 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/socail/SocialUserController.java @@ -38,7 +38,8 @@ public class SocialUserController { @PostMapping("/bind") @Operation(summary = "社交绑定,使用 code 授权码") public CommonResult socialBind(@RequestBody @Valid SocialUserBindReqVO reqVO) { - socialUserService.bindSocialUser(BeanUtils.toBean(reqVO, SocialUserBindReqDTO.class) + socialUserService.bindSocialUser(new SocialUserBindReqDTO().setSocialType(reqVO.getType()) + .setCode(reqVO.getCode()).setState(reqVO.getState()) .setUserId(getLoginUserId()).setUserType(UserTypeEnum.ADMIN.getValue())); return CommonResult.success(true); } diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java index 5a444b5213..62ccb7c611 100755 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/tenant/vo/tenant/TenantRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.tenant.vo.tenant; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java index a360f1af2c..0c7c5a3c33 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserImportExcelVO.java @@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelProperty; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java index 2837318f9a..ba322240e2 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserRespVO.java @@ -3,8 +3,8 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.module.system.enums.DictTypeConstants; -import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; -import com.alibaba.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.ExcelIgnoreUnannotated; +import cn.idev.excel.annotation.ExcelProperty; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; diff --git a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java index ae03d18625..8e3e5db179 100644 --- a/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java +++ b/yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/user/UserUpdateStatusReqVO.java @@ -2,10 +2,11 @@ package cn.iocoder.yudao.module.system.controller.admin.user.vo.user; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.validation.InEnum; +import cn.iocoder.yudao.framework.dict.validation.InDict; +import cn.iocoder.yudao.module.system.enums.DictTypeConstants; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.Data; - import jakarta.validation.constraints.NotNull; +import lombok.Data; @Schema(description = "管理后台 - 用户更新状态 Request VO") @Data @@ -18,6 +19,7 @@ public class UserUpdateStatusReqVO { @Schema(description = "状态,见 CommonStatusEnum 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") @NotNull(message = "状态不能为空") @InEnum(value = CommonStatusEnum.class, message = "修改状态必须是 {value}") + @InDict(type = DictTypeConstants.COMMON_STATUS) private Integer status; } diff --git a/yudao-server/src/main/resources/application-dev.yaml b/yudao-server/src/main/resources/application-dev.yaml index b1b9b1592e..970002f84a 100644 --- a/yudao-server/src/main/resources/application-dev.yaml +++ b/yudao-server/src/main/resources/application-dev.yaml @@ -5,9 +5,10 @@ server: spring: autoconfigure: + # noinspection SpringBootApplicationYaml exclude: - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 @@ -33,14 +34,16 @@ spring: initial-size: 5 # 初始连接数 min-idle: 10 # 最小连接池数量 max-active: 20 # 最大连接池数量 - max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 - min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 - max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 - validation-query: SELECT 1 # 配置检测连接是否有效 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) + validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 test-while-idle: true test-on-borrow: false test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 primary: master datasource: master: diff --git a/yudao-server/src/main/resources/application-local.yaml b/yudao-server/src/main/resources/application-local.yaml index 9ecb5dd446..b12c0bbd04 100644 --- a/yudao-server/src/main/resources/application-local.yaml +++ b/yudao-server/src/main/resources/application-local.yaml @@ -7,11 +7,8 @@ spring: # noinspection SpringBootApplicationYaml exclude: - org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration # 默认 local 环境,不开启 Quartz 的自动配置 - - de.codecentric.boot.admin.server.config.AdminServerAutoConfiguration # 禁用 Spring Boot Admin 的 Server 的自动配置 - - de.codecentric.boot.admin.server.ui.config.AdminServerUiAutoConfiguration # 禁用 Spring Boot Admin 的 Server UI 的自动配置 - - de.codecentric.boot.admin.client.config.SpringBootAdminClientAutoConfiguration # 禁用 Spring Boot Admin 的 Client 的自动配置 - - org.springframework.ai.autoconfigure.vectorstore.qdrant.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 - - org.springframework.ai.autoconfigure.vectorstore.milvus.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 + - org.springframework.ai.vectorstore.qdrant.autoconfigure.QdrantVectorStoreAutoConfiguration # 禁用 AI 模块的 Qdrant,手动创建 + - org.springframework.ai.vectorstore.milvus.autoconfigure.MilvusVectorStoreAutoConfiguration # 禁用 AI 模块的 Milvus,手动创建 # 数据源配置项 datasource: druid: # Druid 【监控】相关的全局配置 @@ -37,14 +34,16 @@ spring: initial-size: 1 # 初始连接数 min-idle: 1 # 最小连接池数量 max-active: 20 # 最大连接池数量 - max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒 - time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒 - min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒 - max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒 + max-wait: 60000 # 配置获取连接等待超时的时间,单位:毫秒(1 分钟) + time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒(1 分钟) + min-evictable-idle-time-millis: 600000 # 配置一个连接在池中最小生存的时间,单位:毫秒(10 分钟) + max-evictable-idle-time-millis: 1800000 # 配置一个连接在池中最大生存的时间,单位:毫秒(30 分钟) validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效 test-while-idle: true test-on-borrow: false test-on-return: false + pool-prepared-statements: true # 是否开启 PreparedStatement 缓存 + max-pool-prepared-statement-per-connection-size: 20 # 每个连接缓存的 PreparedStatement 数量 primary: master datasource: master: diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 025fbbee8b..366008be5a 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -96,7 +96,7 @@ spring: # VO 转换(数据翻译)相关 easy-trans: - is-enable-global: true # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 + is-enable-global: false # 【默认禁用,对性能确认压力大】启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口 --- #################### 验证码相关配置 #################### @@ -150,7 +150,7 @@ spring: vectorstore: # 向量存储 redis: initialize-schema: true - index: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 + index-name: knowledge_index # Redis 中向量索引的名称:用于存储和检索向量数据的索引标识符,所有相关的向量搜索操作都会基于这个索引进行 prefix: "knowledge_segment:" # Redis 中存储向量数据的键名前缀:这个前缀会添加到每个存储在 Redis 中的向量数据键名前,每个 document 都是一个 hash 结构 qdrant: initialize-schema: true @@ -188,13 +188,14 @@ spring: api-key: xxxx moonshot: # 月之暗灭(KIMI) api-key: sk-abc + deepseek: # DeepSeek + api-key: sk-e94db327cc7d457d99a8de8810fc6b12 + chat: + options: + model: deepseek-chat yudao: ai: - deep-seek: # DeepSeek - enable: true - api-key: sk-e94db327cc7d457d99a8de8810fc6b12 - model: deepseek-chat doubao: # 字节豆包 enable: true api-key: 5c1b5747-26d2-4ebd-a4e0-dd0e8d8b4272 diff --git a/yudao-server/src/main/resources/logback-spring.xml b/yudao-server/src/main/resources/logback-spring.xml index b1b9f3fafb..15b28cfdff 100644 --- a/yudao-server/src/main/resources/logback-spring.xml +++ b/yudao-server/src/main/resources/logback-spring.xml @@ -1,76 +1,56 @@ - - - - - - + + + + +       - - - ${PATTERN_DEFAULT} - + + ${CONSOLE_LOG_PATTERN} - - - ${PATTERN_DEFAULT} - + + ${FILE_LOG_PATTERN} ${LOG_FILE} + - - ${LOGBACK_ROLLINGPOLICY_FILE_NAME_PATTERN:-${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz} - - ${LOGBACK_ROLLINGPOLICY_CLEAN_HISTORY_ON_START:-false} - - ${LOGBACK_ROLLINGPOLICY_MAX_FILE_SIZE:-10MB} - - ${LOGBACK_ROLLINGPOLICY_TOTAL_SIZE_CAP:-0} - - ${LOGBACK_ROLLINGPOLICY_MAX_HISTORY:-30} + ${LOG_FILE}.%d{yyyy-MM-dd}.%i.log + 30 + 10MB - - 0 - - 256 + 0 + 512 - - + + - - - - - - - - - - - - - - - - + + + + + + +