feat: 落地聊天记录异步持久化基础设施
- 新增 chatlog 模块、AnalyticalDB 公共层与 common-mq Redis Streams 实现 - 建立 Redis 热态、MySQL 热数据、AnalyticalDB 历史查询与同步链路 - 收紧聊天记录幂等、摘要时序与持久化失败语义
This commit is contained in:
@@ -11,6 +11,10 @@
|
|||||||
<artifactId>easyflow-common-all</artifactId>
|
<artifactId>easyflow-common-all</artifactId>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-ai</artifactId>
|
<artifactId>easyflow-common-ai</artifactId>
|
||||||
@@ -23,6 +27,10 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-cache</artifactId>
|
<artifactId>easyflow-common-cache</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-common-file-storage</artifactId>
|
<artifactId>easyflow-common-file-storage</artifactId>
|
||||||
|
|||||||
48
easyflow-commons/easyflow-common-analytical-db/pom.xml
Normal file
48
easyflow-commons/easyflow-common-analytical-db/pom.xml
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-commons</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>easyflow-common-analytical-db</name>
|
||||||
|
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-base</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.clickhouse</groupId>
|
||||||
|
<artifactId>clickhouse-jdbc</artifactId>
|
||||||
|
<classifier>all</classifier>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-database-clickhouse</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
<T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args);
|
||||||
|
|
||||||
|
<T> T queryOne(String sql, Class<T> requiredType, Object... args);
|
||||||
|
|
||||||
|
<T> T queryOne(String sql, RowMapper<T> rowMapper, Object... args);
|
||||||
|
|
||||||
|
<T> List<T> queryForList(String sql, Class<T> elementType, Object... args);
|
||||||
|
|
||||||
|
int update(String sql, Object... args);
|
||||||
|
|
||||||
|
<T> int[][] batchUpdate(String sql, List<T> items, int batchSize, ParameterizedPreparedStatementSetter<T> setter);
|
||||||
|
|
||||||
|
<T> AnalyticalDBPageResult<T> page(String countSql,
|
||||||
|
Object[] countArgs,
|
||||||
|
String dataSql,
|
||||||
|
Object[] dataArgs,
|
||||||
|
AnalyticalDBPageRequest pageRequest,
|
||||||
|
RowMapper<T> rowMapper);
|
||||||
|
}
|
||||||
@@ -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 <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||||
|
assertAvailable();
|
||||||
|
return jdbcTemplate.query(sql, rowMapper, args);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T queryOne(String sql, Class<T> requiredType, Object... args) {
|
||||||
|
assertAvailable();
|
||||||
|
try {
|
||||||
|
return jdbcTemplate.queryForObject(sql, requiredType, args);
|
||||||
|
} catch (EmptyResultDataAccessException ex) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> T queryOne(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||||
|
assertAvailable();
|
||||||
|
List<T> rows = jdbcTemplate.query(sql, rowMapper, args);
|
||||||
|
return rows.isEmpty() ? null : rows.get(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> List<T> queryForList(String sql, Class<T> 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 <T> int[][] batchUpdate(String sql, List<T> items, int batchSize, ParameterizedPreparedStatementSetter<T> setter) {
|
||||||
|
assertAvailable();
|
||||||
|
if (items == null || items.isEmpty()) {
|
||||||
|
return new int[0][0];
|
||||||
|
}
|
||||||
|
return jdbcTemplate.batchUpdate(sql, items, Math.max(batchSize, 1), setter);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public <T> AnalyticalDBPageResult<T> page(String countSql,
|
||||||
|
Object[] countArgs,
|
||||||
|
String dataSql,
|
||||||
|
Object[] dataArgs,
|
||||||
|
AnalyticalDBPageRequest pageRequest,
|
||||||
|
RowMapper<T> rowMapper) {
|
||||||
|
assertAvailable();
|
||||||
|
AnalyticalDBPageRequest request = pageRequest == null ? new AnalyticalDBPageRequest() : pageRequest;
|
||||||
|
Long total = jdbcTemplate.queryForObject(countSql, Long.class, nullSafeArgs(countArgs));
|
||||||
|
List<T> 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<T> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package tech.easyflow.common.analyticaldb.page;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class AnalyticalDBPageResult<T> {
|
||||||
|
|
||||||
|
private long total;
|
||||||
|
private int pageNumber;
|
||||||
|
private int pageSize;
|
||||||
|
private List<T> 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<T> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecords(List<T> records) {
|
||||||
|
this.records = records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<AnalyticalDBOperations> analyticalDBOperationsProvider;
|
||||||
|
private final AnalyticalDBFlywayProperties flywayProperties;
|
||||||
|
|
||||||
|
public AnalyticalDBHealthSupport(ObjectProvider<AnalyticalDBOperations> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<Map<String, Object>> 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<String, Object> chain = findToolChain(id, name);
|
||||||
|
chain.put("status", "TOOL_CALL");
|
||||||
|
chain.put("result", arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendToolResult(String id, String name, Object result) {
|
||||||
|
Map<String, Object> chain = findToolChain(id, name);
|
||||||
|
chain.put("status", "TOOL_RESULT");
|
||||||
|
chain.put("result", result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getContent() {
|
||||||
|
return content.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> buildPayload() {
|
||||||
|
Map<String, Object> payload = new LinkedHashMap<>();
|
||||||
|
List<Map<String, Object>> payloadChains = new ArrayList<>();
|
||||||
|
if (reasoning.length() > 0) {
|
||||||
|
Map<String, Object> 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<String, Object> findToolChain(String id, String name) {
|
||||||
|
for (Map<String, Object> chain : chains) {
|
||||||
|
if (String.valueOf(chain.get("id")).equals(id)) {
|
||||||
|
if (name != null && !name.isEmpty()) {
|
||||||
|
chain.put("name", name);
|
||||||
|
}
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Map<String, Object> chain = new LinkedHashMap<>();
|
||||||
|
chain.put("id", id);
|
||||||
|
chain.put("name", name);
|
||||||
|
chains.add(chain);
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package tech.easyflow.core.runtime;
|
||||||
|
|
||||||
|
public enum ChatChannel {
|
||||||
|
|
||||||
|
ADMIN,
|
||||||
|
USER_CENTER,
|
||||||
|
PUBLIC_API
|
||||||
|
}
|
||||||
@@ -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<String> attachments = new ArrayList<>();
|
||||||
|
private Map<String, Object> 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<String> getAttachments() {
|
||||||
|
return attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachments(List<String> attachments) {
|
||||||
|
this.attachments = attachments == null ? new ArrayList<>() : attachments;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Map<String, Object> getExt() {
|
||||||
|
return ext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setExt(Map<String, Object> ext) {
|
||||||
|
this.ext = ext == null ? new LinkedHashMap<>() : ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit);
|
||||||
|
}
|
||||||
@@ -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<String, Object> 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<String, Object> getContentPayload() {
|
||||||
|
return contentPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentPayload(Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatRuntimeListener> listenerProvider;
|
||||||
|
|
||||||
|
public CompositeChatRuntimeManager(ObjectProvider<ChatRuntimeListener> 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<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||||
|
for (ChatRuntimeListener listener : listenerProvider.orderedStream().toList()) {
|
||||||
|
try {
|
||||||
|
List<ChatRuntimeMessage> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
easyflow-commons/easyflow-common-mq/pom.xml
Normal file
26
easyflow-commons/easyflow-common-mq/pom.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-commons</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>easyflow-common-mq</name>
|
||||||
|
<artifactId>easyflow-common-mq</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -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<MQDeadLetterHandler> handlersProvider) {
|
||||||
|
List<MQDeadLetterHandler> 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<MQConsumerHandler> handlersProvider) {
|
||||||
|
List<MQConsumerHandler> handlers = handlersProvider.orderedStream().toList();
|
||||||
|
return new RedisMQConsumerContainer(
|
||||||
|
mqRedisResources.connectionFactory(),
|
||||||
|
mqRedisResources.stringRedisTemplate(),
|
||||||
|
mqProperties,
|
||||||
|
mqMessageConverter,
|
||||||
|
mqDeadLetterService,
|
||||||
|
redisStreamKeySupport,
|
||||||
|
handlers
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface MQAcknowledger {
|
||||||
|
|
||||||
|
void acknowledge(List<MQMessage> messages);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
public interface MQConsumerContainer {
|
||||||
|
|
||||||
|
boolean isRunning();
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public interface MQConsumerHandler {
|
||||||
|
|
||||||
|
MQSubscription subscription();
|
||||||
|
|
||||||
|
void handle(List<MQMessage> messages) throws Exception;
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
public interface MQDeadLetterHandler {
|
||||||
|
|
||||||
|
boolean supports(String topic);
|
||||||
|
|
||||||
|
void handle(MQMessage message, String reason);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
public interface MQDeadLetterService {
|
||||||
|
|
||||||
|
void deadLetter(MQMessage message, String reason);
|
||||||
|
}
|
||||||
@@ -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<String, String> 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<String, String> getHeaders() {
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHeaders(Map<String, String> headers) {
|
||||||
|
this.headers = headers == null ? new LinkedHashMap<>() : new LinkedHashMap<>(headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
public interface MQMessageConverter {
|
||||||
|
|
||||||
|
String serialize(MQMessage message);
|
||||||
|
|
||||||
|
MQMessage deserialize(String payload);
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package tech.easyflow.common.mq.core;
|
||||||
|
|
||||||
|
public interface MQProducer {
|
||||||
|
|
||||||
|
String send(MQMessage message);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MQConsumerHandler> 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<MQConsumerHandler> 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<MapRecord<String, Object, Object>> 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<MQMessage> 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<RecordId> 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<MQMessage> toMessages(String streamKey, List<MapRecord<String, Object, Object>> records) {
|
||||||
|
List<MQMessage> messages = new ArrayList<>(records.size());
|
||||||
|
for (MapRecord<String, Object, Object> 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<MQMessage> 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<MQMessage> 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<MQMessage> 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MQDeadLetterHandler> handlers;
|
||||||
|
|
||||||
|
public RedisMQDeadLetterService(StringRedisTemplate stringRedisTemplate,
|
||||||
|
MQMessageConverter messageConverter,
|
||||||
|
RedisStreamKeySupport keySupport,
|
||||||
|
List<MQDeadLetterHandler> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 不可用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,8 @@
|
|||||||
|
|
||||||
<modules>
|
<modules>
|
||||||
<module>easyflow-common-all</module>
|
<module>easyflow-common-all</module>
|
||||||
|
<module>easyflow-common-analytical-db</module>
|
||||||
|
<module>easyflow-common-mq</module>
|
||||||
<module>easyflow-common-web</module>
|
<module>easyflow-common-web</module>
|
||||||
<module>easyflow-common-captcha</module>
|
<module>easyflow-common-captcha</module>
|
||||||
<module>easyflow-common-base</module>
|
<module>easyflow-common-base</module>
|
||||||
|
|||||||
56
easyflow-modules/easyflow-module-chatlog/pom.xml
Normal file
56
easyflow-modules/easyflow-module-chatlog/pom.xml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<parent>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-modules</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<name>easyflow-module-chatlog</name>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-chat-protocol</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-analytical-db</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.mybatis-flex</groupId>
|
||||||
|
<artifactId>mybatis-flex-spring-boot3-starter</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-base</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-cache</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-common-mq</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-jdbc</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.fasterxml.jackson.core</groupId>
|
||||||
|
<artifactId>jackson-databind</artifactId>
|
||||||
|
<version>${jackson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>junit</groupId>
|
||||||
|
<artifactId>junit</artifactId>
|
||||||
|
<version>${junit.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</project>
|
||||||
@@ -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<List<ChatMessageRecord>> 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<BigInteger> listSessionIds(BigInteger userId, long offset, long limit) {
|
||||||
|
if (userId == null || limit <= 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Set<String> values = stringRedisTemplate.opsForZSet()
|
||||||
|
.reverseRange(keySessionIndex(userId), offset, offset + limit - 1);
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<BigInteger> 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<ChatSessionSummary> getSessionSummaries(List<BigInteger> sessionIds) {
|
||||||
|
if (sessionIds == null || sessionIds.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<String> keys = sessionIds.stream().map(this::keySessionSummary).toList();
|
||||||
|
List<String> values = stringRedisTemplate.opsForValue().multiGet(keys);
|
||||||
|
if (values == null || values.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
Map<BigInteger, ChatSessionSummary> 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<ChatSessionSummary> 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<ChatSessionSummary> sessions) {
|
||||||
|
if (sessions == null || sessions.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<BigInteger> 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<ChatMessageRecord> getSessionTail(BigInteger sessionId) {
|
||||||
|
return readList(keySessionTail(sessionId), TAIL_TYPE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSessionTail(BigInteger sessionId, List<ChatMessageRecord> records) {
|
||||||
|
writeValue(keySessionTail(sessionId), trimTail(records), properties.getSessionTailTtl());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void appendTail(ChatMessageRecord record) {
|
||||||
|
if (record == null || record.getSessionId() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<ChatMessageRecord> current = getSessionTail(record.getSessionId());
|
||||||
|
List<ChatMessageRecord> 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<ChatMessageRecord> trimTail(List<ChatMessageRecord> records) {
|
||||||
|
if (records == null || records.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
int maxSize = Math.max(properties.getTailSize(), 1);
|
||||||
|
int size = Math.min(records.size(), maxSize);
|
||||||
|
List<ChatMessageRecord> 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> T readValue(String key, Class<T> 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> T readList(String key, TypeReference<T> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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<String, Object> 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<String, Object> getContentPayload() {
|
||||||
|
return contentPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentPayload(Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatMessageRecord> 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<ChatMessageRecord> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecords(List<ChatMessageRecord> records) {
|
||||||
|
this.records = records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> 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<String, Object> getContentPayload() {
|
||||||
|
return contentPayload;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContentPayload(Map<String, Object> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatSessionSummary> 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<ChatSessionSummary> getRecords() {
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecords(List<ChatSessionSummary> records) {
|
||||||
|
this.records = records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String> 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<String> getTouchedDates() {
|
||||||
|
return touchedDates;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTouchedDates(List<String> touchedDates) {
|
||||||
|
this.touchedDates = touchedDates;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatMessageRecord> 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<ChatMessageRecord> getMessages() {
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessages(List<ChatMessageRecord> messages) {
|
||||||
|
this.messages = messages == null ? new ArrayList<>() : messages;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatPersistDeadLetter> {
|
||||||
|
}
|
||||||
@@ -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<ChatSyncCheckpoint> {
|
||||||
|
}
|
||||||
@@ -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<AnalyticalDBOperations> 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<ChatSessionSummary> 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<ChatMessageRecord> 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<ChatMessageRecord> 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<Object> countArgs = new java.util.ArrayList<>();
|
||||||
|
StringBuilder countSql = new StringBuilder("SELECT COUNT(1) FROM ods_chat_session FINAL WHERE is_deleted=0");
|
||||||
|
List<Object> 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<ChatSessionSummary> 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<LocalDate> 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<String> findHistoryTables() {
|
||||||
|
if (!enabled()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
return analyticalDBOperations.query("SELECT name FROM system.tables WHERE database = currentDatabase()",
|
||||||
|
(rs, rowNum) -> rs.getString(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParameterizedPreparedStatementSetter<ChatSessionSummary> 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<ChatMessageRecord> 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<Object> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatAppendMessageCommand> appendMessages(List<ChatAppendMessageCommand> commands) {
|
||||||
|
if (commands == null || commands.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<ChatAppendMessageCommand> inserted = new ArrayList<>();
|
||||||
|
Map<YearMonth, List<ChatAppendMessageCommand>> 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<YearMonth, List<ChatAppendMessageCommand>> 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<ChatMessageRecord> listRecentTail(BigInteger sessionId, List<YearMonth> months, int limit) {
|
||||||
|
List<ChatMessageRecord> records = new ArrayList<>();
|
||||||
|
for (YearMonth month : months) {
|
||||||
|
String table = tableRouter.resolveLogTable(month);
|
||||||
|
List<ChatMessageRecord> 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<ChatMessageRecord> 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<ChatMessageRecord> loadRepairRows(List<YearMonth> months, Date startTime) {
|
||||||
|
if (months.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<ChatMessageRecord> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<YearMonth> listRecentExistingMonths(int retentionMonths) {
|
||||||
|
List<YearMonth> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatSessionUpsertCommand> 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<ChatSessionSummaryCommand> 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<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
String table = tableRouter.resolveSessionTable();
|
||||||
|
List<Object> 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<Object> 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<ChatSessionSummary> 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<ChatSessionSummary> 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<ChatSessionSummary> findBySessionIds(List<BigInteger> 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<ChatSessionSummary> sessions = jdbcTemplate.query(
|
||||||
|
"SELECT * FROM `" + table + "` WHERE id IN (" + placeholders + ") AND is_deleted=0",
|
||||||
|
sessionRowMapper(),
|
||||||
|
sessionIds.toArray()
|
||||||
|
);
|
||||||
|
Map<BigInteger, ChatSessionSummary> sessionMap = new LinkedHashMap<>();
|
||||||
|
for (ChatSessionSummary session : sessions) {
|
||||||
|
sessionMap.put(session.getId(), session);
|
||||||
|
}
|
||||||
|
List<ChatSessionSummary> 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<ChatSessionRenamePayload> 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<ChatSessionDeletePayload> 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<ChatSessionSummary> 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<ChatSessionSummary> 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<MQMessage> messages) {
|
||||||
|
List<ChatPersistEvent> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatPersistEvent> events) {
|
||||||
|
if (events == null || events.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<BigInteger, ChatSessionUpsertCommand> sessionUpserts = new LinkedHashMap<>();
|
||||||
|
List<ChatAppendMessageCommand> appendCommands = new ArrayList<>();
|
||||||
|
Map<BigInteger, ChatSessionSummaryCommand> summaryCommands = new LinkedHashMap<>();
|
||||||
|
List<ChatSessionRenamePayload> renamePayloads = new ArrayList<>();
|
||||||
|
List<ChatSessionDeletePayload> deletePayloads = new ArrayList<>();
|
||||||
|
Set<YearMonth> 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<ChatAppendMessageCommand> 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<BigInteger, ChatSessionSummaryCommand> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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<ChatSessionSummary> 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<ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit);
|
||||||
|
}
|
||||||
@@ -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> {
|
||||||
|
|
||||||
|
ChatSyncCheckpoint getOrCreate(String syncCode, String shardKey);
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatSessionSummary> listSessions(BigInteger userId, BigInteger assistantId, ChatPageQuery query) {
|
||||||
|
if (assistantId == null) {
|
||||||
|
List<BigInteger> sessionIds = chatHotStateService.listSessionIds(userId, query.getOffset(), query.getPageSize());
|
||||||
|
if (!sessionIds.isEmpty()) {
|
||||||
|
List<ChatSessionSummary> cached = chatHotStateService.getSessionSummaries(sessionIds);
|
||||||
|
if (cached.size() == sessionIds.size()) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<ChatSessionSummary> sessions = sessionRepository.listSessions(userId, null, query);
|
||||||
|
chatHotStateService.cacheSessionSummaries(sessions);
|
||||||
|
return sessions;
|
||||||
|
}
|
||||||
|
List<ChatSessionSummary> 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<BigInteger> sessionIds = chatHotStateService.listSessionIds(userId, query.getOffset(), query.getPageSize());
|
||||||
|
if (sessionIds.isEmpty()) {
|
||||||
|
page.setTotal(chatHotStateService.countSessions(userId));
|
||||||
|
page.setRecords(List.of());
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
List<ChatSessionSummary> 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<ChatMessageRecord> getRecentTail(BigInteger sessionId, int limit) {
|
||||||
|
List<ChatMessageRecord> cached = chatHotStateService.getSessionTail(sessionId);
|
||||||
|
if (cached != null) {
|
||||||
|
return cached.subList(0, Math.min(limit, cached.size()));
|
||||||
|
}
|
||||||
|
List<ChatMessageRecord> records = logRepository.listRecentTail(sessionId, tableManager.listRecentExistingMonths(3), limit);
|
||||||
|
chatHotStateService.setSessionTail(sessionId, records);
|
||||||
|
return records;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatSyncCheckpointMapper, ChatSyncCheckpoint>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatSessionSummary> 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<YearMonth> months = tableManager.listRecentExistingMonths(syncProperties.getRetentionMonths());
|
||||||
|
Set<LocalDate> 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<ChatMessageRecord> 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<ChatMessageRecord> rows = logRepository.loadRepairRows(
|
||||||
|
tableManager.listRecentExistingMonths(syncProperties.getRetentionMonths()),
|
||||||
|
startTime
|
||||||
|
);
|
||||||
|
analyticalDBRepository.appendLogs(rows);
|
||||||
|
Set<LocalDate> 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatRuntimeMessage> loadMessages(ChatRuntimeContext context, int limit) {
|
||||||
|
if (context == null || context.getSessionId() == null || limit <= 0) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
List<ChatMessageRecord> records = new ArrayList<>(chatSessionQueryService.getRecentTail(context.getSessionId(), limit));
|
||||||
|
Collections.reverse(records);
|
||||||
|
List<ChatRuntimeMessage> 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("聊天记录持久化失败,请稍后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<ChatMessageRecord> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<String, Object> toMap(String json) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, new TypeReference<Map<String, Object>>() {
|
||||||
|
});
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("JSON 反序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T fromJson(String json, Class<T> clazz) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, clazz);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("JSON 反序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public <T> T fromJson(String json, TypeReference<T> typeReference) {
|
||||||
|
if (json == null || json.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return objectMapper.readValue(json, typeReference);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalStateException("JSON 反序列化失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
<module>easyflow-module-log</module>
|
<module>easyflow-module-log</module>
|
||||||
<module>easyflow-module-auth</module>
|
<module>easyflow-module-auth</module>
|
||||||
<module>easyflow-module-autoconfig</module>
|
<module>easyflow-module-autoconfig</module>
|
||||||
|
<module>easyflow-module-chatlog</module>
|
||||||
<module>easyflow-module-ai</module>
|
<module>easyflow-module-ai</module>
|
||||||
<module>easyflow-module-job</module>
|
<module>easyflow-module-job</module>
|
||||||
<module>easyflow-module-datacenter</module>
|
<module>easyflow-module-datacenter</module>
|
||||||
|
|||||||
@@ -30,6 +30,10 @@
|
|||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-job</artifactId>
|
<artifactId>easyflow-module-job</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>tech.easyflow</groupId>
|
||||||
|
<artifactId>easyflow-module-chatlog</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>tech.easyflow</groupId>
|
<groupId>tech.easyflow</groupId>
|
||||||
<artifactId>easyflow-module-ai</artifactId>
|
<artifactId>easyflow-module-ai</artifactId>
|
||||||
|
|||||||
@@ -14,5 +14,47 @@ spring:
|
|||||||
password: ${REDIS_PASSWORD:123456}
|
password: ${REDIS_PASSWORD:123456}
|
||||||
|
|
||||||
easyflow:
|
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:
|
storage:
|
||||||
type: xFileStorage
|
type: xFileStorage
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ spring:
|
|||||||
password: root
|
password: root
|
||||||
flyway:
|
flyway:
|
||||||
enabled: true
|
enabled: true
|
||||||
locations: classpath:db/migration
|
locations: classpath:db/migration/mysql
|
||||||
baseline-on-migrate: false
|
baseline-on-migrate: false
|
||||||
validate-on-migrate: true
|
validate-on-migrate: true
|
||||||
clean-disabled: true
|
clean-disabled: true
|
||||||
@@ -79,6 +79,49 @@ easyflow:
|
|||||||
chat:
|
chat:
|
||||||
# SSE 超时时间(毫秒),默认 10 分钟,可按需调整
|
# SSE 超时时间(毫秒),默认 10 分钟,可按需调整
|
||||||
sse-timeout-ms: 600000
|
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:
|
audio:
|
||||||
type: aliAudioService
|
type: aliAudioService
|
||||||
@@ -168,7 +211,7 @@ rag:
|
|||||||
searcher:
|
searcher:
|
||||||
# 搜索方式 默认lucene
|
# 搜索方式 默认lucene
|
||||||
lucene:
|
lucene:
|
||||||
indexDirPath: ./luceneKnowledge
|
indexDirPath: /Users/slience/data/easyflow/luceneKnowledge
|
||||||
elastic:
|
elastic:
|
||||||
host: https://127.0.0.1:9200
|
host: https://127.0.0.1:9200
|
||||||
userName: elastic
|
userName: elastic
|
||||||
|
|||||||
@@ -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);
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user