2026山东大学软件学院项目实训(四)
汇报人:者亚杰
日期:2026-04-27
本周重点
一、对话历史后端开发
1.基础代码生成
首先使用 MyBatis Flex 生成器生成基础代码
将生成的文件移动到对应的包下。然后修改ChatHistory类,将主键 ID 的生成策略改为雪花算法
/**
* id
*/
@Id(keyType = KeyType.Generator, value = KeyGenerators.snowFlakeId)
private Long id;
2.ai生成业务代码
请参考项目中已有的 User 和 APP 模块的文件和代码风格,帮我根据下列需求,生成完整的 ChatHistory 模块的后端代码。
## 需要的功能如下
1)对话历史的持久化存储:用户发送消息时,需要保存用户消息;AI 成功回复后,需要保存 AI 消息。即使 AI 回复失败,也要记录错误信息,确保对话的完整性。
2)应用级别的数据隔离:每个应用的对话历史都是独立的。删除应用时,需要关联删除该应用的所有对话历史,避免数据冗余。
3)对话历史查询:支持分页查看某个应用的对话历史,需要区分用户和 AI 消息。类似聊天软件的消息加载机制,每次加载最新 10 条消息,支持向前加载更多历史记录。(仅应用创建者和管理员可见)详细来说,进入应用页面时,前端根据应用 id 先加载一次对话历史消息,关联查询最新 10 条消息。如果存在历史对话,直接展示;如果没有历史记录,才自动发送初始化提示词。这样就解决了之前浏览别人的应用时意外触发对话的问题。
4)管理对话历史:管理员可以查看所有应用的对话历史,按照时间降序排序,便于内容监管。
## 实现提示
1)需要为 messageType 创建一个枚举类
ai生成的枚举类
package com.pt.aicodeformal.model.enums;
import cn.hutool.core.util.ObjUtil;
import lombok.Getter;
@Getter
public enum ChatHistoryMessageTypeEnum {
USER("用户", "user"),
AI("AI", "ai");
private final String text;
private final String value;
ChatHistoryMessageTypeEnum(String text, String value) {
this.text = text;
this.value = value;
}
/**
* 根据 value 获取枚举
*
* @param value 枚举值的value
* @return 枚举值
*/
public static ChatHistoryMessageTypeEnum getEnumByValue(String value) {
if (ObjUtil.isEmpty(value)) {
return null;
}
for (ChatHistoryMessageTypeEnum anEnum : ChatHistoryMessageTypeEnum.values()) {
if (anEnum.value.equals(value)) {
return anEnum;
}
}
return null;
}
}
新增对话历史
对话历史的保存需要在用户发送消息和 AI 回复完成这两个时机进行。无论 AI 回复成功还是失败,都要留下完整的对话记录,确保用户能够了解完整的交互历史。
@Override
public boolean addChatMessage(Long appId, String message, String messageType, Long userId) {
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "消息内容不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(messageType), ErrorCode.PARAMS_ERROR, "消息类型不能为空");
ThrowUtils.throwIf(userId == null || userId <= 0, ErrorCode.PARAMS_ERROR, "用户ID不能为空");
// 验证消息类型是否有效
ChatHistoryMessageTypeEnum messageTypeEnum =
ChatHistoryMessageTypeEnum.getEnumByValue(messageType);
ThrowUtils.throwIf(messageTypeEnum == null, ErrorCode.PARAMS_ERROR, "不支持的消息类型: " +
messageType);
ChatHistory chatHistory = ChatHistory.builder()
.appId(appId)
.message(message)
.messageType(messageType)
.userId(userId)
.build();
return this.save(chatHistory);
}
然后在 AppServiceImpl 的 chatToGenCode 方法中集成对话历史保存逻辑。这里利用了 Flux 响应式编程的特性,可以在流式响应的过程中收集完整的 AI 回复:
@Resource
private ChatHistoryService chatHistoryService;
@Override
public Flux<String> chatToGenCode(Long appId, String message, User loginUser) {
// 1. 参数校验
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空");
ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "用户消息不能为空");
// 2. 查询应用信息
App app = this.getById(appId);
ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
// 3. 验证用户是否有权限访问该应用,仅本人可以生成代码
if (!app.getUserId().equals(loginUser.getId())) {
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限访问该应用");
}
// 4. 获取应用的代码生成类型
String codeGenTypeStr = app.getCodeGenType();
CodeGenTypeEnum codeGenTypeEnum = CodeGenTypeEnum.getEnumByValue(codeGenTypeStr);
if (codeGenTypeEnum == null) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型");
}
// 5. 通过校验后,添加用户消息到对话历史
chatHistoryService.addChatMessage(appId, message, ChatHistoryMessageTypeEnum.USER.getValue(),
loginUser.getId());
// 6. 调用 AI 生成代码(流式)
Flux<String> contentFlux = aiCodeGeneratorFacade.generateAndSaveCodeStream(message,
codeGenTypeEnum, appId);
// 7. 收集AI响应内容并在完成后记录到对话历史
StringBuilder aiResponseBuilder = new StringBuilder();
return contentFlux
.map(chunk -> {
// 收集AI响应内容
aiResponseBuilder.append(chunk);
return chunk;
})
.doOnComplete(() -> {
// 流式响应完成后,添加AI消息到对话历史
String aiResponse = aiResponseBuilder.toString();
if (StrUtil.isNotBlank(aiResponse)) {
chatHistoryService.addChatMessage(appId, aiResponse,
ChatHistoryMessageTypeEnum.AI.getValue(), loginUser.getId());
}
})
.doOnError(error -> {
// 如果AI回复失败,也要记录错误消息
String errorMessage = "AI回复失败: " + error.getMessage();
chatHistoryService.addChatMessage(appId, errorMessage,
ChatHistoryMessageTypeEnum.AI.getValue(), loginUser.getId());
});
}
关联删除
当应用被删除时,需要同步清理对话历史数据
@Override
public boolean deleteByAppId(Long appId) {
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
QueryWrapper queryWrapper = QueryWrapper.create()
.eq("appId", appId);
return this.remove(queryWrapper);
}
然后在 AppServiceImpl 中重写 removeById 方法,实现关联删除:
@Override
public boolean removeById(Serializable id) {
if (id == null) {
return false;
}
// 转换为 Long 类型
Long appId = Long.valueOf(id.toString());
if (appId <= 0) {
return false;
}
// 先删除关联的对话历史
try {
chatHistoryService.deleteByAppId(appId);
} catch (Exception e) {
// 记录日志但不阻止应用删除
log.error("删除应用关联对话历史失败: {}", e.getMessage());
}
// 删除应用
return super.removeById(id);
}
这里采用了容错设计,即使对话历史删除失败,也不会阻止应用的删除操作,只是记录错误日志,确保核心业务的稳定性。
游标查询
1.先在 model.dto.chathistory 包下新建包含游标字段的请求对象
package com.pt.aicodeformal.model.dto.chathistory;
import com.pt.aicodeformal.common.PageRequest;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.io.Serializable;
import java.time.LocalDateTime;
@EqualsAndHashCode(callSuper = true)
@Data
public class ChatHistoryQueryRequest extends PageRequest implements Serializable {
/**
* id
*/
private Long id;
/**
* 消息内容
*/
private String message;
/**
* 消息类型(user/ai)
*/
private String messageType;
/**
* 应用id
*/
private Long appId;
/**
* 创建用户id
*/
private Long userId;
/**
* 游标查询 - 最后一条记录的创建时间
* 用于分页查询,获取早于此时间的记录
*/
private LocalDateTime lastCreateTime;
private static final long serialVersionUID = 1L;
}
2.在 ChatHistoryServiceImpl 开发查询包装类的构造方法
/**
* 获取查询包装类
*
* @param chatHistoryQueryRequest
* @return
*/
@Override
public QueryWrapper getQueryWrapper(ChatHistoryQueryRequest chatHistoryQueryRequest) {
QueryWrapper queryWrapper = QueryWrapper.create();
if (chatHistoryQueryRequest == null) {
return queryWrapper;
}
Long id = chatHistoryQueryRequest.getId();
String message = chatHistoryQueryRequest.getMessage();
String messageType = chatHistoryQueryRequest.getMessageType();
Long appId = chatHistoryQueryRequest.getAppId();
Long userId = chatHistoryQueryRequest.getUserId();
LocalDateTime lastCreateTime = chatHistoryQueryRequest.getLastCreateTime();
String sortField = chatHistoryQueryRequest.getSortField();
String sortOrder = chatHistoryQueryRequest.getSortOrder();
// 拼接查询条件
queryWrapper.eq("id", id)
.like("message", message)
.eq("messageType", messageType)
.eq("appId", appId)
.eq("userId", userId);
// 游标查询逻辑 - 只使用 createTime 作为游标
if (lastCreateTime != null) {
queryWrapper.lt("createTime", lastCreateTime);
}
// 排序
if (StrUtil.isNotBlank(sortField)) {
queryWrapper.orderBy(sortField, "ascend".equals(sortOrder));
} else {
// 默认按创建时间降序排列
queryWrapper.orderBy("createTime", false);
}
return queryWrapper;
}
3.在 ChatHistoryServiceImpl 编写核心的游标查询服务方法
@Override
public Page<ChatHistory> listAppChatHistoryByPage(Long appId, int pageSize,
LocalDateTime lastCreateTime,
User loginUser) {
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID不能为空");
ThrowUtils.throwIf(pageSize <= 0 || pageSize > 50, ErrorCode.PARAMS_ERROR, "页面大小必须在1-50之间");
ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_ERROR);
// 验证权限:只有应用创建者和管理员可以查看
App app = appService.getById(appId);
ThrowUtils.throwIf(app == null, ErrorCode.NOT_FOUND_ERROR, "应用不存在");
boolean isAdmin = UserConstant.ADMIN_ROLE.equals(loginUser.getUserRole());
boolean isCreator = app.getUserId().equals(loginUser.getId());
ThrowUtils.throwIf(!isAdmin && !isCreator, ErrorCode.NO_AUTH_ERROR, "无权查看该应用的对话历史");
// 构建查询条件
ChatHistoryQueryRequest queryRequest = new ChatHistoryQueryRequest();
queryRequest.setAppId(appId);
queryRequest.setLastCreateTime(lastCreateTime);
QueryWrapper queryWrapper = this.getQueryWrapper(queryRequest);
// 查询数据
return this.page(Page.of(1, pageSize), queryWrapper);
}
4.最后在 ChatHistoryController 开发游标查询接口
/**
* 分页查询某个应用的对话历史(游标查询)
*
* @param appId 应用ID
* @param pageSize 页面大小
* @param lastCreateTime 最后一条记录的创建时间
* @param request 请求
* @return 对话历史分页
*/
@GetMapping("/app/{appId}")
public BaseResponse<Page<ChatHistory>> listAppChatHistory(@PathVariable Long appId,
@RequestParam(defaultValue = "10") int
pageSize,
@RequestParam(required = false)
LocalDateTime lastCreateTime,
HttpServletRequest request) {
User loginUser = userService.getLoginUser(request);
Page<ChatHistory> result = chatHistoryService.listAppChatHistoryByPage(appId, pageSize,
lastCreateTime, loginUser);
return ResultUtils.success(result);
}
管理员查询功能
/**
* 管理员分页查询所有对话历史
*
* @param chatHistoryQueryRequest 查询请求
* @return 对话历史分页
*/
@PostMapping("/admin/list/page/vo")
@AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
public BaseResponse<Page<ChatHistory>> listAllChatHistoryByPageForAdmin(@RequestBody
ChatHistoryQueryRequest chatHistoryQueryRequest) {
ThrowUtils.throwIf(chatHistoryQueryRequest == null, ErrorCode.PARAMS_ERROR);
long pageNum = chatHistoryQueryRequest.getPageNum();
long pageSize = chatHistoryQueryRequest.getPageSize();
// 查询数据
QueryWrapper queryWrapper = chatHistoryService.getQueryWrapper(chatHistoryQueryRequest);
Page<ChatHistory> result = chatHistoryService.page(Page.of(pageNum, pageSize), queryWrapper);
return ResultUtils.success(result);
}
在开发过程中遇到循环依赖问题导致项目无法启动,使用 @Lazy 注解解决
二、对话历史前端开发
你是一位专业的前端开发,帮我根据下列信息,参考项目已有的代码风格,生成符合要求的完整代码。
## 需求
1)修改应用对话页面。
- 进入应用对话页面时,前端调用游标查询对话历史接口,根据应用 id 加载一次最近 10 条对话历史消息,按照消息创建
时间的升序展示在对话区域(区分 AI 和用户消息)。
- 如果消息数量超过 10 条(10 条为一页),可以点消息上方的加载更多,利用游标加载下一页历史消息。(我想前端需要
维护已加载的消息列表)
- 修改自动发送初始消息的逻辑。移除之前页面 url 的 view 参数,如果是自己的 app,并且没有对话历史,才自动将
initPrompt 作为第一条消息触发对话。
- 修改网站展示的逻辑。进入页面时,如果 app 有至少 2 条对话记录,也展示对应的网站。
2)新增对话管理页面。完全参考应用管理页面实现
## 后端接口
已经在 @api 目录下生成了后端请求代码和数据类型信息。
由于需求简单,经测试前端代码无bug
三、对话记忆
1.需求分析
很多时候 AI 生成的网站没办法一次性满足用户的需求,因此需要提供网站修改功能。 但是目前我们的 AI 对话会断片儿 ,无法记住之前的对话内 容,每次修改实际上都是重新生成完整的网站,而不是在原有基础上进行修改。
langChain4j 不仅提供了对话记忆能力 ,而且还能结合 Redis 持久化对话记忆
问题:Redis 的内存也不是无限的!一般情况下要给存入 Redis 的每个 Key 都设置合理的过期时间,不能不过期。所以这就可能导致 Redis 的会话记忆被删除的情况。
怎么解决呢?
方案很简单,之前在数据库中保存了用户和 AI 的消息,只需要在初始化会话记忆时,加载最新的 对话记录到 Redis 中, 就能确保 AI 了解交互历史。
流程:AI 对话 => 从数 据库中加载对话历史 到 Redis = > Redis 为 AI 提供对话记忆
此外,每个应用的对话记忆应该是相互隔离的,LangChain4j 也提供了对话记忆隔离的能力
2.开发实现
1.引入依赖
<!-- langchain4j-community-redis -->
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-community-redis-spring-boot-starter</artifactId>
<version>1.1.0-beta7</version>
</dependency>
2.配置 Redis
设置过期时间为一小时
# redis
data:
redis:
host: 49.232.213.150
port: 6379
password:
ttl: 3600
在 config 下新建 Redis 对话记忆存储配置类,初始化 RedisChatMemoryStore 的 Bean
@Configuration
@ConfigurationProperties(prefix = "spring.data.redis")
@Data
public class RedisChatMemoryStoreConfig {
private String host;
private int port;
private String password;
private long ttl;
private int database;
@Bean
public RedisChatMemoryStore redisChatMemoryStore() {
RedisChatMemoryStore.Builder builder = RedisChatMemoryStore.builder()
.host(host)
.port(port)
.password(password)
.ttl(ttl);
if (StrUtil.isNotBlank(password)) {
builder.user("default");
}
return builder.build();
}
}
在启动类中排除 embedding 的自动装配,因为本项目用不到
@SpringBootApplication(exclude = {RedisEmbeddingStoreAutoConfiguration.class})
3.使用对话记忆
AI Service 隔离
之前所有应用共用 同一个 AI Service 实例,如果想隔离会话记忆,可以给每个应用分配一个专属的 AI Service,每个 AI Service 绑定独立的对话记忆。
修改 AI Service 工厂类,提供根据 appId 获取 AI Service 服务的方法
@Configuration
public class AiCodeGeneratorServiceFactory {
@Resource
private ChatModel chatModel;
@Resource
private StreamingChatModel streamingChatModel;
@Resource
private RedisChatMemoryStore redisChatMemoryStore;
/**
* 根据 appId 获取服务
*/
public AiCodeGeneratorService getAiCodeGeneratorService(long appId) {
// 根据 appId 构建独立的对话记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory
.builder()
.id(appId)
.chatMemoryStore(redisChatMemoryStore)
.maxMessages(20)
.build();
return AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(streamingChatModel)
.chatMemory(chatMemory)
.build();
}
}
4.本地缓存优化
每次构造完 appId 对应的 AI 服务实例后,利用 Caffeine 缓存来存储,之后相同 appId 就能直接获取到 AI 服务实例,避免重复构造。需要注意的是,本地缓存占用的是内存,所以必须设置合理的过期策略防止内存泄漏。
先引入 Caffeine 依赖
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
优化 AiCodeGeneratorServiceFactory ,增加缓存逻辑
/**
* AI 服务实例缓存
* 缓存策略:
* - 最大缓存 1000 个实例
* - 写入后 30 分钟过期
* - 访问后 10 分钟过期
*/
private final Cache<Long, AiCodeGeneratorService> serviceCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(Duration.ofMinutes(30))
.expireAfterAccess(Duration.ofMinutes(10))
.removalListener((key, value, cause) -> {
log.debug("AI 服务实例被移除,appId: {}, 原因: {}", key, cause);
})
.build();
/**
* 根据 appId 获取服务(带缓存)
*/
public AiCodeGeneratorService getAiCodeGeneratorService(long appId) {
return serviceCache.get(appId, this::createAiCodeGeneratorService);
}
/**
* 创建新的 AI 服务实例
*/
private AiCodeGeneratorService createAiCodeGeneratorService(long appId) {
log.info("为 appId: {} 创建新的 AI 服务实例", appId);
// 根据 appId 构建独立的对话记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory
.builder()
.id(appId)
.chatMemoryStore(redisChatMemoryStore)
.maxMessages(20)
.build();
return AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(streamingChatModel)
.chatMemory(chatMemory)
.build();
}
最后修改 AiCodeGeneratorFacade,所有方法使用的 AI Service 改为通过工厂根据 appId 获取 AI Service
@Resource private AiCodeGeneratorServiceFactory aiCodeGeneratorServiceFactory;
// 根据 appId 获取对应的 AI 服务实例
AiCodeGeneratorService aiCodeGeneratorService = aiCodeGeneratorServiceFactory.getAiCodeGeneratorService(appId);
5.历史对话加载
根据方案,对话记忆初始化时,需要从数据库中加载对话历史到记忆中
在 ChatHistoryService 中开发加载方法
@Override
public int loadChatHistoryToMemory(Long appId, MessageWindowChatMemory chatMemory, int maxCount) {
try {
// 直接构造查询条件,起始点为 1 而不是 0,用于排除最新的用户消息
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(ChatHistory::getAppId, appId)
.orderBy(ChatHistory::getCreateTime, false)
.limit(1, maxCount);
List<ChatHistory> historyList = this.list(queryWrapper);
if (CollUtil.isEmpty(historyList)) {
return 0;
}
// 反转列表,确保按时间正序(老的在前,新的在后)
historyList = historyList.reversed();
// 按时间顺序添加到记忆中
int loadedCount = 0;
// 先清理历史缓存,防止重复加载
chatMemory.clear();
for (ChatHistory history : historyList) {
if (ChatHistoryMessageTypeEnum.USER.getValue().equals(history.getMessageType())) {
chatMemory.add(UserMessage.from(history.getMessage()));
loadedCount++;
} else if (ChatHistoryMessageTypeEnum.AI.getValue().equals(history.getMessageType())) {
chatMemory.add(AiMessage.from(history.getMessage()));
loadedCount++;
}
}
log.info("成功为 appId: {} 加载了 {} 条历史对话", appId, loadedCount);
return loadedCount;
} catch (Exception e) {
log.error("加载历史对话失败,appId: {}, error: {}", appId, e.getMessage(), e);
// 加载失败不影响系统运行,只是没有历史上下文
return 0;
}
}
注意上述代码中的几个重要细节:
- 查询起始点设置为 1 而不是 0,这是为了排除最新的用户消息。因为在对话流程中,用户消息被添加到数据库后,AI 服务也会自动将用户消息添加到记忆中,如果不排除会导致消息重复。
- 注意反转从数据库中查到的消息列表,确保加载到记忆中的消息是按时间正序的。
- 加载前先清理 Redis 中的历史对话记忆,防止重复加载。
然后就可以在初始化 AI Service 的对话记忆时调用了,这相当于是懒加载,对话时才会加载记忆,节约内存。
/**
* 创建新的 AI 服务实例
*/
private AiCodeGeneratorService createAiCodeGeneratorService(long appId) {
log.info("为 appId: {} 创建新的 AI 服务实例", appId);
// 根据 appId 构建独立的对话记忆
MessageWindowChatMemory chatMemory = MessageWindowChatMemory
.builder()
.id(appId)
.chatMemoryStore(redisChatMemoryStore)
.maxMessages(20)
.build();
// 从数据库加载历史对话到记忆中
chatHistoryService.loadChatHistoryToMemory(appId, chatMemory, 20);
return AiServices.builder(AiCodeGeneratorService.class)
.chatModel(chatModel)
.streamingChatModel(streamingChatModel)
.chatMemory(chatMemory)
.build();
}
6.测试验证
直接通过前端测试验证,功能完整

