汇报人:者亚杰

日期: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. 查询起始点设置为 1 而不是 0,这是为了排除最新的用户消息。因为在对话流程中,用户消息被添加到数据库后,AI 服务也会自动将用户消息添加到记忆中,如果不排除会导致消息重复。
  2. 注意反转从数据库中查到的消息列表,确保加载到记忆中的消息是按时间正序的。
  3. 加载前先清理 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

思考

技术实现层面

  1. 对话历史模块开发围绕业务场景落地,通过雪花算法优化主键生成、枚举类规范消息类型、游标查询适配聊天场景分页需求,兼顾数据完整性(失败也记录)与隔离性(应用级删除),细节处体现对业务逻辑的精准适配。
  2. 对话记忆方案巧妙结合数据库与 Redis:利用数据库持久化全量历史,Redis 缓存近期对话并设置过期策略,既解决 AI 上下文丢失问题,又通过 Caffeine 本地缓存优化 AI 服务实例创建,平衡性能与资源占用。
  3. 借 Redis 整合契机优化分布式 Session,一举两得提升系统稳定性,体现技术整合的复用思维。

架构设计层面

  1. 核心思路是 “隔离”:应用级数据隔离、对话记忆隔离、AI 服务实例隔离,通过分层(数据库 / 缓存)、分实例(按 appId 创建 AI 服务)实现,规避数据混乱与资源竞争。
  2. 容错设计(如删除应用时对话历史删除失败不阻塞核心流程)、懒加载(对话时才加载历史到 Redis)等策略,兼顾系统稳定性与资源利用率。
  3. 技术选型贴合场景:LangChain4j 适配 AI 记忆、MyBatis Flex 提升开发效率、Redis 兼顾缓存与分布式会话,体现 “合适的技术解决对应问题” 的设计原则。

工程实践层面

  1. 前后端协同清晰,前端按接口逻辑调整交互(如加载更多、初始化消息触发),后端提供精准接口支撑,形成完整业务闭环。
  2. 解决循环依赖、Redis 配置兼容等问题的过程,体现对框架细节的把控,以及在功能实现中对 “异常处理、资源管控” 的重视。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