文章标签:#Java #SpringBoot #SSE #AI编程 #LangChain4j

一、模块整体设计思路

AI应用生成模块是AI零代码应用生成平台的核心功能,负责将单机版AI代码生成能力与应用管理系统深度集成,实现用户-应用-代码的绑定关联。

本模块遵循核心设计原则:

  1. 应用绑定:代码生成与应用ID强关联,文件按应用ID规范存储
  2. 流式响应:采用SSE服务器推送技术,实现代码实时流式输出
  3. 权限校验:仅应用创建者可触发代码生成,保障数据安全
  4. 文件规范:统一文件目录命名规则,避免存储混乱
  5. 异常统一:全局捕获异常,返回友好错误提示

二、AI应用生成服务开发

1. 业务流程设计

平台化代码生成核心流程:

  1. 用户在主页输入提示词创建应用,生成应用记录入库
  2. 获取应用ID后跳转至AI对话页面
  3. 前端调用SSE流式生成接口,后端校验权限并触发AI生成
  4. 代码按codeGenType_appId规则保存至文件系统
  5. 流式返回生成内容至前端,实时展示生成效果

2. 核心代码改造

(1)代码保存模板改造(CodeFileSaverTemplate)

重构代码保存逻辑,新增应用ID参数,实现文件与应用绑定:

/**
 * 模板方法:保存代码的标准流程(使用appId)
 * @param result 代码结果对象
 * @param appId 应用ID
 * @return 保存的目录
 */
public final File saveCode(T result, Long appId) {
    // 1.验证输入
    validateInput(result);
    // 2.构建基于appId的目录
    String baseDirPath = buildUniqueDir(appId);
    // 3.保存文件(具体实现由子类提供)
    saveFiles(result, baseDirPath);
    // 4.返回目录文件对象
    return new File(baseDirPath);
}

/**
 * 构建基于appId的目录路径
 * @param appId 应用ID
 * @return 目录路径
 */
protected final String buildUniqueDir(Long appId) {
    if (appId == null) {
        throw new BusinessException(ErrorCode.PARAMS_ERROR, "应用ID不能为空");
    }
    String codeType = getCodeType().getValue();
    String uniqueDirName = StrUtil.format("{}_{}", codeType, appId);
    String dirPath = FILE_SAVE_ROOT_DIR + File.separator + uniqueDirName;
    FileUtil.mkdir(dirPath);
    return dirPath;
}
(2)代码保存执行器改造(CodeFileSaverExecutor)

修改执行方法,补充appId参数,适配新的保存逻辑:

/**
 * 执行代码保存(使用appId)
 * @param codeResult 代码结果对象
 * @param codeGenType 代码生成类型
 * @param appId 应用ID
 * @return 保存的目录
 */
public static File executeSaver(Object codeResult, CodeGenTypeEnum codeGenType, Long appId) {
    return switch (codeGenType) {
        case HTML -> htmlCodeFileSaver.saveCode((HtmlCodeResult) codeResult, appId);
        case MULTI_FILE -> multiFileCodeFileSaver.saveCode((MultiFileCodeResult) codeResult, appId);
        default -> throw new BusinessException(ErrorCode.SYSTEM_ERROR, "不支持的代码生成类型:" + codeGenType);
    };
}
(3)AI代码生成门面改造(AiCodeGeneratorFacade)

所有生成方法新增appId参数,同步适配流式生成:

/**
 * 统一入口:根据类型生成并保存代码(流式, 使用appId)
 * @param userMessage 用户提示词
 * @param codeGenTypeEnum 生成类型
 * @param appId 应用ID
 * @return 流式响应
 */
public Flux<String> generateAndSaveCodeStream(String userMessage, CodeGenTypeEnum codeGenTypeEnum, Long appId) {
    if (codeGenTypeEnum == null) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "生成类型为空");
    }
    return switch (codeGenTypeEnum) {
        case HTML -> {
            Flux<String> codeStream = aiCodeGeneratorService.generateHtmlCodeStream(userMessage);
            yield processCodeStream(codeStream, CodeGenTypeEnum.HTML, appId);
        }
        case MULTI_FILE -> {
            Flux<String> codeStream = aiCodeGeneratorService.generateMultiFileCodeStream(userMessage);
            yield processCodeStream(codeStream, CodeGenTypeEnum.MULTI_FILE, appId);
        }
        default -> {
            String errorMessage = "不支持的生成类型:" + codeGenTypeEnum.getValue();
            throw new BusinessException(ErrorCode.SYSTEM_ERROR, errorMessage);
        }
    };
}
(4)应用服务生成方法(AppService#chatToGenCode)

实现权限校验、应用查询、AI生成调用的核心逻辑:

@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.调用AI生成代码
    return aiCodeGeneratorFacade.generateAndSaveCodeStream(message, codeGenTypeEnum, appId);
}

