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:
2025-11-02 21:24:03 +08:00
parent 594adcc48d
commit f3f320f390
50 changed files with 1193 additions and 997 deletions

View File

@@ -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);
});
}
}

View File

@@ -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();
}
}

View File

@@ -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 "";
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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 {
}

View File

@@ -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");
}

View File

@@ -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();
}
}

View File

@@ -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());
}
}

View File

@@ -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();
}
);
}
}

View File

@@ -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());
}
}

View File

@@ -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;
});
}
}

View File

@@ -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();
});
}
}

View File

@@ -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();
}
}

View File

@@ -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();
});
}
}

View File

@@ -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();
});
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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());
}
}

View 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;
}

View 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_message")
public class ChatMessageDO {
@TableId(type = IdType.AUTO)
private Long id;
private String chatUuid;
private String content;
private String role;
private LocalDateTime createTime;
}

View File

@@ -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> {
}

View File

@@ -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> {
}

View File

@@ -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;
}

View File

@@ -0,0 +1,7 @@
package com.hanserwei.airobot.exception;
public interface BaseExceptionInterface {
String getErrorCode();
String getErrorMessage();
}

View File

@@ -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();
}
}

View File

@@ -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);
}
}

View File

@@ -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) {
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -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;
}

View 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);
}

View File

@@ -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());
}
}

View File

@@ -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"));
}
}

View 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);
}
});
}
}

View 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;
}
}

View 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);
}
}