From 25e80433a5ba3ca2c779ddcc3247f2e6594ccfea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=AD=90=E9=BB=98?= <925456043@qq.com> Date: Sun, 5 Apr 2026 11:35:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=90=BD=E5=9C=B0=E8=81=8A=E5=A4=A9?= =?UTF-8?q?=E8=AE=B0=E5=BD=95=E5=BC=82=E6=AD=A5=E6=8C=81=E4=B9=85=E5=8C=96?= =?UTF-8?q?=E5=9F=BA=E7=A1=80=E8=AE=BE=E6=96=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 chatlog 模块、AnalyticalDB 公共层与 common-mq Redis Streams 实现 - 建立 Redis 热态、MySQL 热数据、AnalyticalDB 历史查询与同步链路 - 收紧聊天记录幂等、摘要时序与持久化失败语义 --- easyflow-commons/easyflow-common-all/pom.xml | 8 + .../easyflow-common-analytical-db/pom.xml | 48 + .../config/AnalyticalDBConfiguration.java | 72 + .../config/AnalyticalDBFlywayProperties.java | 46 + .../config/AnalyticalDBProperties.java | 122 ++ .../core/AnalyticalDBOperations.java | 34 + .../core/DefaultAnalyticalDBOperations.java | 113 ++ .../exception/AnalyticalDBException.java | 12 + .../page/AnalyticalDBPageRequest.java | 40 + .../page/AnalyticalDBPageResult.java | 44 + .../support/AnalyticalDBHealthSupport.java | 32 + .../runtime/ChatAssistantAccumulator.java | 74 ++ .../easyflow/core/runtime/ChatChannel.java | 8 + .../core/runtime/ChatRuntimeContext.java | 138 ++ .../core/runtime/ChatRuntimeListener.java | 29 + .../core/runtime/ChatRuntimeManager.java | 20 + .../core/runtime/ChatRuntimeMessage.java | 83 ++ .../runtime/CompositeChatRuntimeManager.java | 93 ++ easyflow-commons/easyflow-common-mq/pom.xml | 26 + .../common/mq/config/MQConfiguration.java | 129 ++ .../common/mq/config/MQProperties.java | 100 ++ .../common/mq/config/MQRedisResources.java | 13 + .../common/mq/core/MQAcknowledger.java | 8 + .../common/mq/core/MQConsumerContainer.java | 6 + .../common/mq/core/MQConsumerHandler.java | 10 + .../common/mq/core/MQDeadLetterHandler.java | 8 + .../common/mq/core/MQDeadLetterService.java | 6 + .../easyflow/common/mq/core/MQMessage.java | 91 ++ .../common/mq/core/MQMessageConverter.java | 8 + .../easyflow/common/mq/core/MQProducer.java | 6 + .../common/mq/core/MQSubscription.java | 32 + .../common/mq/exception/MQException.java | 12 + .../mq/redis/JacksonMQMessageConverter.java | 34 + .../mq/redis/RedisMQConsumerContainer.java | 259 ++++ .../mq/redis/RedisMQDeadLetterService.java | 49 + .../common/mq/redis/RedisMQProducer.java | 58 + .../mq/redis/RedisStreamKeySupport.java | 27 + .../common/mq/support/MQHealthSupport.java | 28 + easyflow-commons/pom.xml | 2 + .../easyflow-module-chatlog/pom.xml | 56 + .../chatlog/cache/ChatHotStateService.java | 432 ++++++ .../chatlog/config/ChatCacheProperties.java | 48 + .../chatlog/config/ChatSyncProperties.java | 55 + .../chatlog/config/ChatlogModuleConfig.java | 9 + .../command/ChatAppendMessageCommand.java | 136 ++ .../command/ChatSessionSummaryCommand.java | 81 ++ .../command/ChatSessionUpsertCommand.java | 108 ++ .../chatlog/domain/dto/ChatHistoryPage.java | 45 + .../chatlog/domain/dto/ChatMessageRecord.java | 127 ++ .../chatlog/domain/dto/ChatSessionPage.java | 45 + .../domain/dto/ChatSessionSummary.java | 189 +++ .../chatlog/domain/dto/ChatSyncResult.java | 36 + .../dto/PublicChatSessionRestoreResult.java | 45 + .../domain/entity/ChatPersistDeadLetter.java | 8 + .../domain/entity/ChatSyncCheckpoint.java | 8 + .../base/ChatPersistDeadLetterBase.java | 166 +++ .../entity/base/ChatSyncCheckpointBase.java | 139 ++ .../domain/event/ChatPersistEvent.java | 81 ++ .../domain/event/ChatPersistEventType.java | 10 + .../payload/ChatSessionDeletePayload.java | 45 + .../payload/ChatSessionRenamePayload.java | 54 + .../chatlog/domain/query/ChatPageQuery.java | 29 + .../domain/query/ChatSessionFilterQuery.java | 57 + .../mapper/ChatPersistDeadLetterMapper.java | 7 + .../mapper/ChatSyncCheckpointMapper.java | 7 + .../ChatAnalyticalDBRepository.java | 352 +++++ .../mysql/MySqlChatLogRepository.java | 171 +++ .../mysql/MySqlChatLogTableManager.java | 61 + .../mysql/MySqlChatSessionRepository.java | 275 ++++ .../chatlog/schedule/ChatStartupRunner.java | 21 + .../chatlog/schedule/ChatSyncScheduler.java | 47 + .../service/ChatHistoryManageService.java | 28 + .../service/ChatHistoryQueryService.java | 11 + .../service/ChatPersistDeadLetterService.java | 52 + .../service/ChatPersistDispatcher.java | 140 ++ .../service/ChatPersistEventConsumer.java | 50 + .../service/ChatPersistEventProducer.java | 33 + .../service/ChatPersistMySqlApplyService.java | 153 +++ .../service/ChatSessionCommandService.java | 15 + .../service/ChatSessionQueryService.java | 22 + .../service/ChatSyncCheckpointService.java | 9 + .../chatlog/service/ChatSyncService.java | 16 + .../PublicChatSessionRestoreService.java | 10 + .../impl/ChatHistoryManageServiceImpl.java | 95 ++ .../impl/ChatHistoryQueryServiceImpl.java | 24 + .../impl/ChatSessionCommandServiceImpl.java | 34 + .../impl/ChatSessionQueryServiceImpl.java | 108 ++ .../impl/ChatSyncCheckpointServiceImpl.java | 33 + .../service/impl/ChatSyncServiceImpl.java | 190 +++ .../service/impl/ChatlogRuntimeListener.java | 130 ++ .../PublicChatSessionRestoreServiceImpl.java | 64 + .../chatlog/support/ChatConstants.java | 15 + .../chatlog/support/ChatJsonSupport.java | 64 + .../chatlog/support/ChatTableRouter.java | 29 + .../chatlog/support/ChatTableRouterTest.java | 21 + easyflow-modules/pom.xml | 1 + easyflow-starter/easyflow-starter-all/pom.xml | 4 + .../src/main/resources/application-prod.yml | 42 + .../src/main/resources/application.yml | 47 +- .../analyticaldb/V1__analyticaldb_ods.sql | 62 + .../analyticaldb/V2__analyticaldb_dws.sql | 33 + .../db/migration/mysql/V1__mysql_schema.sql | 1182 +++++++++++++++++ .../db/migration/mysql/V2__mysql_seed.sql | 266 ++++ .../db/migration/mysql/V3__mysql_quartz.sql | 162 +++ pom.xml | 29 + 105 files changed, 8050 insertions(+), 2 deletions(-) create mode 100644 easyflow-commons/easyflow-common-analytical-db/pom.xml create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBConfiguration.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBFlywayProperties.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBProperties.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/AnalyticalDBOperations.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/DefaultAnalyticalDBOperations.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/exception/AnalyticalDBException.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageRequest.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageResult.java create mode 100644 easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/support/AnalyticalDBHealthSupport.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatAssistantAccumulator.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatChannel.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeContext.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeListener.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeManager.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeMessage.java create mode 100644 easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/CompositeChatRuntimeManager.java create mode 100644 easyflow-commons/easyflow-common-mq/pom.xml create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQConfiguration.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQProperties.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQRedisResources.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQAcknowledger.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerContainer.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerHandler.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterHandler.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterService.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessage.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessageConverter.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQProducer.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQSubscription.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/exception/MQException.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/JacksonMQMessageConverter.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQConsumerContainer.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQDeadLetterService.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQProducer.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisStreamKeySupport.java create mode 100644 easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/support/MQHealthSupport.java create mode 100644 easyflow-modules/easyflow-module-chatlog/pom.xml create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/cache/ChatHotStateService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatCacheProperties.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatSyncProperties.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatlogModuleConfig.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatAppendMessageCommand.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionSummaryCommand.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionUpsertCommand.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatHistoryPage.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatMessageRecord.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionPage.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionSummary.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSyncResult.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/PublicChatSessionRestoreResult.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatPersistDeadLetter.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatSyncCheckpoint.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatPersistDeadLetterBase.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatSyncCheckpointBase.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEvent.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEventType.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionDeletePayload.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionRenamePayload.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatPageQuery.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatSessionFilterQuery.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatPersistDeadLetterMapper.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatSyncCheckpointMapper.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/analyticaldb/ChatAnalyticalDBRepository.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogRepository.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogTableManager.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatSessionRepository.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatStartupRunner.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatSyncScheduler.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryQueryService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDeadLetterService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDispatcher.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventConsumer.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventProducer.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistMySqlApplyService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionCommandService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionQueryService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncCheckpointService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryQueryServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionCommandServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncCheckpointServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatlogRuntimeListener.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatConstants.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatJsonSupport.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatTableRouter.java create mode 100644 easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/support/ChatTableRouterTest.java create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V1__analyticaldb_ods.sql create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V2__analyticaldb_dws.sql create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V1__mysql_schema.sql create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V2__mysql_seed.sql create mode 100644 easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V3__mysql_quartz.sql diff --git a/easyflow-commons/easyflow-common-all/pom.xml b/easyflow-commons/easyflow-common-all/pom.xml index b460fdb..92d42b7 100644 --- a/easyflow-commons/easyflow-common-all/pom.xml +++ b/easyflow-commons/easyflow-common-all/pom.xml @@ -11,6 +11,10 @@ easyflow-common-all + + tech.easyflow + easyflow-common-analytical-db + tech.easyflow easyflow-common-ai @@ -23,6 +27,10 @@ tech.easyflow easyflow-common-cache + + tech.easyflow + easyflow-common-mq + tech.easyflow easyflow-common-file-storage diff --git a/easyflow-commons/easyflow-common-analytical-db/pom.xml b/easyflow-commons/easyflow-common-analytical-db/pom.xml new file mode 100644 index 0000000..88e0325 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + tech.easyflow + easyflow-commons + ${revision} + + + easyflow-common-analytical-db + easyflow-common-analytical-db + + + + tech.easyflow + easyflow-common-base + + + org.springframework.boot + spring-boot-starter-jdbc + + + org.springframework.boot + spring-boot-autoconfigure + + + com.clickhouse + clickhouse-jdbc + all + + + org.flywaydb + flyway-core + + + org.flywaydb + flyway-database-clickhouse + + + junit + junit + ${junit.version} + test + + + diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBConfiguration.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBConfiguration.java new file mode 100644 index 0000000..d4971a6 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBConfiguration.java @@ -0,0 +1,72 @@ +package tech.easyflow.common.analyticaldb.config; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.flywaydb.core.Flyway; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.util.StringUtils; +import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations; +import tech.easyflow.common.analyticaldb.core.DefaultAnalyticalDBOperations; + +@Configuration +public class AnalyticalDBConfiguration { + + @Bean(destroyMethod = "close") + @ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true") + public AnalyticalDBResources analyticalDBResources(AnalyticalDBProperties properties) { + HikariConfig config = new HikariConfig(); + config.setPoolName("easyflow-analytical-db"); + config.setDriverClassName(properties.getDriverClassName()); + config.setJdbcUrl(properties.getUrl()); + config.setUsername(properties.getUsername()); + config.setPassword(properties.getPassword()); + config.setMaximumPoolSize(properties.getPool().getMaxPoolSize()); + config.setMinimumIdle(properties.getPool().getMinIdle()); + config.setConnectionTimeout(properties.getPool().getConnectionTimeout()); + config.setValidationTimeout(properties.getPool().getValidationTimeout()); + config.setIdleTimeout(properties.getPool().getIdleTimeout()); + config.setMaxLifetime(properties.getPool().getMaxLifetime()); + config.setInitializationFailTimeout(-1L); + HikariDataSource dataSource = new HikariDataSource(config); + return new AnalyticalDBResources(dataSource, new JdbcTemplate(dataSource)); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true") + public AnalyticalDBOperations analyticalDBOperations(AnalyticalDBResources resources) { + return new DefaultAnalyticalDBOperations(resources.jdbcTemplate()); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.analytical-db", name = "enabled", havingValue = "true") + public InitializingBean analyticalDBFlywayInitializer(AnalyticalDBResources resources, + AnalyticalDBFlywayProperties properties) { + return () -> Flyway.configure() + .dataSource(resources.dataSource()) + .locations(splitLocations(properties.getLocations())) + .table(properties.getTable()) + .baselineOnMigrate(properties.isBaselineOnMigrate()) + .validateOnMigrate(properties.isValidateOnMigrate()) + .load() + .migrate(); + } + + private String[] splitLocations(String locations) { + if (!StringUtils.hasText(locations)) { + return new String[]{"classpath:db/migration/analyticaldb"}; + } + return StringUtils.commaDelimitedListToStringArray(locations); + } + + public record AnalyticalDBResources(HikariDataSource dataSource, JdbcTemplate jdbcTemplate) implements AutoCloseable { + + @Override + public void close() { + dataSource.close(); + } + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBFlywayProperties.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBFlywayProperties.java new file mode 100644 index 0000000..cb4c870 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBFlywayProperties.java @@ -0,0 +1,46 @@ +package tech.easyflow.common.analyticaldb.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "easyflow.flyway.analytical-db") +public class AnalyticalDBFlywayProperties { + + private String locations = "classpath:db/migration/analyticaldb"; + private String table = "flyway_schema_history_analytical_db"; + private boolean baselineOnMigrate = false; + private boolean validateOnMigrate = true; + + public String getLocations() { + return locations; + } + + public void setLocations(String locations) { + this.locations = locations; + } + + public String getTable() { + return table; + } + + public void setTable(String table) { + this.table = table; + } + + public boolean isBaselineOnMigrate() { + return baselineOnMigrate; + } + + public void setBaselineOnMigrate(boolean baselineOnMigrate) { + this.baselineOnMigrate = baselineOnMigrate; + } + + public boolean isValidateOnMigrate() { + return validateOnMigrate; + } + + public void setValidateOnMigrate(boolean validateOnMigrate) { + this.validateOnMigrate = validateOnMigrate; + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBProperties.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBProperties.java new file mode 100644 index 0000000..25d04d8 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/config/AnalyticalDBProperties.java @@ -0,0 +1,122 @@ +package tech.easyflow.common.analyticaldb.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "easyflow.analytical-db") +public class AnalyticalDBProperties { + + private boolean enabled = false; + private String url; + private String username; + private String password; + private String driverClassName = "com.clickhouse.jdbc.ClickHouseDriver"; + private Pool pool = new Pool(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getDriverClassName() { + return driverClassName; + } + + public void setDriverClassName(String driverClassName) { + this.driverClassName = driverClassName; + } + + public Pool getPool() { + return pool; + } + + public void setPool(Pool pool) { + this.pool = pool; + } + + public static class Pool { + + private int maxPoolSize = 10; + private int minIdle = 1; + private long connectionTimeout = 5000L; + private long validationTimeout = 3000L; + private long idleTimeout = 600000L; + private long maxLifetime = 1800000L; + + public int getMaxPoolSize() { + return maxPoolSize; + } + + public void setMaxPoolSize(int maxPoolSize) { + this.maxPoolSize = maxPoolSize; + } + + public int getMinIdle() { + return minIdle; + } + + public void setMinIdle(int minIdle) { + this.minIdle = minIdle; + } + + public long getConnectionTimeout() { + return connectionTimeout; + } + + public void setConnectionTimeout(long connectionTimeout) { + this.connectionTimeout = connectionTimeout; + } + + public long getValidationTimeout() { + return validationTimeout; + } + + public void setValidationTimeout(long validationTimeout) { + this.validationTimeout = validationTimeout; + } + + public long getIdleTimeout() { + return idleTimeout; + } + + public void setIdleTimeout(long idleTimeout) { + this.idleTimeout = idleTimeout; + } + + public long getMaxLifetime() { + return maxLifetime; + } + + public void setMaxLifetime(long maxLifetime) { + this.maxLifetime = maxLifetime; + } + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/AnalyticalDBOperations.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/AnalyticalDBOperations.java new file mode 100644 index 0000000..e7055d3 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/AnalyticalDBOperations.java @@ -0,0 +1,34 @@ +package tech.easyflow.common.analyticaldb.core; + +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest; +import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult; + +import java.util.List; + +public interface AnalyticalDBOperations { + + boolean available(); + + void assertAvailable(); + + List query(String sql, RowMapper rowMapper, Object... args); + + T queryOne(String sql, Class requiredType, Object... args); + + T queryOne(String sql, RowMapper rowMapper, Object... args); + + List queryForList(String sql, Class elementType, Object... args); + + int update(String sql, Object... args); + + int[][] batchUpdate(String sql, List items, int batchSize, ParameterizedPreparedStatementSetter setter); + + AnalyticalDBPageResult page(String countSql, + Object[] countArgs, + String dataSql, + Object[] dataArgs, + AnalyticalDBPageRequest pageRequest, + RowMapper rowMapper); +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/DefaultAnalyticalDBOperations.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/DefaultAnalyticalDBOperations.java new file mode 100644 index 0000000..2a39493 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/core/DefaultAnalyticalDBOperations.java @@ -0,0 +1,113 @@ +package tech.easyflow.common.analyticaldb.core; + +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; +import org.springframework.jdbc.core.RowMapper; +import tech.easyflow.common.analyticaldb.exception.AnalyticalDBException; +import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest; +import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult; + +import java.util.ArrayList; +import java.util.List; + +public class DefaultAnalyticalDBOperations implements AnalyticalDBOperations { + + private final JdbcTemplate jdbcTemplate; + + public DefaultAnalyticalDBOperations(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public boolean available() { + return jdbcTemplate != null; + } + + @Override + public void assertAvailable() { + if (!available()) { + throw new AnalyticalDBException("AnalyticalDB 数据源未启用"); + } + } + + @Override + public List query(String sql, RowMapper rowMapper, Object... args) { + assertAvailable(); + return jdbcTemplate.query(sql, rowMapper, args); + } + + @Override + public T queryOne(String sql, Class requiredType, Object... args) { + assertAvailable(); + try { + return jdbcTemplate.queryForObject(sql, requiredType, args); + } catch (EmptyResultDataAccessException ex) { + return null; + } + } + + @Override + public T queryOne(String sql, RowMapper rowMapper, Object... args) { + assertAvailable(); + List rows = jdbcTemplate.query(sql, rowMapper, args); + return rows.isEmpty() ? null : rows.get(0); + } + + @Override + public List queryForList(String sql, Class elementType, Object... args) { + assertAvailable(); + return jdbcTemplate.queryForList(sql, elementType, args); + } + + @Override + public int update(String sql, Object... args) { + assertAvailable(); + return jdbcTemplate.update(sql, args); + } + + @Override + public int[][] batchUpdate(String sql, List items, int batchSize, ParameterizedPreparedStatementSetter setter) { + assertAvailable(); + if (items == null || items.isEmpty()) { + return new int[0][0]; + } + return jdbcTemplate.batchUpdate(sql, items, Math.max(batchSize, 1), setter); + } + + @Override + public AnalyticalDBPageResult page(String countSql, + Object[] countArgs, + String dataSql, + Object[] dataArgs, + AnalyticalDBPageRequest pageRequest, + RowMapper rowMapper) { + assertAvailable(); + AnalyticalDBPageRequest request = pageRequest == null ? new AnalyticalDBPageRequest() : pageRequest; + Long total = jdbcTemplate.queryForObject(countSql, Long.class, nullSafeArgs(countArgs)); + List records = new ArrayList<>(); + if (total != null && total > 0) { + Object[] argsWithPage = appendPageArgs(dataArgs, request.getSafePageSize(), request.getOffset()); + records = jdbcTemplate.query(dataSql + " LIMIT ? OFFSET ?", rowMapper, argsWithPage); + } + AnalyticalDBPageResult result = new AnalyticalDBPageResult<>(); + result.setTotal(total == null ? 0L : total); + result.setPageNumber(request.getPageNumber()); + result.setPageSize(request.getSafePageSize()); + result.setRecords(records); + return result; + } + + private Object[] appendPageArgs(Object[] args, int limit, int offset) { + Object[] safeArgs = nullSafeArgs(args); + Object[] merged = new Object[safeArgs.length + 2]; + System.arraycopy(safeArgs, 0, merged, 0, safeArgs.length); + merged[safeArgs.length] = limit; + merged[safeArgs.length + 1] = offset; + return merged; + } + + private Object[] nullSafeArgs(Object[] args) { + return args == null ? new Object[0] : args; + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/exception/AnalyticalDBException.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/exception/AnalyticalDBException.java new file mode 100644 index 0000000..dbd4431 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/exception/AnalyticalDBException.java @@ -0,0 +1,12 @@ +package tech.easyflow.common.analyticaldb.exception; + +public class AnalyticalDBException extends RuntimeException { + + public AnalyticalDBException(String message) { + super(message); + } + + public AnalyticalDBException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageRequest.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageRequest.java new file mode 100644 index 0000000..1e03e6f --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageRequest.java @@ -0,0 +1,40 @@ +package tech.easyflow.common.analyticaldb.page; + +public class AnalyticalDBPageRequest { + + private int pageNumber = 1; + private int pageSize = 20; + + public AnalyticalDBPageRequest() { + } + + public AnalyticalDBPageRequest(int pageNumber, int pageSize) { + this.pageNumber = pageNumber; + this.pageSize = pageSize; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public int getOffset() { + int safePageNumber = Math.max(pageNumber, 1); + return (safePageNumber - 1) * getSafePageSize(); + } + + public int getSafePageSize() { + return Math.max(pageSize, 1); + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageResult.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageResult.java new file mode 100644 index 0000000..0d11da9 --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/page/AnalyticalDBPageResult.java @@ -0,0 +1,44 @@ +package tech.easyflow.common.analyticaldb.page; + +import java.util.ArrayList; +import java.util.List; + +public class AnalyticalDBPageResult { + + private long total; + private int pageNumber; + private int pageSize; + private List records = new ArrayList<>(); + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public List getRecords() { + return records; + } + + public void setRecords(List records) { + this.records = records; + } +} diff --git a/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/support/AnalyticalDBHealthSupport.java b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/support/AnalyticalDBHealthSupport.java new file mode 100644 index 0000000..e81e04b --- /dev/null +++ b/easyflow-commons/easyflow-common-analytical-db/src/main/java/tech/easyflow/common/analyticaldb/support/AnalyticalDBHealthSupport.java @@ -0,0 +1,32 @@ +package tech.easyflow.common.analyticaldb.support; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Component; +import tech.easyflow.common.analyticaldb.config.AnalyticalDBFlywayProperties; +import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations; + +@Component +public class AnalyticalDBHealthSupport { + + private final ObjectProvider analyticalDBOperationsProvider; + private final AnalyticalDBFlywayProperties flywayProperties; + + public AnalyticalDBHealthSupport(ObjectProvider analyticalDBOperationsProvider, + AnalyticalDBFlywayProperties flywayProperties) { + this.analyticalDBOperationsProvider = analyticalDBOperationsProvider; + this.flywayProperties = flywayProperties; + } + + public boolean enabled() { + return analyticalDBOperationsProvider.getIfAvailable() != null; + } + + public void selfCheck() { + AnalyticalDBOperations operations = analyticalDBOperationsProvider.getIfAvailable(); + if (operations == null) { + return; + } + operations.queryOne("SELECT 1", Integer.class); + operations.queryOne("SELECT COUNT(1) FROM " + flywayProperties.getTable(), Long.class); + } +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatAssistantAccumulator.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatAssistantAccumulator.java new file mode 100644 index 0000000..c2b74d0 --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatAssistantAccumulator.java @@ -0,0 +1,74 @@ +package tech.easyflow.core.runtime; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ChatAssistantAccumulator { + + private final StringBuilder content = new StringBuilder(); + private final StringBuilder reasoning = new StringBuilder(); + private final List> chains = new ArrayList<>(); + + public void appendContent(String delta) { + if (delta != null && !delta.isEmpty()) { + content.append(delta); + } + } + + public void appendReasoning(String delta) { + if (delta != null && !delta.isEmpty()) { + reasoning.append(delta); + } + } + + public void appendToolCall(String id, String name, Object arguments) { + Map chain = findToolChain(id, name); + chain.put("status", "TOOL_CALL"); + chain.put("result", arguments); + } + + public void appendToolResult(String id, String name, Object result) { + Map chain = findToolChain(id, name); + chain.put("status", "TOOL_RESULT"); + chain.put("result", result); + } + + public String getContent() { + return content.toString(); + } + + public Map buildPayload() { + Map payload = new LinkedHashMap<>(); + List> payloadChains = new ArrayList<>(); + if (reasoning.length() > 0) { + Map think = new LinkedHashMap<>(); + think.put("reasoning_content", reasoning.toString()); + think.put("thinkingStatus", "end"); + think.put("thinlCollapse", Boolean.TRUE); + payloadChains.add(think); + } + payloadChains.addAll(chains); + if (!payloadChains.isEmpty()) { + payload.put("chains", payloadChains); + } + return payload; + } + + private Map findToolChain(String id, String name) { + for (Map chain : chains) { + if (String.valueOf(chain.get("id")).equals(id)) { + if (name != null && !name.isEmpty()) { + chain.put("name", name); + } + return chain; + } + } + Map chain = new LinkedHashMap<>(); + chain.put("id", id); + chain.put("name", name); + chains.add(chain); + return chain; + } +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatChannel.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatChannel.java new file mode 100644 index 0000000..0aecf8e --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatChannel.java @@ -0,0 +1,8 @@ +package tech.easyflow.core.runtime; + +public enum ChatChannel { + + ADMIN, + USER_CENTER, + PUBLIC_API +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeContext.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeContext.java new file mode 100644 index 0000000..5f548d0 --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeContext.java @@ -0,0 +1,138 @@ +package tech.easyflow.core.runtime; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class ChatRuntimeContext implements Serializable { + + private ChatChannel channel; + private BigInteger sessionId; + private BigInteger tenantId; + private BigInteger deptId; + private BigInteger userId; + private String userAccount; + private String userName; + private BigInteger assistantId; + private String assistantCode; + private String assistantName; + private String sessionTitle; + private boolean anonymous; + private List attachments = new ArrayList<>(); + private Map ext = new LinkedHashMap<>(); + + public ChatChannel getChannel() { + return channel; + } + + public void setChannel(ChatChannel channel) { + this.channel = channel; + } + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getTenantId() { + return tenantId; + } + + public void setTenantId(BigInteger tenantId) { + this.tenantId = tenantId; + } + + public BigInteger getDeptId() { + return deptId; + } + + public void setDeptId(BigInteger deptId) { + this.deptId = deptId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public String getUserAccount() { + return userAccount; + } + + public void setUserAccount(String userAccount) { + this.userAccount = userAccount; + } + + public String getUserName() { + return userName; + } + + public void setUserName(String userName) { + this.userName = userName; + } + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public String getAssistantCode() { + return assistantCode; + } + + public void setAssistantCode(String assistantCode) { + this.assistantCode = assistantCode; + } + + public String getAssistantName() { + return assistantName; + } + + public void setAssistantName(String assistantName) { + this.assistantName = assistantName; + } + + public String getSessionTitle() { + return sessionTitle; + } + + public void setSessionTitle(String sessionTitle) { + this.sessionTitle = sessionTitle; + } + + public boolean isAnonymous() { + return anonymous; + } + + public void setAnonymous(boolean anonymous) { + this.anonymous = anonymous; + } + + public List getAttachments() { + return attachments; + } + + public void setAttachments(List attachments) { + this.attachments = attachments == null ? new ArrayList<>() : attachments; + } + + public Map getExt() { + return ext; + } + + public void setExt(Map ext) { + this.ext = ext == null ? new LinkedHashMap<>() : ext; + } +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeListener.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeListener.java new file mode 100644 index 0000000..7486007 --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeListener.java @@ -0,0 +1,29 @@ +package tech.easyflow.core.runtime; + +import java.util.Collections; +import java.util.List; + +public interface ChatRuntimeListener { + + default void onSessionPrepared(ChatRuntimeContext context) { + } + + default void onUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) { + } + + default void onAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) { + } + + default void onAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) { + } + + default void onChatFailed(ChatRuntimeContext context, Throwable throwable) { + } + + default void onChatCompleted(ChatRuntimeContext context) { + } + + default List loadMessages(ChatRuntimeContext context, int limit) { + return Collections.emptyList(); + } +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeManager.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeManager.java new file mode 100644 index 0000000..6c33559 --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeManager.java @@ -0,0 +1,20 @@ +package tech.easyflow.core.runtime; + +import java.util.List; + +public interface ChatRuntimeManager { + + void prepareSession(ChatRuntimeContext context); + + void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message); + + void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message); + + void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message); + + void recordFailure(ChatRuntimeContext context, Throwable throwable); + + void recordCompleted(ChatRuntimeContext context); + + List loadMessages(ChatRuntimeContext context, int limit); +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeMessage.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeMessage.java new file mode 100644 index 0000000..1cbb713 --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/ChatRuntimeMessage.java @@ -0,0 +1,83 @@ +package tech.easyflow.core.runtime; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +public class ChatRuntimeMessage implements Serializable { + + private BigInteger messageId; + private String role; + private String contentType = "TEXT"; + private String contentText; + private Map contentPayload = new LinkedHashMap<>(); + private Date createdAt = new Date(); + private BigInteger senderId; + private String senderName; + + public BigInteger getMessageId() { + return messageId; + } + + public void setMessageId(BigInteger messageId) { + this.messageId = messageId; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getContentText() { + return contentText; + } + + public void setContentText(String contentText) { + this.contentText = contentText; + } + + public Map getContentPayload() { + return contentPayload; + } + + public void setContentPayload(Map contentPayload) { + this.contentPayload = contentPayload == null ? new LinkedHashMap<>() : contentPayload; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public BigInteger getSenderId() { + return senderId; + } + + public void setSenderId(BigInteger senderId) { + this.senderId = senderId; + } + + public String getSenderName() { + return senderName; + } + + public void setSenderName(String senderName) { + this.senderName = senderName; + } +} diff --git a/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/CompositeChatRuntimeManager.java b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/CompositeChatRuntimeManager.java new file mode 100644 index 0000000..87421dc --- /dev/null +++ b/easyflow-commons/easyflow-common-chat-protocol/src/main/java/tech/easyflow/core/runtime/CompositeChatRuntimeManager.java @@ -0,0 +1,93 @@ +package tech.easyflow.core.runtime; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.stereotype.Component; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.util.Collections; +import java.util.List; + +@Component +public class CompositeChatRuntimeManager implements ChatRuntimeManager { + + private static final Logger log = LoggerFactory.getLogger(CompositeChatRuntimeManager.class); + + private final ObjectProvider listenerProvider; + + public CompositeChatRuntimeManager(ObjectProvider listenerProvider) { + this.listenerProvider = listenerProvider; + } + + @Override + public void prepareSession(ChatRuntimeContext context) { + forEach(listener -> listener.onSessionPrepared(context), "prepareSession", context); + } + + @Override + public void recordUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) { + forEach(listener -> listener.onUserMessage(context, message), "recordUserMessage", context); + } + + @Override + public void recordAssistantDelta(ChatRuntimeContext context, ChatRuntimeMessage message) { + forEach(listener -> listener.onAssistantDelta(context, message), "recordAssistantDelta", context); + } + + @Override + public void recordAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) { + forEach(listener -> listener.onAssistantCompleted(context, message), "recordAssistantCompleted", context); + } + + @Override + public void recordFailure(ChatRuntimeContext context, Throwable throwable) { + forEach(listener -> listener.onChatFailed(context, throwable), "recordFailure", context); + } + + @Override + public void recordCompleted(ChatRuntimeContext context) { + forEach(listener -> listener.onChatCompleted(context), "recordCompleted", context); + } + + @Override + public List loadMessages(ChatRuntimeContext context, int limit) { + for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) { + try { + List messages = listener.loadMessages(context, limit); + if (messages != null && !messages.isEmpty()) { + return messages; + } + } catch (Exception ex) { + log.warn("chat runtime loadMessages failed, channel={}, sessionId={}, listener={}", + context == null || context.getChannel() == null ? null : context.getChannel().name(), + context == null ? null : context.getSessionId(), + listener.getClass().getName(), + ex); + } + } + return Collections.emptyList(); + } + + private void forEach(ListenerConsumer consumer, String action, ChatRuntimeContext context) { + for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) { + try { + consumer.accept(listener); + } catch (BusinessException ex) { + throw ex; + } catch (Exception ex) { + log.warn("chat runtime {} failed, channel={}, sessionId={}, listener={}", + action, + context == null || context.getChannel() == null ? null : context.getChannel().name(), + context == null ? null : context.getSessionId(), + listener.getClass().getName(), + ex); + } + } + } + + @FunctionalInterface + private interface ListenerConsumer { + void accept(ChatRuntimeListener listener); + } +} diff --git a/easyflow-commons/easyflow-common-mq/pom.xml b/easyflow-commons/easyflow-common-mq/pom.xml new file mode 100644 index 0000000..710be55 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/pom.xml @@ -0,0 +1,26 @@ + + + 4.0.0 + + tech.easyflow + easyflow-commons + ${revision} + + + easyflow-common-mq + easyflow-common-mq + + + + org.springframework.boot + spring-boot-starter-data-redis + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQConfiguration.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQConfiguration.java new file mode 100644 index 0000000..92ab304 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQConfiguration.java @@ -0,0 +1,129 @@ +package tech.easyflow.common.mq.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.data.redis.RedisProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; +import tech.easyflow.common.mq.core.MQConsumerContainer; +import tech.easyflow.common.mq.core.MQConsumerHandler; +import tech.easyflow.common.mq.core.MQDeadLetterHandler; +import tech.easyflow.common.mq.core.MQDeadLetterService; +import tech.easyflow.common.mq.core.MQMessageConverter; +import tech.easyflow.common.mq.core.MQProducer; +import tech.easyflow.common.mq.redis.JacksonMQMessageConverter; +import tech.easyflow.common.mq.redis.RedisMQConsumerContainer; +import tech.easyflow.common.mq.redis.RedisMQDeadLetterService; +import tech.easyflow.common.mq.redis.RedisMQProducer; +import tech.easyflow.common.mq.redis.RedisStreamKeySupport; +import tech.easyflow.common.mq.support.MQHealthSupport; + +import java.util.List; + +@Configuration +@EnableConfigurationProperties(MQProperties.class) +public class MQConfiguration { + + @Bean(destroyMethod = "close") + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public MQRedisResources mqRedisResources(RedisProperties redisProperties, MQProperties mqProperties) { + RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration(); + configuration.setHostName(redisProperties.getHost()); + configuration.setPort(redisProperties.getPort()); + configuration.setDatabase(mqProperties.getRedis().getDatabase()); + if (redisProperties.getUsername() != null) { + configuration.setUsername(redisProperties.getUsername()); + } + if (redisProperties.getPassword() != null) { + configuration.setPassword(RedisPassword.of(redisProperties.getPassword())); + } + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(configuration); + connectionFactory.afterPropertiesSet(); + return new MQRedisResources(connectionFactory, new StringRedisTemplate(connectionFactory)); + } + + @Bean(name = "mqRedisConnectionFactory", autowireCandidate = false, defaultCandidate = false) + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public LettuceConnectionFactory mqRedisConnectionFactory(MQRedisResources mqRedisResources) { + return mqRedisResources.connectionFactory(); + } + + @Bean(name = "mqStringRedisTemplate", autowireCandidate = false, defaultCandidate = false) + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public StringRedisTemplate mqStringRedisTemplate(MQRedisResources mqRedisResources) { + return mqRedisResources.stringRedisTemplate(); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public RedisStreamKeySupport redisStreamKeySupport(MQProperties mqProperties) { + return new RedisStreamKeySupport(mqProperties); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public MQMessageConverter mqMessageConverter(ObjectMapper objectMapper) { + return new JacksonMQMessageConverter(objectMapper); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public MQDeadLetterService mqDeadLetterService(MQRedisResources mqRedisResources, + MQMessageConverter mqMessageConverter, + RedisStreamKeySupport redisStreamKeySupport, + ObjectProvider handlersProvider) { + List handlers = handlersProvider.orderedStream().toList(); + return new RedisMQDeadLetterService( + mqRedisResources.stringRedisTemplate(), + mqMessageConverter, + redisStreamKeySupport, + handlers + ); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public MQProducer mqProducer(MQRedisResources mqRedisResources, + MQProperties mqProperties, + MQMessageConverter mqMessageConverter, + RedisStreamKeySupport redisStreamKeySupport) { + return new RedisMQProducer( + mqRedisResources.stringRedisTemplate(), + mqProperties, + mqMessageConverter, + redisStreamKeySupport + ); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public MQHealthSupport mqHealthSupport(MQRedisResources mqRedisResources) { + return new MQHealthSupport(mqRedisResources.connectionFactory()); + } + + @Bean + @ConditionalOnProperty(prefix = "easyflow.mq", name = "enabled", havingValue = "true", matchIfMissing = true) + public MQConsumerContainer mqConsumerContainer(MQRedisResources mqRedisResources, + MQProperties mqProperties, + MQMessageConverter mqMessageConverter, + MQDeadLetterService mqDeadLetterService, + RedisStreamKeySupport redisStreamKeySupport, + ObjectProvider handlersProvider) { + List handlers = handlersProvider.orderedStream().toList(); + return new RedisMQConsumerContainer( + mqRedisResources.connectionFactory(), + mqRedisResources.stringRedisTemplate(), + mqProperties, + mqMessageConverter, + mqDeadLetterService, + redisStreamKeySupport, + handlers + ); + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQProperties.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQProperties.java new file mode 100644 index 0000000..3bebbf8 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQProperties.java @@ -0,0 +1,100 @@ +package tech.easyflow.common.mq.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.time.Duration; + +@ConfigurationProperties(prefix = "easyflow.mq") +public class MQProperties { + + private boolean enabled = true; + private String type = "redis"; + private final Redis redis = new Redis(); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Redis getRedis() { + return redis; + } + + public static class Redis { + + private int database = 1; + private String streamPrefix = "easyflow:mq"; + private int chatPersistShardCount = 4; + private int consumerBatchSize = 200; + private Duration consumerBlockTimeout = Duration.ofMillis(2000); + private Duration pendingClaimIdle = Duration.ofMillis(60000); + private int maxRetry = 16; + + public int getDatabase() { + return database; + } + + public void setDatabase(int database) { + this.database = database; + } + + public String getStreamPrefix() { + return streamPrefix; + } + + public void setStreamPrefix(String streamPrefix) { + this.streamPrefix = streamPrefix; + } + + public int getChatPersistShardCount() { + return chatPersistShardCount; + } + + public void setChatPersistShardCount(int chatPersistShardCount) { + this.chatPersistShardCount = chatPersistShardCount; + } + + public int getConsumerBatchSize() { + return consumerBatchSize; + } + + public void setConsumerBatchSize(int consumerBatchSize) { + this.consumerBatchSize = consumerBatchSize; + } + + public Duration getConsumerBlockTimeout() { + return consumerBlockTimeout; + } + + public void setConsumerBlockTimeout(Duration consumerBlockTimeout) { + this.consumerBlockTimeout = consumerBlockTimeout; + } + + public Duration getPendingClaimIdle() { + return pendingClaimIdle; + } + + public void setPendingClaimIdle(Duration pendingClaimIdle) { + this.pendingClaimIdle = pendingClaimIdle; + } + + public int getMaxRetry() { + return maxRetry; + } + + public void setMaxRetry(int maxRetry) { + this.maxRetry = maxRetry; + } + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQRedisResources.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQRedisResources.java new file mode 100644 index 0000000..6c0b3f3 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/config/MQRedisResources.java @@ -0,0 +1,13 @@ +package tech.easyflow.common.mq.config; + +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.StringRedisTemplate; + +public record MQRedisResources(LettuceConnectionFactory connectionFactory, + StringRedisTemplate stringRedisTemplate) implements AutoCloseable { + + @Override + public void close() { + connectionFactory.destroy(); + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQAcknowledger.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQAcknowledger.java new file mode 100644 index 0000000..fb559c2 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQAcknowledger.java @@ -0,0 +1,8 @@ +package tech.easyflow.common.mq.core; + +import java.util.List; + +public interface MQAcknowledger { + + void acknowledge(List messages); +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerContainer.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerContainer.java new file mode 100644 index 0000000..2413351 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerContainer.java @@ -0,0 +1,6 @@ +package tech.easyflow.common.mq.core; + +public interface MQConsumerContainer { + + boolean isRunning(); +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerHandler.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerHandler.java new file mode 100644 index 0000000..ce0b9a6 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQConsumerHandler.java @@ -0,0 +1,10 @@ +package tech.easyflow.common.mq.core; + +import java.util.List; + +public interface MQConsumerHandler { + + MQSubscription subscription(); + + void handle(List messages) throws Exception; +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterHandler.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterHandler.java new file mode 100644 index 0000000..2cfd0a8 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterHandler.java @@ -0,0 +1,8 @@ +package tech.easyflow.common.mq.core; + +public interface MQDeadLetterHandler { + + boolean supports(String topic); + + void handle(MQMessage message, String reason); +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterService.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterService.java new file mode 100644 index 0000000..aa24fbe --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQDeadLetterService.java @@ -0,0 +1,6 @@ +package tech.easyflow.common.mq.core; + +public interface MQDeadLetterService { + + void deadLetter(MQMessage message, String reason); +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessage.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessage.java new file mode 100644 index 0000000..6590c5a --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessage.java @@ -0,0 +1,91 @@ +package tech.easyflow.common.mq.core; + +import java.io.Serializable; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; + +public class MQMessage implements Serializable { + + private String messageId; + private String topic; + private String key; + private String body; + private Date createdAt; + private int retryCount; + private String streamKey; + private String streamMessageId; + private Map headers = new LinkedHashMap<>(); + + public String getMessageId() { + return messageId; + } + + public void setMessageId(String messageId) { + this.messageId = messageId; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getKey() { + return key; + } + + public void setKey(String key) { + this.key = key; + } + + public String getBody() { + return body; + } + + public void setBody(String body) { + this.body = body; + } + + public Date getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Date createdAt) { + this.createdAt = createdAt; + } + + public int getRetryCount() { + return retryCount; + } + + public void setRetryCount(int retryCount) { + this.retryCount = retryCount; + } + + public String getStreamKey() { + return streamKey; + } + + public void setStreamKey(String streamKey) { + this.streamKey = streamKey; + } + + public String getStreamMessageId() { + return streamMessageId; + } + + public void setStreamMessageId(String streamMessageId) { + this.streamMessageId = streamMessageId; + } + + public Map getHeaders() { + return headers; + } + + public void setHeaders(Map headers) { + this.headers = headers == null ? new LinkedHashMap<>() : new LinkedHashMap<>(headers); + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessageConverter.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessageConverter.java new file mode 100644 index 0000000..d1c6315 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQMessageConverter.java @@ -0,0 +1,8 @@ +package tech.easyflow.common.mq.core; + +public interface MQMessageConverter { + + String serialize(MQMessage message); + + MQMessage deserialize(String payload); +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQProducer.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQProducer.java new file mode 100644 index 0000000..bc7cf75 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQProducer.java @@ -0,0 +1,6 @@ +package tech.easyflow.common.mq.core; + +public interface MQProducer { + + String send(MQMessage message); +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQSubscription.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQSubscription.java new file mode 100644 index 0000000..e0f68eb --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/core/MQSubscription.java @@ -0,0 +1,32 @@ +package tech.easyflow.common.mq.core; + +public class MQSubscription { + + private String topic; + private String consumerGroup; + private int shardCount; + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getConsumerGroup() { + return consumerGroup; + } + + public void setConsumerGroup(String consumerGroup) { + this.consumerGroup = consumerGroup; + } + + public int getShardCount() { + return shardCount; + } + + public void setShardCount(int shardCount) { + this.shardCount = shardCount; + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/exception/MQException.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/exception/MQException.java new file mode 100644 index 0000000..0487a28 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/exception/MQException.java @@ -0,0 +1,12 @@ +package tech.easyflow.common.mq.exception; + +public class MQException extends RuntimeException { + + public MQException(String message) { + super(message); + } + + public MQException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/JacksonMQMessageConverter.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/JacksonMQMessageConverter.java new file mode 100644 index 0000000..8f73412 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/JacksonMQMessageConverter.java @@ -0,0 +1,34 @@ +package tech.easyflow.common.mq.redis; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import tech.easyflow.common.mq.core.MQMessage; +import tech.easyflow.common.mq.core.MQMessageConverter; +import tech.easyflow.common.mq.exception.MQException; + +public class JacksonMQMessageConverter implements MQMessageConverter { + + private final ObjectMapper objectMapper; + + public JacksonMQMessageConverter(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + @Override + public String serialize(MQMessage message) { + try { + return objectMapper.writeValueAsString(message); + } catch (JsonProcessingException e) { + throw new MQException("MQ 消息序列化失败", e); + } + } + + @Override + public MQMessage deserialize(String payload) { + try { + return objectMapper.readValue(payload, MQMessage.class); + } catch (JsonProcessingException e) { + throw new MQException("MQ 消息反序列化失败", e); + } + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQConsumerContainer.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQConsumerContainer.java new file mode 100644 index 0000000..aad22d4 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQConsumerContainer.java @@ -0,0 +1,259 @@ +package tech.easyflow.common.mq.redis; + +import jakarta.annotation.PreDestroy; +import org.springframework.context.SmartLifecycle; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStreamCommands; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessage; +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamReadOptions; +import org.springframework.data.redis.core.StringRedisTemplate; +import tech.easyflow.common.mq.config.MQProperties; +import tech.easyflow.common.mq.core.MQAcknowledger; +import tech.easyflow.common.mq.core.MQConsumerContainer; +import tech.easyflow.common.mq.core.MQConsumerHandler; +import tech.easyflow.common.mq.core.MQDeadLetterService; +import tech.easyflow.common.mq.core.MQMessage; +import tech.easyflow.common.mq.core.MQMessageConverter; +import tech.easyflow.common.mq.core.MQSubscription; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +public class RedisMQConsumerContainer implements MQConsumerContainer, SmartLifecycle { + + private final RedisConnectionFactory redisConnectionFactory; + private final StringRedisTemplate stringRedisTemplate; + private final MQProperties properties; + private final MQMessageConverter messageConverter; + private final MQDeadLetterService deadLetterService; + private final RedisStreamKeySupport keySupport; + private final List handlers; + private final ExecutorService executorService = Executors.newCachedThreadPool(); + + private volatile boolean running; + + public RedisMQConsumerContainer(RedisConnectionFactory redisConnectionFactory, + StringRedisTemplate stringRedisTemplate, + MQProperties properties, + MQMessageConverter messageConverter, + MQDeadLetterService deadLetterService, + RedisStreamKeySupport keySupport, + List handlers) { + this.redisConnectionFactory = redisConnectionFactory; + this.stringRedisTemplate = stringRedisTemplate; + this.properties = properties; + this.messageConverter = messageConverter; + this.deadLetterService = deadLetterService; + this.keySupport = keySupport; + this.handlers = handlers; + } + + @Override + public void start() { + if (running) { + return; + } + running = true; + for (MQConsumerHandler handler : handlers) { + MQSubscription subscription = handler.subscription(); + for (int shard = 0; shard < Math.max(subscription.getShardCount(), 1); shard++) { + int currentShard = shard; + executorService.submit(() -> consumeLoop(handler, subscription, currentShard)); + } + } + } + + @Override + public void stop() { + running = false; + executorService.shutdownNow(); + try { + executorService.awaitTermination(5, TimeUnit.SECONDS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } + + @Override + public boolean isRunning() { + return running; + } + + @Override + public int getPhase() { + return Integer.MAX_VALUE; + } + + @PreDestroy + public void destroy() { + stop(); + } + + private void consumeLoop(MQConsumerHandler handler, MQSubscription subscription, int shard) { + String streamKey = keySupport.streamKey(subscription.getTopic(), shard); + String consumerName = subscription.getConsumerGroup() + "-" + shard; + ensureConsumerGroup(streamKey, subscription.getConsumerGroup()); + while (running) { + try { + reclaimPending(streamKey, subscription.getConsumerGroup(), consumerName); + List> records = stringRedisTemplate.opsForStream().read( + Consumer.from(subscription.getConsumerGroup(), consumerName), + StreamReadOptions.empty() + .count(properties.getRedis().getConsumerBatchSize()) + .block(properties.getRedis().getConsumerBlockTimeout()), + StreamOffset.create(streamKey, org.springframework.data.redis.connection.stream.ReadOffset.lastConsumed()) + ); + if (records == null || records.isEmpty()) { + continue; + } + List messages = toMessages(streamKey, records); + if (messages.isEmpty()) { + continue; + } + handleMessages(handler, streamKey, subscription.getConsumerGroup(), messages); + } catch (Exception ignored) { + sleepSilently(1000L); + } + } + } + + private void reclaimPending(String streamKey, String group, String consumerName) { + Duration idle = properties.getRedis().getPendingClaimIdle(); + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + RedisStreamCommands.XPendingOptions options = RedisStreamCommands.XPendingOptions + .range(Range.unbounded(), (long) properties.getRedis().getConsumerBatchSize()); + var pendingMessages = connection.streamCommands() + .xPending(streamKey.getBytes(StandardCharsets.UTF_8), group, options); + if (pendingMessages == null || pendingMessages.isEmpty()) { + return; + } + List ids = new ArrayList<>(); + for (PendingMessage pendingMessage : pendingMessages) { + if (pendingMessage.getElapsedTimeSinceLastDelivery().compareTo(idle) >= 0) { + ids.add(pendingMessage.getId()); + } + } + if (ids.isEmpty()) { + return; + } + stringRedisTemplate.opsForStream().claim( + streamKey, + group, + consumerName, + idle, + ids.toArray(new RecordId[0]) + ); + } + } + + private void ensureConsumerGroup(String streamKey, String group) { + try (RedisConnection connection = redisConnectionFactory.getConnection()) { + connection.streamCommands().xGroupCreate( + streamKey.getBytes(StandardCharsets.UTF_8), + group, + org.springframework.data.redis.connection.stream.ReadOffset.latest(), + true + ); + } catch (Exception ignored) { + } + } + + private List toMessages(String streamKey, List> records) { + List messages = new ArrayList<>(records.size()); + for (MapRecord record : records) { + Object payload = record.getValue().get("payload"); + if (payload == null) { + continue; + } + MQMessage message = messageConverter.deserialize(String.valueOf(payload)); + message.setStreamKey(streamKey); + message.setStreamMessageId(record.getId().getValue()); + messages.add(message); + } + return messages; + } + + private void retryOrDeadLetter(List messages, String reason) { + for (MQMessage message : messages) { + int retryCount = message.getRetryCount() + 1; + message.setRetryCount(retryCount); + message.getHeaders().put("lastError", reason == null ? "" : reason); + if (retryCount > properties.getRedis().getMaxRetry()) { + deadLetterService.deadLetter(message, reason); + } else { + stringRedisTemplate.opsForStream().add( + org.springframework.data.redis.connection.stream.StreamRecords.string( + Map.of("payload", messageConverter.serialize(message)) + ).withStreamKey(message.getStreamKey()) + ); + } + } + } + + private void handleMessages(MQConsumerHandler handler, String streamKey, String group, List messages) throws Exception { + try { + handler.handle(messages); + acknowledge(streamKey, group, messages); + return; + } catch (Exception batchEx) { + if (messages.size() == 1) { + retryOrDeadLetter(messages, resolveReason(batchEx)); + acknowledge(streamKey, group, messages); + return; + } + } + + for (MQMessage message : messages) { + try { + handler.handle(List.of(message)); + } catch (Exception singleEx) { + retryOrDeadLetter(List.of(message), resolveReason(singleEx)); + } finally { + acknowledge(streamKey, group, List.of(message)); + } + } + } + + private void acknowledge(String streamKey, String group, List messages) { + if (messages == null || messages.isEmpty()) { + return; + } + String[] ids = messages.stream() + .map(MQMessage::getStreamMessageId) + .filter(Objects::nonNull) + .toArray(String[]::new); + if (ids.length == 0) { + return; + } + MQAcknowledger acknowledger = records -> stringRedisTemplate.opsForStream().acknowledge(streamKey, group, ids); + acknowledger.acknowledge(messages); + } + + private String resolveReason(Exception exception) { + if (exception == null || exception.getMessage() == null || exception.getMessage().isBlank()) { + return "消费失败"; + } + return exception.getMessage(); + } + + private void sleepSilently(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQDeadLetterService.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQDeadLetterService.java new file mode 100644 index 0000000..f19e91f --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQDeadLetterService.java @@ -0,0 +1,49 @@ +package tech.easyflow.common.mq.redis; + +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.StringRedisTemplate; +import tech.easyflow.common.mq.core.MQDeadLetterHandler; +import tech.easyflow.common.mq.core.MQDeadLetterService; +import tech.easyflow.common.mq.core.MQMessage; +import tech.easyflow.common.mq.core.MQMessageConverter; +import tech.easyflow.common.mq.exception.MQException; + +import java.util.List; +import java.util.Map; + +public class RedisMQDeadLetterService implements MQDeadLetterService { + + private final StringRedisTemplate stringRedisTemplate; + private final MQMessageConverter messageConverter; + private final RedisStreamKeySupport keySupport; + private final List handlers; + + public RedisMQDeadLetterService(StringRedisTemplate stringRedisTemplate, + MQMessageConverter messageConverter, + RedisStreamKeySupport keySupport, + List handlers) { + this.stringRedisTemplate = stringRedisTemplate; + this.messageConverter = messageConverter; + this.keySupport = keySupport; + this.handlers = handlers; + } + + @Override + public void deadLetter(MQMessage message, String reason) { + if (message == null) { + return; + } + message.getHeaders().put("deadLetterReason", reason == null ? "" : reason); + String deadLetterKey = keySupport.deadLetterKey(message.getTopic()); + if (stringRedisTemplate.opsForStream().add( + StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(deadLetterKey) + ) == null) { + throw new MQException("写入死信流失败"); + } + for (MQDeadLetterHandler handler : handlers) { + if (handler.supports(message.getTopic())) { + handler.handle(message, reason); + } + } + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQProducer.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQProducer.java new file mode 100644 index 0000000..f87c512 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisMQProducer.java @@ -0,0 +1,58 @@ +package tech.easyflow.common.mq.redis; + +import org.springframework.data.redis.connection.stream.RecordId; +import org.springframework.data.redis.connection.stream.StreamRecords; +import org.springframework.data.redis.core.StringRedisTemplate; +import tech.easyflow.common.mq.config.MQProperties; +import tech.easyflow.common.mq.core.MQMessage; +import tech.easyflow.common.mq.core.MQProducer; +import tech.easyflow.common.mq.core.MQMessageConverter; +import tech.easyflow.common.mq.exception.MQException; + +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +public class RedisMQProducer implements MQProducer { + + private final StringRedisTemplate stringRedisTemplate; + private final MQProperties properties; + private final MQMessageConverter messageConverter; + private final RedisStreamKeySupport keySupport; + + public RedisMQProducer(StringRedisTemplate stringRedisTemplate, + MQProperties properties, + MQMessageConverter messageConverter, + RedisStreamKeySupport keySupport) { + this.stringRedisTemplate = stringRedisTemplate; + this.properties = properties; + this.messageConverter = messageConverter; + this.keySupport = keySupport; + } + + @Override + public String send(MQMessage message) { + if (message == null) { + throw new MQException("MQ 消息不能为空"); + } + if (message.getTopic() == null || message.getTopic().isBlank()) { + throw new MQException("MQ topic 不能为空"); + } + if (message.getMessageId() == null || message.getMessageId().isBlank()) { + message.setMessageId(UUID.randomUUID().toString()); + } + if (message.getCreatedAt() == null) { + message.setCreatedAt(new Date()); + } + int shardCount = Math.max(properties.getRedis().getChatPersistShardCount(), 1); + int shard = keySupport.resolveShard(message.getKey(), shardCount); + String streamKey = keySupport.streamKey(message.getTopic(), shard); + RecordId recordId = stringRedisTemplate.opsForStream().add( + StreamRecords.string(Map.of("payload", messageConverter.serialize(message))).withStreamKey(streamKey) + ); + if (recordId == null) { + throw new MQException("MQ 消息投递失败"); + } + return recordId.getValue(); + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisStreamKeySupport.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisStreamKeySupport.java new file mode 100644 index 0000000..ff7e1da --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/redis/RedisStreamKeySupport.java @@ -0,0 +1,27 @@ +package tech.easyflow.common.mq.redis; + +import tech.easyflow.common.mq.config.MQProperties; + +public class RedisStreamKeySupport { + + private final MQProperties properties; + + public RedisStreamKeySupport(MQProperties properties) { + this.properties = properties; + } + + public String streamKey(String topic, int shard) { + return properties.getRedis().getStreamPrefix() + ":" + topic + ":" + String.format("%02d", shard); + } + + public String deadLetterKey(String topic) { + return properties.getRedis().getStreamPrefix() + ":dead-letter:" + topic; + } + + public int resolveShard(String key, int shardCount) { + if (shardCount <= 0) { + return 0; + } + return Math.floorMod(key == null ? 0 : key.hashCode(), shardCount); + } +} diff --git a/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/support/MQHealthSupport.java b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/support/MQHealthSupport.java new file mode 100644 index 0000000..a9140b9 --- /dev/null +++ b/easyflow-commons/easyflow-common-mq/src/main/java/tech/easyflow/common/mq/support/MQHealthSupport.java @@ -0,0 +1,28 @@ +package tech.easyflow.common.mq.support; + +import org.springframework.data.redis.connection.RedisConnectionFactory; +import tech.easyflow.common.mq.exception.MQException; + +public class MQHealthSupport { + + private final RedisConnectionFactory redisConnectionFactory; + + public MQHealthSupport(RedisConnectionFactory redisConnectionFactory) { + this.redisConnectionFactory = redisConnectionFactory; + } + + public boolean available() { + try (var connection = redisConnectionFactory.getConnection()) { + String pong = connection.ping(); + return pong != null; + } catch (Exception e) { + return false; + } + } + + public void assertAvailable() { + if (!available()) { + throw new MQException("MQ Redis 不可用"); + } + } +} diff --git a/easyflow-commons/pom.xml b/easyflow-commons/pom.xml index b5bf0f1..f2ba459 100644 --- a/easyflow-commons/pom.xml +++ b/easyflow-commons/pom.xml @@ -13,6 +13,8 @@ easyflow-common-all + easyflow-common-analytical-db + easyflow-common-mq easyflow-common-web easyflow-common-captcha easyflow-common-base diff --git a/easyflow-modules/easyflow-module-chatlog/pom.xml b/easyflow-modules/easyflow-module-chatlog/pom.xml new file mode 100644 index 0000000..070d1af --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/pom.xml @@ -0,0 +1,56 @@ + + + 4.0.0 + + tech.easyflow + easyflow-modules + ${revision} + + + easyflow-module-chatlog + easyflow-module-chatlog + + + + tech.easyflow + easyflow-common-chat-protocol + + + tech.easyflow + easyflow-common-analytical-db + + + com.mybatis-flex + mybatis-flex-spring-boot3-starter + + + tech.easyflow + easyflow-common-base + + + tech.easyflow + easyflow-common-cache + + + tech.easyflow + easyflow-common-mq + + + org.springframework.boot + spring-boot-starter-jdbc + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + junit + junit + ${junit.version} + test + + + diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/cache/ChatHotStateService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/cache/ChatHotStateService.java new file mode 100644 index 0000000..c4d2cab --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/cache/ChatHotStateService.java @@ -0,0 +1,432 @@ +package tech.easyflow.chatlog.cache; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.config.ChatCacheProperties; +import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand; +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; + +import java.io.IOException; +import java.math.BigInteger; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +public class ChatHotStateService { + + private static final TypeReference> TAIL_TYPE = new TypeReference<>() { + }; + + private final StringRedisTemplate stringRedisTemplate; + private final ObjectMapper objectMapper; + private final ChatCacheProperties properties; + + public ChatHotStateService(StringRedisTemplate stringRedisTemplate, + ObjectMapper objectMapper, + ChatCacheProperties properties) { + this.stringRedisTemplate = stringRedisTemplate; + this.objectMapper = objectMapper; + this.properties = properties; + } + + public ChatSessionSummary touchSession(ChatSessionUpsertCommand command) { + ChatSessionSummary summary = getSessionSummary(command.getSessionId()); + if (summary == null) { + summary = new ChatSessionSummary(); + summary.setId(command.getSessionId()); + summary.setTenantId(command.getTenantId()); + summary.setDeptId(command.getDeptId()); + summary.setUserId(command.getUserId()); + summary.setCreated(command.getOperateAt()); + summary.setCreatedBy(command.getOperatorId()); + summary.setMessageCount(0); + summary.setLastMessagePreview(""); + } + summary.setUserAccount(command.getUserAccount()); + summary.setAssistantId(command.getAssistantId()); + summary.setAssistantCode(command.getAssistantCode()); + summary.setAssistantName(command.getAssistantName()); + if (command.getTitle() != null && !command.getTitle().isBlank()) { + summary.setTitle(command.getTitle()); + } + summary.setAccessAt(defaultDate(command.getOperateAt())); + summary.setModified(defaultDate(command.getOperateAt())); + summary.setModifiedBy(command.getOperatorId()); + summary.setIsDeleted(0); + cacheSessionSummaryStrict(summary); + return summary; + } + + public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId, Date operateAt) { + ChatSessionSummary summary = getSessionSummary(sessionId); + if (summary == null) { + summary = new ChatSessionSummary(); + summary.setId(sessionId); + summary.setUserId(userId); + summary.setCreated(defaultDate(operateAt)); + summary.setCreatedBy(operatorId); + summary.setMessageCount(0); + } + summary.setTitle(title); + summary.setModified(defaultDate(operateAt)); + summary.setModifiedBy(operatorId); + summary.setIsDeleted(0); + cacheSessionSummaryStrict(summary); + } + + public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId, Date operateAt) { + ChatSessionSummary summary = getSessionSummary(sessionId); + if (summary != null) { + summary.setIsDeleted(1); + summary.setModified(defaultDate(operateAt)); + summary.setModifiedBy(operatorId); + writeValueStrict(keySessionSummary(sessionId), summary, properties.getSessionSummaryTtl()); + removeFromSessionIndexStrict(userId, sessionId); + } else { + removeFromSessionIndexStrict(userId, sessionId); + } + deleteStrict(keySessionTail(sessionId)); + } + + public void appendMessage(ChatAppendMessageCommand command) { + ChatSessionSummary summary = getSessionSummary(command.getSessionId()); + if (summary == null) { + summary = new ChatSessionSummary(); + summary.setId(command.getSessionId()); + summary.setUserId(command.getUserId()); + summary.setAssistantId(command.getAssistantId()); + summary.setCreated(defaultDate(command.getCreated())); + summary.setCreatedBy(command.getCreatedBy()); + summary.setMessageCount(0); + } + summary.setLastSenderId(command.getSenderId()); + summary.setLastSenderName(command.getSenderName()); + summary.setLastMessagePreview(trimPreview(command.getContentText())); + summary.setLastMessageAt(defaultDate(command.getCreated())); + summary.setAccessAt(defaultDate(command.getCreated())); + summary.setModified(defaultDate(command.getCreated())); + summary.setModifiedBy(command.getCreatedBy()); + summary.setIsDeleted(0); + summary.setMessageCount((summary.getMessageCount() == null ? 0 : summary.getMessageCount()) + 1); + cacheSessionSummaryStrict(summary); + appendTail(toMessageRecord(command)); + } + + public List listSessionIds(BigInteger userId, long offset, long limit) { + if (userId == null || limit <= 0) { + return Collections.emptyList(); + } + try { + Set values = stringRedisTemplate.opsForZSet() + .reverseRange(keySessionIndex(userId), offset, offset + limit - 1); + if (values == null || values.isEmpty()) { + return Collections.emptyList(); + } + List sessionIds = new ArrayList<>(values.size()); + for (String value : values) { + if (value != null && !value.isBlank()) { + sessionIds.add(new BigInteger(value)); + } + } + return sessionIds; + } catch (DataAccessException ignored) { + return Collections.emptyList(); + } + } + + public long countSessions(BigInteger userId) { + if (userId == null) { + return 0L; + } + try { + Long count = stringRedisTemplate.opsForZSet().zCard(keySessionIndex(userId)); + return count == null ? 0L : count; + } catch (DataAccessException ignored) { + return 0L; + } + } + + public boolean hasSessionIndex(BigInteger userId) { + if (userId == null) { + return false; + } + try { + Boolean exists = stringRedisTemplate.hasKey(keySessionIndex(userId)); + return Boolean.TRUE.equals(exists); + } catch (DataAccessException ignored) { + return false; + } + } + + public ChatSessionSummary getSessionSummary(BigInteger sessionId) { + return readValue(keySessionSummary(sessionId), ChatSessionSummary.class); + } + + public List getSessionSummaries(List sessionIds) { + if (sessionIds == null || sessionIds.isEmpty()) { + return Collections.emptyList(); + } + try { + List keys = sessionIds.stream().map(this::keySessionSummary).toList(); + List values = stringRedisTemplate.opsForValue().multiGet(keys); + if (values == null || values.isEmpty()) { + return Collections.emptyList(); + } + Map summaryMap = new LinkedHashMap<>(); + for (String value : values) { + if (value == null || value.isBlank()) { + continue; + } + ChatSessionSummary summary = objectMapper.readValue(value, ChatSessionSummary.class); + if (summary != null && summary.getId() != null) { + summaryMap.put(summary.getId(), summary); + } + } + List results = new ArrayList<>(sessionIds.size()); + for (BigInteger sessionId : sessionIds) { + ChatSessionSummary summary = summaryMap.get(sessionId); + if (summary != null) { + results.add(summary); + } + } + return results; + } catch (IOException | DataAccessException ignored) { + return Collections.emptyList(); + } + } + + public void cacheSessionSummary(ChatSessionSummary summary) { + try { + cacheSessionSummaryStrict(summary); + } catch (IllegalStateException ignored) { + } + } + + private void cacheSessionSummaryStrict(ChatSessionSummary summary) { + if (summary == null || summary.getId() == null) { + return; + } + writeValueStrict(keySessionSummary(summary.getId()), summary, properties.getSessionSummaryTtl()); + BigInteger userId = summary.getUserId(); + if (userId == null) { + return; + } + if (Integer.valueOf(1).equals(summary.getIsDeleted())) { + removeFromSessionIndexStrict(userId, summary.getId()); + return; + } + Date scoreTime = summary.getAccessAt() != null ? summary.getAccessAt() : summary.getModified(); + double score = scoreTime == null ? System.currentTimeMillis() : scoreTime.getTime(); + try { + stringRedisTemplate.opsForZSet().add(keySessionIndex(userId), summary.getId().toString(), score); + } catch (DataAccessException ex) { + throw new IllegalStateException("写入聊天会话索引失败", ex); + } + expireStrict(keySessionIndex(userId), properties.getSessionListTtl()); + } + + public void cacheSessionSummaries(List sessions) { + if (sessions == null || sessions.isEmpty()) { + return; + } + Set dedup = new LinkedHashSet<>(); + for (ChatSessionSummary session : sessions) { + if (session == null || session.getId() == null || !dedup.add(session.getId())) { + continue; + } + cacheSessionSummary(session); + } + } + + public void evictSessionSummary(BigInteger sessionId) { + delete(keySessionSummary(sessionId)); + } + + public List getSessionTail(BigInteger sessionId) { + return readList(keySessionTail(sessionId), TAIL_TYPE); + } + + public void setSessionTail(BigInteger sessionId, List records) { + writeValue(keySessionTail(sessionId), trimTail(records), properties.getSessionTailTtl()); + } + + public void appendTail(ChatMessageRecord record) { + if (record == null || record.getSessionId() == null) { + return; + } + List current = getSessionTail(record.getSessionId()); + List updated = new ArrayList<>(); + updated.add(record); + if (current != null) { + updated.addAll(current); + } + writeValueStrict(keySessionTail(record.getSessionId()), trimTail(updated), properties.getSessionTailTtl()); + } + + public void evictSessionTail(BigInteger sessionId) { + delete(keySessionTail(sessionId)); + } + + public int tailSize() { + return properties.getTailSize(); + } + + private ChatMessageRecord toMessageRecord(ChatAppendMessageCommand command) { + ChatMessageRecord record = new ChatMessageRecord(); + record.setId(command.getMessageId()); + record.setSessionId(command.getSessionId()); + record.setUserId(command.getUserId()); + record.setAssistantId(command.getAssistantId()); + record.setSenderId(command.getSenderId()); + record.setSenderName(command.getSenderName()); + record.setSenderRole(command.getSenderRole()); + record.setContentType(command.getContentType()); + record.setContentText(command.getContentText()); + record.setContentPayload(command.getContentPayload()); + record.setCreated(defaultDate(command.getCreated())); + record.setCreatedBy(command.getCreatedBy()); + record.setSyncVersion(record.getCreated().getTime()); + return record; + } + + private List trimTail(List records) { + if (records == null || records.isEmpty()) { + return Collections.emptyList(); + } + int maxSize = Math.max(properties.getTailSize(), 1); + int size = Math.min(records.size(), maxSize); + List result = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + result.add(records.get(i)); + } + return result; + } + + private String keySessionIndex(BigInteger userId) { + return "chat:session:index:" + userId; + } + + private String keySessionSummary(BigInteger sessionId) { + return "chat:session:summary:" + sessionId; + } + + private String keySessionTail(BigInteger sessionId) { + return "chat:session:tail:" + sessionId; + } + + private void removeFromSessionIndex(BigInteger userId, BigInteger sessionId) { + if (userId == null || sessionId == null) { + return; + } + try { + stringRedisTemplate.opsForZSet().remove(keySessionIndex(userId), sessionId.toString()); + } catch (DataAccessException ignored) { + } + } + + private void removeFromSessionIndexStrict(BigInteger userId, BigInteger sessionId) { + if (userId == null || sessionId == null) { + return; + } + try { + stringRedisTemplate.opsForZSet().remove(keySessionIndex(userId), sessionId.toString()); + } catch (DataAccessException ex) { + throw new IllegalStateException("移除聊天会话索引失败", ex); + } + } + + private void expire(String key, Duration ttl) { + try { + stringRedisTemplate.expire(key, ttl); + } catch (DataAccessException ignored) { + } + } + + private void expireStrict(String key, Duration ttl) { + try { + Boolean success = stringRedisTemplate.expire(key, ttl); + if (Boolean.FALSE.equals(success)) { + throw new IllegalStateException("设置聊天缓存过期时间失败"); + } + } catch (DataAccessException ex) { + throw new IllegalStateException("设置聊天缓存过期时间失败", ex); + } + } + + private void delete(String key) { + try { + stringRedisTemplate.delete(key); + } catch (DataAccessException ignored) { + } + } + + private void deleteStrict(String key) { + try { + stringRedisTemplate.delete(key); + } catch (DataAccessException ex) { + throw new IllegalStateException("删除聊天缓存失败", ex); + } + } + + private void writeValue(String key, Object value, Duration ttl) { + try { + stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl); + } catch (IOException | DataAccessException ignored) { + } + } + + private void writeValueStrict(String key, Object value, Duration ttl) { + try { + stringRedisTemplate.opsForValue().set(key, objectMapper.writeValueAsString(value), ttl); + } catch (IOException | DataAccessException ex) { + throw new IllegalStateException("写入聊天缓存失败", ex); + } + } + + private T readValue(String key, Class clazz) { + try { + String value = stringRedisTemplate.opsForValue().get(key); + if (value == null || value.isBlank()) { + return null; + } + return objectMapper.readValue(value, clazz); + } catch (IOException | DataAccessException ignored) { + return null; + } + } + + private T readList(String key, TypeReference typeReference) { + try { + String value = stringRedisTemplate.opsForValue().get(key); + if (value == null || value.isBlank()) { + return null; + } + return objectMapper.readValue(value, typeReference); + } catch (IOException | DataAccessException ignored) { + return null; + } + } + + private String trimPreview(String text) { + if (text == null) { + return ""; + } + return text.length() <= 200 ? text : text.substring(0, 200); + } + + private Date defaultDate(Date value) { + return value == null ? new Date() : value; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatCacheProperties.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatCacheProperties.java new file mode 100644 index 0000000..02dc6c0 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatCacheProperties.java @@ -0,0 +1,48 @@ +package tech.easyflow.chatlog.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +import java.time.Duration; + +@Component +@ConfigurationProperties(prefix = "easyflow.chat.cache") +public class ChatCacheProperties { + + private Duration sessionListTtl = Duration.ofMinutes(5); + private Duration sessionSummaryTtl = Duration.ofMinutes(10); + private Duration sessionTailTtl = Duration.ofMinutes(30); + private int tailSize = 50; + + public Duration getSessionListTtl() { + return sessionListTtl; + } + + public void setSessionListTtl(Duration sessionListTtl) { + this.sessionListTtl = sessionListTtl; + } + + public Duration getSessionSummaryTtl() { + return sessionSummaryTtl; + } + + public void setSessionSummaryTtl(Duration sessionSummaryTtl) { + this.sessionSummaryTtl = sessionSummaryTtl; + } + + public Duration getSessionTailTtl() { + return sessionTailTtl; + } + + public void setSessionTailTtl(Duration sessionTailTtl) { + this.sessionTailTtl = sessionTailTtl; + } + + public int getTailSize() { + return tailSize; + } + + public void setTailSize(int tailSize) { + this.tailSize = tailSize; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatSyncProperties.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatSyncProperties.java new file mode 100644 index 0000000..472444c --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatSyncProperties.java @@ -0,0 +1,55 @@ +package tech.easyflow.chatlog.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +@Component +@ConfigurationProperties(prefix = "easyflow.chat.sync") +public class ChatSyncProperties { + + private boolean enabled = false; + private int batchSize = 500; + private long fixedDelay = 30000L; + private int repairLookbackDays = 3; + private int retentionMonths = 3; + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public int getBatchSize() { + return batchSize; + } + + public void setBatchSize(int batchSize) { + this.batchSize = batchSize; + } + + public long getFixedDelay() { + return fixedDelay; + } + + public void setFixedDelay(long fixedDelay) { + this.fixedDelay = fixedDelay; + } + + public int getRepairLookbackDays() { + return repairLookbackDays; + } + + public void setRepairLookbackDays(int repairLookbackDays) { + this.repairLookbackDays = repairLookbackDays; + } + + public int getRetentionMonths() { + return retentionMonths; + } + + public void setRetentionMonths(int retentionMonths) { + this.retentionMonths = retentionMonths; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatlogModuleConfig.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatlogModuleConfig.java new file mode 100644 index 0000000..87059d6 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/config/ChatlogModuleConfig.java @@ -0,0 +1,9 @@ +package tech.easyflow.chatlog.config; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.autoconfigure.AutoConfiguration; + +@AutoConfiguration +@MapperScan("tech.easyflow.chatlog.mapper") +public class ChatlogModuleConfig { +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatAppendMessageCommand.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatAppendMessageCommand.java new file mode 100644 index 0000000..ed3abb9 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatAppendMessageCommand.java @@ -0,0 +1,136 @@ +package tech.easyflow.chatlog.domain.command; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; +import java.util.Map; + +public class ChatAppendMessageCommand implements Serializable { + + private BigInteger messageId; + private BigInteger tenantId; + private BigInteger deptId; + private BigInteger sessionId; + private BigInteger userId; + private BigInteger assistantId; + private BigInteger senderId; + private String senderName; + private String senderRole; + private String contentType; + private String contentText; + private Map contentPayload; + private BigInteger createdBy; + private Date created = new Date(); + + public BigInteger getMessageId() { + return messageId; + } + + public void setMessageId(BigInteger messageId) { + this.messageId = messageId; + } + + public BigInteger getTenantId() { + return tenantId; + } + + public void setTenantId(BigInteger tenantId) { + this.tenantId = tenantId; + } + + public BigInteger getDeptId() { + return deptId; + } + + public void setDeptId(BigInteger deptId) { + this.deptId = deptId; + } + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public BigInteger getSenderId() { + return senderId; + } + + public void setSenderId(BigInteger senderId) { + this.senderId = senderId; + } + + public String getSenderName() { + return senderName; + } + + public void setSenderName(String senderName) { + this.senderName = senderName; + } + + public String getSenderRole() { + return senderRole; + } + + public void setSenderRole(String senderRole) { + this.senderRole = senderRole; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getContentText() { + return contentText; + } + + public void setContentText(String contentText) { + this.contentText = contentText; + } + + public Map getContentPayload() { + return contentPayload; + } + + public void setContentPayload(Map contentPayload) { + this.contentPayload = contentPayload; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionSummaryCommand.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionSummaryCommand.java new file mode 100644 index 0000000..f36c61e --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionSummaryCommand.java @@ -0,0 +1,81 @@ +package tech.easyflow.chatlog.domain.command; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatSessionSummaryCommand implements Serializable { + + private BigInteger sessionId; + private BigInteger userId; + private BigInteger lastSenderId; + private String lastSenderName; + private String lastMessagePreview; + private Date lastMessageAt = new Date(); + private BigInteger operatorId; + private int messageIncrement = 1; + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public BigInteger getLastSenderId() { + return lastSenderId; + } + + public void setLastSenderId(BigInteger lastSenderId) { + this.lastSenderId = lastSenderId; + } + + public String getLastSenderName() { + return lastSenderName; + } + + public void setLastSenderName(String lastSenderName) { + this.lastSenderName = lastSenderName; + } + + public String getLastMessagePreview() { + return lastMessagePreview; + } + + public void setLastMessagePreview(String lastMessagePreview) { + this.lastMessagePreview = lastMessagePreview; + } + + public Date getLastMessageAt() { + return lastMessageAt; + } + + public void setLastMessageAt(Date lastMessageAt) { + this.lastMessageAt = lastMessageAt; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public int getMessageIncrement() { + return messageIncrement; + } + + public void setMessageIncrement(int messageIncrement) { + this.messageIncrement = Math.max(messageIncrement, 0); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionUpsertCommand.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionUpsertCommand.java new file mode 100644 index 0000000..dfb0c96 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/command/ChatSessionUpsertCommand.java @@ -0,0 +1,108 @@ +package tech.easyflow.chatlog.domain.command; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatSessionUpsertCommand implements Serializable { + + private BigInteger sessionId; + private BigInteger tenantId; + private BigInteger deptId; + private BigInteger userId; + private String userAccount; + private BigInteger assistantId; + private String assistantCode; + private String assistantName; + private String title; + private BigInteger operatorId; + private Date operateAt = new Date(); + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getTenantId() { + return tenantId; + } + + public void setTenantId(BigInteger tenantId) { + this.tenantId = tenantId; + } + + public BigInteger getDeptId() { + return deptId; + } + + public void setDeptId(BigInteger deptId) { + this.deptId = deptId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public String getUserAccount() { + return userAccount; + } + + public void setUserAccount(String userAccount) { + this.userAccount = userAccount; + } + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public String getAssistantCode() { + return assistantCode; + } + + public void setAssistantCode(String assistantCode) { + this.assistantCode = assistantCode; + } + + public String getAssistantName() { + return assistantName; + } + + public void setAssistantName(String assistantName) { + this.assistantName = assistantName; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public Date getOperateAt() { + return operateAt; + } + + public void setOperateAt(Date operateAt) { + this.operateAt = operateAt; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatHistoryPage.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatHistoryPage.java new file mode 100644 index 0000000..73e2c46 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatHistoryPage.java @@ -0,0 +1,45 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ChatHistoryPage implements Serializable { + + private long total; + private long pageNumber; + private long pageSize; + private List records = new ArrayList<>(); + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public long getPageNumber() { + return pageNumber; + } + + public void setPageNumber(long pageNumber) { + this.pageNumber = pageNumber; + } + + public long getPageSize() { + return pageSize; + } + + public void setPageSize(long pageSize) { + this.pageSize = pageSize; + } + + public List getRecords() { + return records; + } + + public void setRecords(List records) { + this.records = records; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatMessageRecord.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatMessageRecord.java new file mode 100644 index 0000000..7fc95d7 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatMessageRecord.java @@ -0,0 +1,127 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; +import java.util.Map; + +public class ChatMessageRecord implements Serializable { + + private BigInteger id; + private BigInteger sessionId; + private BigInteger userId; + private BigInteger assistantId; + private BigInteger senderId; + private String senderName; + private String senderRole; + private String contentType; + private String contentText; + private Map contentPayload; + private Date created; + private BigInteger createdBy; + private Long syncVersion; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public BigInteger getSenderId() { + return senderId; + } + + public void setSenderId(BigInteger senderId) { + this.senderId = senderId; + } + + public String getSenderName() { + return senderName; + } + + public void setSenderName(String senderName) { + this.senderName = senderName; + } + + public String getSenderRole() { + return senderRole; + } + + public void setSenderRole(String senderRole) { + this.senderRole = senderRole; + } + + public String getContentType() { + return contentType; + } + + public void setContentType(String contentType) { + this.contentType = contentType; + } + + public String getContentText() { + return contentText; + } + + public void setContentText(String contentText) { + this.contentText = contentText; + } + + public Map getContentPayload() { + return contentPayload; + } + + public void setContentPayload(Map contentPayload) { + this.contentPayload = contentPayload; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Long getSyncVersion() { + return syncVersion; + } + + public void setSyncVersion(Long syncVersion) { + this.syncVersion = syncVersion; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionPage.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionPage.java new file mode 100644 index 0000000..4316edf --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionPage.java @@ -0,0 +1,45 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ChatSessionPage implements Serializable { + + private long total; + private long pageNumber; + private long pageSize; + private List records = new ArrayList<>(); + + public long getTotal() { + return total; + } + + public void setTotal(long total) { + this.total = total; + } + + public long getPageNumber() { + return pageNumber; + } + + public void setPageNumber(long pageNumber) { + this.pageNumber = pageNumber; + } + + public long getPageSize() { + return pageSize; + } + + public void setPageSize(long pageSize) { + this.pageSize = pageSize; + } + + public List getRecords() { + return records; + } + + public void setRecords(List records) { + this.records = records; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionSummary.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionSummary.java new file mode 100644 index 0000000..5eba1f1 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSessionSummary.java @@ -0,0 +1,189 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatSessionSummary implements Serializable { + + private BigInteger id; + private BigInteger tenantId; + private BigInteger deptId; + private BigInteger userId; + private String userAccount; + private BigInteger assistantId; + private String assistantCode; + private String assistantName; + private String title; + private String lastMessagePreview; + private BigInteger lastSenderId; + private String lastSenderName; + private Integer messageCount; + private Date accessAt; + private Date lastMessageAt; + private Date created; + private BigInteger createdBy; + private Date modified; + private BigInteger modifiedBy; + private Integer isDeleted; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public BigInteger getTenantId() { + return tenantId; + } + + public void setTenantId(BigInteger tenantId) { + this.tenantId = tenantId; + } + + public BigInteger getDeptId() { + return deptId; + } + + public void setDeptId(BigInteger deptId) { + this.deptId = deptId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public String getUserAccount() { + return userAccount; + } + + public void setUserAccount(String userAccount) { + this.userAccount = userAccount; + } + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public String getAssistantCode() { + return assistantCode; + } + + public void setAssistantCode(String assistantCode) { + this.assistantCode = assistantCode; + } + + public String getAssistantName() { + return assistantName; + } + + public void setAssistantName(String assistantName) { + this.assistantName = assistantName; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getLastMessagePreview() { + return lastMessagePreview; + } + + public void setLastMessagePreview(String lastMessagePreview) { + this.lastMessagePreview = lastMessagePreview; + } + + public BigInteger getLastSenderId() { + return lastSenderId; + } + + public void setLastSenderId(BigInteger lastSenderId) { + this.lastSenderId = lastSenderId; + } + + public String getLastSenderName() { + return lastSenderName; + } + + public void setLastSenderName(String lastSenderName) { + this.lastSenderName = lastSenderName; + } + + public Integer getMessageCount() { + return messageCount; + } + + public void setMessageCount(Integer messageCount) { + this.messageCount = messageCount; + } + + public Date getAccessAt() { + return accessAt; + } + + public void setAccessAt(Date accessAt) { + this.accessAt = accessAt; + } + + public Date getLastMessageAt() { + return lastMessageAt; + } + + public void setLastMessageAt(Date lastMessageAt) { + this.lastMessageAt = lastMessageAt; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public BigInteger getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(BigInteger createdBy) { + this.createdBy = createdBy; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public BigInteger getModifiedBy() { + return modifiedBy; + } + + public void setModifiedBy(BigInteger modifiedBy) { + this.modifiedBy = modifiedBy; + } + + public Integer getIsDeleted() { + return isDeleted; + } + + public void setIsDeleted(Integer isDeleted) { + this.isDeleted = isDeleted; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSyncResult.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSyncResult.java new file mode 100644 index 0000000..b54a7f9 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/ChatSyncResult.java @@ -0,0 +1,36 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class ChatSyncResult implements Serializable { + + private String syncCode; + private int syncedRows; + private List touchedDates = new ArrayList<>(); + + public String getSyncCode() { + return syncCode; + } + + public void setSyncCode(String syncCode) { + this.syncCode = syncCode; + } + + public int getSyncedRows() { + return syncedRows; + } + + public void setSyncedRows(int syncedRows) { + this.syncedRows = syncedRows; + } + + public List getTouchedDates() { + return touchedDates; + } + + public void setTouchedDates(List touchedDates) { + this.touchedDates = touchedDates; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/PublicChatSessionRestoreResult.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/PublicChatSessionRestoreResult.java new file mode 100644 index 0000000..da39463 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/dto/PublicChatSessionRestoreResult.java @@ -0,0 +1,45 @@ +package tech.easyflow.chatlog.domain.dto; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +public class PublicChatSessionRestoreResult implements Serializable { + + private boolean sessionExists; + private String conversationId; + private ChatSessionSummary session; + private List messages = new ArrayList<>(); + + public boolean isSessionExists() { + return sessionExists; + } + + public void setSessionExists(boolean sessionExists) { + this.sessionExists = sessionExists; + } + + public String getConversationId() { + return conversationId; + } + + public void setConversationId(String conversationId) { + this.conversationId = conversationId; + } + + public ChatSessionSummary getSession() { + return session; + } + + public void setSession(ChatSessionSummary session) { + this.session = session; + } + + public List getMessages() { + return messages; + } + + public void setMessages(List messages) { + this.messages = messages == null ? new ArrayList<>() : messages; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatPersistDeadLetter.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatPersistDeadLetter.java new file mode 100644 index 0000000..12e8b1e --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatPersistDeadLetter.java @@ -0,0 +1,8 @@ +package tech.easyflow.chatlog.domain.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.chatlog.domain.entity.base.ChatPersistDeadLetterBase; + +@Table(value = "chat_persist_dead_letter", comment = "聊天持久化死信") +public class ChatPersistDeadLetter extends ChatPersistDeadLetterBase { +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatSyncCheckpoint.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatSyncCheckpoint.java new file mode 100644 index 0000000..71dd35c --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/ChatSyncCheckpoint.java @@ -0,0 +1,8 @@ +package tech.easyflow.chatlog.domain.entity; + +import com.mybatisflex.annotation.Table; +import tech.easyflow.chatlog.domain.entity.base.ChatSyncCheckpointBase; + +@Table(value = "chat_sync_checkpoint", comment = "聊天同步检查点") +public class ChatSyncCheckpoint extends ChatSyncCheckpointBase { +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatPersistDeadLetterBase.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatPersistDeadLetterBase.java new file mode 100644 index 0000000..ba42207 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatPersistDeadLetterBase.java @@ -0,0 +1,166 @@ +package tech.easyflow.chatlog.domain.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatPersistDeadLetterBase implements Serializable { + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "topic") + private String topic; + + @Column(comment = "stream key") + private String streamKey; + + @Column(comment = "stream message id") + private String streamMessageId; + + @Column(comment = "event id") + private String eventId; + + @Column(comment = "session id") + private BigInteger sessionId; + + @Column(comment = "payload") + private String payload; + + @Column(comment = "retry count") + private Integer retryCount; + + @Column(comment = "error message") + private String errorMessage; + + @Column(comment = "first failed at") + private Date firstFailedAt; + + @Column(comment = "last failed at") + private Date lastFailedAt; + + @Column(comment = "status") + private String status; + + @Column(comment = "created") + private Date created; + + @Column(comment = "modified") + private Date modified; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getTopic() { + return topic; + } + + public void setTopic(String topic) { + this.topic = topic; + } + + public String getStreamKey() { + return streamKey; + } + + public void setStreamKey(String streamKey) { + this.streamKey = streamKey; + } + + public String getStreamMessageId() { + return streamMessageId; + } + + public void setStreamMessageId(String streamMessageId) { + this.streamMessageId = streamMessageId; + } + + public String getEventId() { + return eventId; + } + + public void setEventId(String eventId) { + this.eventId = eventId; + } + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } + + public Integer getRetryCount() { + return retryCount; + } + + public void setRetryCount(Integer retryCount) { + this.retryCount = retryCount; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Date getFirstFailedAt() { + return firstFailedAt; + } + + public void setFirstFailedAt(Date firstFailedAt) { + this.firstFailedAt = firstFailedAt; + } + + public Date getLastFailedAt() { + return lastFailedAt; + } + + public void setLastFailedAt(Date lastFailedAt) { + this.lastFailedAt = lastFailedAt; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatSyncCheckpointBase.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatSyncCheckpointBase.java new file mode 100644 index 0000000..d302e6c --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/entity/base/ChatSyncCheckpointBase.java @@ -0,0 +1,139 @@ +package tech.easyflow.chatlog.domain.entity.base; + +import com.mybatisflex.annotation.Column; +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatSyncCheckpointBase implements Serializable { + + @Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键") + private BigInteger id; + + @Column(comment = "同步编码") + private String syncCode; + + @Column(comment = "分片键") + private String shardKey; + + @Column(comment = "游标表名") + private String cursorTable; + + @Column(comment = "游标时间") + private Date cursorTime; + + @Column(comment = "游标ID") + private BigInteger cursorId; + + @Column(comment = "最后成功时间") + private Date lastSuccessTime; + + @Column(comment = "最后批次大小") + private Integer lastBatchSize; + + @Column(comment = "状态") + private String status; + + @Column(comment = "备注") + private String remark; + + @Column(comment = "修改时间") + private Date modified; + + public BigInteger getId() { + return id; + } + + public void setId(BigInteger id) { + this.id = id; + } + + public String getSyncCode() { + return syncCode; + } + + public void setSyncCode(String syncCode) { + this.syncCode = syncCode; + } + + public String getShardKey() { + return shardKey; + } + + public void setShardKey(String shardKey) { + this.shardKey = shardKey; + } + + public String getCursorTable() { + return cursorTable; + } + + public void setCursorTable(String cursorTable) { + this.cursorTable = cursorTable; + } + + public Date getCursorTime() { + return cursorTime; + } + + public void setCursorTime(Date cursorTime) { + this.cursorTime = cursorTime; + } + + public BigInteger getCursorId() { + return cursorId; + } + + public void setCursorId(BigInteger cursorId) { + this.cursorId = cursorId; + } + + public Date getLastSuccessTime() { + return lastSuccessTime; + } + + public void setLastSuccessTime(Date lastSuccessTime) { + this.lastSuccessTime = lastSuccessTime; + } + + public Integer getLastBatchSize() { + return lastBatchSize; + } + + public void setLastBatchSize(Integer lastBatchSize) { + this.lastBatchSize = lastBatchSize; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getRemark() { + return remark; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + public Date getCreated() { + return null; + } + + public void setCreated(Date created) { + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEvent.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEvent.java new file mode 100644 index 0000000..aa972a8 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEvent.java @@ -0,0 +1,81 @@ +package tech.easyflow.chatlog.domain.event; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatPersistEvent implements Serializable { + + private String eventId; + private ChatPersistEventType eventType; + private BigInteger sessionId; + private BigInteger userId; + private BigInteger assistantId; + private Date occurredAt; + private String traceId; + private String payload; + + public String getEventId() { + return eventId; + } + + public void setEventId(String eventId) { + this.eventId = eventId; + } + + public ChatPersistEventType getEventType() { + return eventType; + } + + public void setEventType(ChatPersistEventType eventType) { + this.eventType = eventType; + } + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public Date getOccurredAt() { + return occurredAt; + } + + public void setOccurredAt(Date occurredAt) { + this.occurredAt = occurredAt; + } + + public String getTraceId() { + return traceId; + } + + public void setTraceId(String traceId) { + this.traceId = traceId; + } + + public String getPayload() { + return payload; + } + + public void setPayload(String payload) { + this.payload = payload; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEventType.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEventType.java new file mode 100644 index 0000000..812089b --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/ChatPersistEventType.java @@ -0,0 +1,10 @@ +package tech.easyflow.chatlog.domain.event; + +public enum ChatPersistEventType { + + SESSION_PREPARED, + USER_MESSAGE_APPENDED, + ASSISTANT_MESSAGE_APPENDED, + SESSION_RENAMED, + SESSION_DELETED +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionDeletePayload.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionDeletePayload.java new file mode 100644 index 0000000..a92d2d0 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionDeletePayload.java @@ -0,0 +1,45 @@ +package tech.easyflow.chatlog.domain.event.payload; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatSessionDeletePayload implements Serializable { + + private BigInteger sessionId; + private BigInteger userId; + private BigInteger operatorId; + private Date operateAt; + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public Date getOperateAt() { + return operateAt; + } + + public void setOperateAt(Date operateAt) { + this.operateAt = operateAt; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionRenamePayload.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionRenamePayload.java new file mode 100644 index 0000000..aa5b7fc --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/event/payload/ChatSessionRenamePayload.java @@ -0,0 +1,54 @@ +package tech.easyflow.chatlog.domain.event.payload; + +import java.io.Serializable; +import java.math.BigInteger; +import java.util.Date; + +public class ChatSessionRenamePayload implements Serializable { + + private BigInteger sessionId; + private BigInteger userId; + private String title; + private BigInteger operatorId; + private Date operateAt; + + public BigInteger getSessionId() { + return sessionId; + } + + public void setSessionId(BigInteger sessionId) { + this.sessionId = sessionId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public BigInteger getOperatorId() { + return operatorId; + } + + public void setOperatorId(BigInteger operatorId) { + this.operatorId = operatorId; + } + + public Date getOperateAt() { + return operateAt; + } + + public void setOperateAt(Date operateAt) { + this.operateAt = operateAt; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatPageQuery.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatPageQuery.java new file mode 100644 index 0000000..c9ff2f6 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatPageQuery.java @@ -0,0 +1,29 @@ +package tech.easyflow.chatlog.domain.query; + +import java.io.Serializable; + +public class ChatPageQuery implements Serializable { + + private long pageNumber = 1; + private long pageSize = 20; + + public long getPageNumber() { + return pageNumber; + } + + public void setPageNumber(long pageNumber) { + this.pageNumber = Math.max(pageNumber, 1); + } + + public long getPageSize() { + return pageSize; + } + + public void setPageSize(long pageSize) { + this.pageSize = pageSize <= 0 ? 20 : Math.min(pageSize, 200); + } + + public long getOffset() { + return (pageNumber - 1) * pageSize; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatSessionFilterQuery.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatSessionFilterQuery.java new file mode 100644 index 0000000..26eca10 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/domain/query/ChatSessionFilterQuery.java @@ -0,0 +1,57 @@ +package tech.easyflow.chatlog.domain.query; + +import org.springframework.format.annotation.DateTimeFormat; + +import java.math.BigInteger; +import java.util.Date; + +public class ChatSessionFilterQuery extends ChatPageQuery { + + private BigInteger assistantId; + private BigInteger userId; + private String userAccount; + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date startTime; + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date endTime; + + public BigInteger getAssistantId() { + return assistantId; + } + + public void setAssistantId(BigInteger assistantId) { + this.assistantId = assistantId; + } + + public BigInteger getUserId() { + return userId; + } + + public void setUserId(BigInteger userId) { + this.userId = userId; + } + + public String getUserAccount() { + return userAccount; + } + + public void setUserAccount(String userAccount) { + this.userAccount = userAccount; + } + + public Date getStartTime() { + return startTime; + } + + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + + public Date getEndTime() { + return endTime; + } + + public void setEndTime(Date endTime) { + this.endTime = endTime; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatPersistDeadLetterMapper.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatPersistDeadLetterMapper.java new file mode 100644 index 0000000..b5e40f9 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatPersistDeadLetterMapper.java @@ -0,0 +1,7 @@ +package tech.easyflow.chatlog.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.chatlog.domain.entity.ChatPersistDeadLetter; + +public interface ChatPersistDeadLetterMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatSyncCheckpointMapper.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatSyncCheckpointMapper.java new file mode 100644 index 0000000..a40beb0 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/mapper/ChatSyncCheckpointMapper.java @@ -0,0 +1,7 @@ +package tech.easyflow.chatlog.mapper; + +import com.mybatisflex.core.BaseMapper; +import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint; + +public interface ChatSyncCheckpointMapper extends BaseMapper { +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/analyticaldb/ChatAnalyticalDBRepository.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/analyticaldb/ChatAnalyticalDBRepository.java new file mode 100644 index 0000000..ae750e3 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/analyticaldb/ChatAnalyticalDBRepository.java @@ -0,0 +1,352 @@ +package tech.easyflow.chatlog.repository.analyticaldb; + +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter; +import org.springframework.stereotype.Repository; +import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionPage; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; +import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery; +import tech.easyflow.chatlog.support.ChatJsonSupport; +import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations; +import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest; +import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult; +import tech.easyflow.common.analyticaldb.support.AnalyticalDBHealthSupport; + +import java.math.BigInteger; +import java.sql.Timestamp; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; + +@Repository +public class ChatAnalyticalDBRepository { + + private static final DateTimeFormatter CH_DATE_TIME_FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final AnalyticalDBOperations analyticalDBOperations; + private final AnalyticalDBHealthSupport analyticalDBHealthSupport; + private final ChatJsonSupport jsonSupport; + + public ChatAnalyticalDBRepository(ObjectProvider analyticalDBOperationsProvider, + AnalyticalDBHealthSupport analyticalDBHealthSupport, + ChatJsonSupport jsonSupport) { + this.analyticalDBOperations = analyticalDBOperationsProvider.getIfAvailable(); + this.analyticalDBHealthSupport = analyticalDBHealthSupport; + this.jsonSupport = jsonSupport; + } + + public boolean enabled() { + return analyticalDBOperations != null && analyticalDBOperations.available(); + } + + public void assertAvailable() { + if (!enabled()) { + throw new IllegalStateException("AnalyticalDB 数据源未启用"); + } + } + + public void selfCheck() { + analyticalDBHealthSupport.selfCheck(); + } + + public void upsertSessions(List sessions) { + if (!enabled() || sessions.isEmpty()) { + return; + } + analyticalDBOperations.batchUpdate("INSERT INTO ods_chat_session (" + + "id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, " + + "last_message_preview, last_sender_id, last_sender_name, message_count, access_at, last_message_at, " + + "created, created_by, modified, modified_by, is_deleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + sessions, + sessions.size(), + sessionStatementSetter()); + } + + public void appendLogs(List records) { + if (!enabled() || records.isEmpty()) { + return; + } + analyticalDBOperations.batchUpdate("INSERT INTO ods_chat_log (" + + "id, session_id, user_id, assistant_id, sender_id, sender_name, sender_role, content_type, content_text, " + + "content_payload, created, created_by, sync_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + records, + records.size(), + logStatementSetter()); + } + + public ChatHistoryPage queryHistory(BigInteger sessionId, ChatPageQuery query) { + assertAvailable(); + AnalyticalDBPageResult pageResult = analyticalDBOperations.page( + "SELECT COUNT(1) FROM ods_chat_log FINAL WHERE session_id=?", + new Object[]{sessionId}, + "SELECT id, session_id, user_id, assistant_id, sender_id, sender_name, sender_role, content_type, content_text, " + + "content_payload, created, created_by, sync_version " + + "FROM ods_chat_log FINAL WHERE session_id=? ORDER BY created DESC, id DESC", + new Object[]{sessionId}, + new AnalyticalDBPageRequest(Math.toIntExact(query.getPageNumber()), Math.toIntExact(query.getPageSize())), + (rs, rowNum) -> { + ChatMessageRecord record = new ChatMessageRecord(); + record.setId(bigInteger(rs.getObject("id"))); + record.setSessionId(bigInteger(rs.getObject("session_id"))); + record.setUserId(bigInteger(rs.getObject("user_id"))); + record.setAssistantId(bigInteger(rs.getObject("assistant_id"))); + record.setSenderId(bigInteger(rs.getObject("sender_id"))); + record.setSenderName(rs.getString("sender_name")); + record.setSenderRole(rs.getString("sender_role")); + record.setContentType(rs.getString("content_type")); + record.setContentText(rs.getString("content_text")); + record.setContentPayload(jsonSupport.toMap(rs.getString("content_payload"))); + record.setCreated(rs.getTimestamp("created")); + record.setCreatedBy(bigInteger(rs.getObject("created_by"))); + record.setSyncVersion(rs.getLong("sync_version")); + return record; + } + ); + ChatHistoryPage page = new ChatHistoryPage(); + page.setTotal(pageResult.getTotal()); + page.setPageNumber(pageResult.getPageNumber()); + page.setPageSize(pageResult.getPageSize()); + page.setRecords(pageResult.getRecords()); + return page; + } + + public ChatSessionPage pageSessions(ChatSessionFilterQuery query) { + assertAvailable(); + List countArgs = new java.util.ArrayList<>(); + StringBuilder countSql = new StringBuilder("SELECT COUNT(1) FROM ods_chat_session FINAL WHERE is_deleted=0"); + List dataArgs = new java.util.ArrayList<>(); + StringBuilder dataSql = new StringBuilder("SELECT id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, " + + "last_message_preview, last_sender_id, last_sender_name, message_count, access_at, last_message_at, created, created_by, modified, modified_by, is_deleted " + + "FROM ods_chat_session FINAL WHERE is_deleted=0"); + appendSessionFilters(query, countSql, countArgs); + appendSessionFilters(query, dataSql, dataArgs); + dataSql.append(" ORDER BY access_at DESC, id DESC"); + + AnalyticalDBPageResult pageResult = analyticalDBOperations.page( + countSql.toString(), + countArgs.toArray(), + dataSql.toString(), + dataArgs.toArray(), + new AnalyticalDBPageRequest(Math.toIntExact(query.getPageNumber()), Math.toIntExact(query.getPageSize())), + (rs, rowNum) -> { + ChatSessionSummary summary = new ChatSessionSummary(); + summary.setId(bigInteger(rs.getObject("id"))); + summary.setTenantId(bigInteger(rs.getObject("tenant_id"))); + summary.setDeptId(bigInteger(rs.getObject("dept_id"))); + summary.setUserId(bigInteger(rs.getObject("user_id"))); + summary.setUserAccount(rs.getString("user_account")); + summary.setAssistantId(bigInteger(rs.getObject("assistant_id"))); + summary.setAssistantCode(rs.getString("assistant_code")); + summary.setAssistantName(rs.getString("assistant_name")); + summary.setTitle(rs.getString("title")); + summary.setLastMessagePreview(rs.getString("last_message_preview")); + summary.setLastSenderId(bigInteger(rs.getObject("last_sender_id"))); + summary.setLastSenderName(rs.getString("last_sender_name")); + summary.setMessageCount(rs.getInt("message_count")); + summary.setAccessAt(rs.getTimestamp("access_at")); + summary.setLastMessageAt(rs.getTimestamp("last_message_at")); + summary.setCreated(rs.getTimestamp("created")); + summary.setCreatedBy(bigInteger(rs.getObject("created_by"))); + summary.setModified(rs.getTimestamp("modified")); + summary.setModifiedBy(bigInteger(rs.getObject("modified_by"))); + summary.setIsDeleted(rs.getInt("is_deleted")); + return summary; + } + ); + ChatSessionPage page = new ChatSessionPage(); + page.setTotal(pageResult.getTotal()); + page.setPageNumber(pageResult.getPageNumber()); + page.setPageSize(pageResult.getPageSize()); + page.setRecords(pageResult.getRecords()); + return page; + } + + public ChatSessionSummary getSession(BigInteger sessionId) { + assertAvailable(); + return analyticalDBOperations.queryOne( + "SELECT id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, " + + "last_message_preview, last_sender_id, last_sender_name, message_count, access_at, last_message_at, created, created_by, modified, modified_by, is_deleted " + + "FROM ods_chat_session FINAL WHERE id=? LIMIT 1", + (rs, rowNum) -> { + ChatSessionSummary summary = new ChatSessionSummary(); + summary.setId(bigInteger(rs.getObject("id"))); + summary.setTenantId(bigInteger(rs.getObject("tenant_id"))); + summary.setDeptId(bigInteger(rs.getObject("dept_id"))); + summary.setUserId(bigInteger(rs.getObject("user_id"))); + summary.setUserAccount(rs.getString("user_account")); + summary.setAssistantId(bigInteger(rs.getObject("assistant_id"))); + summary.setAssistantCode(rs.getString("assistant_code")); + summary.setAssistantName(rs.getString("assistant_name")); + summary.setTitle(rs.getString("title")); + summary.setLastMessagePreview(rs.getString("last_message_preview")); + summary.setLastSenderId(bigInteger(rs.getObject("last_sender_id"))); + summary.setLastSenderName(rs.getString("last_sender_name")); + summary.setMessageCount(rs.getInt("message_count")); + summary.setAccessAt(rs.getTimestamp("access_at")); + summary.setLastMessageAt(rs.getTimestamp("last_message_at")); + summary.setCreated(rs.getTimestamp("created")); + summary.setCreatedBy(bigInteger(rs.getObject("created_by"))); + summary.setModified(rs.getTimestamp("modified")); + summary.setModifiedBy(bigInteger(rs.getObject("modified_by"))); + summary.setIsDeleted(rs.getInt("is_deleted")); + return summary; + }, + sessionId + ); + } + + public void refreshDws(Set dates) { + if (!enabled() || dates.isEmpty()) { + return; + } + for (LocalDate date : dates) { + String dateLiteral = date.toString(); + analyticalDBOperations.update("ALTER TABLE dws_chat_assistant_day DELETE WHERE stat_date = toDate(?)", dateLiteral); + analyticalDBOperations.update("INSERT INTO dws_chat_assistant_day " + + "SELECT toDate(created) AS stat_date, assistant_id AS dimension_id, uniqExact(session_id) AS session_count, count() AS message_count " + + "FROM ods_chat_log WHERE toDate(created) = toDate(?) GROUP BY stat_date, dimension_id", dateLiteral); + + analyticalDBOperations.update("ALTER TABLE dws_chat_user_day DELETE WHERE stat_date = toDate(?)", dateLiteral); + analyticalDBOperations.update("INSERT INTO dws_chat_user_day " + + "SELECT toDate(created) AS stat_date, user_id AS dimension_id, uniqExact(session_id) AS session_count, count() AS message_count " + + "FROM ods_chat_log WHERE toDate(created) = toDate(?) GROUP BY stat_date, dimension_id", dateLiteral); + + analyticalDBOperations.update("ALTER TABLE dws_chat_session_day DELETE WHERE stat_date = toDate(?)", dateLiteral); + analyticalDBOperations.update("INSERT INTO dws_chat_session_day " + + "SELECT toDate(created) AS stat_date, session_id AS dimension_id, any(assistant_id) AS assistant_id, any(user_id) AS user_id, count() AS message_count " + + "FROM ods_chat_log WHERE toDate(created) = toDate(?) GROUP BY stat_date, dimension_id", dateLiteral); + } + } + + public List findHistoryTables() { + if (!enabled()) { + return Collections.emptyList(); + } + return analyticalDBOperations.query("SELECT name FROM system.tables WHERE database = currentDatabase()", + (rs, rowNum) -> rs.getString(1)); + } + + private ParameterizedPreparedStatementSetter sessionStatementSetter() { + return (ps, summary) -> { + ps.setObject(1, requiredUInt64(summary.getId())); + ps.setObject(2, requiredUInt64(summary.getTenantId())); + ps.setObject(3, requiredUInt64(summary.getDeptId())); + ps.setObject(4, requiredUInt64(summary.getUserId())); + ps.setString(5, requiredString(summary.getUserAccount())); + ps.setObject(6, requiredUInt64(summary.getAssistantId())); + ps.setString(7, requiredString(summary.getAssistantCode())); + ps.setString(8, requiredString(summary.getAssistantName())); + ps.setString(9, requiredString(summary.getTitle())); + ps.setString(10, requiredString(summary.getLastMessagePreview())); + ps.setObject(11, requiredUInt64(summary.getLastSenderId())); + ps.setString(12, requiredString(summary.getLastSenderName())); + ps.setInt(13, summary.getMessageCount() == null ? 0 : summary.getMessageCount()); + ps.setTimestamp(14, requiredTimestamp(summary.getAccessAt(), summary.getModified(), summary.getCreated())); + ps.setTimestamp(15, timestamp(summary.getLastMessageAt())); + ps.setTimestamp(16, requiredTimestamp(summary.getCreated(), summary.getAccessAt(), summary.getModified())); + ps.setObject(17, requiredUInt64(summary.getCreatedBy())); + ps.setTimestamp(18, requiredTimestamp(summary.getModified(), summary.getAccessAt(), summary.getCreated())); + ps.setObject(19, requiredUInt64(summary.getModifiedBy())); + ps.setInt(20, summary.getIsDeleted() == null ? 0 : summary.getIsDeleted()); + }; + } + + private ParameterizedPreparedStatementSetter logStatementSetter() { + return (ps, record) -> { + ps.setObject(1, requiredUInt64(record.getId())); + ps.setObject(2, requiredUInt64(record.getSessionId())); + ps.setObject(3, requiredUInt64(record.getUserId())); + ps.setObject(4, requiredUInt64(record.getAssistantId())); + ps.setObject(5, requiredUInt64(record.getSenderId())); + ps.setString(6, requiredString(record.getSenderName())); + ps.setString(7, requiredString(record.getSenderRole())); + ps.setString(8, requiredString(record.getContentType())); + ps.setString(9, requiredString(record.getContentText())); + ps.setString(10, requiredString(jsonSupport.toJson(record.getContentPayload()))); + ps.setTimestamp(11, requiredTimestamp(record.getCreated())); + ps.setObject(12, requiredUInt64(record.getCreatedBy())); + ps.setLong(13, record.getSyncVersion() == null ? System.currentTimeMillis() : record.getSyncVersion()); + }; + } + + private Timestamp timestamp(Date date) { + return date == null ? null : new Timestamp(date.getTime()); + } + + private Timestamp requiredTimestamp(Date primary, Date... fallbacks) { + Timestamp timestamp = timestamp(primary); + if (timestamp != null) { + return timestamp; + } + if (fallbacks != null) { + for (Date fallback : fallbacks) { + timestamp = timestamp(fallback); + if (timestamp != null) { + return timestamp; + } + } + } + return new Timestamp(System.currentTimeMillis()); + } + + private BigInteger requiredUInt64(BigInteger value) { + return value == null ? BigInteger.ZERO : value; + } + + private String requiredString(String value) { + return value == null ? "" : value; + } + + private BigInteger bigInteger(Object value) { + if (value == null) { + return null; + } + return new BigInteger(String.valueOf(value)); + } + + private void appendSessionFilters(ChatSessionFilterQuery query, StringBuilder sql, List args) { + if (query == null) { + return; + } + if (query.getAssistantId() != null) { + sql.append(" AND assistant_id=?"); + args.add(query.getAssistantId()); + } + if (query.getUserId() != null) { + sql.append(" AND user_id=?"); + args.add(query.getUserId()); + } + if (query.getUserAccount() != null && !query.getUserAccount().isBlank()) { + sql.append(" AND user_account LIKE ?"); + args.add("%" + query.getUserAccount().trim() + "%"); + } + if (query.getStartTime() != null) { + String startTime = formatDateTime(query.getStartTime()); + sql.append(" AND ((last_message_at IS NOT NULL AND last_message_at >= toDateTime(?)) OR (last_message_at IS NULL AND access_at >= toDateTime(?)))"); + args.add(startTime); + args.add(startTime); + } + if (query.getEndTime() != null) { + String endTime = formatDateTime(query.getEndTime()); + sql.append(" AND ((last_message_at IS NOT NULL AND last_message_at <= toDateTime(?)) OR (last_message_at IS NULL AND access_at <= toDateTime(?)))"); + args.add(endTime); + args.add(endTime); + } + } + + private String formatDateTime(Date date) { + if (date == null) { + return null; + } + return CH_DATE_TIME_FORMATTER.format(Instant.ofEpochMilli(date.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime()); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogRepository.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogRepository.java new file mode 100644 index 0000000..b925cd1 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogRepository.java @@ -0,0 +1,171 @@ +package tech.easyflow.chatlog.repository.mysql; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.support.ChatJsonSupport; +import tech.easyflow.chatlog.support.ChatTableRouter; + +import java.math.BigInteger; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Repository +public class MySqlChatLogRepository { + + private final JdbcTemplate jdbcTemplate; + private final ChatTableRouter tableRouter; + private final ChatJsonSupport jsonSupport; + + public MySqlChatLogRepository(JdbcTemplate jdbcTemplate, + ChatTableRouter tableRouter, + ChatJsonSupport jsonSupport) { + this.jdbcTemplate = jdbcTemplate; + this.tableRouter = tableRouter; + this.jsonSupport = jsonSupport; + } + + public List appendMessages(List commands) { + if (commands == null || commands.isEmpty()) { + return Collections.emptyList(); + } + List inserted = new ArrayList<>(); + Map> grouped = new LinkedHashMap<>(); + for (ChatAppendMessageCommand command : commands) { + YearMonth month = YearMonth.from(command.getCreated().toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate()); + grouped.computeIfAbsent(month, key -> new ArrayList<>()).add(command); + } + for (Map.Entry> entry : grouped.entrySet()) { + String table = tableRouter.resolveLogTable(entry.getKey()); + String sql = "INSERT IGNORE INTO `" + table + "` " + + "(id, tenant_id, dept_id, session_id, user_id, assistant_id, sender_id, sender_name, sender_role, content_type, content_text, content_payload, created, created_by, sync_version) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; + int[] results = jdbcTemplate.batchUpdate(sql, new org.springframework.jdbc.core.BatchPreparedStatementSetter() { + @Override + public void setValues(java.sql.PreparedStatement ps, int i) throws java.sql.SQLException { + ChatAppendMessageCommand command = entry.getValue().get(i); + Timestamp created = new Timestamp(command.getCreated().getTime()); + ps.setObject(1, command.getMessageId()); + ps.setObject(2, command.getTenantId()); + ps.setObject(3, command.getDeptId()); + ps.setObject(4, command.getSessionId()); + ps.setObject(5, command.getUserId()); + ps.setObject(6, command.getAssistantId()); + ps.setObject(7, command.getSenderId()); + ps.setString(8, command.getSenderName()); + ps.setString(9, command.getSenderRole()); + ps.setString(10, command.getContentType()); + ps.setString(11, command.getContentText()); + ps.setString(12, jsonSupport.toJson(command.getContentPayload())); + ps.setTimestamp(13, created); + ps.setObject(14, command.getCreatedBy()); + ps.setLong(15, command.getCreated().getTime()); + } + + @Override + public int getBatchSize() { + return entry.getValue().size(); + } + }); + for (int i = 0; i < results.length; i++) { + if (results[i] != 0 && results[i] != Statement.EXECUTE_FAILED) { + inserted.add(entry.getValue().get(i)); + } + } + } + return inserted; + } + + public List listRecentTail(BigInteger sessionId, List months, int limit) { + List records = new ArrayList<>(); + for (YearMonth month : months) { + String table = tableRouter.resolveLogTable(month); + List current = jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE session_id=? ORDER BY created DESC, id DESC LIMIT ?", + (rs, rowNum) -> mapRow(rs), + sessionId, + limit + ); + records.addAll(current); + if (records.size() >= limit) { + break; + } + } + return records.stream() + .sorted((a, b) -> { + int compare = b.getCreated().compareTo(a.getCreated()); + if (compare != 0) { + return compare; + } + return b.getId().compareTo(a.getId()); + }) + .limit(limit) + .collect(Collectors.toList()); + } + + public List loadIncremental(String table, Date cursorTime, BigInteger cursorId, int limit) { + Timestamp timestamp = cursorTime == null ? new Timestamp(0L) : new Timestamp(cursorTime.getTime()); + return jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE (created > ?) OR (created = ? AND id > ?) " + + "ORDER BY created ASC, id ASC LIMIT ?", + (rs, rowNum) -> mapRow(rs), + timestamp, + timestamp, + cursorId == null ? BigInteger.ZERO : cursorId, + limit + ); + } + + public List loadRepairRows(List months, Date startTime) { + if (months.isEmpty()) { + return Collections.emptyList(); + } + List records = new ArrayList<>(); + for (YearMonth month : months) { + String table = tableRouter.resolveLogTable(month); + records.addAll(jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE created >= ? ORDER BY created ASC, id ASC", + (rs, rowNum) -> mapRow(rs), + new Timestamp(startTime.getTime()) + )); + } + return records; + } + + private ChatMessageRecord mapRow(ResultSet rs) throws SQLException { + ChatMessageRecord record = new ChatMessageRecord(); + record.setId(bigInteger(rs, "id")); + record.setSessionId(bigInteger(rs, "session_id")); + record.setUserId(bigInteger(rs, "user_id")); + record.setAssistantId(bigInteger(rs, "assistant_id")); + record.setSenderId(bigInteger(rs, "sender_id")); + record.setSenderName(rs.getString("sender_name")); + record.setSenderRole(rs.getString("sender_role")); + record.setContentType(rs.getString("content_type")); + record.setContentText(rs.getString("content_text")); + record.setContentPayload(jsonSupport.toMap(rs.getString("content_payload"))); + record.setCreated(rs.getTimestamp("created")); + record.setCreatedBy(bigInteger(rs, "created_by")); + record.setSyncVersion(rs.getLong("sync_version")); + return record; + } + + private BigInteger bigInteger(ResultSet rs, String column) throws SQLException { + Object value = rs.getObject(column); + if (value == null) { + return null; + } + return new BigInteger(String.valueOf(value)); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogTableManager.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogTableManager.java new file mode 100644 index 0000000..659c9a0 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatLogTableManager.java @@ -0,0 +1,61 @@ +package tech.easyflow.chatlog.repository.mysql; + +import com.mybatisflex.core.row.Db; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import tech.easyflow.chatlog.support.ChatConstants; +import tech.easyflow.chatlog.support.ChatTableRouter; + +import java.time.YearMonth; +import java.util.ArrayList; +import java.util.List; + +@Repository +public class MySqlChatLogTableManager { + + private final JdbcTemplate jdbcTemplate; + private final ChatTableRouter tableRouter; + + public MySqlChatLogTableManager(JdbcTemplate jdbcTemplate, ChatTableRouter tableRouter) { + this.jdbcTemplate = jdbcTemplate; + this.tableRouter = tableRouter; + } + + public void ensureCurrentAndNextMonth() { + ensureMonthTable(YearMonth.now()); + ensureMonthTable(YearMonth.now().plusMonths(1)); + } + + public void ensureMonthTable(YearMonth month) { + String tableName = tableRouter.resolveLogTable(month); + if (tableExists(tableName)) { + return; + } + Db.selectObject("CREATE TABLE IF NOT EXISTS `" + tableName + "` LIKE `" + ChatConstants.CHAT_LOG_TEMPLATE + "`"); + } + + public List listRecentExistingMonths(int retentionMonths) { + List months = new ArrayList<>(); + YearMonth current = YearMonth.now(); + for (int i = 0; i < retentionMonths; i++) { + YearMonth month = current.minusMonths(i); + if (tableExists(tableRouter.resolveLogTable(month))) { + months.add(month); + } + } + return months; + } + + public void dropMonthTable(YearMonth month) { + Db.selectObject("DROP TABLE IF EXISTS `" + tableRouter.resolveLogTable(month) + "`"); + } + + public boolean tableExists(String tableName) { + Integer count = jdbcTemplate.queryForObject( + "SELECT COUNT(1) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?", + Integer.class, + tableName + ); + return count != null && count > 0; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatSessionRepository.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatSessionRepository.java new file mode 100644 index 0000000..5ead304 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/repository/mysql/MySqlChatSessionRepository.java @@ -0,0 +1,275 @@ +package tech.easyflow.chatlog.repository.mysql; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; +import tech.easyflow.chatlog.domain.command.ChatSessionSummaryCommand; +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.event.payload.ChatSessionDeletePayload; +import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; +import tech.easyflow.chatlog.support.ChatTableRouter; + +import java.math.BigInteger; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +@Repository +public class MySqlChatSessionRepository { + + private final JdbcTemplate jdbcTemplate; + private final ChatTableRouter tableRouter; + + public MySqlChatSessionRepository(JdbcTemplate jdbcTemplate, ChatTableRouter tableRouter) { + this.jdbcTemplate = jdbcTemplate; + this.tableRouter = tableRouter; + } + + public void createOrTouchBatch(List commands) { + if (commands == null || commands.isEmpty()) { + return; + } + String table = tableRouter.resolveSessionTable(); + String sql = "INSERT INTO `" + table + "` " + + "(id, tenant_id, dept_id, user_id, user_account, assistant_id, assistant_code, assistant_name, title, last_message_preview, message_count, access_at, created, created_by, modified, modified_by, is_deleted) " + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, '', 0, ?, ?, ?, ?, ?, 0) " + + "ON DUPLICATE KEY UPDATE user_account=VALUES(user_account), assistant_id=VALUES(assistant_id), assistant_code=VALUES(assistant_code), " + + "assistant_name=VALUES(assistant_name), title=VALUES(title), access_at=VALUES(access_at), modified=VALUES(modified), modified_by=VALUES(modified_by), is_deleted=0"; + jdbcTemplate.batchUpdate(sql, commands, commands.size(), (ps, command) -> { + Timestamp operateAt = timestamp(command.getOperateAt()); + ps.setObject(1, command.getSessionId()); + ps.setObject(2, command.getTenantId()); + ps.setObject(3, command.getDeptId()); + ps.setObject(4, command.getUserId()); + ps.setString(5, command.getUserAccount()); + ps.setObject(6, command.getAssistantId()); + ps.setString(7, command.getAssistantCode()); + ps.setString(8, command.getAssistantName()); + ps.setString(9, command.getTitle()); + ps.setTimestamp(10, operateAt); + ps.setTimestamp(11, operateAt); + ps.setObject(12, command.getOperatorId()); + ps.setTimestamp(13, operateAt); + ps.setObject(14, command.getOperatorId()); + }); + } + + public void updateSummaries(List commands) { + if (commands == null || commands.isEmpty()) { + return; + } + String table = tableRouter.resolveSessionTable(); + String sql = "UPDATE `" + table + "` SET " + + "last_sender_id=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE last_sender_id END, " + + "last_sender_name=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE last_sender_name END, " + + "last_message_preview=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE last_message_preview END, " + + "last_message_at=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE last_message_at END, " + + "access_at=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE access_at END, " + + "message_count=COALESCE(message_count, 0) + ?, " + + "modified=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE modified END, " + + "modified_by=CASE WHEN last_message_at IS NULL OR last_message_at <= ? THEN ? ELSE modified_by END " + + "WHERE id=?"; + jdbcTemplate.batchUpdate(sql, commands, commands.size(), (ps, command) -> { + Timestamp lastMessageAt = timestamp(command.getLastMessageAt()); + ps.setTimestamp(1, lastMessageAt); + ps.setObject(2, command.getLastSenderId()); + ps.setTimestamp(3, lastMessageAt); + ps.setString(4, command.getLastSenderName()); + ps.setTimestamp(5, lastMessageAt); + ps.setString(6, command.getLastMessagePreview()); + ps.setTimestamp(7, lastMessageAt); + ps.setTimestamp(8, lastMessageAt); + ps.setTimestamp(9, lastMessageAt); + ps.setTimestamp(10, lastMessageAt); + ps.setInt(11, Math.max(command.getMessageIncrement(), 1)); + ps.setTimestamp(12, lastMessageAt); + ps.setTimestamp(13, lastMessageAt); + ps.setTimestamp(14, lastMessageAt); + ps.setObject(15, command.getOperatorId()); + ps.setObject(16, command.getSessionId()); + }); + } + + public List listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) { + String table = tableRouter.resolveSessionTable(); + List params = new ArrayList<>(); + StringBuilder sql = new StringBuilder("SELECT * FROM `").append(table) + .append("` WHERE user_id=? AND is_deleted=0"); + params.add(userId); + if (assistantId != null) { + sql.append(" AND assistant_id=?"); + params.add(assistantId); + } + sql.append(" ORDER BY access_at DESC, id DESC LIMIT ? OFFSET ?"); + params.add(query.getPageSize()); + params.add(query.getOffset()); + return jdbcTemplate.query(sql.toString(), sessionRowMapper(), params.toArray()); + } + + public long countSessions(BigInteger userId, BigInteger assistantId) { + String table = tableRouter.resolveSessionTable(); + List params = new ArrayList<>(); + StringBuilder sql = new StringBuilder("SELECT COUNT(1) FROM `").append(table) + .append("` WHERE user_id=? AND is_deleted=0"); + params.add(userId); + if (assistantId != null) { + sql.append(" AND assistant_id=?"); + params.add(assistantId); + } + Long count = jdbcTemplate.queryForObject(sql.toString(), Long.class, params.toArray()); + return count == null ? 0L : count; + } + + public ChatSessionSummary findBySessionIdAndUserId(BigInteger sessionId, BigInteger userId) { + String table = tableRouter.resolveSessionTable(); + List list = jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE id=? AND user_id=? AND is_deleted=0 LIMIT 1", + sessionRowMapper(), + sessionId, + userId + ); + return list.isEmpty() ? null : list.get(0); + } + + public ChatSessionSummary findBySessionId(BigInteger sessionId) { + String table = tableRouter.resolveSessionTable(); + List list = jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE id=? AND is_deleted=0 LIMIT 1", + sessionRowMapper(), + sessionId + ); + if (!list.isEmpty()) { + return list.get(0); + } + return null; + } + + public List findBySessionIds(List sessionIds) { + if (sessionIds == null || sessionIds.isEmpty()) { + return List.of(); + } + String table = tableRouter.resolveSessionTable(); + StringJoiner placeholders = new StringJoiner(", "); + for (int i = 0; i < sessionIds.size(); i++) { + placeholders.add("?"); + } + List sessions = jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE id IN (" + placeholders + ") AND is_deleted=0", + sessionRowMapper(), + sessionIds.toArray() + ); + Map sessionMap = new LinkedHashMap<>(); + for (ChatSessionSummary session : sessions) { + sessionMap.put(session.getId(), session); + } + List ordered = new ArrayList<>(sessionIds.size()); + for (BigInteger sessionId : sessionIds) { + ChatSessionSummary session = sessionMap.get(sessionId); + if (session != null) { + ordered.add(session); + } + } + return ordered; + } + + public void renameSessions(List payloads) { + if (payloads == null || payloads.isEmpty()) { + return; + } + String table = tableRouter.resolveSessionTable(); + String sql = "UPDATE `" + table + "` SET title=?, modified=?, modified_by=? WHERE id=? AND user_id=? AND is_deleted=0"; + jdbcTemplate.batchUpdate(sql, payloads, payloads.size(), (ps, payload) -> { + ps.setString(1, payload.getTitle()); + ps.setTimestamp(2, timestamp(payload.getOperateAt())); + ps.setObject(3, payload.getOperatorId()); + ps.setObject(4, payload.getSessionId()); + ps.setObject(5, payload.getUserId()); + }); + } + + public void deleteSessions(List payloads) { + if (payloads == null || payloads.isEmpty()) { + return; + } + String table = tableRouter.resolveSessionTable(); + String sql = "UPDATE `" + table + "` SET is_deleted=1, modified=?, modified_by=? WHERE id=? AND user_id=? AND is_deleted=0"; + jdbcTemplate.batchUpdate(sql, payloads, payloads.size(), (ps, payload) -> { + ps.setTimestamp(1, timestamp(payload.getOperateAt())); + ps.setObject(2, payload.getOperatorId()); + ps.setObject(3, payload.getSessionId()); + ps.setObject(4, payload.getUserId()); + }); + } + + public List loadModifiedAfter(Date cursorTime, BigInteger cursorId, int limit) { + String table = tableRouter.resolveSessionTable(); + Timestamp timestamp = cursorTime == null ? new Timestamp(0L) : new Timestamp(cursorTime.getTime()); + return jdbcTemplate.query( + "SELECT * FROM `" + table + "` WHERE (modified > ?) OR (modified = ? AND id > ?) " + + "ORDER BY modified ASC, id ASC LIMIT ?", + sessionRowMapper(), + timestamp, + timestamp, + cursorId == null ? BigInteger.ZERO : cursorId, + limit + ); + } + + public int deleteExpiredSessions(Date beforeTime, int limit) { + String table = tableRouter.resolveSessionTable(); + return jdbcTemplate.update( + "DELETE FROM `" + table + "` WHERE access_at < ? LIMIT " + limit, + new Timestamp(beforeTime.getTime()) + ); + } + + private RowMapper sessionRowMapper() { + return new RowMapper<>() { + @Override + public ChatSessionSummary mapRow(ResultSet rs, int rowNum) throws SQLException { + ChatSessionSummary summary = new ChatSessionSummary(); + summary.setId(bigInteger(rs, "id")); + summary.setTenantId(bigInteger(rs, "tenant_id")); + summary.setDeptId(bigInteger(rs, "dept_id")); + summary.setUserId(bigInteger(rs, "user_id")); + summary.setUserAccount(rs.getString("user_account")); + summary.setAssistantId(bigInteger(rs, "assistant_id")); + summary.setAssistantCode(rs.getString("assistant_code")); + summary.setAssistantName(rs.getString("assistant_name")); + summary.setTitle(rs.getString("title")); + summary.setLastMessagePreview(rs.getString("last_message_preview")); + summary.setLastSenderId(bigInteger(rs, "last_sender_id")); + summary.setLastSenderName(rs.getString("last_sender_name")); + summary.setMessageCount(rs.getInt("message_count")); + summary.setAccessAt(rs.getTimestamp("access_at")); + summary.setLastMessageAt(rs.getTimestamp("last_message_at")); + summary.setCreated(rs.getTimestamp("created")); + summary.setCreatedBy(bigInteger(rs, "created_by")); + summary.setModified(rs.getTimestamp("modified")); + summary.setModifiedBy(bigInteger(rs, "modified_by")); + summary.setIsDeleted(rs.getInt("is_deleted")); + return summary; + } + }; + } + + private BigInteger bigInteger(ResultSet rs, String column) throws SQLException { + Object value = rs.getObject(column); + if (value == null) { + return null; + } + return new BigInteger(String.valueOf(value)); + } + + private Timestamp timestamp(Date value) { + return new Timestamp((value == null ? new Date() : value).getTime()); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatStartupRunner.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatStartupRunner.java new file mode 100644 index 0000000..eafb93f --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatStartupRunner.java @@ -0,0 +1,21 @@ +package tech.easyflow.chatlog.schedule; + +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.stereotype.Component; +import tech.easyflow.chatlog.service.ChatSyncService; + +@Component +public class ChatStartupRunner implements ApplicationRunner { + + private final ChatSyncService chatSyncService; + + public ChatStartupRunner(ChatSyncService chatSyncService) { + this.chatSyncService = chatSyncService; + } + + @Override + public void run(ApplicationArguments args) { + chatSyncService.startupCheck(); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatSyncScheduler.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatSyncScheduler.java new file mode 100644 index 0000000..7f5233f --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/schedule/ChatSyncScheduler.java @@ -0,0 +1,47 @@ +package tech.easyflow.chatlog.schedule; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import tech.easyflow.chatlog.config.ChatSyncProperties; +import tech.easyflow.chatlog.service.ChatSyncService; + +@Component +public class ChatSyncScheduler { + + private final ChatSyncService chatSyncService; + private final ChatSyncProperties syncProperties; + + public ChatSyncScheduler(ChatSyncService chatSyncService, ChatSyncProperties syncProperties) { + this.chatSyncService = chatSyncService; + this.syncProperties = syncProperties; + } + + @Scheduled(fixedDelayString = "${easyflow.chat.sync.fixed-delay:30000}", initialDelay = 10000L) + public void syncSessions() { + if (!syncProperties.isEnabled()) { + return; + } + chatSyncService.syncSessions(); + } + + @Scheduled(fixedDelayString = "${easyflow.chat.sync.fixed-delay:30000}", initialDelay = 15000L) + public void syncLogs() { + if (!syncProperties.isEnabled()) { + return; + } + chatSyncService.syncLogs(); + } + + @Scheduled(cron = "0 15 3 * * *") + public void repairLogs() { + if (!syncProperties.isEnabled()) { + return; + } + chatSyncService.repairLogs(); + } + + @Scheduled(cron = "0 0 2 * * *") + public void maintainMysqlTables() { + chatSyncService.maintainMysqlTables(); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java new file mode 100644 index 0000000..b1ab625 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryManageService.java @@ -0,0 +1,28 @@ +package tech.easyflow.chatlog.service; + +import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.dto.ChatSessionPage; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; +import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery; + +import java.math.BigInteger; + +public interface ChatHistoryManageService { + + ChatSessionPage queryUserSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query); + + ChatSessionPage queryAdminSessions(ChatSessionFilterQuery query); + + ChatSessionSummary getUserSession(BigInteger userId, BigInteger sessionId); + + ChatSessionSummary getAdminSession(BigInteger sessionId); + + ChatHistoryPage queryUserMessages(BigInteger userId, BigInteger sessionId, ChatPageQuery query); + + ChatHistoryPage queryAdminMessages(BigInteger sessionId, ChatPageQuery query); + + void renameUserSession(BigInteger userId, BigInteger sessionId, String title, BigInteger operatorId); + + void deleteUserSession(BigInteger userId, BigInteger sessionId, BigInteger operatorId); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryQueryService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryQueryService.java new file mode 100644 index 0000000..1c5678b --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatHistoryQueryService.java @@ -0,0 +1,11 @@ +package tech.easyflow.chatlog.service; + +import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; + +import java.math.BigInteger; + +public interface ChatHistoryQueryService { + + ChatHistoryPage queryHistoryMessages(BigInteger sessionId, ChatPageQuery query); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDeadLetterService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDeadLetterService.java new file mode 100644 index 0000000..480de89 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDeadLetterService.java @@ -0,0 +1,52 @@ +package tech.easyflow.chatlog.service; + +import org.springframework.stereotype.Component; +import tech.easyflow.chatlog.domain.entity.ChatPersistDeadLetter; +import tech.easyflow.chatlog.domain.event.ChatPersistEvent; +import tech.easyflow.chatlog.mapper.ChatPersistDeadLetterMapper; +import tech.easyflow.chatlog.support.ChatConstants; +import tech.easyflow.chatlog.support.ChatJsonSupport; +import tech.easyflow.common.mq.core.MQDeadLetterHandler; +import tech.easyflow.common.mq.core.MQMessage; + +import java.util.Date; + +@Component +public class ChatPersistDeadLetterService implements MQDeadLetterHandler { + + private final ChatPersistDeadLetterMapper deadLetterMapper; + private final ChatJsonSupport chatJsonSupport; + + public ChatPersistDeadLetterService(ChatPersistDeadLetterMapper deadLetterMapper, + ChatJsonSupport chatJsonSupport) { + this.deadLetterMapper = deadLetterMapper; + this.chatJsonSupport = chatJsonSupport; + } + + @Override + public boolean supports(String topic) { + return ChatConstants.CHAT_PERSIST_TOPIC.equals(topic); + } + + @Override + public void handle(MQMessage message, String reason) { + Date now = new Date(); + ChatPersistEvent event = chatJsonSupport.fromJson(message.getBody(), ChatPersistEvent.class); + + ChatPersistDeadLetter deadLetter = new ChatPersistDeadLetter(); + deadLetter.setTopic(message.getTopic()); + deadLetter.setStreamKey(message.getStreamKey()); + deadLetter.setStreamMessageId(message.getStreamMessageId()); + deadLetter.setEventId(event == null ? message.getMessageId() : event.getEventId()); + deadLetter.setSessionId(event == null ? null : event.getSessionId()); + deadLetter.setPayload(message.getBody()); + deadLetter.setRetryCount(message.getRetryCount()); + deadLetter.setErrorMessage(reason); + deadLetter.setFirstFailedAt(now); + deadLetter.setLastFailedAt(now); + deadLetter.setStatus("OPEN"); + deadLetter.setCreated(now); + deadLetter.setModified(now); + deadLetterMapper.insert(deadLetter); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDispatcher.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDispatcher.java new file mode 100644 index 0000000..6012eb0 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistDispatcher.java @@ -0,0 +1,140 @@ +package tech.easyflow.chatlog.service; + +import org.slf4j.MDC; +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.cache.ChatHotStateService; +import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand; +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.event.ChatPersistEvent; +import tech.easyflow.chatlog.domain.event.ChatPersistEventType; +import tech.easyflow.chatlog.domain.event.payload.ChatSessionDeletePayload; +import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload; +import tech.easyflow.chatlog.support.ChatJsonSupport; + +import java.math.BigInteger; +import java.util.Date; +import java.util.UUID; + +@Service +public class ChatPersistDispatcher { + + private final ChatHotStateService chatHotStateService; + private final ChatPersistEventProducer eventProducer; + private final ChatJsonSupport chatJsonSupport; + + public ChatPersistDispatcher(ChatHotStateService chatHotStateService, + ChatPersistEventProducer eventProducer, + ChatJsonSupport chatJsonSupport) { + this.chatHotStateService = chatHotStateService; + this.eventProducer = eventProducer; + this.chatJsonSupport = chatJsonSupport; + } + + public ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command) { + ChatSessionSummary summary = chatHotStateService.touchSession(command); + eventProducer.send(buildEvent( + UUID.randomUUID().toString(), + ChatPersistEventType.SESSION_PREPARED, + command.getSessionId(), + command.getUserId(), + command.getAssistantId(), + command.getOperateAt(), + chatJsonSupport.toJson(command) + )); + return summary; + } + + public void appendUserMessage(ChatAppendMessageCommand command) { + appendMessage(command, ChatPersistEventType.USER_MESSAGE_APPENDED); + } + + public void appendAssistantMessage(ChatAppendMessageCommand command) { + appendMessage(command, ChatPersistEventType.ASSISTANT_MESSAGE_APPENDED); + } + + public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId) { + Date operateAt = new Date(); + chatHotStateService.renameSession(sessionId, userId, title, operatorId, operateAt); + + ChatSessionRenamePayload payload = new ChatSessionRenamePayload(); + payload.setSessionId(sessionId); + payload.setUserId(userId); + payload.setTitle(title); + payload.setOperatorId(operatorId); + payload.setOperateAt(operateAt); + eventProducer.send(buildEvent( + UUID.randomUUID().toString(), + ChatPersistEventType.SESSION_RENAMED, + sessionId, + userId, + BigInteger.ZERO, + operateAt, + chatJsonSupport.toJson(payload) + )); + } + + public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId) { + Date operateAt = new Date(); + chatHotStateService.deleteSession(sessionId, userId, operatorId, operateAt); + + ChatSessionDeletePayload payload = new ChatSessionDeletePayload(); + payload.setSessionId(sessionId); + payload.setUserId(userId); + payload.setOperatorId(operatorId); + payload.setOperateAt(operateAt); + eventProducer.send(buildEvent( + UUID.randomUUID().toString(), + ChatPersistEventType.SESSION_DELETED, + sessionId, + userId, + BigInteger.ZERO, + operateAt, + chatJsonSupport.toJson(payload) + )); + } + + private void appendMessage(ChatAppendMessageCommand command, ChatPersistEventType eventType) { + chatHotStateService.appendMessage(command); + eventProducer.send(buildEvent( + eventId("message", command.getMessageId()), + eventType, + command.getSessionId(), + command.getUserId(), + command.getAssistantId(), + command.getCreated(), + chatJsonSupport.toJson(command) + )); + } + + private ChatPersistEvent buildEvent(String eventId, + ChatPersistEventType eventType, + BigInteger sessionId, + BigInteger userId, + BigInteger assistantId, + Date occurredAt, + String payload) { + ChatPersistEvent event = new ChatPersistEvent(); + event.setEventId(eventId); + event.setEventType(eventType); + event.setSessionId(sessionId); + event.setUserId(userId); + event.setAssistantId(assistantId); + event.setOccurredAt(occurredAt == null ? new Date() : occurredAt); + event.setTraceId(resolveTraceId(eventId)); + event.setPayload(payload); + return event; + } + + private String resolveTraceId(String fallback) { + String traceId = MDC.get("traceId"); + if (traceId == null || traceId.isBlank()) { + return fallback; + } + return traceId; + } + + private String eventId(String prefix, BigInteger id) { + return prefix + "-" + (id == null ? UUID.randomUUID() : id); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventConsumer.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventConsumer.java new file mode 100644 index 0000000..721eea4 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventConsumer.java @@ -0,0 +1,50 @@ +package tech.easyflow.chatlog.service; + +import org.springframework.stereotype.Component; +import tech.easyflow.chatlog.domain.event.ChatPersistEvent; +import tech.easyflow.chatlog.support.ChatConstants; +import tech.easyflow.chatlog.support.ChatJsonSupport; +import tech.easyflow.common.mq.config.MQProperties; +import tech.easyflow.common.mq.core.MQConsumerHandler; +import tech.easyflow.common.mq.core.MQMessage; +import tech.easyflow.common.mq.core.MQSubscription; + +import java.util.ArrayList; +import java.util.List; + +@Component +public class ChatPersistEventConsumer implements MQConsumerHandler { + + private final MQProperties mqProperties; + private final ChatJsonSupport chatJsonSupport; + private final ChatPersistMySqlApplyService applyService; + + public ChatPersistEventConsumer(MQProperties mqProperties, + ChatJsonSupport chatJsonSupport, + ChatPersistMySqlApplyService applyService) { + this.mqProperties = mqProperties; + this.chatJsonSupport = chatJsonSupport; + this.applyService = applyService; + } + + @Override + public MQSubscription subscription() { + MQSubscription subscription = new MQSubscription(); + subscription.setTopic(ChatConstants.CHAT_PERSIST_TOPIC); + subscription.setConsumerGroup(ChatConstants.CHAT_PERSIST_GROUP); + subscription.setShardCount(Math.max(mqProperties.getRedis().getChatPersistShardCount(), 1)); + return subscription; + } + + @Override + public void handle(List messages) { + List events = new ArrayList<>(messages.size()); + for (MQMessage message : messages) { + ChatPersistEvent event = chatJsonSupport.fromJson(message.getBody(), ChatPersistEvent.class); + if (event != null) { + events.add(event); + } + } + applyService.apply(events); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventProducer.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventProducer.java new file mode 100644 index 0000000..76d5aae --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistEventProducer.java @@ -0,0 +1,33 @@ +package tech.easyflow.chatlog.service; + +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.domain.event.ChatPersistEvent; +import tech.easyflow.chatlog.support.ChatConstants; +import tech.easyflow.chatlog.support.ChatJsonSupport; +import tech.easyflow.common.mq.core.MQMessage; +import tech.easyflow.common.mq.core.MQProducer; + +@Service +public class ChatPersistEventProducer { + + private final MQProducer mqProducer; + private final ChatJsonSupport chatJsonSupport; + + public ChatPersistEventProducer(MQProducer mqProducer, ChatJsonSupport chatJsonSupport) { + this.mqProducer = mqProducer; + this.chatJsonSupport = chatJsonSupport; + } + + public void send(ChatPersistEvent event) { + MQMessage message = new MQMessage(); + message.setMessageId(event.getEventId()); + message.setTopic(ChatConstants.CHAT_PERSIST_TOPIC); + message.setKey(event.getSessionId() == null ? event.getEventId() : event.getSessionId().toString()); + message.setBody(chatJsonSupport.toJson(event)); + message.setCreatedAt(event.getOccurredAt()); + if (event.getTraceId() != null && !event.getTraceId().isBlank()) { + message.getHeaders().put("traceId", event.getTraceId()); + } + mqProducer.send(message); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistMySqlApplyService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistMySqlApplyService.java new file mode 100644 index 0000000..e6e8f7f --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatPersistMySqlApplyService.java @@ -0,0 +1,153 @@ +package tech.easyflow.chatlog.service; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand; +import tech.easyflow.chatlog.domain.command.ChatSessionSummaryCommand; +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.event.ChatPersistEvent; +import tech.easyflow.chatlog.domain.event.ChatPersistEventType; +import tech.easyflow.chatlog.domain.event.payload.ChatSessionDeletePayload; +import tech.easyflow.chatlog.domain.event.payload.ChatSessionRenamePayload; +import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository; +import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager; +import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository; +import tech.easyflow.chatlog.support.ChatJsonSupport; + +import java.math.BigInteger; +import java.time.YearMonth; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +@Service +public class ChatPersistMySqlApplyService { + + private final MySqlChatSessionRepository sessionRepository; + private final MySqlChatLogRepository logRepository; + private final MySqlChatLogTableManager tableManager; + private final ChatJsonSupport chatJsonSupport; + + public ChatPersistMySqlApplyService(MySqlChatSessionRepository sessionRepository, + MySqlChatLogRepository logRepository, + MySqlChatLogTableManager tableManager, + ChatJsonSupport chatJsonSupport) { + this.sessionRepository = sessionRepository; + this.logRepository = logRepository; + this.tableManager = tableManager; + this.chatJsonSupport = chatJsonSupport; + } + + @Transactional(rollbackFor = Exception.class) + public void apply(List events) { + if (events == null || events.isEmpty()) { + return; + } + + Map sessionUpserts = new LinkedHashMap<>(); + List appendCommands = new ArrayList<>(); + Map summaryCommands = new LinkedHashMap<>(); + List renamePayloads = new ArrayList<>(); + List deletePayloads = new ArrayList<>(); + Set months = new LinkedHashSet<>(); + + for (ChatPersistEvent event : events) { + if (event == null || event.getEventType() == null) { + continue; + } + switch (event.getEventType()) { + case SESSION_PREPARED -> { + ChatSessionUpsertCommand command = chatJsonSupport.fromJson(event.getPayload(), ChatSessionUpsertCommand.class); + if (command != null && command.getSessionId() != null) { + sessionUpserts.put(command.getSessionId(), command); + } + } + case USER_MESSAGE_APPENDED, ASSISTANT_MESSAGE_APPENDED -> { + ChatAppendMessageCommand command = chatJsonSupport.fromJson(event.getPayload(), ChatAppendMessageCommand.class); + if (command == null || command.getSessionId() == null || command.getMessageId() == null) { + continue; + } + appendCommands.add(command); + months.add(resolveMonth(command.getCreated())); + accumulateSummary(summaryCommands, command); + } + case SESSION_RENAMED -> { + ChatSessionRenamePayload payload = chatJsonSupport.fromJson(event.getPayload(), ChatSessionRenamePayload.class); + if (payload != null && payload.getSessionId() != null) { + renamePayloads.add(payload); + } + } + case SESSION_DELETED -> { + ChatSessionDeletePayload payload = chatJsonSupport.fromJson(event.getPayload(), ChatSessionDeletePayload.class); + if (payload != null && payload.getSessionId() != null) { + deletePayloads.add(payload); + } + } + default -> { + } + } + } + + if (!sessionUpserts.isEmpty()) { + sessionRepository.createOrTouchBatch(new ArrayList<>(sessionUpserts.values())); + } + if (!months.isEmpty()) { + for (YearMonth month : months) { + tableManager.ensureMonthTable(month); + } + } + List insertedCommands = List.of(); + if (!appendCommands.isEmpty()) { + insertedCommands = logRepository.appendMessages(appendCommands); + } + if (!insertedCommands.isEmpty()) { + summaryCommands.clear(); + for (ChatAppendMessageCommand insertedCommand : insertedCommands) { + accumulateSummary(summaryCommands, insertedCommand); + } + sessionRepository.updateSummaries(new ArrayList<>(summaryCommands.values())); + } + if (!renamePayloads.isEmpty()) { + sessionRepository.renameSessions(renamePayloads); + } + if (!deletePayloads.isEmpty()) { + sessionRepository.deleteSessions(deletePayloads); + } + } + + private void accumulateSummary(Map summaryCommands, + ChatAppendMessageCommand command) { + ChatSessionSummaryCommand summary = summaryCommands.computeIfAbsent(command.getSessionId(), key -> { + ChatSessionSummaryCommand created = new ChatSessionSummaryCommand(); + created.setSessionId(command.getSessionId()); + created.setUserId(command.getUserId()); + created.setMessageIncrement(0); + return created; + }); + summary.setMessageIncrement(summary.getMessageIncrement() + 1); + if (summary.getLastMessageAt() == null || !command.getCreated().before(summary.getLastMessageAt())) { + summary.setLastSenderId(command.getSenderId()); + summary.setLastSenderName(command.getSenderName()); + summary.setLastMessagePreview(trimPreview(command.getContentText())); + summary.setLastMessageAt(command.getCreated()); + summary.setOperatorId(command.getCreatedBy()); + } + } + + private YearMonth resolveMonth(Date createdAt) { + Date created = createdAt == null ? new Date() : createdAt; + return YearMonth.from(created.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + } + + private String trimPreview(String text) { + if (text == null) { + return ""; + } + return text.length() <= 200 ? text : text.substring(0, 200); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionCommandService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionCommandService.java new file mode 100644 index 0000000..c4af7b6 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionCommandService.java @@ -0,0 +1,15 @@ +package tech.easyflow.chatlog.service; + +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; + +import java.math.BigInteger; + +public interface ChatSessionCommandService { + + ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command); + + void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId); + + void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionQueryService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionQueryService.java new file mode 100644 index 0000000..80ca64b --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSessionQueryService.java @@ -0,0 +1,22 @@ +package tech.easyflow.chatlog.service; + +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionPage; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; + +import java.math.BigInteger; +import java.util.List; + +public interface ChatSessionQueryService { + + List listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query); + + long countSessions(BigInteger userId, BigInteger assistantId); + + ChatSessionPage pageSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query); + + ChatSessionSummary getSessionSummary(BigInteger sessionId); + + List getRecentTail(BigInteger sessionId, int limit); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncCheckpointService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncCheckpointService.java new file mode 100644 index 0000000..6762207 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncCheckpointService.java @@ -0,0 +1,9 @@ +package tech.easyflow.chatlog.service; + +import com.mybatisflex.core.service.IService; +import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint; + +public interface ChatSyncCheckpointService extends IService { + + ChatSyncCheckpoint getOrCreate(String syncCode, String shardKey); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncService.java new file mode 100644 index 0000000..d834e66 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/ChatSyncService.java @@ -0,0 +1,16 @@ +package tech.easyflow.chatlog.service; + +import tech.easyflow.chatlog.domain.dto.ChatSyncResult; + +public interface ChatSyncService { + + ChatSyncResult syncSessions(); + + ChatSyncResult syncLogs(); + + ChatSyncResult repairLogs(); + + void maintainMysqlTables(); + + void startupCheck(); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java new file mode 100644 index 0000000..90c9eea --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/PublicChatSessionRestoreService.java @@ -0,0 +1,10 @@ +package tech.easyflow.chatlog.service; + +import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult; + +import java.math.BigInteger; + +public interface PublicChatSessionRestoreService { + + PublicChatSessionRestoreResult restoreSession(BigInteger userId, BigInteger assistantId, BigInteger sessionId, Integer limit); +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java new file mode 100644 index 0000000..ac1d1bb --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryManageServiceImpl.java @@ -0,0 +1,95 @@ +package tech.easyflow.chatlog.service.impl; + +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionPage; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; +import tech.easyflow.chatlog.domain.query.ChatSessionFilterQuery; +import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository; +import tech.easyflow.chatlog.service.ChatHistoryManageService; +import tech.easyflow.chatlog.service.ChatHistoryQueryService; +import tech.easyflow.chatlog.service.ChatSessionCommandService; +import tech.easyflow.chatlog.service.ChatSessionQueryService; +import tech.easyflow.common.web.exceptions.BusinessException; + +import java.math.BigInteger; + +@Service +public class ChatHistoryManageServiceImpl implements ChatHistoryManageService { + + private final ChatSessionQueryService chatSessionQueryService; + private final ChatSessionCommandService chatSessionCommandService; + private final ChatHistoryQueryService chatHistoryQueryService; + private final ChatAnalyticalDBRepository chatAnalyticalDBRepository; + + public ChatHistoryManageServiceImpl(ChatSessionQueryService chatSessionQueryService, + ChatSessionCommandService chatSessionCommandService, + ChatHistoryQueryService chatHistoryQueryService, + ChatAnalyticalDBRepository chatAnalyticalDBRepository) { + this.chatSessionQueryService = chatSessionQueryService; + this.chatSessionCommandService = chatSessionCommandService; + this.chatHistoryQueryService = chatHistoryQueryService; + this.chatAnalyticalDBRepository = chatAnalyticalDBRepository; + } + + @Override + public ChatSessionPage queryUserSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) { + return chatSessionQueryService.pageSessions(userId, assistantId, query); + } + + @Override + public ChatSessionPage queryAdminSessions(ChatSessionFilterQuery query) { + return chatAnalyticalDBRepository.pageSessions(query); + } + + @Override + public ChatSessionSummary getUserSession(BigInteger userId, BigInteger sessionId) { + ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId); + if (summary == null || summary.getIsDeleted() != null && summary.getIsDeleted() == 1) { + throw new BusinessException("会话不存在"); + } + if (!summary.getUserId().equals(userId)) { + throw new BusinessException("无权访问该会话"); + } + return summary; + } + + @Override + public ChatSessionSummary getAdminSession(BigInteger sessionId) { + ChatSessionSummary summary = chatAnalyticalDBRepository.getSession(sessionId); + if (summary == null || summary.getIsDeleted() != null && summary.getIsDeleted() == 1) { + throw new BusinessException("会话不存在"); + } + return summary; + } + + @Override + public ChatHistoryPage queryUserMessages(BigInteger userId, BigInteger sessionId, ChatPageQuery query) { + getUserSession(userId, sessionId); + return chatHistoryQueryService.queryHistoryMessages(sessionId, query); + } + + @Override + public ChatHistoryPage queryAdminMessages(BigInteger sessionId, ChatPageQuery query) { + getAdminSession(sessionId); + return chatHistoryQueryService.queryHistoryMessages(sessionId, query); + } + + @Override + public void renameUserSession(BigInteger userId, BigInteger sessionId, String title, BigInteger operatorId) { + if (!StringUtils.hasText(title)) { + throw new BusinessException("标题不能为空"); + } + getUserSession(userId, sessionId); + chatSessionCommandService.renameSession(sessionId, userId, title.trim(), operatorId); + } + + @Override + public void deleteUserSession(BigInteger userId, BigInteger sessionId, BigInteger operatorId) { + getUserSession(userId, sessionId); + chatSessionCommandService.deleteSession(sessionId, userId, operatorId); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryQueryServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryQueryServiceImpl.java new file mode 100644 index 0000000..2a5af5f --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatHistoryQueryServiceImpl.java @@ -0,0 +1,24 @@ +package tech.easyflow.chatlog.service.impl; + +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.domain.dto.ChatHistoryPage; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; +import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository; +import tech.easyflow.chatlog.service.ChatHistoryQueryService; + +import java.math.BigInteger; + +@Service +public class ChatHistoryQueryServiceImpl implements ChatHistoryQueryService { + + private final ChatAnalyticalDBRepository chatAnalyticalDBRepository; + + public ChatHistoryQueryServiceImpl(ChatAnalyticalDBRepository chatAnalyticalDBRepository) { + this.chatAnalyticalDBRepository = chatAnalyticalDBRepository; + } + + @Override + public ChatHistoryPage queryHistoryMessages(BigInteger sessionId, ChatPageQuery query) { + return chatAnalyticalDBRepository.queryHistory(sessionId, query); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionCommandServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionCommandServiceImpl.java new file mode 100644 index 0000000..9fa604c --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionCommandServiceImpl.java @@ -0,0 +1,34 @@ +package tech.easyflow.chatlog.service.impl; + +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.service.ChatPersistDispatcher; +import tech.easyflow.chatlog.service.ChatSessionCommandService; + +import java.math.BigInteger; + +@Service +public class ChatSessionCommandServiceImpl implements ChatSessionCommandService { + + private final ChatPersistDispatcher chatPersistDispatcher; + + public ChatSessionCommandServiceImpl(ChatPersistDispatcher chatPersistDispatcher) { + this.chatPersistDispatcher = chatPersistDispatcher; + } + + @Override + public ChatSessionSummary createOrTouchSession(ChatSessionUpsertCommand command) { + return chatPersistDispatcher.createOrTouchSession(command); + } + + @Override + public void renameSession(BigInteger sessionId, BigInteger userId, String title, BigInteger operatorId) { + chatPersistDispatcher.renameSession(sessionId, userId, title, operatorId); + } + + @Override + public void deleteSession(BigInteger sessionId, BigInteger userId, BigInteger operatorId) { + chatPersistDispatcher.deleteSession(sessionId, userId, operatorId); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java new file mode 100644 index 0000000..d33d425 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSessionQueryServiceImpl.java @@ -0,0 +1,108 @@ +package tech.easyflow.chatlog.service.impl; + +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.cache.ChatHotStateService; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionPage; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.query.ChatPageQuery; +import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository; +import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager; +import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository; +import tech.easyflow.chatlog.service.ChatSessionQueryService; + +import java.math.BigInteger; +import java.util.List; + +@Service +public class ChatSessionQueryServiceImpl implements ChatSessionQueryService { + + private final MySqlChatSessionRepository sessionRepository; + private final MySqlChatLogRepository logRepository; + private final MySqlChatLogTableManager tableManager; + private final ChatHotStateService chatHotStateService; + + public ChatSessionQueryServiceImpl(MySqlChatSessionRepository sessionRepository, + MySqlChatLogRepository logRepository, + MySqlChatLogTableManager tableManager, + ChatHotStateService chatHotStateService) { + this.sessionRepository = sessionRepository; + this.logRepository = logRepository; + this.tableManager = tableManager; + this.chatHotStateService = chatHotStateService; + } + + @Override + public List listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) { + if (assistantId == null) { + List sessionIds = chatHotStateService.listSessionIds(userId, query.getOffset(), query.getPageSize()); + if (!sessionIds.isEmpty()) { + List cached = chatHotStateService.getSessionSummaries(sessionIds); + if (cached.size() == sessionIds.size()) { + return cached; + } + } + List sessions = sessionRepository.listSessions(userId, null, query); + chatHotStateService.cacheSessionSummaries(sessions); + return sessions; + } + List sessions = sessionRepository.listSessions(userId, assistantId, query); + chatHotStateService.cacheSessionSummaries(sessions); + return sessions; + } + + @Override + public long countSessions(BigInteger userId, BigInteger assistantId) { + return sessionRepository.countSessions(userId, assistantId); + } + + @Override + public ChatSessionPage pageSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) { + ChatSessionPage page = new ChatSessionPage(); + page.setPageNumber(query.getPageNumber()); + page.setPageSize(query.getPageSize()); + + if (assistantId == null && chatHotStateService.hasSessionIndex(userId)) { + List sessionIds = chatHotStateService.listSessionIds(userId, query.getOffset(), query.getPageSize()); + if (sessionIds.isEmpty()) { + page.setTotal(chatHotStateService.countSessions(userId)); + page.setRecords(List.of()); + return page; + } + List cached = chatHotStateService.getSessionSummaries(sessionIds); + if (cached.size() == sessionIds.size()) { + page.setTotal(chatHotStateService.countSessions(userId)); + page.setRecords(cached); + return page; + } + } + + page.setTotal(sessionRepository.countSessions(userId, assistantId)); + page.setRecords(listSessions(userId, assistantId, query)); + return page; + } + + @Override + public ChatSessionSummary getSessionSummary(BigInteger sessionId) { + ChatSessionSummary cached = chatHotStateService.getSessionSummary(sessionId); + if (cached != null) { + return cached; + } + ChatSessionSummary summary = sessionRepository.findBySessionId(sessionId); + if (summary != null) { + chatHotStateService.cacheSessionSummary(summary); + } + return summary; + } + + @Override + public List getRecentTail(BigInteger sessionId, int limit) { + List cached = chatHotStateService.getSessionTail(sessionId); + if (cached != null) { + return cached.subList(0, Math.min(limit, cached.size())); + } + List records = logRepository.listRecentTail(sessionId, tableManager.listRecentExistingMonths(3), limit); + chatHotStateService.setSessionTail(sessionId, records); + return records; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncCheckpointServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncCheckpointServiceImpl.java new file mode 100644 index 0000000..5db4684 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncCheckpointServiceImpl.java @@ -0,0 +1,33 @@ +package tech.easyflow.chatlog.service.impl; + +import com.mybatisflex.core.query.QueryWrapper; +import com.mybatisflex.spring.service.impl.ServiceImpl; +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint; +import tech.easyflow.chatlog.mapper.ChatSyncCheckpointMapper; +import tech.easyflow.chatlog.service.ChatSyncCheckpointService; + +import java.util.Date; + +@Service +public class ChatSyncCheckpointServiceImpl extends ServiceImpl + implements ChatSyncCheckpointService { + + @Override + public ChatSyncCheckpoint getOrCreate(String syncCode, String shardKey) { + QueryWrapper wrapper = QueryWrapper.create() + .eq(ChatSyncCheckpoint::getSyncCode, syncCode) + .eq(ChatSyncCheckpoint::getShardKey, shardKey); + ChatSyncCheckpoint checkpoint = getMapper().selectOneByQuery(wrapper); + if (checkpoint != null) { + return checkpoint; + } + ChatSyncCheckpoint created = new ChatSyncCheckpoint(); + created.setSyncCode(syncCode); + created.setShardKey(shardKey); + created.setStatus("READY"); + created.setModified(new Date()); + save(created); + return created; + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncServiceImpl.java new file mode 100644 index 0000000..7956def --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatSyncServiceImpl.java @@ -0,0 +1,190 @@ +package tech.easyflow.chatlog.service.impl; + +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.config.ChatSyncProperties; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.dto.ChatSyncResult; +import tech.easyflow.chatlog.domain.entity.ChatSyncCheckpoint; +import tech.easyflow.chatlog.repository.analyticaldb.ChatAnalyticalDBRepository; +import tech.easyflow.chatlog.repository.mysql.MySqlChatLogRepository; +import tech.easyflow.chatlog.repository.mysql.MySqlChatLogTableManager; +import tech.easyflow.chatlog.repository.mysql.MySqlChatSessionRepository; +import tech.easyflow.chatlog.service.ChatSyncCheckpointService; +import tech.easyflow.chatlog.service.ChatSyncService; +import tech.easyflow.chatlog.support.ChatConstants; + +import java.math.BigInteger; +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Date; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +@Service +public class ChatSyncServiceImpl implements ChatSyncService { + + private final MySqlChatSessionRepository sessionRepository; + private final MySqlChatLogRepository logRepository; + private final MySqlChatLogTableManager tableManager; + private final ChatAnalyticalDBRepository analyticalDBRepository; + private final ChatSyncCheckpointService checkpointService; + private final ChatSyncProperties syncProperties; + + public ChatSyncServiceImpl(MySqlChatSessionRepository sessionRepository, + MySqlChatLogRepository logRepository, + MySqlChatLogTableManager tableManager, + ChatAnalyticalDBRepository analyticalDBRepository, + ChatSyncCheckpointService checkpointService, + ChatSyncProperties syncProperties) { + this.sessionRepository = sessionRepository; + this.logRepository = logRepository; + this.tableManager = tableManager; + this.analyticalDBRepository = analyticalDBRepository; + this.checkpointService = checkpointService; + this.syncProperties = syncProperties; + } + + @Override + public ChatSyncResult syncSessions() { + ChatSyncResult result = new ChatSyncResult(); + result.setSyncCode(ChatConstants.CHECKPOINT_SYNC_CODE_SESSION); + if (!analyticalDBRepository.enabled()) { + return result; + } + int totalRows = 0; + ChatSyncCheckpoint checkpoint = checkpointService.getOrCreate(ChatConstants.CHECKPOINT_SYNC_CODE_SESSION, "default"); + List rows = sessionRepository.loadModifiedAfter( + checkpoint.getCursorTime(), + checkpoint.getCursorId(), + syncProperties.getBatchSize() + ); + if (!rows.isEmpty()) { + analyticalDBRepository.upsertSessions(rows); + ChatSessionSummary last = rows.get(rows.size() - 1); + checkpoint.setCursorTime(last.getModified()); + checkpoint.setCursorId(last.getId()); + checkpoint.setCursorTable(ChatConstants.SESSION_TABLE); + checkpoint.setLastBatchSize(rows.size()); + checkpoint.setLastSuccessTime(new Date()); + checkpoint.setStatus("SUCCESS"); + checkpoint.setModified(new Date()); + checkpointService.updateById(checkpoint); + totalRows += rows.size(); + } + result.setSyncedRows(totalRows); + return result; + } + + @Override + public ChatSyncResult syncLogs() { + ChatSyncResult result = new ChatSyncResult(); + result.setSyncCode(ChatConstants.CHECKPOINT_SYNC_CODE_LOG); + if (!analyticalDBRepository.enabled()) { + return result; + } + ChatSyncCheckpoint checkpoint = checkpointService.getOrCreate(ChatConstants.CHECKPOINT_SYNC_CODE_LOG, "default"); + List months = tableManager.listRecentExistingMonths(syncProperties.getRetentionMonths()); + Set touchedDates = new LinkedHashSet<>(); + int totalRows = 0; + for (YearMonth month : months) { + String table = "chat_log_" + month.toString().replace("-", ""); + if (checkpoint.getCursorTable() != null && checkpoint.getCursorTable().compareTo(table) > 0) { + continue; + } + List rows = logRepository.loadIncremental( + table, + table.equals(checkpoint.getCursorTable()) ? checkpoint.getCursorTime() : null, + table.equals(checkpoint.getCursorTable()) ? checkpoint.getCursorId() : BigInteger.ZERO, + syncProperties.getBatchSize() + ); + if (rows.isEmpty()) { + continue; + } + analyticalDBRepository.appendLogs(rows); + ChatMessageRecord last = rows.get(rows.size() - 1); + checkpoint.setCursorTable(table); + checkpoint.setCursorTime(last.getCreated()); + checkpoint.setCursorId(last.getId()); + checkpoint.setLastBatchSize(rows.size()); + checkpoint.setLastSuccessTime(new Date()); + checkpoint.setStatus("SUCCESS"); + checkpoint.setModified(new Date()); + checkpointService.updateById(checkpoint); + totalRows += rows.size(); + rows.stream() + .map(item -> item.getCreated().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()) + .forEach(touchedDates::add); + } + analyticalDBRepository.refreshDws(touchedDates); + result.setSyncedRows(totalRows); + result.setTouchedDates(new ArrayList<>(touchedDates.stream().map(LocalDate::toString).toList())); + return result; + } + + @Override + public ChatSyncResult repairLogs() { + ChatSyncResult result = new ChatSyncResult(); + result.setSyncCode("chat_log_repair"); + if (!analyticalDBRepository.enabled()) { + return result; + } + Date startTime = Date.from(LocalDate.now() + .minusDays(syncProperties.getRepairLookbackDays()) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + List rows = logRepository.loadRepairRows( + tableManager.listRecentExistingMonths(syncProperties.getRetentionMonths()), + startTime + ); + analyticalDBRepository.appendLogs(rows); + Set dates = new LinkedHashSet<>(); + rows.stream().map(item -> item.getCreated().toInstant().atZone(ZoneId.systemDefault()).toLocalDate()) + .forEach(dates::add); + analyticalDBRepository.refreshDws(dates); + result.setSyncedRows(rows.size()); + result.setTouchedDates(new ArrayList<>(dates.stream().map(LocalDate::toString).toList())); + return result; + } + + @Override + public void maintainMysqlTables() { + tableManager.ensureCurrentAndNextMonth(); + clearExpiredSessions(); + if (!analyticalDBRepository.enabled()) { + return; + } + ChatSyncCheckpoint checkpoint = checkpointService.getOrCreate(ChatConstants.CHECKPOINT_SYNC_CODE_LOG, "default"); + YearMonth threshold = YearMonth.now().minusMonths(syncProperties.getRetentionMonths()); + for (int i = syncProperties.getRetentionMonths() + 1; i <= 24; i++) { + YearMonth month = YearMonth.now().minusMonths(i); + if (checkpoint.getCursorTable() != null && checkpoint.getCursorTable().compareTo("chat_log_" + month.toString().replace("-", "")) <= 0) { + continue; + } + if (month.isBefore(threshold) && tableManager.tableExists("chat_log_" + month.toString().replace("-", ""))) { + tableManager.dropMonthTable(month); + } + } + } + + @Override + public void startupCheck() { + tableManager.ensureCurrentAndNextMonth(); + if (analyticalDBRepository.enabled()) { + analyticalDBRepository.selfCheck(); + } + } + + private void clearExpiredSessions() { + Date expireBefore = Date.from(LocalDate.now() + .minusMonths(syncProperties.getRetentionMonths()) + .atStartOfDay(ZoneId.systemDefault()) + .toInstant()); + while (sessionRepository.deleteExpiredSessions(expireBefore, syncProperties.getBatchSize()) > 0) { + // loop until all expired session hot data is purged + } + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatlogRuntimeListener.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatlogRuntimeListener.java new file mode 100644 index 0000000..48041b4 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/ChatlogRuntimeListener.java @@ -0,0 +1,130 @@ +package tech.easyflow.chatlog.service.impl; + +import com.mybatisflex.core.keygen.impl.SnowFlakeIDKeyGenerator; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import tech.easyflow.chatlog.domain.command.ChatAppendMessageCommand; +import tech.easyflow.chatlog.domain.command.ChatSessionUpsertCommand; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.service.ChatPersistDispatcher; +import tech.easyflow.chatlog.service.ChatSessionQueryService; +import tech.easyflow.common.web.exceptions.BusinessException; +import tech.easyflow.core.runtime.ChatRuntimeContext; +import tech.easyflow.core.runtime.ChatRuntimeListener; +import tech.easyflow.core.runtime.ChatRuntimeMessage; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +@Component +@Order(100) +public class ChatlogRuntimeListener implements ChatRuntimeListener { + + private final SnowFlakeIDKeyGenerator idGenerator = new SnowFlakeIDKeyGenerator(); + + private final ChatPersistDispatcher chatPersistDispatcher; + private final ChatSessionQueryService chatSessionQueryService; + + public ChatlogRuntimeListener(ChatPersistDispatcher chatPersistDispatcher, + ChatSessionQueryService chatSessionQueryService) { + this.chatPersistDispatcher = chatPersistDispatcher; + this.chatSessionQueryService = chatSessionQueryService; + } + + @Override + public void onSessionPrepared(ChatRuntimeContext context) { + try { + ChatSessionUpsertCommand command = new ChatSessionUpsertCommand(); + command.setSessionId(context.getSessionId()); + command.setTenantId(defaultNumber(context.getTenantId())); + command.setDeptId(defaultNumber(context.getDeptId())); + command.setUserId(defaultNumber(context.getUserId())); + command.setUserAccount(context.getUserAccount()); + command.setAssistantId(defaultNumber(context.getAssistantId())); + command.setAssistantCode(context.getAssistantCode()); + command.setAssistantName(context.getAssistantName()); + command.setTitle(context.getSessionTitle()); + command.setOperatorId(defaultNumber(context.getUserId())); + chatPersistDispatcher.createOrTouchSession(command); + } catch (RuntimeException ex) { + throw persistFailed(ex); + } + } + + @Override + public void onUserMessage(ChatRuntimeContext context, ChatRuntimeMessage message) { + try { + chatPersistDispatcher.appendUserMessage(toAppendCommand(context, message)); + } catch (RuntimeException ex) { + throw persistFailed(ex); + } + } + + @Override + public void onAssistantCompleted(ChatRuntimeContext context, ChatRuntimeMessage message) { + try { + chatPersistDispatcher.appendAssistantMessage(toAppendCommand(context, message)); + } catch (RuntimeException ex) { + throw persistFailed(ex); + } + } + + @Override + public List loadMessages(ChatRuntimeContext context, int limit) { + if (context == null || context.getSessionId() == null || limit <= 0) { + return Collections.emptyList(); + } + List records = new ArrayList<>(chatSessionQueryService.getRecentTail(context.getSessionId(), limit)); + Collections.reverse(records); + List messages = new ArrayList<>(records.size()); + for (ChatMessageRecord record : records) { + if (record.getContentText() == null || record.getContentText().isBlank()) { + continue; + } + ChatRuntimeMessage message = new ChatRuntimeMessage(); + message.setMessageId(record.getId()); + message.setRole(record.getSenderRole()); + message.setContentType(record.getContentType()); + message.setContentText(record.getContentText()); + message.setContentPayload(record.getContentPayload()); + message.setCreatedAt(record.getCreated()); + message.setSenderId(record.getSenderId()); + message.setSenderName(record.getSenderName()); + messages.add(message); + } + return messages; + } + + private ChatAppendMessageCommand toAppendCommand(ChatRuntimeContext context, ChatRuntimeMessage message) { + ChatAppendMessageCommand command = new ChatAppendMessageCommand(); + command.setMessageId(message.getMessageId() == null ? BigInteger.valueOf(idGenerator.nextId()) : message.getMessageId()); + command.setTenantId(defaultNumber(context.getTenantId())); + command.setDeptId(defaultNumber(context.getDeptId())); + command.setSessionId(context.getSessionId()); + command.setUserId(defaultNumber(context.getUserId())); + command.setAssistantId(defaultNumber(context.getAssistantId())); + command.setSenderId(defaultNumber(message.getSenderId())); + command.setSenderName(message.getSenderName()); + command.setSenderRole(message.getRole()); + command.setContentType(message.getContentType()); + command.setContentText(message.getContentText()); + command.setContentPayload(message.getContentPayload()); + command.setCreatedBy(defaultNumber(context.getUserId())); + command.setCreated(message.getCreatedAt()); + return command; + } + + private BigInteger defaultNumber(BigInteger value) { + return value == null ? BigInteger.ZERO : value; + } + + private BusinessException persistFailed(RuntimeException ex) { + if (ex instanceof BusinessException businessException) { + return businessException; + } + return new BusinessException("聊天记录持久化失败,请稍后重试"); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java new file mode 100644 index 0000000..d4023f0 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/service/impl/PublicChatSessionRestoreServiceImpl.java @@ -0,0 +1,64 @@ +package tech.easyflow.chatlog.service.impl; + +import org.springframework.stereotype.Service; +import tech.easyflow.chatlog.config.ChatCacheProperties; +import tech.easyflow.chatlog.domain.dto.ChatMessageRecord; +import tech.easyflow.chatlog.domain.dto.ChatSessionSummary; +import tech.easyflow.chatlog.domain.dto.PublicChatSessionRestoreResult; +import tech.easyflow.chatlog.service.ChatSessionQueryService; +import tech.easyflow.chatlog.service.PublicChatSessionRestoreService; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +@Service +public class PublicChatSessionRestoreServiceImpl implements PublicChatSessionRestoreService { + + private final ChatSessionQueryService chatSessionQueryService; + private final ChatCacheProperties chatCacheProperties; + + public PublicChatSessionRestoreServiceImpl(ChatSessionQueryService chatSessionQueryService, + ChatCacheProperties chatCacheProperties) { + this.chatSessionQueryService = chatSessionQueryService; + this.chatCacheProperties = chatCacheProperties; + } + + @Override + public PublicChatSessionRestoreResult restoreSession(BigInteger userId, BigInteger assistantId, BigInteger sessionId, Integer limit) { + PublicChatSessionRestoreResult result = new PublicChatSessionRestoreResult(); + result.setConversationId(sessionId == null ? null : sessionId.toString()); + if (userId == null || assistantId == null || sessionId == null) { + return result; + } + + ChatSessionSummary summary = chatSessionQueryService.getSessionSummary(sessionId); + if (summary == null || Integer.valueOf(1).equals(summary.getIsDeleted())) { + return result; + } + if (!Objects.equals(summary.getUserId(), userId)) { + return result; + } + if (!Objects.equals(summary.getAssistantId(), assistantId)) { + return result; + } + + List tailMessages = new ArrayList<>(chatSessionQueryService.getRecentTail(sessionId, resolveLimit(limit))); + Collections.reverse(tailMessages); + + result.setSessionExists(true); + result.setSession(summary); + result.setMessages(tailMessages); + return result; + } + + private int resolveLimit(Integer limit) { + int defaultLimit = Math.max(chatCacheProperties.getTailSize(), 1); + if (limit == null || limit <= 0) { + return defaultLimit; + } + return Math.min(limit, defaultLimit); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatConstants.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatConstants.java new file mode 100644 index 0000000..4cf66be --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatConstants.java @@ -0,0 +1,15 @@ +package tech.easyflow.chatlog.support; + +public final class ChatConstants { + + public static final String SESSION_TABLE = "chat_session"; + public static final String CHAT_LOG_TEMPLATE = "chat_log_template"; + public static final String CHAT_LOG_PREFIX = "chat_log_"; + public static final String CHAT_PERSIST_TOPIC = "chat-persist"; + public static final String CHAT_PERSIST_GROUP = "chat-persist-group"; + public static final String CHECKPOINT_SYNC_CODE_SESSION = "chat_session_sync"; + public static final String CHECKPOINT_SYNC_CODE_LOG = "chat_log_sync"; + + private ChatConstants() { + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatJsonSupport.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatJsonSupport.java new file mode 100644 index 0000000..8818c60 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatJsonSupport.java @@ -0,0 +1,64 @@ +package tech.easyflow.chatlog.support; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Component; + +import java.util.Collections; +import java.util.Map; + +@Component +public class ChatJsonSupport { + + private final ObjectMapper objectMapper; + + public ChatJsonSupport(ObjectMapper objectMapper) { + this.objectMapper = objectMapper; + } + + public String toJson(Object value) { + if (value == null) { + return null; + } + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new IllegalStateException("JSON 序列化失败", e); + } + } + + public Map toMap(String json) { + if (json == null || json.isBlank()) { + return Collections.emptyMap(); + } + try { + return objectMapper.readValue(json, new TypeReference>() { + }); + } catch (JsonProcessingException e) { + throw new IllegalStateException("JSON 反序列化失败", e); + } + } + + public T fromJson(String json, Class clazz) { + if (json == null || json.isBlank()) { + return null; + } + try { + return objectMapper.readValue(json, clazz); + } catch (JsonProcessingException e) { + throw new IllegalStateException("JSON 反序列化失败", e); + } + } + + public T fromJson(String json, TypeReference typeReference) { + if (json == null || json.isBlank()) { + return null; + } + try { + return objectMapper.readValue(json, typeReference); + } catch (JsonProcessingException e) { + throw new IllegalStateException("JSON 反序列化失败", e); + } + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatTableRouter.java b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatTableRouter.java new file mode 100644 index 0000000..1ada9e1 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/main/java/tech/easyflow/chatlog/support/ChatTableRouter.java @@ -0,0 +1,29 @@ +package tech.easyflow.chatlog.support; + +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.time.YearMonth; +import java.time.ZoneId; +import java.util.Date; + +@Component +public class ChatTableRouter { + + public String resolveSessionTable() { + return ChatConstants.SESSION_TABLE; + } + + public String resolveLogTable(Date created) { + LocalDate localDate = created.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + return resolveLogTable(YearMonth.from(localDate)); + } + + public String resolveLogTable(YearMonth month) { + return ChatConstants.CHAT_LOG_PREFIX + month.toString().replace("-", ""); + } + + public YearMonth resolveLogMonth(Date created) { + return YearMonth.from(created.toInstant().atZone(ZoneId.systemDefault()).toLocalDate()); + } +} diff --git a/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/support/ChatTableRouterTest.java b/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/support/ChatTableRouterTest.java new file mode 100644 index 0000000..0556a38 --- /dev/null +++ b/easyflow-modules/easyflow-module-chatlog/src/test/java/tech/easyflow/chatlog/support/ChatTableRouterTest.java @@ -0,0 +1,21 @@ +package tech.easyflow.chatlog.support; + +import org.junit.Assert; +import org.junit.Test; + +import java.time.YearMonth; + +public class ChatTableRouterTest { + + private final ChatTableRouter router = new ChatTableRouter(); + + @Test + public void shouldResolveSessionTable() { + Assert.assertEquals("chat_session", router.resolveSessionTable()); + } + + @Test + public void shouldResolveMonthLogTable() { + Assert.assertEquals("chat_log_202604", router.resolveLogTable(YearMonth.of(2026, 4))); + } +} diff --git a/easyflow-modules/pom.xml b/easyflow-modules/pom.xml index 1eeb0f4..08764a3 100644 --- a/easyflow-modules/pom.xml +++ b/easyflow-modules/pom.xml @@ -15,6 +15,7 @@ easyflow-module-log easyflow-module-auth easyflow-module-autoconfig + easyflow-module-chatlog easyflow-module-ai easyflow-module-job easyflow-module-datacenter diff --git a/easyflow-starter/easyflow-starter-all/pom.xml b/easyflow-starter/easyflow-starter-all/pom.xml index 6e47031..95421d1 100644 --- a/easyflow-starter/easyflow-starter-all/pom.xml +++ b/easyflow-starter/easyflow-starter-all/pom.xml @@ -30,6 +30,10 @@ tech.easyflow easyflow-module-job + + tech.easyflow + easyflow-module-chatlog + tech.easyflow easyflow-module-ai diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/application-prod.yml b/easyflow-starter/easyflow-starter-all/src/main/resources/application-prod.yml index ec41fd3..5483297 100644 --- a/easyflow-starter/easyflow-starter-all/src/main/resources/application-prod.yml +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/application-prod.yml @@ -14,5 +14,47 @@ spring: password: ${REDIS_PASSWORD:123456} easyflow: + chat: + cache: + session-list-ttl: 300s + session-summary-ttl: 600s + session-tail-ttl: 1800s + tail-size: 50 + sync: + enabled: true + batch-size: 500 + fixed-delay: 30000 + repair-lookback-days: 3 + retention-months: 3 + mq: + enabled: true + type: redis + redis: + database: 1 + stream-prefix: easyflow:mq + chat-persist-shard-count: 4 + consumer-batch-size: 200 + consumer-block-timeout: 2000ms + pending-claim-idle: 60000ms + max-retry: 16 + analytical-db: + enabled: true + url: ${EASYFLOW_ANALYTICAL_DB_URL:jdbc:clickhouse://127.0.0.1:8123/easyflow?jdbc_ignore_unsupported_values=true&socket_timeout=30000&compress=false&ssl=false} + username: ${EASYFLOW_ANALYTICAL_DB_USERNAME:default} + password: ${EASYFLOW_ANALYTICAL_DB_PASSWORD:} + driver-class-name: com.clickhouse.jdbc.ClickHouseDriver + pool: + max-pool-size: 10 + min-idle: 1 + connection-timeout: 5000 + validation-timeout: 3000 + idle-timeout: 600000 + max-lifetime: 1800000 + flyway: + analytical-db: + locations: classpath:db/migration/analyticaldb + table: flyway_schema_history_analytical_db + baseline-on-migrate: false + validate-on-migrate: true storage: type: xFileStorage diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml index 1a2fbce..0ae352a 100644 --- a/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/application.yml @@ -23,7 +23,7 @@ spring: password: root flyway: enabled: true - locations: classpath:db/migration + locations: classpath:db/migration/mysql baseline-on-migrate: false validate-on-migrate: true clean-disabled: true @@ -79,6 +79,49 @@ easyflow: chat: # SSE 超时时间(毫秒),默认 10 分钟,可按需调整 sse-timeout-ms: 600000 + cache: + session-list-ttl: 300s + session-summary-ttl: 600s + session-tail-ttl: 1800s + tail-size: 50 + sync: + #是否启用分析数据同步,启用分析数据库后,此处应同步开启 + enabled: true + batch-size: 500 + fixed-delay: 30000 + repair-lookback-days: 3 + retention-months: 3 + mq: + enabled: true + type: redis + redis: + database: 1 + stream-prefix: easyflow:mq + chat-persist-shard-count: 4 + consumer-batch-size: 200 + consumer-block-timeout: 2000ms + pending-claim-idle: 60000ms + max-retry: 16 + analytical-db: + # 是否启用分析数据库 + enabled: true + url: jdbc:clickhouse://127.0.0.1:38123/easyflow?jdbc_ignore_unsupported_values=true&socket_timeout=30000&compress=false&ssl=false + username: easyflow + password: 123456 + driver-class-name: com.clickhouse.jdbc.ClickHouseDriver + pool: + max-pool-size: 10 + min-idle: 1 + connection-timeout: 5000 + validation-timeout: 3000 + idle-timeout: 600000 + max-lifetime: 1800000 + flyway: + analytical-db: + locations: classpath:db/migration/analyticaldb + table: flyway_schema_history_analytical_db + baseline-on-migrate: false + validate-on-migrate: true # 语音播放、识别服务(阿里云) audio: type: aliAudioService @@ -168,7 +211,7 @@ rag: searcher: # 搜索方式 默认lucene lucene: - indexDirPath: ./luceneKnowledge + indexDirPath: /Users/slience/data/easyflow/luceneKnowledge elastic: host: https://127.0.0.1:9200 userName: elastic diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V1__analyticaldb_ods.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V1__analyticaldb_ods.sql new file mode 100644 index 0000000..06c7a0a --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V1__analyticaldb_ods.sql @@ -0,0 +1,62 @@ +CREATE TABLE IF NOT EXISTS flyway_schema_history_analytical_db +( + `installed_rank` Int32, + `version` Nullable(String), + `description` String, + `type` String, + `script` String, + `checksum` Nullable(Int32), + `installed_by` String, + `installed_on` DateTime DEFAULT now(), + `execution_time` Int32, + `success` UInt8 +) +ENGINE = ReplacingMergeTree(installed_on) +ORDER BY installed_rank; + +CREATE TABLE IF NOT EXISTS ods_chat_session +( + `id` UInt64, + `tenant_id` UInt64, + `dept_id` UInt64, + `user_id` UInt64, + `user_account` String, + `assistant_id` UInt64, + `assistant_code` Nullable(String), + `assistant_name` Nullable(String), + `title` String, + `last_message_preview` Nullable(String), + `last_sender_id` Nullable(UInt64), + `last_sender_name` Nullable(String), + `message_count` UInt32, + `access_at` DateTime, + `last_message_at` Nullable(DateTime), + `created` DateTime, + `created_by` UInt64, + `modified` DateTime, + `modified_by` UInt64, + `is_deleted` UInt8 +) +ENGINE = ReplacingMergeTree(modified) +PARTITION BY toYYYYMM(access_at) +ORDER BY (id); + +CREATE TABLE IF NOT EXISTS ods_chat_log +( + `id` UInt64, + `session_id` UInt64, + `user_id` UInt64, + `assistant_id` UInt64, + `sender_id` Nullable(UInt64), + `sender_name` Nullable(String), + `sender_role` String, + `content_type` String, + `content_text` Nullable(String), + `content_payload` Nullable(String), + `created` DateTime, + `created_by` UInt64, + `sync_version` UInt64 +) +ENGINE = ReplacingMergeTree(sync_version) +PARTITION BY toYYYYMM(created) +ORDER BY (session_id, created, id); diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V2__analyticaldb_dws.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V2__analyticaldb_dws.sql new file mode 100644 index 0000000..1cc4307 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/analyticaldb/V2__analyticaldb_dws.sql @@ -0,0 +1,33 @@ +CREATE TABLE IF NOT EXISTS dws_chat_assistant_day +( + `stat_date` Date, + `dimension_id` UInt64, + `session_count` UInt64, + `message_count` UInt64 +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(stat_date) +ORDER BY (stat_date, dimension_id); + +CREATE TABLE IF NOT EXISTS dws_chat_user_day +( + `stat_date` Date, + `dimension_id` UInt64, + `session_count` UInt64, + `message_count` UInt64 +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(stat_date) +ORDER BY (stat_date, dimension_id); + +CREATE TABLE IF NOT EXISTS dws_chat_session_day +( + `stat_date` Date, + `dimension_id` UInt64, + `assistant_id` UInt64, + `user_id` UInt64, + `message_count` UInt64 +) +ENGINE = MergeTree +PARTITION BY toYYYYMM(stat_date) +ORDER BY (stat_date, dimension_id); diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V1__mysql_schema.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V1__mysql_schema.sql new file mode 100644 index 0000000..61009f0 --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V1__mysql_schema.sql @@ -0,0 +1,1182 @@ +SET NAMES utf8mb4; +SET +FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for tb_bot +-- ---------------------------- +CREATE TABLE `tb_bot` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键ID', + `alias` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '别名', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `category_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '分类ID', + `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `icon` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图标', + `model_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '模型 ID', + `model_options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '模型配置', + `status` int NULL DEFAULT 0 COMMENT '数据状态', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '选项', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建者ID', + `modified` datetime NULL DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '修改者ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `tb_ai_bot_alias_uindex`(`alias`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_bot_category +-- ---------------------------- +CREATE TABLE `tb_bot_category` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `category_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分类名称', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot分类' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_bot_document_collection +-- ---------------------------- +CREATE TABLE `tb_bot_document_collection` +( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `bot_id` bigint UNSIGNED NULL DEFAULT NULL, + `document_collection_id` bigint UNSIGNED NULL DEFAULT NULL, + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_bot_document_collection`(`bot_id`, `document_collection_id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 36 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的知识库' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for chat_session +-- ---------------------------- +CREATE TABLE `chat_session` +( + `id` bigint UNSIGNED NOT NULL COMMENT '会话ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `user_id` bigint UNSIGNED NOT NULL COMMENT '用户ID', + `user_account` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户账号', + `assistant_id` bigint UNSIGNED NOT NULL COMMENT '助手ID', + `assistant_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '助手编码', + `assistant_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '助手名称', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '会话标题', + `last_message_preview` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '最后消息预览', + `last_sender_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '最后发送方ID', + `last_sender_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '最后发送方名称', + `message_count` int NOT NULL DEFAULT 0 COMMENT '消息数量', + `access_at` datetime NOT NULL COMMENT '访问时间', + `last_message_at` datetime NULL DEFAULT NULL COMMENT '最后消息时间', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_user_deleted_access` (`user_id`, `is_deleted`, `access_at`, `id`) USING BTREE, + KEY `idx_user_assistant_deleted_access` (`user_id`, `assistant_id`, `is_deleted`, `access_at`, `id`) USING BTREE, + KEY `idx_modified` (`modified`, `id`) USING BTREE, + KEY `idx_access_at` (`access_at`, `id`) USING BTREE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天会话热数据表'; + +-- ---------------------------- +-- Table structure for chat_log_template +-- ---------------------------- +CREATE TABLE `chat_log_template` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `session_id` bigint UNSIGNED NOT NULL COMMENT '会话ID', + `user_id` bigint UNSIGNED NOT NULL COMMENT '用户ID', + `assistant_id` bigint UNSIGNED NOT NULL COMMENT '助手ID', + `sender_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '发送方ID', + `sender_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '发送方名称', + `sender_role` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '发送方角色', + `content_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '内容类型', + `content_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '文本内容', + `content_payload` json NULL COMMENT '结构化内容', + `created` datetime NOT NULL COMMENT '发送时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `sync_version` bigint UNSIGNED NOT NULL COMMENT '同步版本', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_session_created` (`session_id`, `created`, `id`) USING BTREE, + KEY `idx_user_created` (`user_id`, `created`, `id`) USING BTREE, + KEY `idx_assistant_created` (`assistant_id`, `created`, `id`) USING BTREE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天消息月表模板'; + +-- ---------------------------- +-- Table structure for chat_sync_checkpoint +-- ---------------------------- +CREATE TABLE `chat_sync_checkpoint` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `sync_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '同步编码', + `shard_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '同步游标键', + `cursor_table` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '游标表名', + `cursor_time` datetime NULL DEFAULT NULL COMMENT '游标时间', + `cursor_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '游标ID', + `last_success_time` datetime NULL DEFAULT NULL COMMENT '最后成功时间', + `last_batch_size` int NULL DEFAULT 0 COMMENT '最后批次大小', + `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'READY' COMMENT '状态', + `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注', + `modified` datetime NOT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uk_sync` (`sync_code`, `shard_key`) USING BTREE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天同步检查点'; + +-- ---------------------------- +-- Table structure for chat_persist_dead_letter +-- ---------------------------- +CREATE TABLE `chat_persist_dead_letter` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `topic` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'topic', + `stream_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'stream key', + `stream_message_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'stream message id', + `event_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '事件ID', + `session_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '会话ID', + `payload` json NULL COMMENT '事件负载', + `retry_count` int NOT NULL DEFAULT 0 COMMENT '重试次数', + `error_message` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '错误信息', + `first_failed_at` datetime NOT NULL COMMENT '首次失败时间', + `last_failed_at` datetime NOT NULL COMMENT '最后失败时间', + `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '状态', + `created` datetime NOT NULL COMMENT '创建时间', + `modified` datetime NOT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_chat_persist_dead_letter_event` (`event_id`) USING BTREE, + KEY `idx_chat_persist_dead_letter_session` (`session_id`) USING BTREE, + KEY `idx_chat_persist_dead_letter_status` (`status`, `last_failed_at`) USING BTREE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天持久化死信表'; + +-- ---------------------------- +-- Table structure for tb_bot_model +-- ---------------------------- +CREATE TABLE `tb_bot_model` +( + `id` bigint UNSIGNED NOT NULL, + `bot_id` bigint UNSIGNED NULL DEFAULT NULL, + `model_id` bigint UNSIGNED NULL DEFAULT NULL, + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的大模型' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_bot_plugin +-- ---------------------------- +CREATE TABLE `tb_bot_plugin` +( + `id` bigint UNSIGNED NOT NULL, + `bot_id` bigint UNSIGNED NULL DEFAULT NULL, + `plugin_item_id` bigint UNSIGNED NULL DEFAULT NULL, + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_bot_plugin`(`bot_id`, `plugin_item_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的插件' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_bot_recently_used +-- ---------------------------- +CREATE TABLE `tb_bot_recently_used` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `bot_id` bigint UNSIGNED NOT NULL COMMENT 'botId', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_bot_recently_used`(`created_by`, `bot_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '最近使用' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_bot_workflow +-- ---------------------------- +CREATE TABLE `tb_bot_workflow` +( + `id` bigint UNSIGNED NOT NULL, + `bot_id` bigint UNSIGNED NULL DEFAULT NULL, + `workflow_id` bigint UNSIGNED NULL DEFAULT NULL, + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_bot_workflow`(`bot_id`, `workflow_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'bot绑定的工作流' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_datacenter_table +-- ---------------------------- +CREATE TABLE `tb_datacenter_table` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `source_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '数据源ID', + `catalog_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '目录ID', + `table_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '数据表名', + `table_desc` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '数据表描述', + `actual_table` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '物理表名', + `table_kind` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'EXTERNAL_TABLE' COMMENT '表类型', + `access_mode` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'READ_ONLY' COMMENT '访问模式', + `materialized_table` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '物化表名', + `versioning_enabled` int NOT NULL DEFAULT 0 COMMENT '是否开启版本', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展项', + `capabilities_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '能力声明', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据中枢表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_datacenter_table_field +-- ---------------------------- +CREATE TABLE `tb_datacenter_table_field` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `table_id` bigint UNSIGNED NOT NULL COMMENT '数据表ID', + `field_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '字段名称', + `source_column_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '源字段名', + `field_desc` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '字段描述', + `field_type` int NOT NULL COMMENT '字段类型', + `jdbc_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'JDBC类型', + `precision` int NULL DEFAULT NULL COMMENT '精度', + `scale` int NULL DEFAULT NULL COMMENT '小数位', + `required` int NOT NULL DEFAULT 0 COMMENT '是否必填', + `queryable` int NOT NULL DEFAULT 1 COMMENT '可查询', + `sortable` int NOT NULL DEFAULT 1 COMMENT '可排序', + `writable` int NOT NULL DEFAULT 1 COMMENT '可写入', + `indexed` int NOT NULL DEFAULT 0 COMMENT '是否索引', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展项', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据中枢字段表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_document +-- ---------------------------- +CREATE TABLE `tb_document` +( + `id` bigint UNSIGNED NOT NULL, + `collection_id` bigint UNSIGNED NOT NULL COMMENT '知识库ID', + `document_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文档类型 pdf/word/aieditor 等', + `document_path` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文档路径', + `title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '标题', + `content` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '内容', + `content_type` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '内容类型', + `slug` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'URL 别名', + `order_no` int NULL DEFAULT NULL COMMENT '排序序号', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '其他配置项', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人ID', + `modified` datetime NULL DEFAULT NULL COMMENT '最后的修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '最后的修改人的ID', + PRIMARY KEY (`id`) USING BTREE, + INDEX `knowledge_id`(`collection_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '文档' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_document_chunk +-- ---------------------------- +CREATE TABLE `tb_document_chunk` +( + `id` bigint UNSIGNED NOT NULL, + `document_id` bigint UNSIGNED NOT NULL COMMENT '文档ID', + `document_collection_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '知识库ID', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '分块内容', + `sorting` int NULL DEFAULT NULL COMMENT '分割顺序', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展元信息', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '文档分块表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_document_collection +-- ---------------------------- +CREATE TABLE `tb_document_collection` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'Id', + `collection_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'DOCUMENT' COMMENT '知识库类型: DOCUMENT/FAQ', + `alias` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '别名', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `icon` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'ICON', + `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `slug` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'URL 别名', + `vector_store_enable` tinyint NULL DEFAULT NULL COMMENT '是否启用向量存储', + `vector_store_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '向量数据库类型', + `vector_store_collection` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '向量数据库集合', + `vector_store_config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '向量数据库配置', + `vector_embed_model_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT 'Embedding 模型ID', + `dimension_of_vector_model` int NULL DEFAULT NULL COMMENT '向量模型维度', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建用户ID', + `modified` datetime NULL DEFAULT NULL COMMENT '最后一次修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '最后一次修改用户ID', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '其他配置', + `rerank_model_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '重排模型id', + `search_engine_enable` tinyint NULL DEFAULT NULL COMMENT '是否启用搜索引擎', + `english_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '英文名称', + `category_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '分类ID', + `visibility_scope` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '可见范围', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `tb_ai_knowledge_alias_uindex`(`alias`) USING BTREE, + INDEX `idx_collection_type`(`collection_type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '知识库' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_document_collection_category +-- ---------------------------- +CREATE TABLE `tb_document_collection_category` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `category_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分类名称', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_faq_category +-- ---------------------------- +CREATE TABLE `tb_faq_category` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `collection_id` bigint UNSIGNED NOT NULL COMMENT '知识库ID', + `parent_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '父分类ID,0表示根', + `ancestors` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '0' COMMENT '祖先路径(逗号分隔)', + `level_no` tinyint UNSIGNED NOT NULL DEFAULT 1 COMMENT '层级(1-3)', + `category_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分类名称', + `sort_no` int NOT NULL DEFAULT 0 COMMENT '排序', + `is_default` tinyint NOT NULL DEFAULT 0 COMMENT '是否默认分类', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人', + `modified` datetime NULL DEFAULT NULL COMMENT '更新时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '更新人', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_faq_category_collection_parent_sort`(`collection_id`, `parent_id`, `sort_no`) USING BTREE, + INDEX `idx_faq_category_collection_level`(`collection_id`, `level_no`) USING BTREE, + INDEX `idx_faq_category_collection_status`(`collection_id`, `status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'FAQ分类' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_faq_item +-- ---------------------------- +CREATE TABLE `tb_faq_item` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `collection_id` bigint UNSIGNED NOT NULL COMMENT '知识库ID', + `category_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT 'FAQ分类ID', + `question` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '问题', + `answer_html` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '答案HTML', + `answer_text` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '答案纯文本', + `order_no` int NULL DEFAULT 0 COMMENT '排序', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展项', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人', + `modified` datetime NULL DEFAULT NULL COMMENT '更新时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '更新人', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_faq_collection_id`(`collection_id`) USING BTREE, + INDEX `idx_faq_collection_order`(`collection_id`, `order_no`) USING BTREE, + INDEX `idx_faq_collection_category_order`(`collection_id`, `category_id`, `order_no`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'FAQ条目' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_document_history +-- ---------------------------- +CREATE TABLE `tb_document_history` +( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `document_id` bigint NULL DEFAULT NULL COMMENT '修改的文档ID', + `old_title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '旧标题', + `new_title` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '新标题', + `old_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '旧内容', + `new_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '新内容', + `old_document_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '旧的文档类型', + `new_document_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '新的额文档类型', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint NULL DEFAULT NULL COMMENT '创建人ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '文档历史记录' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_model +-- ---------------------------- +CREATE TABLE `tb_model` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'ID', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `provider_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '供应商id', + `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '标题或名称', + `icon` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'ICON', + `description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `endpoint` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '大模型请求地址', + `request_path` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求路径', + `model_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '大模型名称', + `api_key` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '大模型 API KEY', + `extra_config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '大模型其他属性配置', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '其他配置内容', + `group_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '分组名称', + `model_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '模型类型: chatModel/embeddingModel/rerankModel/orc..', + `with_used` tinyint NULL DEFAULT NULL COMMENT '是否使用', + `support_thinking` tinyint NULL DEFAULT NULL COMMENT '是否支持推理', + `support_tool` tinyint NULL DEFAULT NULL COMMENT '是否支持工具', + `support_image` tinyint NULL DEFAULT NULL COMMENT '是否支持图片', + `support_image_b64_only` tinyint NULL DEFAULT NULL COMMENT '仅支持 base64 的图片类型', + `support_video` tinyint NULL DEFAULT NULL COMMENT '是否支持视频', + `support_audio` tinyint NULL DEFAULT NULL COMMENT '是否支持音频', + `support_free` tinyint NULL DEFAULT NULL COMMENT '是否免费', + `support_tool_message` tinyint NULL DEFAULT NULL COMMENT '是否支持tool消息', + `invoke_code` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '统一模型调用对外标识', + `publish_enabled` tinyint NULL DEFAULT 0 COMMENT '是否开启统一模型调用发布', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_model_invoke_code`(`invoke_code`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '大模型管理' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_model_provider +-- ---------------------------- +CREATE TABLE `tb_model_provider` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'id', + `provider_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '供应商名称', + `provider_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '不同的 client 实现,默认为 openai', + `icon` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图标', + `api_key` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'apiKey', + `endpoint` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'endPoint', + `chat_path` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '对话地址', + `embed_path` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '向量地址', + `rerank_path` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '重排路径', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '大模型供应商,比如 Aliyun/Gitee/火山引擎 等' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_plugin +-- ---------------------------- +CREATE TABLE `tb_plugin` +( + `id` bigint UNSIGNED NOT NULL COMMENT '插件id', + `alias` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '别名', + `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '名称', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `type` int NULL DEFAULT NULL COMMENT '类型', + `base_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '基础URL', + `auth_type` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '认证方式 【apiKey/none】', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '图标地址', + `position` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '认证参数位置 【headers, query】', + `headers` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求头', + `token_key` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'token键', + `token_value` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'token值', + `dept_id` bigint NULL DEFAULT NULL COMMENT '部门id', + `tenant_id` bigint NULL DEFAULT NULL COMMENT '租户id', + `created_by` bigint NULL DEFAULT NULL COMMENT '创建人', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `tb_ai_plugin_alias_uindex`(`alias`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '插件表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_plugin_category +-- ---------------------------- +CREATE TABLE `tb_plugin_category` +( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT, + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL, + `created_at` datetime NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 44 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '插件分类' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_plugin_category_mapping +-- ---------------------------- +CREATE TABLE `tb_plugin_category_mapping` +( + `category_id` bigint UNSIGNED NOT NULL, + `plugin_id` bigint UNSIGNED NOT NULL +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '插件分类关联表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_plugin_item +-- ---------------------------- +CREATE TABLE `tb_plugin_item` +( + `id` bigint UNSIGNED NOT NULL COMMENT '插件工具id', + `plugin_id` bigint UNSIGNED NOT NULL COMMENT '插件id', + `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '名称', + `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `base_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '基础路径', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `status` int NULL DEFAULT 0 COMMENT '是否启用', + `input_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输入参数', + `output_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输出参数', + `request_method` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '请求方式【Post, Get, Put, Delete】', + `service_status` int NULL DEFAULT NULL COMMENT '服务状态[0 下线 1 上线]', + `debug_status` int NULL DEFAULT NULL COMMENT '调试状态【0失败 1成功】', + `english_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '英文名称', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '插件工具表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_resource +-- ---------------------------- +CREATE TABLE `tb_resource` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `resource_type` int NOT NULL COMMENT '素材类型', + `resource_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '素材名称', + `suffix` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '后缀', + `resource_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '素材地址', + `origin` int NOT NULL DEFAULT 0 COMMENT '素材来源', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展项', + `file_size` bigint UNSIGNED NULL DEFAULT NULL COMMENT '文件大小', + `category_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '分类ID', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '素材库' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_resource_category +-- ---------------------------- +CREATE TABLE `tb_resource_category` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `category_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分类名称', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '素材分类' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_sys_account +-- ---------------------------- +CREATE TABLE `tb_sys_account` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `login_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '登录账号', + `password` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '密码', + `password_reset_required` tinyint NOT NULL DEFAULT 0 COMMENT '是否需要重置密码', + `account_type` tinyint NOT NULL DEFAULT 0 COMMENT '账户类型', + `nickname` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '昵称', + `mobile` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '手机电话', + `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '邮件', + `avatar` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '账户头像', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + `is_deleted` tinyint NOT NULL DEFAULT 0 COMMENT '逻辑删除标识:0未删除,1已删除', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_login_name`(`login_name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_account_position +-- ---------------------------- +CREATE TABLE `tb_sys_account_position` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `account_id` bigint UNSIGNED NOT NULL COMMENT '用户ID', + `position_id` bigint UNSIGNED NOT NULL COMMENT '职位ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_account_position`(`account_id`, `position_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户-职位表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_account_role +-- ---------------------------- +CREATE TABLE `tb_sys_account_role` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `account_id` bigint UNSIGNED NOT NULL COMMENT '用户ID', + `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_account_role`(`account_id`, `role_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户-角色表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_api_key +-- ---------------------------- +CREATE TABLE `tb_sys_api_key` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'id', + `api_key` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'apiKey', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `dept_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '部门id', + `tenant_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '租户id', + `expired_at` datetime NULL DEFAULT NULL COMMENT '失效时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_api_key`(`api_key`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = 'apikey表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_api_key_resource +-- ---------------------------- +CREATE TABLE `tb_sys_api_key_resource` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'id', + `request_interface` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '请求接口', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '标题', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE KEY `uni_api` (`request_interface`) +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '请求接口表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_sys_api_key_resource_mapping +-- ---------------------------- +CREATE TABLE `tb_sys_api_key_resource_mapping` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'id', + `api_key_id` bigint UNSIGNED NOT NULL COMMENT 'api_key_id', + `api_key_resource_id` bigint UNSIGNED NOT NULL COMMENT '请求接口资源访问id', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_api_key_resource`(`api_key_id`, `api_key_resource_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'apikey-请求接口表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_sys_dept +-- ---------------------------- +CREATE TABLE `tb_sys_dept` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `parent_id` bigint UNSIGNED NOT NULL COMMENT '父级ID', + `ancestors` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '父级部门ID集合', + `dept_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '部门名称', + `dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '部门编码', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '部门表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_dict +-- ---------------------------- +CREATE TABLE `tb_sys_dict` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '数据字典名称', + `code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '字典编码', + `description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '字典描述或备注', + `dict_type` tinyint NULL DEFAULT NULL COMMENT '字典类型 1 自定义字典、2 数据表字典、 3 枚举类字典、 4 系统字典(自定义 DictLoader)', + `sort_no` int NULL DEFAULT NULL COMMENT '排序编号', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展字典 存放 json', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `modified` datetime NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `key`(`code`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统字典表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_dict_item +-- ---------------------------- +CREATE TABLE `tb_sys_dict_item` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dict_id` bigint UNSIGNED NOT NULL COMMENT '归属哪个字典', + `text` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '名称或内容', + `value` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT '' COMMENT '值', + `description` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `sort_no` int NOT NULL DEFAULT 0 COMMENT '排序', + `css_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT 'css样式内容', + `css_class` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'css样式类名', + `remark` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `modified` datetime NULL DEFAULT NULL COMMENT '修改时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据字典内容' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_job +-- ---------------------------- +CREATE TABLE `tb_sys_job` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `job_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务名称', + `job_type` int NOT NULL COMMENT '任务类型', + `job_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '任务参数', + `cron_expression` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT 'cron表达式', + `allow_concurrent` int NOT NULL DEFAULT 0 COMMENT '是否并发执行', + `misfire_policy` int NOT NULL DEFAULT 3 COMMENT '错过策略', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '其他配置', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统任务表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_job_log +-- ---------------------------- +CREATE TABLE `tb_sys_job_log` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `job_id` bigint UNSIGNED NOT NULL COMMENT '任务ID', + `job_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务名称', + `job_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '任务参数', + `job_result` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '执行结果', + `error_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '错误信息', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `start_time` datetime NOT NULL COMMENT '开始时间', + `end_time` datetime NOT NULL COMMENT '结束时间', + `created` datetime NOT NULL COMMENT '创建时间', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统任务日志' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_log +-- ---------------------------- +CREATE TABLE `tb_sys_log` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'ID', + `account_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '操作人', + `action_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作名称', + `action_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作的类型', + `action_class` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作涉及的类', + `action_method` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作涉及的方法', + `action_url` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作涉及的 URL 地址', + `action_ip` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作涉及的用户 IP 地址', + `action_params` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '操作请求参数', + `action_body` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '操作请求body', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NULL DEFAULT NULL COMMENT '操作时间', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '操作日志表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_menu +-- ---------------------------- +CREATE TABLE `tb_sys_menu` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `parent_id` bigint UNSIGNED NOT NULL COMMENT '父菜单id', + `menu_type` int NOT NULL COMMENT '菜单类型', + `menu_title` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '菜单标题', + `menu_url` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '菜单url', + `component` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '组件路径', + `menu_icon` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '图标/图片地址', + `is_show` int NOT NULL DEFAULT 1 COMMENT '是否显示', + `permission_tag` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '权限标识', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_option +-- ---------------------------- +CREATE TABLE `tb_sys_option` +( + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '配置KEY', + `value` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '配置内容', + UNIQUE INDEX `uni_key`(`tenant_id`, `key`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统配置信息表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_position +-- ---------------------------- +CREATE TABLE `tb_sys_position` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `position_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '岗位名称', + `position_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '岗位编码', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '职位表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_role +-- ---------------------------- +CREATE TABLE `tb_sys_role` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `role_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色名称', + `role_key` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '角色标识', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '备注', + `data_scope` int NULL DEFAULT 1 COMMENT '数据权限(EnumDataScope)', + `menu_check_strictly` tinyint NULL DEFAULT 1 COMMENT '菜单树选择项是否关联显示', + `dept_check_strictly` tinyint NULL DEFAULT 1 COMMENT '部门树选择项是否关联显示', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_tenant_role`(`tenant_id`, `role_key`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统角色' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_role_dept +-- ---------------------------- +CREATE TABLE `tb_sys_role_dept` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_role_dept`(`role_id`, `dept_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-部门表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_role_menu +-- ---------------------------- +CREATE TABLE `tb_sys_role_menu` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', + `menu_id` bigint UNSIGNED NOT NULL COMMENT '菜单ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_role_menu`(`role_id`, `menu_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色-菜单表' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_workflow +-- ---------------------------- +CREATE TABLE `tb_workflow` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'ID 主键', + `alias` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '别名', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '标题', + `description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `icon` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'ICON', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '工作流设计的 JSON 内容', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建人', + `modified` datetime NULL DEFAULT NULL COMMENT '最后修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '最后修改的人', + `english_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '英文名称', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `category_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '分类ID', + `visibility_scope` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '可见范围', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `tb_ai_workflow_alias_uindex`(`alias`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_workflow_category +-- ---------------------------- +CREATE TABLE `tb_workflow_category` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `category_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '分类名称', + `sort_no` int NULL DEFAULT 0 COMMENT '排序', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建者', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL COMMENT '修改者', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流分类' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_workflow_exec_result +-- ---------------------------- +CREATE TABLE `tb_workflow_exec_result` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `exec_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '执行标识', + `workflow_id` bigint UNSIGNED NOT NULL COMMENT '工作流ID', + `title` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '标题', + `description` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `input` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输入', + `output` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输出', + `workflow_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '工作流执行时的配置', + `start_time` datetime(3) NOT NULL COMMENT '开始时间', + `end_time` datetime(3) NULL DEFAULT NULL COMMENT '结束时间', + `tokens` bigint UNSIGNED NULL DEFAULT NULL COMMENT '消耗总token', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `created_key` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '执行人标识[有可能是用户|外部|定时任务等情况]', + `created_by` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '执行人', + `error_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '错误信息', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_exec_key`(`exec_key`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '工作流执行记录' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_workflow_exec_step +-- ---------------------------- +CREATE TABLE `tb_workflow_exec_step` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `record_id` bigint UNSIGNED NOT NULL COMMENT '执行记录ID', + `exec_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '执行标识', + `node_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '节点ID', + `node_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '节点名称', + `input` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输入', + `output` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输出', + `node_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '节点信息', + `start_time` datetime(3) NOT NULL COMMENT '开始时间', + `end_time` datetime(3) NULL DEFAULT NULL COMMENT '结束时间', + `tokens` bigint UNSIGNED NULL DEFAULT NULL COMMENT '消耗总token', + `status` int NOT NULL DEFAULT 0 COMMENT '数据状态', + `error_info` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '错误信息', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_exec`(`exec_key`) USING BTREE, + INDEX `idx_record_id`(`record_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '执行记录步骤' ROW_FORMAT = DYNAMIC; + +-- ---------------------------- +-- Table structure for tb_sys_user_feedback +-- ---------------------------- +CREATE TABLE `tb_sys_user_feedback` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键id', + `feedback_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '问题摘要', + `feedback_type` int NOT NULL COMMENT '问题类型(1-功能故障 2-优化建议 3-账号问题 4-其他)', + `contact_info` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '联系方式【手机号/邮箱】', + `attachment_url` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '附件url', + `status` int NULL DEFAULT NULL COMMENT '反馈处理状态(0-未查看 1-已查看 2-已处理)', + `handler_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '处理人id', + `handle_time` datetime NULL DEFAULT NULL COMMENT '处理时间', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL COMMENT '创建人', + `modified` datetime NULL DEFAULT NULL COMMENT '最后修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '最后修改的人', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_mcp +-- ---------------------------- +CREATE TABLE `tb_mcp` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'id', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '标题', + `description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '描述', + `config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '完整MCP配置JSON', + `dept_id` bigint UNSIGNED NOT NULL COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL COMMENT '租户ID', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建者ID', + `modified` datetime NULL DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '修改者ID', + `status` tinyint NULL DEFAULT 0 COMMENT '是否启用', + PRIMARY KEY (`id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'mcp表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Table structure for tb_bot_mcp +-- ---------------------------- +CREATE TABLE `tb_bot_mcp` +( + `id` bigint UNSIGNED NOT NULL COMMENT 'id', + `bot_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT 'botId', + `mcp_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT 'mcpId', + `mcp_tool_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'mcp工具名称', + `mcp_tool_description` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'mcp工具描述', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_bot_mcp`(`bot_id`, `mcp_id`, `mcp_tool_name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +CREATE TABLE `tb_sys_role_category_scope` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `role_id` bigint UNSIGNED NOT NULL COMMENT '角色ID', + `resource_type` varchar(32) NOT NULL COMMENT '资源类型', + `scope_mode` varchar(16) NOT NULL COMMENT '范围模式', + `created` datetime NULL DEFAULT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '创建者', + `modified` datetime NULL DEFAULT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NULL DEFAULT NULL COMMENT '修改者', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_role_resource_type`(`role_id`, `resource_type`) USING BTREE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色分类权限范围'; + +CREATE TABLE `tb_sys_role_category_scope_item` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `scope_id` bigint UNSIGNED NOT NULL COMMENT '范围ID', + `category_id` bigint UNSIGNED NOT NULL COMMENT '分类ID', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uni_scope_category`(`scope_id`, `category_id`) USING BTREE +) ENGINE = InnoDB DEFAULT CHARSET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色分类权限明细'; + +CREATE TABLE `tb_datacenter_source` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID', + `source_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '数据源名称', + `source_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '数据源编码', + `source_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '数据源类型', + `access_mode` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'READ_ONLY' COMMENT '访问模式', + `builtin_flag` int NOT NULL DEFAULT 0 COMMENT '是否内置', + `driver_class_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '驱动类名', + `jdbc_url` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'JDBC URL', + `host` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '主机', + `port` int NULL DEFAULT NULL COMMENT '端口', + `database_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '数据库名', + `schema_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT 'Schema名', + `username` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名', + `credential_cipher` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '凭据密文', + `config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '连接配置', + `capabilities_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '能力声明', + `last_test_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '最近测试状态', + `last_test_message` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '最近测试信息', + `last_tested_at` datetime NULL DEFAULT NULL COMMENT '最近测试时间', + `status` int NOT NULL DEFAULT 0 COMMENT '状态', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展项', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '修改人', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_datacenter_source_tenant_type` (`tenant_id`, `source_type`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据中心数据源' ROW_FORMAT = DYNAMIC; + +CREATE TABLE `tb_datacenter_catalog` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID', + `source_id` bigint UNSIGNED NOT NULL COMMENT '数据源ID', + `catalog_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '目录名', + `catalog_desc` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '目录描述', + `catalog_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL DEFAULT 'DATABASE' COMMENT '目录类型', + `status` int NOT NULL DEFAULT 0 COMMENT '状态', + `options` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '扩展项', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '修改人', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_datacenter_catalog_source` (`source_id`, `catalog_name`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据中心逻辑库/命名空间' ROW_FORMAT = DYNAMIC; + +CREATE TABLE `tb_datacenter_dataset_version` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID', + `table_id` bigint UNSIGNED NOT NULL COMMENT '表ID', + `version_no` int NOT NULL DEFAULT 1 COMMENT '版本号', + `version_label` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '版本标签', + `materialized_table` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '物化表名', + `snapshot_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '版本快照', + `status` int NOT NULL DEFAULT 0 COMMENT '状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '修改人', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_dataset_version_table` (`table_id`, `version_no`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据集版本' ROW_FORMAT = DYNAMIC; + +CREATE TABLE `tb_datacenter_import_job` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID', + `source_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '数据源ID', + `catalog_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '目录ID', + `table_id` bigint UNSIGNED NULL DEFAULT NULL COMMENT '表ID', + `job_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务类型', + `file_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文件名', + `storage_path` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '文件存储路径', + `status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '任务状态', + `total_rows` bigint NULL DEFAULT NULL COMMENT '总行数', + `success_rows` bigint NULL DEFAULT NULL COMMENT '成功行数', + `error_rows` bigint NULL DEFAULT NULL COMMENT '失败行数', + `error_summary` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '错误摘要', + `payload_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '任务载荷', + `started_at` datetime NULL DEFAULT NULL COMMENT '开始时间', + `finished_at` datetime NULL DEFAULT NULL COMMENT '结束时间', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '修改人', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_import_job_source` (`source_id`, `status`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据中心导入任务' ROW_FORMAT = DYNAMIC; + +CREATE TABLE `tb_datacenter_derived_table` +( + `id` bigint UNSIGNED NOT NULL COMMENT '主键', + `dept_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '部门ID', + `tenant_id` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '租户ID', + `source_table_id` bigint UNSIGNED NOT NULL COMMENT '源表ID', + `derived_table_id` bigint UNSIGNED NOT NULL COMMENT '派生表ID', + `derive_type` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '派生类型', + `derive_config_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '派生配置', + `status` int NOT NULL DEFAULT 0 COMMENT '状态', + `created` datetime NOT NULL COMMENT '创建时间', + `created_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '创建人', + `modified` datetime NOT NULL COMMENT '修改时间', + `modified_by` bigint UNSIGNED NOT NULL DEFAULT 0 COMMENT '修改人', + PRIMARY KEY (`id`) USING BTREE, + KEY `idx_derived_table_source` (`source_table_id`, `derived_table_id`) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '数据中心派生表关系' ROW_FORMAT = DYNAMIC; + +SET +FOREIGN_KEY_CHECKS = 1; diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V2__mysql_seed.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V2__mysql_seed.sql new file mode 100644 index 0000000..67e879d --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V2__mysql_seed.sql @@ -0,0 +1,266 @@ +SET NAMES utf8mb4; + +-- ---------------------------- +-- Records of tb_sys_account +-- ---------------------------- +INSERT INTO `tb_sys_account` (`id`, `dept_id`, `tenant_id`, `login_name`, `password`, `password_reset_required`, `account_type`, `nickname`, `mobile`, `email`, `avatar`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (1, 1, 1000000, 'admin', '$2a$10$Kmro/PF/MXkcV.o2I7.GiOEx/Y8l/E78ZyGW8OUgldDC46Hi.yidm', 0, 99, '超级管理员', '15555555555', 'bbb@qq.com', 'https://static.agentscenter.cn/public/1/2025/12/17/684ea528-8e42-489c-b254-4e52e0679431/b.jpeg', 1, '2025-06-06 11:32:21', 1, '2025-12-17 17:51:16', 1, ''); + +-- ---------------------------- +-- Records of tb_sys_account_role +-- ---------------------------- +INSERT INTO `tb_sys_account_role` (`id`, `account_id`, `role_id`) VALUES (302654483522224128, 1, 1); + +-- ---------------------------- +-- Records of tb_sys_dept +-- ---------------------------- +INSERT INTO `tb_sys_dept` (`id`, `tenant_id`, `parent_id`, `ancestors`, `dept_name`, `dept_code`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (1, 1000000, 0, '0', '总公司', 'root_dept', 0, 1, '2025-03-17 09:09:57', 1, '2025-03-17 09:10:00', 1, ''); + +-- ---------------------------- +-- Records of tb_sys_menu +-- ---------------------------- +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (183724390000000001, 258052082618335232, 0, 'menus.system.sysPosition', '/sys/sysPosition', '/system/sysPosition/SysPositionList', 'svg:position', 1, '', 300, 1, '2026-01-05 09:07:06', 0, '2026-01-05 09:12:37', 1, '岗位管理菜单'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (183724390000000002, 183724390000000001, 1, '查询', '', '', '', 0, '/api/v1/sysPosition/query', 100, 1, '2026-01-05 09:07:06', 0, '2026-01-05 09:07:06', 0, '岗位管理-查询'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (183724390000000003, 183724390000000001, 1, '保存', '', '', '', 0, '/api/v1/sysPosition/save', 101, 1, '2026-01-05 09:07:06', 0, '2026-01-05 09:07:06', 0, '岗位管理-保存'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (183724390000000004, 183724390000000001, 1, '删除', '', '', '', 0, '/api/v1/sysPosition/remove', 102, 1, '2026-01-05 09:07:06', 0, '2026-01-05 09:07:06', 0, '岗位管理-删除'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (258052082618335232, 0, 0, 'menus.system.title', '/sys', '', 'ant-design:appstore-outlined', 1, '', 200, 0, '2025-03-14 15:07:51', 1, '2025-12-02 13:39:37', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (258052774330368000, 258052082618335232, 0, 'menus.system.sysAccount', '/sys/sysAccount', '/system/sysAccount/SysAccountList', 'svg:account', 1, '', 100, 0, '2025-03-14 15:10:36', 1, '2025-12-25 16:49:03', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (258075705244676096, 258052082618335232, 0, 'menus.system.sysRole', '/sys/sysRole', '/system/sysRole/SysRoleList', 'svg:role', 1, '', 400, 0, '2025-03-14 16:41:43', 1, '2025-12-25 16:49:10', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (258075850434703360, 258052082618335232, 0, 'menus.system.sysMenu', '/sys/sysMenu', '/system/sysMenu/SysMenuList', 'svg:menu', 1, '', 500, 0, '2025-03-14 16:42:18', 1, '2025-12-25 16:49:23', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259048038847483904, 258052082618335232, 0, 'menus.system.sysDept', '/sys/sysDept', '/system/sysDept/SysDeptList', 'svg:department', 1, '', 200, 0, '2025-03-17 09:05:25', 1, '2025-12-25 16:49:41', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259168916721754112, 258052082618335232, 0, 'menus.settings.settingsConfig', '/sys/settings', '/config/settings/Settings', 'svg:setting', 1, '', 1000, 0, '2025-03-17 17:05:45', 1, '2025-12-26 11:17:36', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259169318720626688, 258052082618335232, 0, 'menus.system.sysLog', '/sys/logs', '/system/sysLog/SysLogList', 'svg:log', 1, '', 600, 0, '2025-03-17 17:07:21', 1, '2025-12-25 16:49:59', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259169837824466944, 259169540360232960, 0, 'menus.ai.bots', '/ai/bots', '/ai/bots/index', 'svg:talk', 1, '', 11, 0, '2025-03-17 17:09:24', 1, '2026-01-06 10:46:41', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259169982154661888, 0, 0, 'menus.ai.plugin', '/ai/plugin', '/ai/plugin/Plugin', 'svg:plugin', 1, '', 21, 0, '2025-03-17 17:09:59', 1, '2025-12-26 11:14:29', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259170117110587392, 0, 0, 'menus.ai.workflow', '/ai/workflow', '/ai/workflow/WorkflowList', 'svg:workflow', 1, '', 31, 0, '2025-03-17 17:10:31', 1, '2025-12-26 11:35:41', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259170422338478080, 0, 0, 'menus.ai.documentCollection', '/ai/documentCollection', '/ai/documentCollection/DocumentCollection', 'svg:knowledge', 1, '', 51, 0, '2025-03-17 17:11:44', 1, '2025-12-26 11:14:42', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (259170538264846336, 0, 0, 'menus.ai.model', '/ai/model', '/ai/model/Model', 'svg:llm', 1, '', 61, 0, '2025-03-17 17:12:11', 1, '2025-12-26 11:15:16', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (270761213536096256, 258052082618335232, 0, 'menus.settings.apiKey', '/sys/sysApiKey', '/config/apikey/SysApiKey', 'svg:api', 1, '', 800, 0, '2025-04-18 16:49:24', 1, '2025-12-26 11:17:49', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (282254669269082112, 258052082618335232, 0, 'menus.system.sysJob', '/sys/sysJob', '/system/sysJob/SysJobList', 'svg:time', 1, '', 700, 0, '2025-05-20 10:00:17', 1, '2025-12-25 16:49:50', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243919118950400, 258052774330368000, 1, '查询', '', '', '', 0, '/api/v1/sysAccount/query', 1, 0, '2025-07-03 12:55:51', 1, '2025-07-03 12:55:51', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243919727124480, 258052774330368000, 1, '保存', '', '', '', 0, '/api/v1/sysAccount/save', 1, 0, '2025-07-03 12:55:51', 1, '2025-07-03 12:55:51', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243920205275136, 258052774330368000, 1, '删除', '', '', '', 0, '/api/v1/sysAccount/remove', 1, 0, '2025-07-03 12:55:51', 1, '2025-07-03 12:55:51', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243920679231488, 258075705244676096, 1, '查询', '', '', '', 0, '/api/v1/sysRole/query', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243921161576448, 258075705244676096, 1, '保存', '', '', '', 0, '/api/v1/sysRole/save', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243921639727104, 258075705244676096, 1, '删除', '', '', '', 0, '/api/v1/sysRole/remove', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243922155626496, 258075850434703360, 1, '查询', '', '', '', 0, '/api/v1/sysMenu/query', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243922637971456, 258075850434703360, 1, '保存', '', '', '', 0, '/api/v1/sysMenu/save', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243923116122112, 258075850434703360, 1, '删除', '', '', '', 0, '/api/v1/sysMenu/remove', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243923627827200, 259048038847483904, 1, '查询', '', '', '', 0, '/api/v1/sysDept/query', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243924105977856, 259048038847483904, 1, '保存', '', '', '', 0, '/api/v1/sysDept/save', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243924571545600, 259048038847483904, 1, '删除', '', '', '', 0, '/api/v1/sysDept/remove', 1, 0, '2025-07-03 12:55:52', 1, '2025-07-03 12:55:52', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243926450593792, 259168916721754112, 1, '查询', '', '', '', 0, '/api/v1/sysOption/query', 1, 0, '2025-07-03 12:55:53', 1, '2025-07-03 12:55:53', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243926920355840, 259168916721754112, 1, '保存', '', '', '', 0, '/api/v1/sysOption/save', 1, 0, '2025-07-03 12:55:53', 1, '2025-07-03 12:55:53', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243927385923584, 259168916721754112, 1, '删除', '', '', '', 0, '/api/v1/sysOption/remove', 1, 0, '2025-07-03 12:55:53', 1, '2025-07-03 12:55:53', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243929294331904, 259169318720626688, 1, '查询', '', '', '', 0, '/api/v1/sysLog/query', 1, 0, '2025-07-03 12:55:54', 1, '2025-07-03 12:55:54', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243929755705344, 259169318720626688, 1, '保存', '', '', '', 0, '/api/v1/sysLog/save', 1, 0, '2025-07-03 12:55:54', 1, '2025-07-03 12:55:54', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243930225467392, 259169318720626688, 1, '删除', '', '', '', 0, '/api/v1/sysLog/remove', 1, 0, '2025-07-03 12:55:54', 1, '2025-07-03 12:55:54', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243932104515584, 259169837824466944, 1, '查询', '', '', '', 0, '/api/v1/bot/query', 1, 0, '2025-07-03 12:55:55', 1, '2025-11-20 16:10:04', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243932607832064, 259169837824466944, 1, '保存', '', '', '', 0, '/api/v1/bot/save', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243933077594112, 259169837824466944, 1, '删除', '', '', '', 0, '/api/v1/bot/remove', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243933547356160, 259169982154661888, 1, '查询', '', '', '', 0, '/api/v1/plugin/query', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243934025506816, 259169982154661888, 1, '保存', '', '', '', 0, '/api/v1/plugin/save', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243934495268864, 259169982154661888, 1, '删除', '', '', '', 0, '/api/v1/plugin/remove', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243934960836608, 259170117110587392, 1, '查询', '', '', '', 0, '/api/v1/workflow/query', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243935430598656, 259170117110587392, 1, '保存', '', '', '', 0, '/api/v1/workflow/save', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243935904555008, 259170117110587392, 1, '删除', '', '', '', 0, '/api/v1/workflow/remove', 1, 0, '2025-07-03 12:55:55', 1, '2025-07-03 12:55:55', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243936374317056, 259170422338478080, 1, '查询', '', '', '', 0, '/api/v1/documentCollection/query', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243936848273408, 259170422338478080, 1, '保存', '', '', '', 0, '/api/v1/documentCollection/save', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243937313841152, 259170422338478080, 1, '删除', '', '', '', 0, '/api/v1/documentCollection/remove', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243937783603200, 259170538264846336, 1, '查询', '', '', '', 0, '/api/v1/model/query', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243938240782336, 259170538264846336, 1, '保存', '', '', '', 0, '/api/v1/model/save', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243938706350080, 259170538264846336, 1, '删除', '', '', '', 0, '/api/v1/model/remove', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243939180306432, 270761213536096256, 1, '查询', '', '', '', 0, '/api/v1/sysApiKey/query', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243939650068480, 270761213536096256, 1, '保存', '', '', '', 0, '/api/v1/sysApiKey/save', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243940115636224, 270761213536096256, 1, '删除', '', '', '', 0, '/api/v1/sysApiKey/remove', 1, 0, '2025-07-03 12:55:56', 1, '2025-07-03 12:55:56', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243940614758400, 282254669269082112, 1, '查询', '', '', '', 0, '/api/v1/sysJob/query', 1, 0, '2025-07-03 12:55:57', 1, '2025-07-03 12:55:57', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243941080326144, 282254669269082112, 1, '保存', '', '', '', 0, '/api/v1/sysJob/save', 1, 0, '2025-07-03 12:55:57', 1, '2025-07-03 12:55:57', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (298243941545893888, 282254669269082112, 1, '删除', '', '', '', 0, '/api/v1/sysJob/remove', 1, 0, '2025-07-03 12:55:57', 1, '2025-07-03 12:55:57', 1, 'gen'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (300008008381800448, 0, 0, 'menus.ai.resources', '/ai/resource', '/ai/resource/ResourceList', 'svg:resource', 1, '', 52, 0, '2025-07-08 09:45:43', 1, '2025-12-26 11:37:42', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (300008359986110464, 300008008381800448, 1, '查询', '', '', '', 0, '/api/v1/resource/query', 0, 0, '2025-07-08 09:47:07', 1, '2025-07-08 09:47:07', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (300012644643815424, 300008008381800448, 1, '保存', '', '', '', 0, '/api/v1/resource/save', 0, 0, '2025-07-08 10:04:08', 1, '2025-07-08 10:04:08', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (300013092268326912, 300008008381800448, 1, '删除', '', '', '', 1, '/api/v1/resource/remove', 0, 0, '2025-07-08 10:05:55', 1, '2025-07-08 10:05:55', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (300817858217091072, 0, 0, 'menus.ai.datacenter', '/datacenter', '/datacenter/DatacenterWorkspace', 'svg:data-center', 1, '', 53, 0, '2025-07-10 15:23:46', 1, '2025-12-26 11:15:08', 1, '数据中心统一数据接入平台'); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (363168956276838400, 258052082618335232, 0, 'menus.system.sysFeedback', '/sys/sysFeedback', '/system/sysFeedback/sysFeedbackList', 'svg:user-feedback', 1, '', 900, 0, '2025-12-29 16:44:47', 1, '2025-12-29 16:45:39', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (363435797225017344, 363168956276838400, 1, '删除', '', '', '', 0, '/api/v1/sysUserFeedback/remove', 3, 0, '2025-12-30 10:25:06', 1, '2025-12-30 10:26:15', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (363435949415337984, 363168956276838400, 1, '保存', '', '', '', 0, '/api/v1/sysUserFeedback/save', 2, 0, '2025-12-30 10:25:43', 1, '2025-12-30 10:26:10', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (363533849449447424, 363168956276838400, 1, '查询', '', '', '', 0, '/api/v1/sysUserFeedback/query', 1, 0, '2025-12-30 16:54:44', 1, '2025-12-30 16:54:44', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (365312682481553408, 0, 0, 'menus.ai.mcp', '/ai/mcp', '/ai/mcp/Mcp', 'svg:mcp', 1, '', 62, 0, '2026-01-04 14:43:11', 1, '2026-01-06 10:03:08', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (365314158356467712, 365312682481553408, 1, '查询', '', '', '', 0, '/api/v1/mcp/query', 1, 0, '2026-01-04 14:49:03', 1, '2026-01-04 14:49:03', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (365314258952654848, 365312682481553408, 1, '保存', '', '', '', 0, '/api/v1/mcp/save', 2, 0, '2026-01-04 14:49:27', 1, '2026-01-04 14:49:27', 1, ''); +INSERT INTO `tb_sys_menu` (`id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`) VALUES (365314364238073856, 365312682481553408, 1, '删除', '', '', '', 0, '/api/v1/mcp/remove', 3, 0, '2026-01-04 14:49:52', 1, '2026-01-04 14:49:52', 1, ''); + +-- ---------------------------- +-- Records of tb_sys_role +-- ---------------------------- +INSERT INTO `tb_sys_role` (`id`, `tenant_id`, `role_name`, `role_key`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark`, `data_scope`, `menu_check_strictly`, `dept_check_strictly`) VALUES (1, 1000000, '超级管理员', 'super_admin', 1, '2025-03-14 14:52:37', 1, '2025-03-14 14:52:37', 1, '', 1, 0, 0); + +-- ---------------------------- +-- Records of tb_sys_role_menu +-- ---------------------------- +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (183724390000000005, 1, 183724390000000001); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (183724390000000006, 1, 183724390000000002); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (183724390000000007, 1, 183724390000000003); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (183724390000000008, 1, 183724390000000004); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259111372649250817, 1, 258052774330368000); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259111372649250818, 1, 258075705244676096); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259111372649250819, 1, 258075850434703360); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259111372649250822, 1, 259048038847483904); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259111372649250825, 1, 258052082618335232); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259168916826611712, 1, 259168916721754112); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259169318829678592, 1, 259169318720626688); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259169837941907456, 1, 259169837824466944); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259169982280491008, 1, 259169982154661888); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259170117223833600, 1, 259170117110587392); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259170422447529984, 1, 259170422338478080); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (259170538378092544, 1, 259170538264846336); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (270761213603205120, 1, 270761213536096256); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (282254669390716928, 1, 282254669269082112); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243919488049152, 1, 298243919118950400); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243919966199808, 1, 298243919727124480); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243920444350464, 1, 298243920205275136); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243920926695424, 1, 298243920679231488); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243921404846080, 1, 298243921161576448); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243921908162560, 1, 298243921639727104); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243922398896128, 1, 298243922155626496); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243922877046784, 1, 298243922637971456); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243923367780352, 1, 298243923116122112); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243923866902528, 1, 298243923627827200); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243924336664576, 1, 298243924105977856); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243924810620928, 1, 298243924571545600); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243926689669120, 1, 298243926450593792); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243927155236864, 1, 298243926920355840); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243927620804608, 1, 298243927385923584); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243929525018624, 1, 298243929294331904); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243929990586368, 1, 298243929755705344); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243930460348416, 1, 298243930225467392); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243932368756736, 1, 298243932104515584); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243932846907392, 1, 298243932607832064); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243933312475136, 1, 298243933077594112); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243933786431488, 1, 298243933547356160); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243934260387840, 1, 298243934025506816); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243934730149888, 1, 298243934495268864); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243935199911936, 1, 298243934960836608); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243935669673984, 1, 298243935430598656); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243936139436032, 1, 298243935904555008); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243936621780992, 1, 298243936374317056); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243937083154432, 1, 298243936848273408); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243937552916480, 1, 298243937313841152); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243938010095616, 1, 298243937783603200); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243938475663360, 1, 298243938240782336); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243938949619712, 1, 298243938706350080); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243939415187456, 1, 298243939180306432); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243939884949504, 1, 298243939650068480); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243940358905856, 1, 298243940115636224); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243940845445120, 1, 298243940614758400); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243941315207168, 1, 298243941080326144); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (298243941780774912, 1, 298243941545893888); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (300008008490852352, 1, 300008008381800448); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (300008360078385152, 1, 300008359986110464); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (300012644702535680, 1, 300012644643815424); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (300013092310269952, 1, 300013092268326912); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (300817858284199936, 1, 300817858217091072); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (363168956335558656, 1, 363168956276838400); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (363533849537527808, 1, 363533849449447424); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (365312682603188224, 1, 365312682481553408); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (365314158469713920, 1, 365314158356467712); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (365314259057512448, 1, 365314258952654848); +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) VALUES (365314364334542848, 1, 365314364238073856); + + +-- ---------------------------- +-- Records of tb_model_provider +-- ---------------------------- +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (359110667376132096, '硅基流动', 'siliconflow', '', '', 'https://api.siliconflow.cn', '/v1/chat/completions', '/v1/embeddings', '/v1/rerank', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (359110690079899648, 'Ollama', 'ollama', '', '', 'http://127.0.0.1:11434', '/v1/chat/completions', '/api/embed', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (359111120310632448, 'DeepSeek', 'deepseek', '', '', 'https://api.deepseek.com', '/chat/completions', '', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (359111228158771200, 'OpenAI', 'openai', '', '', 'https://api.openai.com', '/v1/chat/completions', '/v1/embeddings', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (359111448204541952, '阿里百炼', 'aliyun', '', '', 'https://dashscope.aliyuncs.com', '/compatible-mode/v1/chat/completions', '/compatible-mode/v1/embeddings', '/api/v1/services/rerank/text-rerank/text-rerank', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000001, '智谱', 'zhipu', '', '', 'https://open.bigmodel.cn', '/api/paas/v4/chat/completions', '/api/paas/v4/embeddings', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000002, 'MiniMax', 'minimax', '', '', 'https://api.minimax.io', '/v1/chat/completions', '', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000003, 'Kimi', 'kimi', '', '', 'https://api.moonshot.cn', '/v1/chat/completions', '', '', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); +INSERT INTO `tb_model_provider` (`id`, `provider_name`, `provider_type`, `icon`, `api_key`, `endpoint`, `chat_path`, `embed_path`, `rerank_path`, `created`, `created_by`, `modified`, `modified_by`) VALUES (366100000000000004, '自部署', 'self-hosted', '', '', 'http://127.0.0.1:8000', '/v1/chat/completions', '/v1/embeddings', '/v1/score', '2026-03-10 10:00:00', 1, '2026-03-10 10:00:00', 1); + +-- ---------------------------- +-- Records of tb_sys_option +-- ---------------------------- + +INSERT INTO `tb_mcp` (`id`, `title`, `description`, `config_json`, `dept_id`, `tenant_id`, `created`, `created_by`, `modified`, `modified_by`, `status`) VALUES (365597368948781056, '测试everything', 'MCP测试功能', '{ + \"mcpServers\": { + \"everything\": { + \"command\": \"npx\", + \"args\": [ + \"-y\", + \"@modelcontextprotocol/server-everything\" + ] + } + } +}', 1, 1000000, '2026-01-06 09:57:07', 1, '2026-01-06 09:57:07', 1, 0); +INSERT INTO `tb_mcp` (`id`, `title`, `description`, `config_json`, `dept_id`, `tenant_id`, `created`, `created_by`, `modified`, `modified_by`, `status`) VALUES (365956218142994432, '12306购票综合查询', '12306购票综合查询', '{ + \"mcpServers\": { + \"12306-mcp\": { + \"command\": \"npx\", + \"args\": [ + \"-y\", + \"12306-mcp\" + ] + } + } +}', 1, 1000000, '2026-01-06 09:56:44', 1, '2026-01-06 09:56:44', 1, 0); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) VALUES ( + 366200000000000001, 0, 0, 'menus.dashboard.workspace', '/dashboard/workspace', '/dashboard/workspace/index', + 'carbon:workspace', 1, '', 1, 0, '2026-03-24 10:00:00', 1, '2026-03-24 10:00:00', 1, '管理员工作台' +); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) VALUES ( + 366200000000000002, 366200000000000001, 1, '查询', '', '', '', + 0, '/api/v1/dashboard/query', 1, 0, '2026-03-24 10:00:00', 1, '2026-03-24 10:00:00', 1, '工作台统计查询权限' +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +VALUES (366200000000000101, 1, 366200000000000001), + (366200000000000103, 1, 366200000000000002); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, `menu_icon`, + `is_show`, `permission_tag`, `sort_no`, `status`, `created`, `created_by`, `modified`, `modified_by`, `remark` +) VALUES ( + 366200000000000003, 0, 0, '聊天历史', '/ai/chat-history', '/ai/chatHistory/index', + 'svg:chat-history', 1, '', 63, 0, '2026-04-04 17:00:00', 1, '2026-04-04 17:00:00', 1, '管理端聊天历史菜单' +); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +VALUES (366200000000000104, 1, 366200000000000003); + +INSERT INTO `tb_sys_api_key_resource` (`id`, `request_interface`, `title`) +VALUES + (366700000000000001, '/v1/chat/completions', '统一模型调用(OpenAI Chat Completions)'), + (366700000000000002, '/public-api/bot/chat', '外链聊天助手调用'); + +INSERT INTO `tb_sys_role_category_scope` (`id`, `role_id`, `resource_type`, `scope_mode`, `created`, `created_by`, `modified`, `modified_by`) +VALUES + (366800000000000001, 1, 'BOT', 'ALL', NOW(), 1, NOW(), 1), + (366800000000000002, 1, 'PLUGIN', 'ALL', NOW(), 1, NOW(), 1), + (366800000000000003, 1, 'WORKFLOW', 'ALL', NOW(), 1, NOW(), 1), + (366800000000000004, 1, 'KNOWLEDGE', 'ALL', NOW(), 1, NOW(), 1), + (366800000000000005, 1, 'RESOURCE', 'ALL', NOW(), 1, NOW(), 1); + +INSERT INTO `tb_sys_menu` ( + `id`, `parent_id`, `menu_type`, `menu_title`, `menu_url`, `component`, + `menu_icon`, `is_show`, `permission_tag`, `sort_no`, `status`, + `created`, `created_by`, `modified`, `modified_by`, `remark` +) +VALUES + (366300000000000003, 300817858217091072, 1, '查询', '', '', '', 0, '/api/v1/datacenterSource/query', 1, 0, '2026-03-24 10:00:00', 1, '2026-03-24 10:00:00', 1, '数据源接入-查询'), + (366300000000000004, 300817858217091072, 1, '保存', '', '', '', 0, '/api/v1/datacenterSource/save', 2, 0, '2026-03-24 10:00:00', 1, '2026-03-24 10:00:00', 1, '数据源接入-保存'); + +INSERT INTO `tb_sys_role_menu` (`id`, `role_id`, `menu_id`) +VALUES + (366300000000000103, 1, 366300000000000003), + (366300000000000104, 1, 366300000000000004); diff --git a/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V3__mysql_quartz.sql b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V3__mysql_quartz.sql new file mode 100644 index 0000000..80d899d --- /dev/null +++ b/easyflow-starter/easyflow-starter-all/src/main/resources/db/migration/mysql/V3__mysql_quartz.sql @@ -0,0 +1,162 @@ +SET FOREIGN_KEY_CHECKS = 0; + + +CREATE TABLE TB_QRTZ_JOB_DETAILS( +SCHED_NAME VARCHAR(120) NOT NULL, +JOB_NAME VARCHAR(190) NOT NULL, +JOB_GROUP VARCHAR(190) NOT NULL, +DESCRIPTION VARCHAR(250) NULL, +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 BLOB NULL, +PRIMARY KEY (SCHED_NAME,JOB_NAME,JOB_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_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, +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(190) NULL, +MISFIRE_INSTR SMALLINT NULL, +JOB_DATA BLOB NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +CONSTRAINT FK_TB_QRTZ_TRIGGERS_JOB_DETAILS FOREIGN KEY (SCHED_NAME,JOB_NAME,JOB_GROUP) +REFERENCES TB_QRTZ_JOB_DETAILS(SCHED_NAME,JOB_NAME,JOB_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_SIMPLE_TRIGGERS ( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(190) NOT NULL, +TRIGGER_GROUP VARCHAR(190) 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), +CONSTRAINT FK_TB_QRTZ_SIMPLE_TRIGGERS_TRIGGERS FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +REFERENCES TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_CRON_TRIGGERS ( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(190) NOT NULL, +TRIGGER_GROUP VARCHAR(190) NOT NULL, +CRON_EXPRESSION VARCHAR(120) NOT NULL, +TIME_ZONE_ID VARCHAR(80), +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +CONSTRAINT FK_TB_QRTZ_CRON_TRIGGERS_TRIGGERS FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +REFERENCES TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_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, + 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 VARCHAR(1) NULL, + BOOL_PROP_2 VARCHAR(1) NULL, + PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), + CONSTRAINT FK_TB_QRTZ_SIMPROP_TRIGGERS_TRIGGERS FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) + REFERENCES TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_BLOB_TRIGGERS ( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_NAME VARCHAR(190) NOT NULL, +TRIGGER_GROUP VARCHAR(190) NOT NULL, +BLOB_DATA BLOB NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP), +INDEX (SCHED_NAME,TRIGGER_NAME, TRIGGER_GROUP), +CONSTRAINT FK_TB_QRTZ_BLOB_TRIGGERS_TRIGGERS FOREIGN KEY (SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP) +REFERENCES TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_CALENDARS ( +SCHED_NAME VARCHAR(120) NOT NULL, +CALENDAR_NAME VARCHAR(190) NOT NULL, +CALENDAR BLOB NOT NULL, +PRIMARY KEY (SCHED_NAME,CALENDAR_NAME)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_PAUSED_TRIGGER_GRPS ( +SCHED_NAME VARCHAR(120) NOT NULL, +TRIGGER_GROUP VARCHAR(190) NOT NULL, +PRIMARY KEY (SCHED_NAME,TRIGGER_GROUP)) +ENGINE=InnoDB; + +CREATE TABLE TB_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 BIGINT NOT NULL, +SCHED_TIME BIGINT NOT NULL, +PRIORITY INTEGER NOT NULL, +STATE VARCHAR(16) NOT NULL, +JOB_NAME VARCHAR(190) NULL, +JOB_GROUP VARCHAR(190) NULL, +IS_NONCONCURRENT VARCHAR(1) NULL, +REQUESTS_RECOVERY VARCHAR(1) NULL, +PRIMARY KEY (SCHED_NAME,ENTRY_ID)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_SCHEDULER_STATE ( +SCHED_NAME VARCHAR(120) NOT NULL, +INSTANCE_NAME VARCHAR(190) NOT NULL, +LAST_CHECKIN_TIME BIGINT NOT NULL, +CHECKIN_INTERVAL BIGINT NOT NULL, +PRIMARY KEY (SCHED_NAME,INSTANCE_NAME)) +ENGINE=InnoDB; + +CREATE TABLE TB_QRTZ_LOCKS ( +SCHED_NAME VARCHAR(120) NOT NULL, +LOCK_NAME VARCHAR(40) NOT NULL, +PRIMARY KEY (SCHED_NAME,LOCK_NAME)) +ENGINE=InnoDB; + +CREATE INDEX IDX_TB_QRTZ_J_REQ_RECOVERY ON TB_QRTZ_JOB_DETAILS(SCHED_NAME,REQUESTS_RECOVERY); +CREATE INDEX IDX_TB_QRTZ_J_GRP ON TB_QRTZ_JOB_DETAILS(SCHED_NAME,JOB_GROUP); + +CREATE INDEX IDX_TB_QRTZ_T_J ON TB_QRTZ_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); +CREATE INDEX IDX_TB_QRTZ_T_JG ON TB_QRTZ_TRIGGERS(SCHED_NAME,JOB_GROUP); +CREATE INDEX IDX_TB_QRTZ_T_C ON TB_QRTZ_TRIGGERS(SCHED_NAME,CALENDAR_NAME); +CREATE INDEX IDX_TB_QRTZ_T_G ON TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); +CREATE INDEX IDX_TB_QRTZ_T_STATE ON TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE); +CREATE INDEX IDX_TB_QRTZ_T_N_STATE ON TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP,TRIGGER_STATE); +CREATE INDEX IDX_TB_QRTZ_T_N_G_STATE ON TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_GROUP,TRIGGER_STATE); +CREATE INDEX IDX_TB_QRTZ_T_NEXT_FIRE_TIME ON TB_QRTZ_TRIGGERS(SCHED_NAME,NEXT_FIRE_TIME); +CREATE INDEX IDX_TB_QRTZ_T_NFT_ST ON TB_QRTZ_TRIGGERS(SCHED_NAME,TRIGGER_STATE,NEXT_FIRE_TIME); +CREATE INDEX IDX_TB_QRTZ_T_NFT_MISFIRE ON TB_QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME); +CREATE INDEX IDX_TB_QRTZ_T_NFT_ST_MISFIRE ON TB_QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_STATE); +CREATE INDEX IDX_TB_QRTZ_T_NFT_ST_MISFIRE_GRP ON TB_QRTZ_TRIGGERS(SCHED_NAME,MISFIRE_INSTR,NEXT_FIRE_TIME,TRIGGER_GROUP,TRIGGER_STATE); + +CREATE INDEX IDX_TB_QRTZ_FT_TRIG_INST_NAME ON TB_QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME); +CREATE INDEX IDX_TB_QRTZ_FT_INST_JOB_REQ_RCVRY ON TB_QRTZ_FIRED_TRIGGERS(SCHED_NAME,INSTANCE_NAME,REQUESTS_RECOVERY); +CREATE INDEX IDX_TB_QRTZ_FT_J_G ON TB_QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_NAME,JOB_GROUP); +CREATE INDEX IDX_TB_QRTZ_FT_JG ON TB_QRTZ_FIRED_TRIGGERS(SCHED_NAME,JOB_GROUP); +CREATE INDEX IDX_TB_QRTZ_FT_T_G ON TB_QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_NAME,TRIGGER_GROUP); +CREATE INDEX IDX_TB_QRTZ_FT_TG ON TB_QRTZ_FIRED_TRIGGERS(SCHED_NAME,TRIGGER_GROUP); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/pom.xml b/pom.xml index 2cec0c7..bd21262 100644 --- a/pom.xml +++ b/pom.xml @@ -26,6 +26,8 @@ 4.9.3 3.5.9 10.20.1 + 10.20.0 + 0.7.1 2.0.17 4.0.3 2.0.57 @@ -142,6 +144,13 @@ 8.3.0 + + com.clickhouse + clickhouse-jdbc + ${clickhouse.version} + all + + com.mybatis-flex mybatis-flex-codegen @@ -255,6 +264,11 @@ flyway-mysql ${flyway.version} + + org.flywaydb + flyway-database-clickhouse + ${flyway.clickhouse.version} + com.alibaba @@ -304,6 +318,11 @@ easyflow-common-ai ${revision} + + tech.easyflow + easyflow-common-analytical-db + ${revision} + tech.easyflow easyflow-common-all @@ -354,6 +373,11 @@ easyflow-common-chat-protocol ${revision} + + tech.easyflow + easyflow-common-mq + ${revision} + @@ -376,6 +400,11 @@ easyflow-module-log ${revision} + + tech.easyflow + easyflow-module-chatlog + ${revision} + tech.easyflow easyflow-module-system