完成"AI零代码应用生成平台"的应用模块扩展
扩展 - 应用版本对比
扩展思路 里提到了一些可以继续完善的功能,我参考 Diffchecker 的展示形式,实现了一个轻量级的版本对比能力:每次重新生成应用代码时,先把旧代码目录归档到历史版本目录,再覆盖写入最新代码。这样不需要立即修改数据库表结构,也能让用户对比“历史版本”和“当前版本”的文件内容差异。
详细代码可查看我的 GitHub 仓库 ,喜欢麻烦点个 star 吧 😄。
实现方案如下:
- 后端保存代码前自动归档旧版本,目录结构为
tmp/code_versions/{codeGenType}_{appId}/{versionKey} - 后端提供版本列表接口和版本对比接口
- 前端在应用详情弹窗中增加 “版本对比” 按钮
- 版本对比弹窗支持选择旧版本、新版本和文件,并展示左右并排的代码差异
1、保存代码时归档历史版本
修改 CodeFileSaverTemplate ,在覆盖当前代码目录前先归档旧版本,并清理当前目录,避免已经删除的文件继续残留:
▼java复制代码public final File saveCode(T result, Long appId) { validateInput(result); String baseDirPath = buildUniqueDir(appId); archiveOldVersion(baseDirPath); FileUtil.clean(baseDirPath); saveFiles(result, baseDirPath); return new File(baseDirPath); } private void archiveOldVersion(String baseDirPath) { File baseDir = new File(baseDirPath); File[] files = baseDir.listFiles(); if (!baseDir.exists() || files == null || files.length == 0) { return; } String appDirName = baseDir.getName(); String versionKey = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS")); String versionDirPath = AppConstant.CODE_VERSION_ROOT_DIR + File.separator + appDirName + File.separator + versionKey; FileUtil.copyContent(baseDir, new File(versionDirPath), true); }
在 AppConstant 中新增历史版本目录常量:
▼java复制代码/** * 应用历史版本目录 */ String CODE_VERSION_ROOT_DIR = System.getProperty("user.dir") + "/tmp/code_versions";
2、后端版本对比接口
新增请求类 AppVersionCompareRequest ,用于指定要对比的应用、旧版本、新版本和文件路径:
▼java复制代码@Data public class AppVersionCompareRequest implements Serializable { private Long appId; private String oldVersionKey; private String newVersionKey; private String filePath; private static final long serialVersionUID = 1L; }
新增返回类 AppVersionVO 和 AppVersionCompareVO ,分别表示版本信息和文件对比结果:
▼java复制代码@Data public class AppVersionVO implements Serializable { private String versionKey; private String versionName; private LocalDateTime createTime; private Boolean current; private static final long serialVersionUID = 1L; }
▼java复制代码@Data public class AppVersionCompareVO implements Serializable { private List<String> fileList; private String filePath; private String oldContent; private String newContent; private Integer additions; private Integer removals; private static final long serialVersionUID = 1L; }
在 AppController 中新增两个接口:
▼java复制代码@GetMapping("/version/list") public BaseResponse<List<AppVersionVO>> listAppVersions(@RequestParam Long appId, HttpServletRequest request) { ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空"); User loginUser = userService.getLoginUser(request); return ResultUtils.success(appService.listAppVersions(appId, loginUser)); } @PostMapping("/version/compare") public BaseResponse<AppVersionCompareVO> compareAppVersion(@RequestBody AppVersionCompareRequest appVersionCompareRequest, HttpServletRequest request) { ThrowUtils.throwIf(appVersionCompareRequest == null, ErrorCode.PARAMS_ERROR); User loginUser = userService.getLoginUser(request); return ResultUtils.success(appService.compareAppVersion(appVersionCompareRequest, loginUser)); }
核心对比逻辑在 AppServiceImpl 中,先校验本人或管理员权限,再读取两边文件内容,并统计新增和删除行数:
▼java复制代码@Override public AppVersionCompareVO compareAppVersion(AppVersionCompareRequest request, User loginUser) { ThrowUtils.throwIf(request == null, ErrorCode.PARAMS_ERROR); Long appId = request.getAppId(); ThrowUtils.throwIf(appId == null || appId <= 0, ErrorCode.PARAMS_ERROR, "应用 ID 不能为空"); App app = getAndCheckVersionPermission(appId, loginUser); File oldVersionDir = resolveVersionDir(app, request.getOldVersionKey()); File newVersionDir = resolveVersionDir(app, request.getNewVersionKey()); ThrowUtils.throwIf(!oldVersionDir.exists() || !oldVersionDir.isDirectory(), ErrorCode.NOT_FOUND_ERROR, "旧版本不存在"); ThrowUtils.throwIf(!newVersionDir.exists() || !newVersionDir.isDirectory(), ErrorCode.NOT_FOUND_ERROR, "新版本不存在"); List<String> fileList = collectComparableFiles(oldVersionDir, newVersionDir); ThrowUtils.throwIf(fileList.isEmpty(), ErrorCode.NOT_FOUND_ERROR, "暂无可对比的代码文件"); String filePath = StrUtil.blankToDefault(request.getFilePath(), fileList.get(0)); ThrowUtils.throwIf(!fileList.contains(filePath), ErrorCode.PARAMS_ERROR, "文件路径无效"); String oldContent = readVersionFile(oldVersionDir, filePath); String newContent = readVersionFile(newVersionDir, filePath); int[] diffCounts = countLineDiff(oldContent, newContent); AppVersionCompareVO compareVO = new AppVersionCompareVO(); compareVO.setFileList(fileList); compareVO.setFilePath(filePath); compareVO.setOldContent(oldContent); compareVO.setNewContent(newContent); compareVO.setRemovals(diffCounts[0]); compareVO.setAdditions(diffCounts[1]); return compareVO; }
3、前端版本对比弹窗
在 appController.ts 中手动补充接口调用:
▼typescript复制代码export async function listAppVersions( params: API.listAppVersionsParams, options?: { [key: string]: any } ) { return request<API.BaseResponseListAppVersionVO>('/app/version/list', { method: 'GET', params: { ...params, }, ...(options || {}), }) } export async function compareAppVersion( body: API.AppVersionCompareRequest, options?: { [key: string]: any } ) { return request<API.BaseResponseAppVersionCompareVO>('/app/version/compare', { method: 'POST', headers: { 'Content-Type': 'application/json', }, data: body, ...(options || {}), }) }
新增组件 AppVersionCompareModal.vue ,参考 Diffchecker 做成左右并排的代码对比面板:
▼html复制代码<a-modal v-model:open="visible" title="版本对比" :footer="null" width="92vw"> <div class="compare-toolbar"> <a-select v-model:value="oldVersionKey" placeholder="选择旧版本" :options="versionOptions" @change="loadCompare" /> <SwapOutlined /> <a-select v-model:value="newVersionKey" placeholder="选择新版本" :options="versionOptions" @change="loadCompare" /> <a-select v-model:value="selectedFile" placeholder="选择文件" :options="fileOptions" @change="loadCompare" /> <span class="removal-count">{{ compareResult?.removals || 0 }} removals</span> <span class="addition-count">{{ compareResult?.additions || 0 }} additions</span> </div> <div class="diff-board"> <div class="diff-pane"> <div class="pane-header"> <span>{{ oldLines.length }} lines</span> <a-button type="link" size="small" @click="copyContent(oldContent)">Copy</a-button> </div> <div class="code-list"> <div v-for="(line, index) in oldRows" :key="index" class="code-line" :class="line.type"> <span class="line-no">{{ line.no || '' }}</span> <pre>{{ line.text }}</pre> </div> </div> </div> <div class="diff-pane"> <div class="pane-header"> <span>{{ newLines.length }} lines</span> <a-button type="link" size="small" @click="copyContent(newContent)">Copy</a-button> </div> <div class="code-list"> <div v-for="(line, index) in newRows" :key="index" class="code-line" :class="line.type"> <span class="line-no">{{ line.no || '' }}</span> <pre>{{ line.text }}</pre> </div> </div> </div> </div> </a-modal>
这里没有引入额外依赖,而是在前端用最长公共子序列算法生成左右对齐的差异行:
▼typescript复制代码const buildDiffRows = (oldLineList: string[], newLineList: string[]) => { const oldLength = oldLineList.length const newLength = newLineList.length const dp = Array.from({ length: oldLength + 1 }, () => Array(newLength + 1).fill(0)) for (let i = oldLength - 1; i >= 0; i--) { for (let j = newLength - 1; j >= 0; j--) { dp[i][j] = oldLineList[i] === newLineList[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]) } } // 根据 dp 结果组装 oldRows 和 newRows return { oldRows, newRows } }
最后,在 AppDetailModal.vue 的操作栏中增加入口:
▼vue复制代码<a-button @click="showVersionCompare = true"> <template #icon> <BranchesOutlined /> </template> 版本对比 </a-button> <AppVersionCompareModal v-model:open="showVersionCompare" :app-id="app?.id" />
效果上,用户重新生成应用后,点击 “应用详情” - “版本对比”,就能选择历史版本和当前版本,对指定文件进行左右并排对比,并看到新增行数和删除行数。
4、测试验证
1)创建应用并生成一次代码,此时还没有历史版本,版本对比弹窗会提示暂无历史版本。

2)继续在对话框中发送修改需求并重新生成代码,后端会把第一次生成的代码归档为历史版本。

3)打开应用详情,点击 “版本对比”,选择历史版本和当前版本,确认能看到左右代码差异。

4)如果某个文件在新版本中被删除,左侧会显示删除内容,右侧为空行;如果新版本新增文件,则左侧为空行,右侧显示新增内容。

