如何实现AI生成应用部署功能

实现AI生成应用部署的4种方式

1、Server工具(配置简单,不过需要额外依赖于Node.js环境,独立启动web服务,性能相对较低)

2、SpringBoot接口实现静态服务资源(适用于预览 AI 生成的网页)

3、Nginx映射(额外引入Nginx组件,提供网站部署服务,性能高,适合生产环境)

4、使用COS对象存储的静态网站访问能力来同时实现存储与访问(需要有自定义域名)

因此我们可以采用组合模式SpringBoot接口实现AI生成的网页预览+Nginx映射提供网站部署服务

Nginx映射

首先下载Nginx:https://nginx.org/en/download.html,然后解压到全英文的目录中

首次启动按默认配置可能会出现404问题,

在conf里的如图位置删掉sky即可:

接着如果你原本就启动了Nginx需要先结束nginx的进程再重新启动即可:

首先在后端项目的tmp目录下新建一个目录code_deploy存放部署的代码文件,与原本的output区分开来,接着需要修改conf里的配置才能实现对应项目的需求,并将原本server里的location注释掉,修改后如图:

复制代码
# 静态资源服务器 - 80 端口
    server {
        listen       80;
        server_name  localhost;
        charset      utf-8;
        charset_types text/css application/javascript text/plain text/xml application/json;
        # 项目部署根目录
        root         D:/lingou-projects/AI-NoCode/xiaolou-nocode-backend/tmp/code_deploy;
        
        # 处理所有请求
        location ~ ^/([^/]+)/(.*)$ {
            try_files /$1/$2 /$1/index.html =404;
        }

        # location / {
        #    root   html;
        #    index  index.html index.htm;
        # }

然后按刚才的步骤先进行save保存后重新启动nginx,将原本生成的文件代码复制到code_deploy中:

访问http://localhost/123456/进行测试效果,成功访问到我们AI生成的个人博客:

搞定Nginx,接下来来进行springboot部署的接口开发,

部署接口接受 appld 作为请求参数,返回可访问的 URL地址 ${部署域名}/{deployKey} 。

springboot部署的接口开发

部署流程如下

1.参数校验:比如是否存在 App、用户是否有权限部署该应用(仅本人可以部署)

2.生成 deployKey:deployKey 的生成逻辑(6 位大小写字母+数字),还要注意不能跟已有的 key 重复;此外,每个 app 只生成一次 deployKey,已有则不生成。

3.部署操作:本质是将 code_output 目录下的临时文件复制到 code_deploy 目录下,为了简化访问地址,直接将 deployKey 作为文件名。

首先在constant包下的AppConstant常量接口中补充3个路径的常量:

复制代码
/**
 * 应用生成目录
 */
String CODE_OUTPUT_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_output";

/**
 * 应用部署目录
 */
String CODE_DEPLOY_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_deploy";

/**
 * 应用部署域名
 */
String CODE_DEPLOY_HOST = "http://localhost";

然后将CodeFileSaverTemplate类的路径常量进行修改:

复制代码
protected static final String FILE_SAVE_ROOT_DIR = AppConstant.CODE_OUTPUT_ROOT_DIR;

然后在controller包下编写一个用于浏览网站的接口:

复制代码
@RestController
@RequestMapping("/static")
public class StaticResourceController {

    // 应用生成根目录(用于浏览)
    private static final String PREVIEW_ROOT_DIR = AppConstant.CODE_OUTPUT_ROOT_DIR;

    /**
     * 提供静态资源访问,支持目录重定向
     * 访问格式:http://localhost:8123/api/static/{deployKey}[/{fileName}]
     */
    @GetMapping("/{deployKey}/**")
    public ResponseEntity<Resource> serveStaticResource(
        @PathVariable String deployKey,
        HttpServletRequest request) {
        try {
            // 获取资源路径
            String resourcePath = (String) request.getAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE);
            resourcePath = resourcePath.substring(("/static/" + deployKey).length());
            // 如果是目录访问(不带斜杠),重定向到带斜杠的URL
            if (resourcePath.isEmpty()) {
                HttpHeaders headers = new HttpHeaders();
                headers.add("Location", request.getRequestURI() + "/");
                return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);
            }
            // 默认返回 index.html
            if (resourcePath.equals("/")) {
                resourcePath = "/index.html";
            }
            // 构建文件路径
            String filePath = PREVIEW_ROOT_DIR + "/" + deployKey + resourcePath;
            File file = new File(filePath);
            // 检查文件是否存在
            if (!file.exists()) {
                return ResponseEntity.notFound().build();
            }
            // 返回文件资源
            Resource resource = new FileSystemResource(file);
            return ResponseEntity.ok()
            .header("Content-Type", getContentTypeWithCharset(filePath))
            .body(resource);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 根据文件扩展名返回带字符编码的 Content-Type
     */
    private String getContentTypeWithCharset(String filePath) {
        if (filePath.endsWith(".html")) return "text/html; charset=UTF-8";
        if (filePath.endsWith(".css")) return "text/css; charset=UTF-8";
        if (filePath.endsWith(".js")) return "application/javascript; charset=UTF-8";
        if (filePath.endsWith(".png")) return "image/png";
        if (filePath.endsWith(".jpg")) return "image/jpeg";
        return "application/octet-stream";
    }
}

在AppService中编写一个部署服务的代码:

复制代码
/**
 * 部署应用
 * 
 * @param appId
 * @param loginUser
 * @return
 */
String deployApp(Long appId, User loginUser);

实现类:

复制代码
@Override
public String deployApp(Long appId, User loginUser) {
    // 1. 参数校验
    ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空");
    ThrowUtils.throwIf(loginUser == null, ErrorCode.NOT_LOGIN_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. 检查是否已有 deployKey
    String deployKey = app.getDeployKey();
    // 没有则生成 6 位 deployKey(大小写字母 + 数字)
    if (StrUtil.isBlank(deployKey)) {
        deployKey = RandomUtil.randomString(6);
    }
    // 5. 获取代码生成类型,构建源目录路径
    String codeGenType = app.getCodeGenType();
    String sourceDirName = codeGenType + "_" + appId;
    String sourceDirPath = AppConstant.CODE_OUTPUT_ROOT_DIR + File.separator + sourceDirName;
    // 6. 检查源目录是否存在
    File sourceDir = new File(sourceDirPath);
    if (!sourceDir.exists() || !sourceDir.isDirectory()) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "应用代码不存在,请先生成代码");
    }
    // 7. 复制文件到部署目录
    String deployDirPath = AppConstant.CODE_DEPLOY_ROOT_DIR + File.separator + deployKey;
    try {
        FileUtil.copyContent(sourceDir, new File(deployDirPath), true);
    } catch (Exception e) {
        throw new BusinessException(ErrorCode.SYSTEM_ERROR, "部署失败:" + e.getMessage());
    }
    // 8. 更新应用的 deployKey 和部署时间
    App updateApp = new App();
    updateApp.setId(appId);
    updateApp.setDeployKey(deployKey);
    updateApp.setDeployedTime(LocalDateTime.now());
    boolean updateResult = this.updateById(updateApp);
    ThrowUtils.throwIf(!updateResult, ErrorCode.OPERATION_ERROR, "更新应用部署信息失败");
    // 9. 返回可访问的 URL
    return String.format("%s/%s/", AppConstant.CODE_DEPLOY_HOST, deployKey);
}