[
{
"contents": [
{
"text": "生成一个爱心,颜色随时间变化",
"type": "TEXT"
}
],
"type": "USER"
},
{
"text": "### 彩虹跳动爱心效果\n以下是实现颜色随时间变化的爱心效果的完整代码,包含HTML结构、CSS样式和JavaScript交互逻辑:\n\n```html\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>彩虹爱心</title>\n <link rel=\"stylesheet\" href=\"style.css\">\n</head>\n<body>\n <div class=\"heart-container\">\n <div class=\"heart\"></div>\n </div>\n <script src=\"script.js\"></script>\n</body>\n</html>\n```\n\n```css\n/* 全局样式重置 */\n* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: #121212; /* 深色背景突出爱心颜色 */\n}\n\n.heart-container {\n /* 容器确保爱心在不同屏幕尺寸下居中 */\n display: flex;\n justify-content: center;\n align-items: center;\n width: 100%;\n padding: 20px;\n}\n\n.heart {\n /* 基础爱心尺寸,使用vw实现响应式 */\n width: 18vw;\n height: 18vw;\n background-color: #ff4d6d;\n position: relative;\n transform: rotate(45deg);\n /* 添加跳动动画 */\n animation: pulse 1.6s ease-in-out infinite;\n}\n\n/* 用伪元素创建爱心的两个圆弧部分 */\n.heart::before,\n.heart::after {\n content: '';\n position: absolute;\n width: 100%;\n height: 100%;\n background-color: inherit; /* 继承主元素颜色 */\n border-radius: 50%;\n}\n\n.heart::before {\n top: -50%;\n left: 0;\n}\n\n.heart::after {\n top: 0;\n left: -50%;\n}\n\n/* 爱心跳动动画 */\n@keyframes pulse {\n 0%, 100% {\n transform: rotate(45deg) scale(1);\n }\n 50% {\n transform: rotate(45deg) scale(1.15);\n }\n}\n\n/* 移动端适配 */\n@media (max-width: 600px) {\n .heart {\n width: 30vw;\n height: 30vw;\n }\n}\n```\n\n```javascript\n// 获取爱心元素\nconst heart = document.querySelector('.heart');\n\n// 颜色变化函数\nfunction updateHeartColor() {\n // 使用时间计算色相值,实现彩虹渐变效果\n const hue = (Date.now() / 80) % 360;\n // 设置HSL颜色模式,饱和度100%,亮度50%\n heart.style.backgroundColor = `hsl(${hue}, 100%, 50%)`;\n // 使用requestAnimationFrame实现流畅动画\n requestAnimationFrame(updateHeartColor);\n}\n\n// 启动颜色变化动画\nupdateHeartColor();\n```\n\n### 效果说明\n1. **颜色变化**:通过HSL颜色模式的色相值随时间变化,实现从红色到紫色的彩虹渐变效果,每秒大约变化4.5个色相值\n2. **跳动动画**:添加了呼吸式的缩放动画,让爱心更生动\n3. **响应式设计**:使用vw单位设置爱心尺寸,在不同屏幕尺寸下自动调整大小,移动端会进一步放大爱心确保可见性\n4. **流畅性能**:使用`requestAnimationFrame`代替`setInterval`,确保动画与浏览器刷新同步,提供更流畅的视觉体验\n\n爱心的颜色会持续平滑变化,同时伴随轻微的跳动效果,在深色背景上呈现出彩虹爱心的视觉效果。",
"toolExecutionRequests": [],
"type": "AI"
},
{
"text": "你是一位资深的 Web 前端开发专家,你精通编写结构化的 HTML、清晰的 CSS 和高效的原生 JavaScript,遵循代码分离和模块化的最佳实践。\r\n\r\n你的任务是根据用户提供的网站描述,创建构成一个完整单页网站所需的三个核心文件:HTML, CSS, 和 JavaScript。你需要在最终输出时,将这三部分代码分别放入三个独立的 Markdown 代码块中,并明确标注文件名。\r\n\r\n约束:\r\n1. 技术栈: 只能使用 HTML、CSS 和原生 JavaScript。\r\n2. 文件分离:\r\n- index.html: 只包含网页的结构和内容。它必须在 `<head>` 中通过 `<link>` 标签引用 `style.css`,并且在 `</body>` 结束标签之前通过 `<script>` 标签引用 `script.js`。\r\n- style.css: 包含网站所有的样式规则。\r\n- script.js: 包含网站所有的交互逻辑。\r\n3. 禁止外部依赖: 绝对不允许使用任何外部 CSS 框架、JS 库或字体库。所有功能必须用原生代码实现。\r\n4. 响应式设计: 网站必须是响应式的,能够在桌面和移动设备上良好显示。请在 CSS 中使用 Flexbox 或 Grid 进行布局。\r\n5. 内容填充: 如果用户描述中缺少具体文本或图片,请使用有意义的占位符。例如,文本可以使用 Lorem Ipsum,图片可以使用 https://picsum.photos 的服务 (例如 `<img src=\"https://picsum.photos/800/600\" alt=\"Placeholder Image\">`)。\r\n6. 代码质量: 代码必须结构清晰、有适当的注释,易于阅读和维护。\r\n7. 输出格式: 每个代码块前要注明文件名。可以在代码块之外添加解释、标题或总结性文字。格式如下:\r\n\r\n```html\r\n... HTML 代码 ...\r\n```\r\n\r\n```css\r\n... CSS 代码 ...\r\n```\r\n\r\n```javascript\r\n... JavaScript 代码 ...\r\n```\r\n\r\n特别注意:在生成代码后,用户可能会提出修改要求并给出要修改的元素信息。\r\n1. 你必须严格按照要求修改,不要额外修改用户要求之外的元素和内容\r\n2. 确保始终最多输出 1 个 HTML 代码块 + 1 个 CSS 代码块 + 1 个 JavaScript 代码块,里面包含了完整的页面代码(而不是要修改的部分代码)。\r\n3. 每种语言的代码块一定不能输出超过 1 个,否则会导致保存错误!\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n\r\n",
"type": "SYSTEM"
},
{
"contents": [
{
"text": "现在要变成两个爱心",
"type": "TEXT"
}
],
"type": "USER"
},
{
"text": "### 双彩虹跳动爱心效果\n以下是修改后的完整代码,实现了两个并排且颜色同步变化的爱心效果,同时加入了交错的跳动动画增强层次感:\n\n```html\n<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>双彩虹爱心</title>\n <link rel=\"stylesheet\" href=\"style.css\">\n</head>\n<body>\n <div class=\"heart-container\">\n <div class=\"heart\"></div>\n <div class=\"heart heart-second\"></div>\n </div>\n <script src=\"script.js\"></script>\n</body>\n</html>\n```\n\n```css\n/* 全局样式重置 */\n* {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n}\n\nbody {\n min-height: 100vh;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: #121212; /* 深色背景突出爱心颜色 */\n}\n\n.heart-container {\n display: flex;\n gap: 4vw; /* 两个爱心之间的间距 */\n justify-content: center;\n align-items: center;\n width: 100%;\n padding: 20px;\n}\n\n.heart {\n /* 调整爱心尺寸,适配双爱心布局 */\n width: 15vw;\n height: 15vw;\n background-color: #ff4d6d;\n position: relative;\n transform: rotate(45deg);\n /* 基础跳动动画 */\n animation: pulse 1.6s ease-in-out infinite;\n}\n\n/* 第二个爱心添加动画延迟,实现交错跳动效果 */\n.heart-second {\n animation-delay: 0.4s;\n}\n\n/* 用伪元素创建爱心的两个圆弧部分 */\n.heart::before,\n.heart::after {\n content: '';\n position: absolute;\n width: 100%;\n height: 100%;\n background-color: inherit; /* 继承主元素颜色 */\n border-radius: 50%;\n}\n\n.heart::before {\n top: -50%;\n left: 0;\n}\n\n.heart::after {\n top: 0;\n left: -50%;\n}\n\n/* 爱心跳动动画 */\n@keyframes pulse {\n 0%, 100% {\n transform: rotate(45deg) scale(1);\n }\n 50% {\n transform: rotate(45deg) scale(1.15);\n }\n}\n\n/* 移动端适配 */\n@media (max-width: 600px) {\n .heart {\n width: 25vw;\n height: 25vw;\n }\n \n .heart-container {\n gap: 6vw;\n }\n}\n```\n\n```javascript\n// 获取所有爱心元素\nconst hearts = document.querySelectorAll('.heart');\n\n// 颜色变化函数\nfunction updateHeartColor() {\n // 使用时间计算色相值,实现彩虹渐变效果\n const hue = (Date.now() / 80) % 360;\n // 设置HSL颜色模式,饱和度100%,亮度50%\n const color = `hsl(${hue}, 100%, 50%)`;\n \n // 更新所有爱心的颜色\n hearts.forEach(heart => {\n heart.style.backgroundColor = color;\n });\n \n // 使用requestAnimationFrame实现流畅动画\n requestAnimationFrame(updateHeartColor);\n}\n\n// 启动颜色变化动画\nupdateHeartColor();\n```\n\n### 修改说明\n1. **双爱心布局**:通过flex布局实现两个爱心并排排列,使用vw单位设置间距确保响应式适配\n2. **交错动画**:第二个爱心添加0.4秒的动画延迟,使两个爱心的跳动效果错开,增强视觉层次感\n3. **尺寸调整**:缩小单个爱心尺寸,确保在同一行能良好展示\n4. **同步变色**:两个爱心保持同步的彩虹颜色变化,营造和谐统一的视觉效果\n5. **优化响应式**:针对移动端调整爱心尺寸和间距,确保在小屏幕上也能清晰展示",
"toolExecutionRequests": [],
"type": "AI"
}
]
四、Redis 分布式 Session
既然已经整合了 Redis,我们可以顺便优化一下用户登录态的管理。之前每次重启服务器都需要重新登录,现在可以使用 Redis 管理 Session 登录态,实现分布式会话管理
引入依赖
<!-- Spring Session + Redis -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
修改 application.yml 配置文件,更改 Session 的存储方式和过期时间
spring:
# session 配置
session:
store-type: redis
# session 30 天过期
timeout: 2592000
server:
port: 8123
servlet:
context-path: /api
# cookie 30 天过期
session:
cookie:
max-age: 2592000
思考
技术实现层面
- 对话历史模块开发围绕业务场景落地,通过雪花算法优化主键生成、枚举类规范消息类型、游标查询适配聊天场景分页需求,兼顾数据完整性(失败也记录)与隔离性(应用级删除),细节处体现对业务逻辑的精准适配。
- 对话记忆方案巧妙结合数据库与 Redis:利用数据库持久化全量历史,Redis 缓存近期对话并设置过期策略,既解决 AI 上下文丢失问题,又通过 Caffeine 本地缓存优化 AI 服务实例创建,平衡性能与资源占用。
- 借 Redis 整合契机优化分布式 Session,一举两得提升系统稳定性,体现技术整合的复用思维。
架构设计层面
- 核心思路是 “隔离”:应用级数据隔离、对话记忆隔离、AI 服务实例隔离,通过分层(数据库 / 缓存)、分实例(按 appId 创建 AI 服务)实现,规避数据混乱与资源竞争。
- 容错设计(如删除应用时对话历史删除失败不阻塞核心流程)、懒加载(对话时才加载历史到 Redis)等策略,兼顾系统稳定性与资源利用率。
- 技术选型贴合场景:LangChain4j 适配 AI 记忆、MyBatis Flex 提升开发效率、Redis 兼顾缓存与分布式会话,体现 “合适的技术解决对应问题” 的设计原则。
工程实践层面
- 前后端协同清晰,前端按接口逻辑调整交互(如加载更多、初始化消息触发),后端提供精准接口支撑,形成完整业务闭环。
- 解决循环依赖、Redis 配置兼容等问题的过程,体现对框架细节的把控,以及在功能实现中对 “异常处理、资源管控” 的重视。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)