feat(chat): 实现新的对话接口和相关功能模块
- 添加了 AI 对话请求 VO 类 (AiChatReqVO),支持模型名称、温度等参数 - 新增 AI 响应实体类 (AiResponse)用于封装返回结果 - 创建 API 操作日志注解 (@ApiOperationLog) 和切面类 (ApiOperationLogAspect) - 配置数据源使用 P6Spy 驱动并优化 HikariCP 连接池设置 - 更新 DashScope 模型配置,调整默认模型为 qwen-plus 及温度值 - 引入全局异常处理机制,包括基础异常接口和业务异常类- 新增对话控制器 (ChatController) 支持新建对话及流式交互- 创建对话及相关消息的数据访问对象 (ChatDO, ChatMessageDO) 和映射器 - 实现聊天服务接口及其实现类,支持创建新对话记录 - 添加自定义流式日志顾问 (CustomStreamLoggerAdvisor) 用于调试输出 - 删除旧版控制器和相关模型类,移除冗余配置项 - 增加日期常量工具类统一时间格式管理 - 修改 .gitignore 忽略 /logs/ 目录避免日志文件被提交
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -30,3 +30,4 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
/logs/
|
||||
|
||||
100
pom.xml
100
pom.xml
@@ -17,39 +17,27 @@
|
||||
<spring-ai.version>1.0.3</spring-ai.version>
|
||||
<commons-lang3.version>3.19.0</commons-lang3.version>
|
||||
<lombok.version>1.18.40</lombok.version>
|
||||
<jsonschema-generator.version>4.38.0</jsonschema-generator.version>
|
||||
<p6spy.version>3.9.1</p6spy.version>
|
||||
<guava.version>33.0.0-jre</guava.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<!-- 排除默认的 Logback 依赖 -->
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-logging</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.github.ulisesbocchio</groupId>
|
||||
<artifactId>jasypt-spring-boot-starter</artifactId>
|
||||
<version>3.0.5</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Deepseek 模型 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-deepseek</artifactId>
|
||||
</dependency>
|
||||
<!-- Ollama -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-ollama</artifactId>
|
||||
</dependency>
|
||||
<!-- 智谱AI -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-zhipuai</artifactId>
|
||||
</dependency>
|
||||
<!-- OpenAI -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-model-openai</artifactId>
|
||||
</dependency>
|
||||
<!-- 阿里云AI -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.cloud.ai</groupId>
|
||||
@@ -65,19 +53,6 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- 阿里百炼 SDK -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>dashscope-sdk-java</artifactId>
|
||||
<version>2.21.13</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-simple</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
@@ -88,6 +63,53 @@
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>${lombok.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.github.victools</groupId>
|
||||
<artifactId>jsonschema-generator</artifactId>
|
||||
<version>${jsonschema-generator.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.ai</groupId>
|
||||
<artifactId>spring-ai-starter-vector-store-pgvector</artifactId>
|
||||
</dependency>
|
||||
<!-- Mybatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
|
||||
</dependency>
|
||||
<!-- p6spy 组件 -->
|
||||
<dependency>
|
||||
<groupId>p6spy</groupId>
|
||||
<artifactId>p6spy</artifactId>
|
||||
<version>${p6spy.version}</version>
|
||||
</dependency>
|
||||
<!-- AOP 切面 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-aop</artifactId>
|
||||
</dependency>
|
||||
<!-- Log4j2 日志 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-log4j2</artifactId>
|
||||
</dependency>
|
||||
<!-- 参数校验 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
<!-- 相关工具类 -->
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>${guava.version}</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
@@ -105,6 +127,14 @@
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<!-- Mybatis Plus -->
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-bom</artifactId>
|
||||
<version>3.5.14</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.hanserwei.airobot.advisor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.ai.chat.client.ChatClientRequest;
|
||||
import org.springframework.ai.chat.client.ChatClientResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.StreamAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
@Slf4j
|
||||
public class CustomStreamLoggerAdvisor implements StreamAdvisor {
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 99; // order 值越小,越先执行
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@NotNull
|
||||
@Override
|
||||
public Flux<ChatClientResponse> adviseStream(@NotNull ChatClientRequest chatClientRequest, StreamAdvisorChain streamAdvisorChain) {
|
||||
|
||||
Flux<ChatClientResponse> chatClientResponseFlux = streamAdvisorChain.nextStream(chatClientRequest);
|
||||
|
||||
// 创建 AI 流式回答聚合容器(线程安全)
|
||||
AtomicReference<StringBuilder> fullContent = new AtomicReference<>(new StringBuilder());
|
||||
|
||||
// 返回处理后的流
|
||||
return chatClientResponseFlux
|
||||
.doOnNext(response -> {
|
||||
// 逐块收集内容
|
||||
String chunk = null;
|
||||
if (response.chatResponse() != null) {
|
||||
chunk = response.chatResponse().getResult().getOutput().getText();
|
||||
}
|
||||
|
||||
log.info("## chunk: {}", chunk);
|
||||
|
||||
// 若 chunk 块不为空,则追加到 fullContent 中
|
||||
if (chunk != null) {
|
||||
fullContent.get().append(chunk);
|
||||
}
|
||||
})
|
||||
.doOnComplete(() -> {
|
||||
// 流完成后打印完整回答
|
||||
String completeResponse = fullContent.get().toString();
|
||||
log.info("\n==== FULL AI RESPONSE ====\n{}\n========================", completeResponse);
|
||||
})
|
||||
.doOnError(error -> {
|
||||
// 出错时打印已收集的部分
|
||||
String partialResponse = fullContent.get().toString();
|
||||
log.error("## Stream 流出现错误,已收集回答如下: {}", partialResponse, error);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package com.hanserwei.airobot.advisor;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClientRequest;
|
||||
import org.springframework.ai.chat.client.ChatClientResponse;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.api.CallAdvisorChain;
|
||||
|
||||
@Slf4j
|
||||
public class MyLoggerAdvisor implements CallAdvisor {
|
||||
|
||||
@Override
|
||||
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
|
||||
log.info("## 请求入参: {}", chatClientRequest);
|
||||
ChatClientResponse chatClientResponse = callAdvisorChain.nextCall(chatClientRequest);
|
||||
log.info("## 请求出参: {}", chatClientResponse);
|
||||
return chatClientResponse;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOrder() {
|
||||
return 1; // order 值越小,越先执行
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
// 获取类名称
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.hanserwei.airobot.aspect;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD})
|
||||
@Documented
|
||||
public @interface ApiOperationLog {
|
||||
/**
|
||||
* API 功能描述
|
||||
*
|
||||
* @return API 功能描述
|
||||
*/
|
||||
String description() default "";
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package com.hanserwei.airobot.aspect;
|
||||
|
||||
import com.hanserwei.airobot.utils.JsonUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.annotation.Pointcut;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Arrays;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* API操作日志切面类,用于记录被 @ApiOperationLog 注解标记的方法的执行信息,
|
||||
* 包括方法描述、入参、出参以及执行耗时等。
|
||||
*/
|
||||
@Aspect
|
||||
@Component
|
||||
@Slf4j
|
||||
public class ApiOperationLogAspect {
|
||||
|
||||
/** 以自定义 @ApiOperationLog 注解为切点,凡是添加 @ApiOperationLog 的方法,都会执行环绕中的代码 */
|
||||
@Pointcut("@annotation(com.hanserwei.airobot.aspect.ApiOperationLog)")
|
||||
public void apiOperationLog() {}
|
||||
|
||||
/**
|
||||
* 环绕通知方法,用于记录目标方法的执行日志。
|
||||
* 包括方法开始时间、类名、方法名、入参、功能描述、执行结果和耗时。
|
||||
*
|
||||
* @param joinPoint 切点对象,封装了目标方法的相关信息
|
||||
* @return 目标方法的返回值
|
||||
* @throws Throwable 目标方法可能抛出的异常
|
||||
*/
|
||||
@Around("apiOperationLog()")
|
||||
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
// 请求开始时间
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
// 获取被请求的类和方法
|
||||
String className = joinPoint.getTarget().getClass().getSimpleName();
|
||||
String methodName = joinPoint.getSignature().getName();
|
||||
|
||||
// 请求入参
|
||||
Object[] args = joinPoint.getArgs();
|
||||
// 入参转 JSON 字符串
|
||||
String argsJsonStr = Arrays.stream(args).map(toJsonStr()).collect(Collectors.joining(", "));
|
||||
|
||||
// 功能描述信息
|
||||
String description = getApiOperationLogDescription(joinPoint);
|
||||
|
||||
// 打印请求相关参数
|
||||
log.info("====== 请求开始: [{}], 入参: {}, 请求类: {}, 请求方法: {} =================================== ",
|
||||
description, argsJsonStr, className, methodName);
|
||||
|
||||
// 执行切点方法
|
||||
Object result = joinPoint.proceed();
|
||||
|
||||
// 执行耗时
|
||||
long executionTime = System.currentTimeMillis() - startTime;
|
||||
|
||||
// 打印出参等相关信息
|
||||
log.info("====== 请求结束: [{}], 耗时: {}ms, 出参: {} =================================== ",
|
||||
description, executionTime, JsonUtil.toJsonString(result));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目标方法上 @ApiOperationLog 注解的描述信息。
|
||||
*
|
||||
* @param joinPoint 切点对象,用于获取目标方法信息
|
||||
* @return 注解中定义的功能描述字符串
|
||||
*/
|
||||
private String getApiOperationLogDescription(ProceedingJoinPoint joinPoint) {
|
||||
// 1. 从 ProceedingJoinPoint 获取 MethodSignature
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
|
||||
// 2. 使用 MethodSignature 获取当前被注解的 Method
|
||||
Method method = signature.getMethod();
|
||||
|
||||
// 3. 从 Method 中提取 LogExecution 注解
|
||||
ApiOperationLog apiOperationLog = method.getAnnotation(ApiOperationLog.class);
|
||||
|
||||
// 4. 从 LogExecution 注解中获取 description 属性
|
||||
return apiOperationLog.description();
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回一个将对象转换为 JSON 字符串的函数。
|
||||
*
|
||||
* @return 将对象序列化为 JSON 字符串的函数
|
||||
*/
|
||||
private Function<Object, String> toJsonStr() {
|
||||
return JsonUtil::toJsonString;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,20 +1,16 @@
|
||||
package com.hanserwei.airobot.config;
|
||||
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.hanserwei.airobot.advisor.MyLoggerAdvisor;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
|
||||
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ChatClientConfig {
|
||||
@Resource
|
||||
private ChatMemory chatMemory;
|
||||
|
||||
/**
|
||||
* 初始化 ChatClient 客户端
|
||||
@@ -25,11 +21,6 @@ public class ChatClientConfig {
|
||||
@Bean
|
||||
public ChatClient chatClient(DashScopeChatModel chatModel) {
|
||||
return ChatClient.builder(chatModel)
|
||||
// .defaultSystem("请你扮演一名犬小哈 Java 项目实战专栏的客服人员")
|
||||
.defaultAdvisors(
|
||||
new SimpleLoggerAdvisor(),
|
||||
new MyLoggerAdvisor(),
|
||||
MessageChatMemoryAdvisor.builder(chatMemory).build())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.hanserwei.airobot.config;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.ai.chat.memory.ChatMemoryRepository;
|
||||
import org.springframework.ai.chat.memory.MessageWindowChatMemory;
|
||||
import org.springframework.ai.chat.memory.repository.cassandra.CassandraChatMemoryRepository;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ChatMemoryConfig {
|
||||
|
||||
/**
|
||||
* 记忆存储
|
||||
*/
|
||||
@Resource
|
||||
private CassandraChatMemoryRepository chatMemoryRepository;
|
||||
|
||||
/**
|
||||
* 初始化一个 ChatMemory 实例,并注入到 Spring 容器中
|
||||
* @return ChatMemory
|
||||
*/
|
||||
@Bean
|
||||
public ChatMemory chatMemory() {
|
||||
return MessageWindowChatMemory.builder()
|
||||
.maxMessages(50) // 最大消息窗口为 50,默认值为 20
|
||||
.chatMemoryRepository(chatMemoryRepository) // 记忆存储
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.hanserwei.airobot.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.deser.YearMonthDeserializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
|
||||
import com.fasterxml.jackson.datatype.jsr310.ser.YearMonthSerializer;
|
||||
import com.hanserwei.airobot.constant.DateConstants;
|
||||
import com.hanserwei.airobot.utils.JsonUtil;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.YearMonth;
|
||||
import java.util.TimeZone;
|
||||
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
// 初始化一个 ObjectMapper 对象,用于自定义 Jackson 的行为
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
// 忽略未知属性
|
||||
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
// 设置凡是为 null 的字段,返参中均不返回,请根据项目组约定是否开启
|
||||
// objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
|
||||
|
||||
// 设置时区
|
||||
objectMapper.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
|
||||
|
||||
// JavaTimeModule 用于指定序列化和反序列化规则
|
||||
JavaTimeModule javaTimeModule = new JavaTimeModule();
|
||||
|
||||
// 支持 LocalDateTime、LocalDate、LocalTime
|
||||
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S));
|
||||
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateConstants.DATE_FORMAT_Y_M_D_H_M_S));
|
||||
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateConstants.DATE_FORMAT_Y_M_D));
|
||||
javaTimeModule.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateConstants.DATE_FORMAT_Y_M_D));
|
||||
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateConstants.DATE_FORMAT_H_M_S));
|
||||
javaTimeModule.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateConstants.DATE_FORMAT_H_M_S));
|
||||
// 支持 YearMonth
|
||||
javaTimeModule.addSerializer(YearMonth.class, new YearMonthSerializer(DateConstants.DATE_FORMAT_Y_M));
|
||||
javaTimeModule.addDeserializer(YearMonth.class, new YearMonthDeserializer(DateConstants.DATE_FORMAT_Y_M));
|
||||
|
||||
objectMapper.registerModule(javaTimeModule);
|
||||
|
||||
// 初始化 JsonUtils 中的 ObjectMapper
|
||||
JsonUtil.init(objectMapper);
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.hanserwei.airobot.config;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
@MapperScan("com.hanserwei.airobot.domain.mapper")
|
||||
public class MybatisPlusConfig {
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.hanserwei.airobot.constant;
|
||||
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public interface DateConstants {
|
||||
|
||||
/**
|
||||
* DateTimeFormatter:年-月-日 时:分:秒
|
||||
*/
|
||||
DateTimeFormatter DATE_FORMAT_Y_M_D_H_M_S = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||
|
||||
/**
|
||||
* DateTimeFormatter:年-月-日
|
||||
*/
|
||||
DateTimeFormatter DATE_FORMAT_Y_M_D = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
|
||||
/**
|
||||
* DateTimeFormatter:月-日
|
||||
*/
|
||||
DateTimeFormatter DATE_FORMAT_M_D = DateTimeFormatter.ofPattern("MM-dd");
|
||||
|
||||
/**
|
||||
* DateTimeFormatter:时:分:秒
|
||||
*/
|
||||
DateTimeFormatter DATE_FORMAT_H_M_S = DateTimeFormatter.ofPattern("HH:mm:ss");
|
||||
|
||||
/**
|
||||
* DateTimeFormatter:时:分
|
||||
*/
|
||||
DateTimeFormatter DATE_FORMAT_H_M = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
/**
|
||||
* DateTimeFormatter:年-月
|
||||
*/
|
||||
DateTimeFormatter DATE_FORMAT_Y_M = DateTimeFormatter.ofPattern("yyyy-MM");
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.memory.ChatMemory;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v2/ai")
|
||||
public class ChatClientController {
|
||||
|
||||
@Resource
|
||||
private ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* 普通对话
|
||||
*
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping("/generate")
|
||||
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 一次性返回结果
|
||||
return chatClient.prompt()
|
||||
.user(message)
|
||||
.call()
|
||||
.content();
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
|
||||
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message,
|
||||
@RequestParam(value = "chatId") String chatId) {
|
||||
return chatClient.prompt()
|
||||
.user(message) // 提示词
|
||||
.advisors(a -> a.param(ChatMemory.CONVERSATION_ID, chatId))
|
||||
.stream() // 流式输出
|
||||
.content();
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import com.alibaba.cloud.ai.dashscope.api.DashScopeApi;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatOptions;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.hanserwei.airobot.advisor.CustomStreamLoggerAdvisor;
|
||||
import com.hanserwei.airobot.aspect.ApiOperationLog;
|
||||
import com.hanserwei.airobot.model.vo.chat.AiChatReqVO;
|
||||
import com.hanserwei.airobot.model.vo.chat.AiResponse;
|
||||
import com.hanserwei.airobot.model.vo.chat.NewChatReqVO;
|
||||
import com.hanserwei.airobot.service.ChatService;
|
||||
import com.hanserwei.airobot.utils.Response;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.client.advisor.api.Advisor;
|
||||
import org.springframework.ai.chat.model.ChatModel;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/chat")
|
||||
@Slf4j
|
||||
public class ChatController {
|
||||
|
||||
@Resource
|
||||
private ChatService chatService;
|
||||
|
||||
@Value("${spring.ai.dashscope.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
@PostMapping("/new")
|
||||
@ApiOperationLog(description = "新建对话")
|
||||
public Response<?> newChat(@RequestBody @Validated NewChatReqVO newChatReqVO) {
|
||||
return chatService.newChat(newChatReqVO);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/completion", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
@ApiOperationLog(description = "流式对话")
|
||||
public Flux<AiResponse> chat(@RequestBody @Validated AiChatReqVO aiChatReqVO) {
|
||||
// 用户消息
|
||||
String message = aiChatReqVO.getMessage();
|
||||
// 模型名称
|
||||
String modelName = aiChatReqVO.getModelName();
|
||||
// 温度
|
||||
Double temperature = aiChatReqVO.getTemperature();
|
||||
|
||||
// 构建ChatModel
|
||||
ChatModel chatModel = DashScopeChatModel.builder()
|
||||
.dashScopeApi(DashScopeApi.builder()
|
||||
.apiKey(apiKey)
|
||||
.build())
|
||||
.build();
|
||||
|
||||
// 动态设置模型名称和温度
|
||||
ChatClient.ChatClientRequestSpec chatClientRequestSpec = ChatClient.create(chatModel)
|
||||
.prompt()
|
||||
.options(DashScopeChatOptions.builder()
|
||||
.withModel(modelName)
|
||||
.withTemperature(temperature)
|
||||
.build())
|
||||
.user(message);
|
||||
|
||||
// Advisor 集合
|
||||
List<Advisor> advisors = Lists.newArrayList();
|
||||
// 添加自定义打印流式对话日志 Advisor
|
||||
advisors.add(new CustomStreamLoggerAdvisor());
|
||||
|
||||
// 应用 Advisor 集合
|
||||
chatClientRequestSpec.advisors(advisors);
|
||||
|
||||
// 流式输出
|
||||
return chatClientRequestSpec.stream()
|
||||
.content()
|
||||
.mapNotNull(text -> AiResponse.builder().v(text).build());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.hanserwei.airobot.model.AIResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.messages.AssistantMessage;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v6/ai")
|
||||
public class DashscopeAIController {
|
||||
|
||||
// 存储聊天对话
|
||||
private final Map<String, List<Message>> chatMemoryStore = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private DashScopeChatModel dashScopeChatModel;
|
||||
|
||||
@Value("classpath:/prompts/code-assistant.st")
|
||||
private org.springframework.core.io.Resource templateResource;
|
||||
|
||||
/**
|
||||
* 普通对话
|
||||
*
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping("/generate")
|
||||
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message,
|
||||
@RequestParam(value = "chatId") String chatId)
|
||||
{
|
||||
// 提示词模板
|
||||
PromptTemplate promptTemplate = new PromptTemplate(templateResource);
|
||||
// 根据 chatId 获取对话记录
|
||||
List<Message> messages = chatMemoryStore.get(chatId);
|
||||
// 若不存在,则初始化一份
|
||||
if (CollectionUtils.isEmpty(messages)) {
|
||||
messages = new ArrayList<>();
|
||||
chatMemoryStore.put(chatId, messages);
|
||||
}
|
||||
|
||||
// 添加 “用户角色消息” 到聊天记录中
|
||||
messages.add(new UserMessage(message));
|
||||
|
||||
// 构建提示词
|
||||
Prompt prompt = new Prompt(messages);
|
||||
// 一次性返回结果
|
||||
ChatClient chatClient = ChatClient.builder(dashScopeChatModel).build();
|
||||
String responseText = Objects.requireNonNull(chatClient.prompt(prompt)
|
||||
.call()
|
||||
.chatResponse())
|
||||
.getResult()
|
||||
.getOutput()
|
||||
.getText();
|
||||
// 添加 “助手角色消息” 到聊天记录中
|
||||
if (responseText != null) {
|
||||
messages.add(new AssistantMessage(responseText));
|
||||
}
|
||||
|
||||
return responseText;
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
*
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<AIResponse> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
ChatClient chatClient = ChatClient.builder(dashScopeChatModel).build();
|
||||
return chatClient.prompt()
|
||||
.user(message)
|
||||
.stream()
|
||||
.chatResponse()
|
||||
.mapNotNull(
|
||||
chatResponse -> {
|
||||
Generation generation = chatResponse.getResult();
|
||||
String text = generation.getOutput().getText();
|
||||
return AIResponse.builder().v(text).build();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/ai")
|
||||
public class DeepSeekChatController {
|
||||
|
||||
@Resource
|
||||
private DeepSeekChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 普通对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping("/generate")
|
||||
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 一次性返回结果
|
||||
return chatModel.call(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
|
||||
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 构建提示词
|
||||
Prompt prompt = new Prompt(new UserMessage(message));
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> chatResponse.getResult().getOutput().getText());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.deepseek.DeepSeekAssistantMessage;
|
||||
import org.springframework.ai.deepseek.DeepSeekChatModel;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v1/ai")
|
||||
public class DeepSeekR1ChatController {
|
||||
|
||||
@Resource
|
||||
private DeepSeekChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
|
||||
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 构建提示词
|
||||
Prompt prompt = new Prompt(new UserMessage(message));
|
||||
|
||||
// 使用原子布尔值跟踪分隔线状态(每个请求独立)
|
||||
AtomicBoolean needSeparator = new AtomicBoolean(true);
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> {
|
||||
// 获取响应内容
|
||||
DeepSeekAssistantMessage deepSeekAssistantMessage = (DeepSeekAssistantMessage) chatResponse.getResult().getOutput();
|
||||
// 推理内容
|
||||
String reasoningContent = deepSeekAssistantMessage.getReasoningContent();
|
||||
// 推理结束后的正式回答
|
||||
String text = deepSeekAssistantMessage.getText();
|
||||
|
||||
// 是否是正式回答
|
||||
boolean isTextResponse = false;
|
||||
// 若推理内容有值,则响应推理内容,否则,说明推理结束了,响应正式回答
|
||||
String rawContent;
|
||||
if (Objects.isNull(text)) {
|
||||
rawContent = reasoningContent;
|
||||
} else {
|
||||
rawContent = text;
|
||||
isTextResponse = true; // 标记为正式回答
|
||||
}
|
||||
|
||||
// 处理换行
|
||||
String processed = StringUtils.isNotBlank(rawContent) ? rawContent.replace("\n", "<br>") : rawContent;
|
||||
|
||||
// 在正式回答内容之前,添加一个分隔线
|
||||
if (isTextResponse
|
||||
&& needSeparator.compareAndSet(true, false)) {
|
||||
processed = "<hr>" + processed; // 使用 HTML 的 <hr> 标签实现
|
||||
}
|
||||
|
||||
return processed;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.content.Media;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v9/ai")
|
||||
public class MultimodalityController {
|
||||
|
||||
@Resource
|
||||
private DashScopeChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
* @param message
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
|
||||
public Flux<String> generateStream(@RequestParam(value = "message") String message) {
|
||||
// 1. 创建媒体资源
|
||||
Media image = new Media(
|
||||
MimeTypeUtils.IMAGE_PNG,
|
||||
new ClassPathResource("/images/img.png")
|
||||
);
|
||||
|
||||
// 2. 附加选项(可选),如温度值等等
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("temperature", 0.7);
|
||||
|
||||
// 3. 构建多模态消息
|
||||
UserMessage userMessage = UserMessage.builder()
|
||||
.text(message)
|
||||
.media(image)
|
||||
.metadata(metadata)
|
||||
.build();
|
||||
|
||||
// 4. 构建提示词
|
||||
Prompt prompt = new Prompt(List.of(userMessage));
|
||||
// 5. 流式调用
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> {
|
||||
Generation generation = chatResponse.getResult();
|
||||
return generation.getOutput().getText();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.model.ChatResponse;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.ollama.OllamaChatModel;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v3/ai")
|
||||
public class OllamaController {
|
||||
|
||||
@Resource
|
||||
private OllamaChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 普通对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping("/generate")
|
||||
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 构建提示词,调用大模型
|
||||
ChatResponse chatResponse = chatModel.call(new Prompt(message));
|
||||
|
||||
// 响应回答内容
|
||||
return chatResponse.getResult().getOutput().getText();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.openai.OpenAiChatModel;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v5/ai")
|
||||
public class OpenAIController {
|
||||
|
||||
@Resource
|
||||
private OpenAiChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 普通对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping("/generate")
|
||||
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 一次性返回结果
|
||||
return chatModel.call(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 构建提示词
|
||||
Prompt prompt = new Prompt(new UserMessage(message));
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> {
|
||||
Generation generation = chatResponse.getResult();
|
||||
return generation.getOutput().getText();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import com.alibaba.cloud.ai.dashscope.chat.DashScopeChatModel;
|
||||
import com.hanserwei.airobot.model.AIResponse;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.messages.Message;
|
||||
import org.springframework.ai.chat.model.Generation;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.chat.prompt.SystemPromptTemplate;
|
||||
import org.springframework.ai.template.st.StTemplateRenderer;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v7/ai")
|
||||
public class PromptTemplateController {
|
||||
|
||||
@Resource
|
||||
private DashScopeChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 智能代码生成
|
||||
* @param message
|
||||
* @param lang
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<AIResponse> generateStream(@RequestParam(value = "message") String message,
|
||||
@RequestParam(value = "lang") String lang) {
|
||||
// 提示词模板
|
||||
String template = """
|
||||
你是一位资深 {lang} 开发工程师。请严格遵循以下要求编写代码:
|
||||
1. 功能描述:{description}
|
||||
2. 代码需包含详细注释
|
||||
3. 使用业界最佳实践
|
||||
""";
|
||||
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template);
|
||||
|
||||
// 填充提示词占位符,转换为 Prompt 提示词对象
|
||||
Prompt prompt = promptTemplate.create(Map.of("description", message, "lang", lang));
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> {
|
||||
Generation generation = chatResponse.getResult();
|
||||
String text = generation.getOutput().getText();
|
||||
return AIResponse.builder().v(text).build();
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 智能代码生成 2
|
||||
* @param message
|
||||
* @param lang
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/generateStream2", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<AIResponse> generateStream2(@RequestParam(value = "message") String message,
|
||||
@RequestParam(value = "lang") String lang) {
|
||||
// 提示词模板
|
||||
PromptTemplate promptTemplate = PromptTemplate.builder()
|
||||
.renderer(StTemplateRenderer.builder().startDelimiterToken('<').endDelimiterToken('>').build()) // 自定义占位符
|
||||
.template("""
|
||||
你是一位资深 <lang> 开发工程师。请严格遵循以下要求编写代码:
|
||||
1. 功能描述:<description>
|
||||
2. 代码需包含详细注释
|
||||
3. 使用业界最佳实践
|
||||
""")
|
||||
.build();
|
||||
|
||||
// 填充提示词占位符,转换为 Prompt 提示词对象
|
||||
Prompt prompt = promptTemplate.create(Map.of("description", message, "lang", lang));
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> {
|
||||
Generation generation = chatResponse.getResult();
|
||||
String text = generation.getOutput().getText();
|
||||
return AIResponse.builder().v(text).build();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 智能代码生成 3
|
||||
* @param message
|
||||
* @param lang
|
||||
* @return
|
||||
*/
|
||||
@GetMapping(value = "/generateStream3", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<AIResponse> generateStream3(@RequestParam(value = "message") String message,
|
||||
@RequestParam(value = "lang") String lang) {
|
||||
|
||||
// 系统角色提示词模板
|
||||
String systemPrompt = """
|
||||
你是一位资深 {lang} 开发工程师, 已经从业数十年,经验非常丰富。
|
||||
""";
|
||||
SystemPromptTemplate systemPromptTemplate = new SystemPromptTemplate(systemPrompt);
|
||||
// 填充提示词占位符,并转换为 Message 对象
|
||||
Message systemMessage = systemPromptTemplate.createMessage(Map.of("lang", lang));
|
||||
|
||||
// 用户角色提示词模板
|
||||
String userPrompt = """
|
||||
请严格遵循以下要求编写代码:
|
||||
1. 功能描述:{description}
|
||||
2. 代码需包含详细注释
|
||||
3. 使用业界最佳实践
|
||||
""";
|
||||
PromptTemplate promptTemplate = new PromptTemplate(userPrompt);
|
||||
// 填充提示词占位符,并转换为 Message 对象
|
||||
Message userMessage = promptTemplate.createMessage(Map.of("description", message));
|
||||
|
||||
|
||||
// 组合多角色消息,构建提示词 Prompt
|
||||
Prompt prompt = new Prompt(List.of(systemMessage, userMessage));
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> {
|
||||
Generation generation = chatResponse.getResult();
|
||||
String text = generation.getOutput().getText();
|
||||
return AIResponse.builder().v(text).build();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import com.hanserwei.airobot.model.ActorFilmography;
|
||||
import com.hanserwei.airobot.model.Book;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.client.ChatClient;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.chat.prompt.PromptTemplate;
|
||||
import org.springframework.ai.converter.BeanOutputConverter;
|
||||
import org.springframework.ai.converter.ListOutputConverter;
|
||||
import org.springframework.ai.converter.MapOutputConverter;
|
||||
import org.springframework.core.convert.support.DefaultConversionService;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v8/ai")
|
||||
public class StructuredOutputController {
|
||||
|
||||
@Resource
|
||||
private ChatClient chatClient;
|
||||
|
||||
/**
|
||||
* 示例1: BeanOutputConverter - 获取演员电影作品集
|
||||
*
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/actor/films")
|
||||
public ActorFilmography generate(@RequestParam(value = "name") String name) {
|
||||
// 一次性返回结果
|
||||
return chatClient.prompt()
|
||||
.user(u -> u.text("""
|
||||
请为演员 {actor} 生成包含5部代表作的电影作品集,
|
||||
只包含 {actor} 担任主演的电影,不要包含任何解释说明。
|
||||
""")
|
||||
.param("actor", name))
|
||||
.call()
|
||||
.entity(ActorFilmography.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例2: MapOutputConverter - 获取编程语言信息
|
||||
* @param language
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/language-info")
|
||||
public Map<String, Object> getLanguageInfo(@RequestParam(value = "lang") String language) {
|
||||
|
||||
String userText = """
|
||||
请提供关于编程语言 {language} 的结构化信息,包含以下字段:"
|
||||
name (语言名称), "
|
||||
popularity (流行度排名,整数), "
|
||||
features (主要特性,字符串数组), "
|
||||
releaseYear (首次发布年份). "
|
||||
不要包含任何解释说明,直接输出 JSON 格式数据。
|
||||
""";
|
||||
|
||||
return chatClient.prompt()
|
||||
.user(u -> u.text(userText).param("language", language))
|
||||
.call()
|
||||
.entity(new MapOutputConverter());
|
||||
}
|
||||
|
||||
/**
|
||||
* 示例3: ListOutputConverter - 获取城市列表
|
||||
* @param country
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/city-list")
|
||||
public List<String> getCityList(@RequestParam(value = "country") String country) {
|
||||
|
||||
return chatClient.prompt()
|
||||
.user(u -> u.text(
|
||||
"""
|
||||
列出 {country} 的8个主要城市名称。
|
||||
不要包含任何编号、解释或其他文本,直接输出城市名称列表。
|
||||
""")
|
||||
.param("country", country))
|
||||
.call()
|
||||
.entity(new ListOutputConverter(new DefaultConversionService()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用低级 API 的 BeanOutputConverter - 获取书籍信息
|
||||
* @param bookTitle
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/book-info")
|
||||
public Book getBookInfo(@RequestParam(value = "name") String bookTitle) {
|
||||
|
||||
// 使用 BeanOutputConverter 定义输出格式
|
||||
BeanOutputConverter<Book> converter = new BeanOutputConverter<>(Book.class);
|
||||
|
||||
// 提示词模板
|
||||
String template = """
|
||||
请提供关于书籍《{bookTitle}》的详细信息:
|
||||
1. 作者姓名
|
||||
2. 出版年份
|
||||
3. 主要类型(数组)
|
||||
4. 书籍描述(不少于50字)
|
||||
|
||||
不要包含任何解释说明,直接按指定格式输出。
|
||||
{format}
|
||||
""";
|
||||
|
||||
// 创建 Prompt
|
||||
PromptTemplate promptTemplate = new PromptTemplate(template);
|
||||
Prompt prompt = promptTemplate.create(Map.of(
|
||||
"bookTitle", bookTitle,
|
||||
"format", converter.getFormat()
|
||||
));
|
||||
|
||||
// 调用模型并转换结果
|
||||
String result = chatClient.prompt(prompt)
|
||||
.call()
|
||||
.content();
|
||||
|
||||
// 结构化转换
|
||||
return converter.convert(result);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesis;
|
||||
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisParam;
|
||||
import com.alibaba.dashscope.aigc.imagesynthesis.ImageSynthesisResult;
|
||||
import com.alibaba.dashscope.exception.ApiException;
|
||||
import com.alibaba.dashscope.exception.NoApiKeyException;
|
||||
import com.alibaba.dashscope.utils.JsonUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v10/ai")
|
||||
@Slf4j
|
||||
public class Text2ImgController {
|
||||
|
||||
@Value("${spring.ai.dashscope.api-key}")
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 调用阿里百炼图生文大模型
|
||||
* @param prompt 提示词
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/text2img")
|
||||
public String text2Image(@RequestParam(value = "prompt") String prompt) {
|
||||
// 构建文生图参数
|
||||
ImageSynthesisParam param = ImageSynthesisParam.builder()
|
||||
.apiKey(apiKey) // 阿里百炼 API Key
|
||||
.model("wanx2.1-t2i-plus") // 模型名称
|
||||
.prompt(prompt) // 提示词
|
||||
.n(1) // 生成图片的数量,这里指定为一张
|
||||
.size("1024*1024") // 输出图像的分辨率
|
||||
.build();
|
||||
|
||||
// 同步调用 AI 大模型,生成图片
|
||||
ImageSynthesis imageSynthesis = new ImageSynthesis();
|
||||
ImageSynthesisResult result = null;
|
||||
try {
|
||||
log.info("## 同步调用,请稍等一会...");
|
||||
result = imageSynthesis.call(param);
|
||||
} catch (ApiException | NoApiKeyException e){
|
||||
log.error("", e);
|
||||
}
|
||||
|
||||
// 返回生成的结果(包含图片的 URL 链接)
|
||||
return JsonUtils.toJson(result);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package com.hanserwei.airobot.controller;
|
||||
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.ai.chat.messages.UserMessage;
|
||||
import org.springframework.ai.chat.prompt.Prompt;
|
||||
import org.springframework.ai.zhipuai.ZhiPuAiChatModel;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/v4/ai")
|
||||
public class ZhiPuController {
|
||||
|
||||
@Resource
|
||||
private ZhiPuAiChatModel chatModel;
|
||||
|
||||
/**
|
||||
* 普通对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping("/generate")
|
||||
public String generate(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 一次性返回结果
|
||||
return chatModel.call(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 流式对话
|
||||
* @param message 对话输入内容
|
||||
* @return 对话结果
|
||||
*/
|
||||
@GetMapping(value = "/generateStream", produces = "text/html;charset=utf-8")
|
||||
public Flux<String> generateStream(@RequestParam(value = "message", defaultValue = "你是谁?") String message) {
|
||||
// 构建提示词
|
||||
Prompt prompt = new Prompt(new UserMessage(message));
|
||||
|
||||
// 流式输出
|
||||
return chatModel.stream(prompt)
|
||||
.mapNotNull(chatResponse -> chatResponse.getResult().getOutput().getText());
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
26
src/main/java/com/hanserwei/airobot/domain/dos/ChatDO.java
Normal file
26
src/main/java/com/hanserwei/airobot/domain/dos/ChatDO.java
Normal file
@@ -0,0 +1,26 @@
|
||||
package com.hanserwei.airobot.domain.dos;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("t_chat")
|
||||
public class ChatDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String uuid;
|
||||
private String summary;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.hanserwei.airobot.domain.dos;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@TableName("t_chat_message")
|
||||
public class ChatMessageDO {
|
||||
|
||||
@TableId(type = IdType.AUTO)
|
||||
private Long id;
|
||||
private String chatUuid;
|
||||
private String content;
|
||||
private String role;
|
||||
private LocalDateTime createTime;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.hanserwei.airobot.domain.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.hanserwei.airobot.domain.dos.ChatDO;
|
||||
|
||||
public interface ChatMapper extends BaseMapper<ChatDO> {
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.hanserwei.airobot.domain.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.hanserwei.airobot.domain.dos.ChatMessageDO;
|
||||
|
||||
public interface ChatMessageMapper extends BaseMapper<ChatMessageDO> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.hanserwei.airobot.enums;
|
||||
|
||||
import com.hanserwei.airobot.exception.BaseExceptionInterface;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
public enum ResponseCodeEnum implements BaseExceptionInterface {
|
||||
|
||||
// ----------- 通用异常状态码 -----------
|
||||
SYSTEM_ERROR("10000", "出错啦,后台小哥正在努力修复中..."),
|
||||
PARAM_NOT_VALID("10001", "参数错误"),
|
||||
|
||||
|
||||
// ----------- 业务异常状态码 -----------
|
||||
// TODO 待填充
|
||||
;
|
||||
|
||||
// 异常码
|
||||
private final String errorCode;
|
||||
// 错误信息
|
||||
private final String errorMessage;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.hanserwei.airobot.exception;
|
||||
|
||||
public interface BaseExceptionInterface {
|
||||
String getErrorCode();
|
||||
|
||||
String getErrorMessage();
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.hanserwei.airobot.exception;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class BizException extends RuntimeException {
|
||||
// 异常码
|
||||
private String errorCode;
|
||||
// 错误信息
|
||||
private String errorMessage;
|
||||
|
||||
public BizException(BaseExceptionInterface baseExceptionInterface) {
|
||||
this.errorCode = baseExceptionInterface.getErrorCode();
|
||||
this.errorMessage = baseExceptionInterface.getErrorMessage();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.hanserwei.airobot.exception;
|
||||
|
||||
import com.hanserwei.airobot.enums.ResponseCodeEnum;
|
||||
import com.hanserwei.airobot.utils.Response;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.validation.BindingResult;
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseBody;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 全局异常处理器,用于统一处理系统中抛出的各类异常,并返回格式化的错误响应。
|
||||
*/
|
||||
@ControllerAdvice
|
||||
@Slf4j
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 捕获自定义业务异常 BizException,并记录警告日志,返回对应的失败响应。
|
||||
*
|
||||
* @param request 当前HTTP请求对象
|
||||
* @param e 抛出的业务异常对象
|
||||
* @return 返回封装后的失败响应对象
|
||||
*/
|
||||
@ExceptionHandler({ BizException.class })
|
||||
@ResponseBody
|
||||
public Response<Object> handleBizException(HttpServletRequest request, BizException e) {
|
||||
log.warn("{} request fail, errorCode: {}, errorMessage: {}", request.getRequestURI(), e.getErrorCode(), e.getErrorMessage());
|
||||
return Response.fail(e);
|
||||
}
|
||||
|
||||
/**
|
||||
* 捕获参数校验异常 MethodArgumentNotValidException,提取字段校验错误信息并组合成可读性较强的错误描述,
|
||||
* 记录警告日志后返回参数校验失败的响应。
|
||||
*
|
||||
* @param request 当前HTTP请求对象
|
||||
* @param e 参数校验异常对象
|
||||
* @return 返回封装后的参数校验失败响应对象
|
||||
*/
|
||||
@ExceptionHandler({ MethodArgumentNotValidException.class })
|
||||
@ResponseBody
|
||||
public Response<Object> handleMethodArgumentNotValidException(HttpServletRequest request, MethodArgumentNotValidException e) {
|
||||
// 参数错误异常码
|
||||
String errorCode = ResponseCodeEnum.PARAM_NOT_VALID.getErrorCode();
|
||||
|
||||
// 获取 BindingResult
|
||||
BindingResult bindingResult = e.getBindingResult();
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
// 获取校验不通过的字段,并组合错误信息,格式为: email 邮箱格式不正确, 当前值: '123124qq.com';
|
||||
Optional.of(bindingResult.getFieldErrors()).ifPresent(errors -> {
|
||||
errors.forEach(error ->
|
||||
sb.append(error.getField())
|
||||
.append(" ")
|
||||
.append(error.getDefaultMessage())
|
||||
.append(", 当前值: '")
|
||||
.append(error.getRejectedValue())
|
||||
.append("'; ")
|
||||
|
||||
);
|
||||
});
|
||||
|
||||
// 错误信息
|
||||
String errorMessage = sb.toString();
|
||||
|
||||
log.warn("{} request error, errorCode: {}, errorMessage: {}", request.getRequestURI(), errorCode, errorMessage);
|
||||
|
||||
return Response.fail(errorCode, errorMessage);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 捕获其他未被处理的异常类型,记录错误日志并返回系统内部错误的响应。
|
||||
*
|
||||
* @param request 当前HTTP请求对象
|
||||
* @param e 抛出的异常对象
|
||||
* @return 返回封装后的系统错误响应对象
|
||||
*/
|
||||
@ExceptionHandler({ Exception.class })
|
||||
@ResponseBody
|
||||
public Response<Object> handleOtherException(HttpServletRequest request, Exception e) {
|
||||
log.error("{} request error, ", request.getRequestURI(), e);
|
||||
return Response.fail(ResponseCodeEnum.SYSTEM_ERROR);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.hanserwei.airobot.model;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@JsonPropertyOrder({"actor", "movies"})
|
||||
public record ActorFilmography(String actor, List<String> movies) {
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package com.hanserwei.airobot.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class Book {
|
||||
/**
|
||||
* 书名
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 作者
|
||||
*/
|
||||
private String author;
|
||||
|
||||
/**
|
||||
* 发布年份
|
||||
*/
|
||||
private Integer publishYear;
|
||||
|
||||
/**
|
||||
* 类型
|
||||
*/
|
||||
private List<String> genres;
|
||||
|
||||
/**
|
||||
* 简介
|
||||
*/
|
||||
private String description;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.hanserwei.airobot.model.vo.chat;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class AiChatReqVO {
|
||||
|
||||
@NotBlank(message = "用户消息不能为空")
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 对话 ID
|
||||
*/
|
||||
private String chatId;
|
||||
|
||||
/**
|
||||
* 联网搜索
|
||||
*/
|
||||
private Boolean networkSearch = false;
|
||||
|
||||
@NotBlank(message = "调用的 AI 大模型名称不能为空")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 温度值,默认为 0.7
|
||||
*/
|
||||
private Double temperature = 0.7;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.hanserwei.airobot.model;
|
||||
package com.hanserwei.airobot.model.vo.chat;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
@@ -6,10 +6,12 @@ import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@Builder
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class AIResponse {
|
||||
// 流式响应内容
|
||||
@Builder
|
||||
public class AiResponse {
|
||||
/**
|
||||
* 响应内容
|
||||
*/
|
||||
private String v;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.hanserwei.airobot.model.vo.chat;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class NewChatReqVO {
|
||||
|
||||
@NotBlank(message = "用户消息不能为空")
|
||||
private String message;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.hanserwei.airobot.model.vo.chat;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Builder
|
||||
public class NewChatRspVO {
|
||||
/**
|
||||
* 摘要
|
||||
*/
|
||||
private String summary;
|
||||
|
||||
/**
|
||||
* 对话 UUID
|
||||
*/
|
||||
private String uuid;
|
||||
}
|
||||
15
src/main/java/com/hanserwei/airobot/service/ChatService.java
Normal file
15
src/main/java/com/hanserwei/airobot/service/ChatService.java
Normal file
@@ -0,0 +1,15 @@
|
||||
package com.hanserwei.airobot.service;
|
||||
|
||||
import com.hanserwei.airobot.model.vo.chat.NewChatReqVO;
|
||||
import com.hanserwei.airobot.model.vo.chat.NewChatRspVO;
|
||||
import com.hanserwei.airobot.utils.Response;
|
||||
|
||||
public interface ChatService {
|
||||
|
||||
/**
|
||||
* 新建对话
|
||||
* @param newChatReqVO 新建对话请求参数
|
||||
* @return 新建对话结果
|
||||
*/
|
||||
Response<NewChatRspVO> newChat(NewChatReqVO newChatReqVO);
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.hanserwei.airobot.service.impl;
|
||||
|
||||
import com.hanserwei.airobot.domain.dos.ChatDO;
|
||||
import com.hanserwei.airobot.domain.mapper.ChatMapper;
|
||||
import com.hanserwei.airobot.model.vo.chat.NewChatReqVO;
|
||||
import com.hanserwei.airobot.model.vo.chat.NewChatRspVO;
|
||||
import com.hanserwei.airobot.service.ChatService;
|
||||
import com.hanserwei.airobot.utils.Response;
|
||||
import com.hanserwei.airobot.utils.StringUtil;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@Service
|
||||
public class ChatServiceImpl implements ChatService {
|
||||
|
||||
@Resource
|
||||
private ChatMapper chatMapper;
|
||||
|
||||
@Override
|
||||
public Response<NewChatRspVO> newChat(NewChatReqVO newChatReqVO) {
|
||||
// 用户发来的消息
|
||||
String message = newChatReqVO.getMessage();
|
||||
|
||||
// 生成对话的UUID
|
||||
String uuid = UUID.randomUUID().toString();
|
||||
// 截取用户发送的消息,作为对话的摘要
|
||||
String summary = StringUtil.truncate(message, 20);
|
||||
// 存储对话记录到数据库中
|
||||
chatMapper.insert(ChatDO.builder()
|
||||
.summary(summary)
|
||||
.uuid(uuid)
|
||||
.createTime(LocalDateTime.now())
|
||||
.updateTime(LocalDateTime.now())
|
||||
.build());
|
||||
|
||||
// 将摘要、UUID 返回给前端
|
||||
return Response.success(NewChatRspVO.builder()
|
||||
.uuid(uuid)
|
||||
.summary(summary)
|
||||
.build());
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package com.hanserwei.airobot.utils;
|
||||
|
||||
import org.jasypt.util.text.AES256TextEncryptor;
|
||||
|
||||
public class EncryptorUtil {
|
||||
public static void main(String[] args) {
|
||||
AES256TextEncryptor textEncryptor = new AES256TextEncryptor();
|
||||
textEncryptor.setPassword("hanserwei");
|
||||
System.out.println(textEncryptor.encrypt("sk-QXBlsyIonybNTcG5tt5GvmMpg2WpdMLPTvU55TXrt9urWpL8"));
|
||||
}
|
||||
}
|
||||
124
src/main/java/com/hanserwei/airobot/utils/JsonUtil.java
Normal file
124
src/main/java/com/hanserwei/airobot/utils/JsonUtil.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.hanserwei.airobot.utils;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.SerializationFeature;
|
||||
import com.fasterxml.jackson.databind.type.CollectionType;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import lombok.SneakyThrows;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* JSON 工具类,提供对象与 JSON 字符串之间的相互转换功能。
|
||||
* 支持普通对象、List、Set、Map 等结构的序列化和反序列化。
|
||||
*/
|
||||
public class JsonUtil {
|
||||
|
||||
private static ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
static {
|
||||
// 忽略未知属性,防止反序列化失败
|
||||
OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
// 允许序列化空对象
|
||||
OBJECT_MAPPER.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
|
||||
// 注册 JavaTimeModule 以支持 LocalDateTime 的序列化
|
||||
OBJECT_MAPPER.registerModules(new JavaTimeModule());
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化:统一使用 Spring Boot 个性化配置的 ObjectMapper
|
||||
*
|
||||
* @param objectMapper 外部传入的 ObjectMapper 实例
|
||||
*/
|
||||
public static void init(ObjectMapper objectMapper) {
|
||||
OBJECT_MAPPER = objectMapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将对象转换为 JSON 字符串
|
||||
*
|
||||
* @param obj 待转换的对象
|
||||
* @return 转换后的 JSON 字符串
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static String toJsonString(Object obj) {
|
||||
return OBJECT_MAPPER.writeValueAsString(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为指定类型的对象
|
||||
*
|
||||
* @param jsonStr JSON 字符串
|
||||
* @param clazz 目标对象的类类型
|
||||
* @param <T> 泛型参数,表示目标对象的类型
|
||||
* @return 转换后的对象实例,如果输入为空则返回 null
|
||||
*/
|
||||
@SneakyThrows
|
||||
public static <T> T parseObject(String jsonStr, Class<T> clazz) {
|
||||
if (StringUtils.isBlank(jsonStr)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return OBJECT_MAPPER.readValue(jsonStr, clazz);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串转换为指定键值类型的 Map 对象
|
||||
*
|
||||
* @param jsonStr JSON 字符串
|
||||
* @param keyClass Map 键的类型
|
||||
* @param valueClass Map 值的类型
|
||||
* @param <K> 泛型参数,表示 Map 键的类型
|
||||
* @param <V> 泛型参数,表示 Map 值的类型
|
||||
* @return 转换后的 Map 实例
|
||||
* @throws Exception 当解析失败时抛出异常
|
||||
*/
|
||||
public static <K, V> Map<K, V> parseMap(String jsonStr, Class<K> keyClass, Class<V> valueClass) throws Exception {
|
||||
// 构造 Map 类型并进行反序列化
|
||||
return OBJECT_MAPPER.readValue(jsonStr, OBJECT_MAPPER.getTypeFactory().constructMapType(Map.class, keyClass, valueClass));
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串解析为指定元素类型的 List 对象
|
||||
*
|
||||
* @param jsonStr JSON 字符串
|
||||
* @param clazz List 中元素的类型
|
||||
* @param <T> 泛型参数,表示 List 元素的类型
|
||||
* @return 转换后的 List 实例
|
||||
* @throws Exception 当解析失败时抛出异常
|
||||
*/
|
||||
public static <T> List<T> parseList(String jsonStr, Class<T> clazz) throws Exception {
|
||||
// 构造 List 类型并进行反序列化
|
||||
return OBJECT_MAPPER.readValue(jsonStr, new TypeReference<List<T>>() {
|
||||
@Override
|
||||
public CollectionType getType() {
|
||||
return OBJECT_MAPPER.getTypeFactory().constructCollectionType(List.class, clazz);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 JSON 字符串解析为指定元素类型的 Set 对象
|
||||
*
|
||||
* @param jsonStr JSON 字符串
|
||||
* @param clazz Set 中元素的类型
|
||||
* @param <T> 泛型参数,表示 Set 元素的类型
|
||||
* @return 转换后的 Set 实例
|
||||
* @throws Exception 当解析失败时抛出异常
|
||||
*/
|
||||
public static <T> Set<T> parseSet(String jsonStr, Class<T> clazz) throws Exception {
|
||||
// 构造 Set 类型并进行反序列化
|
||||
return OBJECT_MAPPER.readValue(jsonStr, new TypeReference<>() {
|
||||
@Override
|
||||
public CollectionType getType() {
|
||||
return OBJECT_MAPPER.getTypeFactory().constructCollectionType(Set.class, clazz);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
70
src/main/java/com/hanserwei/airobot/utils/Response.java
Normal file
70
src/main/java/com/hanserwei/airobot/utils/Response.java
Normal file
@@ -0,0 +1,70 @@
|
||||
package com.hanserwei.airobot.utils;
|
||||
|
||||
import com.hanserwei.airobot.exception.BaseExceptionInterface;
|
||||
import com.hanserwei.airobot.exception.BizException;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
@Data
|
||||
public class Response<T> implements Serializable {
|
||||
|
||||
// 是否成功,默认为 true
|
||||
private boolean success = true;
|
||||
// 响应消息
|
||||
private String message;
|
||||
// 异常码
|
||||
private String errorCode;
|
||||
// 响应数据
|
||||
private T data;
|
||||
|
||||
// =================================== 成功响应 ===================================
|
||||
public static <T> Response<T> success() {
|
||||
return new Response<>();
|
||||
}
|
||||
|
||||
public static <T> Response<T> success(T data) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setData(data);
|
||||
return response;
|
||||
}
|
||||
|
||||
// =================================== 失败响应 ===================================
|
||||
public static <T> Response<T> fail() {
|
||||
Response<T> response = new Response<>();
|
||||
response.setSuccess(false);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(String errorMessage) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setSuccess(false);
|
||||
response.setMessage(errorMessage);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(String errorCode, String errorMessage) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setSuccess(false);
|
||||
response.setErrorCode(errorCode);
|
||||
response.setMessage(errorMessage);
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(BizException bizException) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setSuccess(false);
|
||||
response.setErrorCode(bizException.getErrorCode());
|
||||
response.setMessage(bizException.getErrorMessage());
|
||||
return response;
|
||||
}
|
||||
|
||||
public static <T> Response<T> fail(BaseExceptionInterface baseExceptionInterface) {
|
||||
Response<T> response = new Response<>();
|
||||
response.setSuccess(false);
|
||||
response.setErrorCode(baseExceptionInterface.getErrorCode());
|
||||
response.setMessage(baseExceptionInterface.getErrorMessage());
|
||||
return response;
|
||||
}
|
||||
|
||||
}
|
||||
30
src/main/java/com/hanserwei/airobot/utils/StringUtil.java
Normal file
30
src/main/java/com/hanserwei/airobot/utils/StringUtil.java
Normal file
@@ -0,0 +1,30 @@
|
||||
package com.hanserwei.airobot.utils;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
public class StringUtil {
|
||||
|
||||
/**
|
||||
* 截取用户问题的前面部分文字作为摘要
|
||||
*
|
||||
* @param message 用户问题
|
||||
* @param maxLength 最大截取长度
|
||||
* @return 摘要文本,如果原问题长度不足则返回原问题
|
||||
*/
|
||||
public static String truncate(String message, int maxLength) {
|
||||
// 判空
|
||||
if (StringUtils.isBlank(message)) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String trimmed = message.trim();
|
||||
|
||||
// 如果文本长度小于等于最大长度,直接返回
|
||||
if (trimmed.length() <= maxLength) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
// 截取指定长度
|
||||
return trimmed.substring(0, maxLength);
|
||||
}
|
||||
}
|
||||
@@ -2,45 +2,32 @@
|
||||
spring:
|
||||
application:
|
||||
name: han-ai-robot-springboot
|
||||
datasource:
|
||||
driver-class-name: com.p6spy.engine.spy.P6SpyDriver # 数据库驱动类名
|
||||
url: jdbc:p6spy:postgresql://localhost:5432/han_ai_robot # 数据库连接 URL
|
||||
username: postgres # 数据库用户名
|
||||
password: postgressql # 数据库密码
|
||||
hikari: # HikariCP 连接池配置
|
||||
pool-name: AI-Robot-HikariCP # 自定义连接池名称
|
||||
auto-commit: true # 是否自动提交事务
|
||||
connection-timeout: 30000 # 连接超时时间(毫秒)
|
||||
idle-timeout: 600000 # 空闲连接存活最大时间(毫秒)
|
||||
max-lifetime: 1800000 # 连接最大存活时间(毫秒)
|
||||
minimum-idle: 5 # 最小空闲连接数
|
||||
maximum-pool-size: 20 # 最大连接池大小
|
||||
connection-test-query: SELECT 1 # 连接测试查询
|
||||
validation-timeout: 5000 # 验证连接的有效性
|
||||
cassandra:
|
||||
contact-points: 127.0.0.1 # Cassandra 集群节点地址(可配置多个,用逗号分隔)
|
||||
port: 9042 # 端口号
|
||||
local-datacenter: datacenter1 # 必须与集群配置的数据中心名称一致(大小写敏感)
|
||||
ai:
|
||||
deepseek:
|
||||
api-key: ENC(MROXdiEHmWk08koE63bTzFqW52MaXLpMkM9Cyl40Ubj+Lw1yKeZuHLEcs6jTFY8ditY4gJ1365LMAY8Z9G1uwfYFYaYdb3NyijplX7GuDZA=) # 填写 DeepSeek Api Key, 改成你自己的
|
||||
base-url: https://api.deepseek.com # DeepSeek 的请求 URL, 可不填,默认值为 api.deepseek.com
|
||||
chat:
|
||||
options:
|
||||
model: deepseek-chat # 使用哪个模型
|
||||
temperature: 0.8 # 温度值
|
||||
ollama:
|
||||
base-url: http://localhost:11434 # Ollama 服务的访问地址, 11434 端口是 Ollama 默认的启动端口
|
||||
chat:
|
||||
options: # 模型参数
|
||||
model: qwen3:14b # 指定 Ollama 使用的大模型名称,根据你实际安装的来,我运行的是 14b
|
||||
temperature: 0.7 # 温度值
|
||||
zhipuai:
|
||||
base-url: https://open.bigmodel.cn/api/paas # 智谱 AI 的请求 URL, 可不填,默认值为 open.bigmodel.cn/api/paas
|
||||
api-key: ENC(Rz1O0AygSzG3q4UrIpIPHRwFoTQXCUZkWZ54vNzl1kgdBkQECzCYa3LoOADM9NlGLlAwCKTMtkj0nd6cP98T59DohcKtzc3iYyiAoNRfH0rsiu483CpaCciyMwxCUi5O) # 填写智谱 AI 的 API Key, 该成你自己的
|
||||
chat:
|
||||
options: # 模型参数
|
||||
model: GLM-4.6 # 模型名称,使用智谱 AI 哪个模型
|
||||
temperature: 0.7 # 温度值
|
||||
openai:
|
||||
base-url: https://api.master-jsx.top # OpenAI 服务的访问地址,这里使用的第三方代理商:智增增
|
||||
api-key: ENC(D6ETp0VBeDYXvM612dcoGkyHaGUcPuwOVuSLtL92TOCxydyMKXL7/VBndWjFkxAQP/AS7TeQeegla+Ny6TrLStwdJtd28mVhoyf2YsKuXIdRnKF/mv8/uZ0MpzMdv9YR) # 填写智增增的 API Key, 该成你自己的
|
||||
chat:
|
||||
options:
|
||||
model: gpt-4o # 模型名称
|
||||
temperature: 0.7 # 温度值
|
||||
dashscope:
|
||||
api-key: ENC(cMgcKZkFllyE88DIbGwLKot9Vg02co+gsmY8L8o4/o3UjhcmqO4lJzFU35Sx0n+qFG8pDL0wBjoWrT8X6BuRw9vNlQhY1LgRWHaF9S1zzyM=)
|
||||
chat:
|
||||
options:
|
||||
model: qwen-omni-turbo
|
||||
temperature: 0.7
|
||||
multi-model: true
|
||||
model: qwen-plus
|
||||
temperature: 0.5
|
||||
chat:
|
||||
memory:
|
||||
repository:
|
||||
@@ -49,7 +36,6 @@ spring:
|
||||
table: t_ai_chat_memory
|
||||
time-to-live: 1095d
|
||||
initialize-schema: true
|
||||
|
||||
jasypt:
|
||||
encryptor:
|
||||
password: ${jasypt.encryptor.password}
|
||||
@@ -58,3 +44,4 @@ jasypt:
|
||||
logging:
|
||||
level:
|
||||
org.springframework.ai.chat.client.advisor: debug
|
||||
config: classpath:log4j2.xml # 设置日志配置文件路径
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 510 KiB |
94
src/main/resources/log4j2.xml
Normal file
94
src/main/resources/log4j2.xml
Normal file
@@ -0,0 +1,94 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
status: 设置 Log4j2 内部日志的级别,可选值: OFF, FATAL, ERROR, WARN, INFO, DEBUG, TRACE, ALL
|
||||
monitorInterval: 配置自动检测间隔(秒),在此时间后检查配置文件是否修改并自动重新加载,无需重启应用
|
||||
-->
|
||||
<Configuration status="WARN" monitorInterval="30">
|
||||
<!-- 定义全局属性 -->
|
||||
<Properties>
|
||||
<!-- 日志输出格式模式 -->
|
||||
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n</Property>
|
||||
<!-- 日志文件存储根目录 -->
|
||||
<Property name="APP_LOG_ROOT">./logs</Property>
|
||||
<!-- 使用日期变量作为日志文件名的一部分 -->
|
||||
<Property name="LOG_DATE_PATTERN">%d{yyyy-MM-dd}</Property>
|
||||
</Properties>
|
||||
|
||||
<Appenders>
|
||||
<!-- 控制台输出 -->
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<!-- 使用预定义的日志格式 -->
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
</Console>
|
||||
|
||||
<!--
|
||||
滚动文件附加器
|
||||
fileName: 当前正在写入的日志文件路径和名称
|
||||
filePattern: 滚动(归档)日志文件的命名模式
|
||||
-->
|
||||
<RollingFile name="FileAppender"
|
||||
fileName="${APP_LOG_ROOT}/application-${date:yyyy-MM-dd}.log"
|
||||
filePattern="${APP_LOG_ROOT}/application-%d{yyyy-MM-dd}-%i.log">
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
<Policies>
|
||||
<!-- 基于时间的滚动策略: 每天滚动一次 -->
|
||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||
<!-- 基于文件大小的滚动策略: 当日志文件达到 10MB 时滚动 -->
|
||||
<SizeBasedTriggeringPolicy size="10 MB"/>
|
||||
</Policies>
|
||||
<!--
|
||||
滚动策略配置: 自动删除旧日志文件
|
||||
maxDepth: 搜索深度,1 表示只搜索当前目录
|
||||
-->
|
||||
<DefaultRolloverStrategy>
|
||||
<Delete basePath="${APP_LOG_ROOT}" maxDepth="1">
|
||||
<!-- 匹配文件名模式 -->
|
||||
<IfFileName glob="application-*.log" />
|
||||
<!-- 保留最近 30 天的日志文件 -->
|
||||
<IfLastModified age="30d" />
|
||||
</Delete>
|
||||
</DefaultRolloverStrategy>
|
||||
</RollingFile>
|
||||
|
||||
<!--
|
||||
错误专用日志附加器 - 只记录 ERROR 级别及以上的日志
|
||||
-->
|
||||
<RollingFile name="ErrorAppender"
|
||||
fileName="${APP_LOG_ROOT}/error-${date:yyyy-MM-dd}.log"
|
||||
filePattern="${APP_LOG_ROOT}/error-%d{yyyy-MM-dd}-%i.log">
|
||||
<PatternLayout pattern="${LOG_PATTERN}"/>
|
||||
<!-- Filters: 过滤器配置,只接受 ERROR 及以上级别的日志 -->
|
||||
<Filters>
|
||||
<ThresholdFilter level="ERROR" onMatch="ACCEPT" onMismatch="DENY"/>
|
||||
</Filters>
|
||||
<Policies>
|
||||
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
|
||||
<SizeBasedTriggeringPolicy size="10 MB"/>
|
||||
</Policies>
|
||||
<!-- 滚动策略: 最多保留 30 个归档文件 -->
|
||||
<DefaultRolloverStrategy max="30"/>
|
||||
</RollingFile>
|
||||
</Appenders>
|
||||
|
||||
<Loggers>
|
||||
<!--
|
||||
根日志记录器: 所有日志记录的默认配置
|
||||
level: 日志级别阈值,只有等于或高于此级别的日志才会被处理
|
||||
-->
|
||||
<Root level="info">
|
||||
<!-- 引用控制台附加器 -->
|
||||
<AppenderRef ref="Console"/>
|
||||
<!-- 引用滚动文件附加器 -->
|
||||
<AppenderRef ref="FileAppender"/>
|
||||
<!-- 引用错误专用日志附加器 -->
|
||||
<AppenderRef ref="ErrorAppender"/>
|
||||
</Root>
|
||||
|
||||
<!-- 设置特定包的日志级别(未来备用) -->
|
||||
<Logger name="com.example.demo" level="debug" additivity="false">
|
||||
<AppenderRef ref="Console"/>
|
||||
<AppenderRef ref="FileAppender"/>
|
||||
<AppenderRef ref="ErrorAppender"/>
|
||||
</Logger>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
@@ -1,4 +0,0 @@
|
||||
你是一位资深 {lang} 开发工程师。请严格遵循以下要求编写代码:
|
||||
1. 功能描述:{description}
|
||||
2. 代码需包含详细注释
|
||||
3. 使用业界最佳实践
|
||||
33
src/main/resources/spy.properties
Normal file
33
src/main/resources/spy.properties
Normal file
@@ -0,0 +1,33 @@
|
||||
# 模块列表,根据版本选择合适的配置
|
||||
modulelist=com.baomidou.mybatisplus.extension.p6spy.MybatisPlusLogFactory,com.p6spy.engine.outage.P6OutageFactory
|
||||
|
||||
# 自定义日志格式
|
||||
logMessageFormat=com.baomidou.mybatisplus.extension.p6spy.P6SpyLogger
|
||||
|
||||
# 日志输出到控制台
|
||||
appender=com.baomidou.mybatisplus.extension.p6spy.StdoutLogger
|
||||
|
||||
# 取消JDBC驱动注册
|
||||
deregisterdrivers=true
|
||||
|
||||
# 使用前缀
|
||||
useprefix=true
|
||||
|
||||
# 排除的日志类别
|
||||
excludecategories=info,debug,result,commit,resultset
|
||||
|
||||
# 日期格式
|
||||
dateformat=yyyy-MM-dd HH:mm:ss
|
||||
|
||||
# 实际驱动列表
|
||||
# driverlist=org.h2.Driver
|
||||
|
||||
# 开启慢SQL记录
|
||||
outagedetection=true
|
||||
|
||||
# 慢SQL记录标准(单位:秒)
|
||||
outagedetectioninterval=2
|
||||
|
||||
# 过滤 flw_ 开头的表 SQL 打印
|
||||
filter=true
|
||||
exclude=flw_*
|
||||
@@ -1,13 +0,0 @@
|
||||
package com.hanserwei.airobot;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class HanAiRobotSpringbootApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||
31
src/test/java/com/hanserwei/airobot/MybatisPlusTests.java
Normal file
31
src/test/java/com/hanserwei/airobot/MybatisPlusTests.java
Normal file
@@ -0,0 +1,31 @@
|
||||
package com.hanserwei.airobot;
|
||||
|
||||
import com.hanserwei.airobot.domain.dos.ChatDO;
|
||||
import com.hanserwei.airobot.domain.mapper.ChatMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.UUID;
|
||||
|
||||
@SpringBootTest
|
||||
class MybatisPlusTests {
|
||||
|
||||
@Resource
|
||||
private ChatMapper chatMapper;
|
||||
|
||||
/**
|
||||
* 添加数据
|
||||
*/
|
||||
@Test
|
||||
void testInsert() {
|
||||
chatMapper.insert(ChatDO.builder()
|
||||
.uuid(UUID.randomUUID().toString())
|
||||
.summary("新对话")
|
||||
.createTime(LocalDateTime.now())
|
||||
.updateTime(LocalDateTime.now())
|
||||
.build());
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user