2026山东大学软件学院项目实训个人blog(四)
目录
一、本期个人核心任务
负责AI 生成网页的部署模块全流程开发,完成从部署方案选型、核心逻辑设计到接口落地的完整实现,解决本地生成文件无法在线访问的核心问题,实现 “AI 生成代码 → 在线部署访问” 的业务闭环。
二、核心开发与技术落地
本地生成的网页文件仅能在本地访问,无法满足平台化场景下 “用户生成应用后可在线预览、分享” 的核心诉求。
因此部署模块的核心目标是:将本地生成的静态文件同步至 Web 可访问的服务器目录,并提供统一、稳定、易访问的 URL 地址。
(一)思路设计
我将部署的核心逻辑拆解为 “文件迁移 + 访问映射” 两大环节:
-
文件迁移:将临时生成的代码文件(code_output 目录)同步至服务器持久化部署目录(code_deploy 目录);
-
访问映射:为每个应用分配唯一标识(deployKey),通过 Web 服务将 “域名 + deployKey” 映射到对应的部署文件目录,实现在线访问。
(二)方案设计
部署方案的选型需兼顾 “开发效率、运维成本、访问性能、场景适配” 四大维度,我对三种主流部署方案进行了对比分析:
1. Serve 工具快速部署
基于 Node.js 的 serve 包可以快速搭建一套轻量级 Web 服务,直接把本地目录变成可访问的静态资源站点。
1)先全局安装 serve 工具:
npm i -g serve
2)以 code_output 作为部署根目录,只需要进入该目录并执行启动命令,即可开启 Web 服务:

3)启动成功后,会在控制台输出本地访问地址,直接在浏览器打开即可查看生成的网页效果:


这样在使用时,我们可以提前在服务器上启动 serve 并指向专用部署目录(如 deployed),后续只需要把 AI 生成的文件移动到该目录,用户就能通过链接访问。
这种方案的优势是配置极简、开箱即用;缺点是依赖 Node.js 环境,需要单独维护服务进程,在高并发场景下性能较弱。
2.Spring Boot 静态资源接口
我们可以直接在现有 Spring Boot 后端项目中,开发一个静态资源访问接口,通过传入部署路径,自动读取并返回服务器上对应的网页文件。
1)在项目中定义资源读取接口,根据部署标识找到对应文件目录,以流的形式返回 HTML、CSS、JS 等静态资源,实现直接访问。
2)接口开发完成后,前端通过拼接好的 URL 即可直接访问已部署的 AI 生成应用,测试效果稳定,能够满足基础预览需求。
/**
* 静态资源访问
*/
@RestController
@RequestMapping("/static")
public class StaticResourceController {
// 应用生成根目录(用于浏览)
private static final String PREVIEW_ROOT_DIR = AppConstant.CODE_OUTPUT_ROOT_DIR;
/**
* 提供静态资源访问,支持目录重定向
* 访问格式:http://localhost:8080/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";
}
}
这种方案的优点是无需额外启动服务进程,与现有系统无缝集成;缺点是仅具备基础静态资源能力,没有缓存、压缩等优化。
3.Nginx 目录映射
Nginx 是专业的 Web 服务器,性能优异,功能丰富。
1)安装并配置好 Nginx 后,打开核心配置文件 nginx.conf,在 http 块中新增一个 server 块,将 root 指向项目的统一部署根目录,实现 URL 与文件目录的自动映射。
# 静态资源服务器 - 80 端口
server {
listen 80;
server_name localhost;
charset utf-8;
charset_types text/css application/javascript text/plain text/xml application/json;
# 项目部署根目录
root /Users/yupi/Code/yu-ai-code-mother/tmp/code_deploy;
# 处理所有请求
location ~ ^/([^/]+)/(.*)$ {
try_files /$1/$2 /$1/index.html =404;
}
}
配置中使用了 try_files 指令,能够按顺序查找目标文件,优先返回对应资源,不存在时则默认返回入口文件,最后返回 404 页面。这种方式可以完美适配单页面应用路由,支持后续 Vue、小程序等项目的正常刷新与访问。
2)配置完成后,启动 Nginx 或重载配置文件即可生效,无需重启服务。
该方案性能最优、支持高并发、功能最完善;唯一的成本是需要额外部署和维护 Nginx 组件。
4.方案对比
| 方案 | 开发成本 | 运维成本 | 访问性能 | 场景适配性 |
|---|---|---|---|---|
| Serve 工具 | 低 | 中 | 低 | 开发调试 |
| Spring Boot 接口 | 中 | 低 | 中 | 中台集成 |
| Nginx 目录映射 | 中 | 低 | 高 | 生产环境 |
基于场景适配性与性能要求,我最终确定使用混合部署方案:
-
预览场景:通过 Spring Boot 接口实现 AI 生成代码的即时预览;
-
正式部署:通过 Nginx 提供高并发、高性能的在线访问服务。
三、开发
部署接口接收 appId 作为请求参数,最终返回可直接访问的在线地址:${部署域名}/{deployKey}。
整体部署流程如下:
1.参数与权限校验
校验应用是否存在、当前登录用户是否为应用创建者,确保仅本人可部署应用。
2.生成唯一 deployKey
采用 6 位大小写字母 + 数字组合生成唯一标识,生成前校验重复;每个应用只生成一次,已存在则直接复用,保证访问地址不变。
3.执行文件部署
将 code_output 目录下的临时代码文件,复制到正式部署目录 code_deploy,并以 deployKey 作为目录名,简化访问路径。
1)首先在 AppConstant 中定义常量:
在 AppConstant 中定义全局常量,统一管理临时目录、部署目录、deployKey 长度等信息:
/**
* 应用生成目录
*/
public static String CODE_OUTPUT_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_output";
/**
* 应用部署目录
*/
public static String CODE_DEPLOY_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_deploy";
/**
* 应用部署域名
*/
public static String CODE_DEPLOY_HOST = "http://localhost";
CodeFileSaverTemplate 中使用文件保存根目录常量(用于保存生成 的文件):
// 文件保存根目录
protected static final String FILE_SAVE_ROOT_DIR = AppConstant.CODE_OUTPUT_ROOT_DIR;
StaticResourceController中使用文件保存根目录常量,因为要在生成时就预览效果:
// 应用生成根目录(用于浏览)
private static final String PREVIEW_ROOT_DIR = AppConstant.CODE_OUTPUT_ROOT_DIR;
2)编写部署请求类:
封装接口请求参数,完成基础校验,确保传入的 appId 合法有效。
@Data
public class AppDeployRequest implements Serializable {
/**
* 应用 id
*/
private Long appId;
private static final long serialVersionUID = 1L;
}
3)基于上述流程,在 App Service 中 编写部署服务的代码 :
按照校验、生成 deployKey、文件复制的流程,编写部署业务逻辑。
@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);
}
该实现的优势:
-
支持重复部署,已有
deployKey直接复用,保证 URL 稳定; -
新代码自动覆盖旧文件,支持应用内容更新;
-
全局唯一标识,避免目录冲突。
不足之处:当前版本暂不支持同一应用的多版本管理。
4)编写对外接口:
提供标准 REST 接口,接收前端请求,调用部署服务并返回可访问的在线链接。
/**
* 应用部署
*
* @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);
}
四、开发过程中的问题与解决方案
问题 :deployKey 重复导致部署冲突
现象:
测试阶段偶现两个不同应用部署后访问到同一页面,排查发现是 deployKey 随机生成时出现重复。
原因:
1.初始随机生成逻辑仅做简单的 6 位字符串生成,未做全局唯一性校验;
2.未考虑 “并发部署” 场景,多个请求同时生成相同 deployKey 时,数据库校验存在时间窗口漏洞。
解决方案:
1.优化 deployKey 生成逻辑:生成后先查询数据库,确认无重复后再写入,若重复则重新生成;
2.增加数据库唯一索引:在 app 表的 deployKey 字段添加唯一索引,即使并发场景下出现重复,数据库会抛出约束异常,避免脏数据写入;
3.异常兜底处理:捕获唯一索引冲突异常后,自动重新生成 deployKey 并再次尝试,仍失败则提示。
五、实训收获与技术成长
本次开发我完成了从 “本地文件” 到 “在线服务” 的跨越,收获还是很大的:
-
掌握多维度技术方案选型思路:从场景、性能、成本等维度对比不同部署方案,不再局限于 “能实现”,而是追求 “最优解”,理解了 “技术选型需匹配业务阶段” 的核心原则;
-
理解 Nginx 静态资源部署与目录映射原理,掌握了生产环境高性能 Web 服务的配置方式,真正具备了将项目上线部署的能力;
六、后续开发计划
1.实现对话历史模块:支持用户查看、管理历史生成记录,实现对话记录回溯、重新生成功能,提升平台使用连贯性。
2.完善 Vue 项目生成能力:优化 Vue 代码生成结构,支持组件化、工程化 Vue 项目输出,增强生成代码的可维护性与实用性。
3.持续优化生成与部署体验:进一步提升 AI 生成准确率、流式输出流畅度及部署稳定性,丰富平台功能场景。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐




所有评论(0)