再在dto的app包下编写一个部署请求类:

复制代码
@Data
public class AppDeployRequest implements Serializable {

    /**
     * 应用 id
     */
    private Long appId;

    private static final long serialVersionUID = 1L;
}

最后在AppController类编写部署网站的接口:

复制代码
/**
 * 应用部署
 *
 * @param appDeployRequest 部署请求
 * @param request          请求
 * @return 部署 URL
 */
@PostMapping("/deploy")
public BaseResponse<String> deployApp(@RequestBody AppDeployRequest appDeployRequest, HttpServletRequest request) {
ThrowUtils.throwIf(appDeployRequest == null, ErrorCode.PARAMS_ERROR);
Long appId = appDeployRequest.getAppId();
ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空");
// 获取当前登录用户
User loginUser = userService.getLoginUser(request);
// 调用服务部署应用
String deployUrl = appService.deployApp(appId, loginUser);
return ResultUtils.success(deployUrl);
}

测试效果

最后可以在接口文档部署接口进行测试:

访问地址查看网站:

相关推荐
ai小陈3 小时前
PyTorch DataLoader数据加载性能排查:GPU利用率低的实操指南
人工智能·pytorch·python·深度学习·ai·gpu算力
2601_962071576 小时前
【Java报错已解决】org.springframework.beans.factory.BeanCreationException
java·开发语言
2601_962078037 小时前
百度双重主要上市9月1日生效,“全栈AI”如何打开估值空间?
百度·ai·双重上市·估值空间·港股通
zww89491117 小时前
酒馆预约系统开发实战:从需求分析到上线全流程指南
java·eclipse
东风破_8 小时前
从跨域到 WebSocket:前端跨域方案、SSE 与双向实时通信详解
前端·后端
zww894911110 小时前
匿名树洞系统开发实战:从需求分析到部署指南
java·eclipse
行百里er10 小时前
Gateway 上别硬塞 MVC:DeferredImportSelector 看菜下锅
spring boot·后端·开源
名字还没想好☜10 小时前
Java 遍历时删元素抛 ConcurrentModificationException:fail-fast 原理与三种正确删法
后端
IT_陈寒11 小时前
React Hooks闭包陷阱差点让我加班到凌晨
前端·人工智能·后端
黑马程序员毕设11 小时前
基于Java的医院药品管理系统的优化设计与实现
java·开发语言·spring boot·小程序·架构·课程设计·毕设