三、SSE流式接口开发

1. 接口设计

采用SSE(Server-Sent Events) 实现流式响应,使用GET请求便于前端EventSource对接,声明响应类型为text/event-stream

2. 核心接口代码(AppController)

/**
 * 应用聊天生成代码(流式SSE)
 * @param appId 应用ID
 * @param message 用户消息
 * @param request 请求对象
 * @return 生成结果流
 */
@GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<String> chatToGenCode(@RequestParam Long appId,
                                 @RequestParam String message,
                                 HttpServletRequest request) {
    // 参数校验
    ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID无效");
    ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "用户消息不能为空");
    // 获取当前登录用户
    User loginUser = userService.getLoginUser(request);
    // 调用服务生成代码(流式)
    return appService.chatToGenCode(appId, message, loginUser);
}

四、SSE流式接口优化

原生SSE接口存在空格丢失无法区分正常结束/异常中断两大问题,需针对性优化。

1. 优化1:解决前端空格丢失问题

问题原因

前端EventSource解析纯文本流式数据时,空格/换行符会被异常过滤,导致代码格式错乱。

解决方案

将流式数据封装为ServerSentEvent,数据转为JSON格式传输,保留原始格式。

2. 优化2:添加生成完成标识

问题原因

SSE默认通过关闭连接标识传输结束,无法区分正常完成网络异常中断

解决方案

追加done事件,前端通过监听事件类型判断生成状态。

3. 优化后完整接口代码

/**
 * 应用聊天生成代码(优化版SSE)
 * @param appId 应用ID
 * @param message 用户消息
 * @param request 请求对象
 * @return 优化后流式响应
 */
@GetMapping(value = "/chat/gen/code", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> chatToGenCode(@RequestParam Long appId,
                                                   @RequestParam String message,
                                                   HttpServletRequest request) {
    // 参数校验
    ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用ID无效");
    ThrowUtils.throwIf(StrUtil.isBlank(message), ErrorCode.PARAMS_ERROR, "用户消息不能为空");
    // 获取当前登录用户
    User loginUser = userService.getLoginUser(request);
    // 调用服务生成代码
    Flux<String> contentFlux = appService.chatToGenCode(appId, message, loginUser);
    
    // 封装数据+添加结束事件
    return contentFlux
            .map(chunk -> {
                // 封装为JSON,保留空格格式
                Map<String, String> wrapper = Map.of("d", chunk);
                String jsonData = JSONUtil.toJsonStr(wrapper);
                return ServerSentEvent.<String>builder()
                        .data(jsonData)
                        .build();
            })
            .concatWith(Mono.just(
                    // 发送done事件标识生成完成
                    ServerSentEvent.<String>builder()
                            .event("done")
                            .data("")
                            .build()
            ));
}

五、接口测试

1. 测试步骤

  1. 用户登录获取会话
  2. 调用SSE流式生成接口
  3. 查看实时输出结果

2. 测试命令(CURL)

# 1.用户登录,保存cookie
curl -X POST "http://localhost:8123/api/user/login" \
-H "Content-Type: application/json" \
-d '{"userAccount":"yupi","userPassword":"12345678"}' \
-c cookies.txt

# 2.调用SSE流式生成接口
curl -G "http://localhost:8123/api/app/chat/gen/code" \
--data-urlencode "appId=303320512563961856" \
--data-urlencode "message=我需要一个简单的任务记录工具网站" \
-H "Accept: text/event-stream" \
-H "Cache-Control: no-cache" \
-b cookies.txt \
--no-buffer

3. 测试效果

优化后接口可正常保留空格/换行,生成完成后触发done事件,前端可精准判断生成状态。

六、模块核心要点总结

  1. 应用绑定:通过appId关联代码文件,解决平台化存储混乱问题
  2. 流式响应:SSE技术实现代码实时输出,提升用户交互体验
  3. 双层优化:JSON封装解决空格丢失,done事件明确生成结束状态
  4. 权限管控:仅应用创建者可触发生成,保障平台数据安全
  5. 规范统一:文件目录、接口返回、异常处理全链路标准化
Logo

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

更多推荐