757 lines
32 KiB
Markdown
757 lines
32 KiB
Markdown
# One-Click Project Generation Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** 把当前分散的设计器和预览器流程收束成默认一键流程:用户输入项目名称,系统自动生成后端、用户前台、管理前台、SQL,并启动在线运行预览。高级调整继续保留原有分步能力。
|
||
|
||
**Architecture:** 复用现有 AI 任务中心,在 `generateType=one_click_project` 下新增后端编排服务。编排服务串联项目准备、应用蓝图、数据库设计、业务动作设计、页面初始化、源码预览、运行预览,并把阶段进度写回 `front_ai_generation_task.current_step`、`progress`、`result_payload`。前端默认展示一键入口、阶段进度、运行地址和源码预览入口,原有分步设计器移入高级调整。
|
||
|
||
**Tech Stack:** Java 8、Spring Boot、MyBatis、JUnit4、Mockito、Vue 3、Element Plus、Node test、Vite。
|
||
|
||
---
|
||
|
||
## Task 1: Backend Result Contract and Task Type Acceptance
|
||
|
||
**Files:**
|
||
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationReport.java`
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationResult.java`
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/OneClickProjectGenerationException.java`
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IOneClickProjectGenerationService.java`
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerationTaskServiceImpl.java`
|
||
- `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/AiGenerationTaskServiceImplTest.java`
|
||
|
||
**Intent:** 固定一键任务的后端契约,让任务中心能创建 `one_click_project` 任务,并给前端稳定的结果 JSON。
|
||
|
||
- [ ] 在 `AiGenerationTaskServiceImplTest` 添加失败测试 `createTaskQueuesOneClickProjectGeneration`。
|
||
|
||
```java
|
||
AiGenerationTaskCreateRequest request = new AiGenerationTaskCreateRequest();
|
||
request.setGenerateType("one_click_project");
|
||
request.setProjectName("客户关系管理系统");
|
||
request.setProjectDesc("");
|
||
request.setExtraRequirements("");
|
||
|
||
AiGenerationTaskStatusResponse response = service.createTask(10L, 20L, request);
|
||
|
||
assertNotNull(response);
|
||
assertEquals("one_click_project", response.getGenerateType());
|
||
assertEquals("QUEUED", response.getStatus());
|
||
verify(taskMapper).insert(argThat(task ->
|
||
"one_click_project".equals(task.getGenerateType())
|
||
&& Long.valueOf(10L).equals(task.getUserId())
|
||
&& Long.valueOf(20L).equals(task.getProjectId())));
|
||
verify(taskWorker).submit(any(AiGenerationTask.class));
|
||
```
|
||
|
||
- [ ] 新建 `OneClickProjectGenerationReport`,字段为 `tableCount`、`businessActionCount`、`frontendPageCount`、`adminPageCount`,全部使用 `int`,并提供标准 getter 和 setter。
|
||
|
||
- [ ] 新建 `OneClickProjectGenerationResult`,字段和常量如下。
|
||
|
||
```java
|
||
public class OneClickProjectGenerationResult {
|
||
public static final String STAGE_PROJECT_PREPARE = "PROJECT_PREPARE";
|
||
public static final String STAGE_APP_BLUEPRINT = "APP_BLUEPRINT";
|
||
public static final String STAGE_DATABASE = "DATABASE";
|
||
public static final String STAGE_BUSINESS_BLUEPRINT = "BUSINESS_BLUEPRINT";
|
||
public static final String STAGE_PAGE_INITIALIZE = "PAGE_INITIALIZE";
|
||
public static final String STAGE_SOURCE_PREVIEW = "SOURCE_PREVIEW";
|
||
public static final String STAGE_RUN_PREVIEW = "RUN_PREVIEW";
|
||
public static final String STAGE_COMPLETED = "COMPLETED";
|
||
|
||
private Long projectId;
|
||
private String stage;
|
||
private String failedStage;
|
||
private boolean downloadReady;
|
||
private String previewStatus;
|
||
private String backendUrl;
|
||
private String frontendUrl;
|
||
private String adminFrontendUrl;
|
||
private String errorMessage;
|
||
private OneClickProjectGenerationReport report;
|
||
private List<String> suggestedActions = new ArrayList<String>();
|
||
}
|
||
```
|
||
|
||
- [ ] 新建 `OneClickProjectGenerationException`。
|
||
|
||
```java
|
||
public class OneClickProjectGenerationException extends RuntimeException {
|
||
private final OneClickProjectGenerationResult result;
|
||
|
||
public OneClickProjectGenerationException(String message, OneClickProjectGenerationResult result, Throwable cause) {
|
||
super(message, cause);
|
||
this.result = result;
|
||
}
|
||
|
||
public OneClickProjectGenerationResult getResult() {
|
||
return result;
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] 新建 `IOneClickProjectGenerationService`。
|
||
|
||
```java
|
||
public interface IOneClickProjectGenerationService {
|
||
OneClickProjectGenerationResult generate(AiGenerationTask task, AiGenerationTaskCreateRequest request);
|
||
}
|
||
```
|
||
|
||
- [ ] 修改 `AiGenerationTaskServiceImpl.validateRequest`,允许新类型。
|
||
|
||
```java
|
||
if (!"app_blueprint".equals(request.getGenerateType())
|
||
&& !"database".equals(request.getGenerateType())
|
||
&& !"business_blueprint".equals(request.getGenerateType())
|
||
&& !"one_click_project".equals(request.getGenerateType())) {
|
||
throw new ServiceException("不支持的AI生成类型");
|
||
}
|
||
```
|
||
|
||
- [ ] 运行测试。
|
||
|
||
```powershell
|
||
mvn -q -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest test
|
||
```
|
||
|
||
Expected result: 新增的一键任务创建测试和既有任务服务测试全部通过。
|
||
|
||
---
|
||
|
||
## Task 2: Worker Dispatch and Failure Payload Preservation
|
||
|
||
**Files:**
|
||
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/AiGenerationTaskWorker.java`
|
||
- `ruoyi-generator/src/test/java/com/ruoyi/generator/async/AiGenerationTaskWorkerTest.java`
|
||
|
||
**Intent:** 让异步 worker 知道如何执行一键任务,并在最终失败时保留阶段化 `resultPayload`。
|
||
|
||
- [ ] 在 `AiGenerationTaskWorkerTest` 添加 mock 并注入。
|
||
|
||
```java
|
||
@Mock
|
||
private IOneClickProjectGenerationService oneClickProjectGenerationService;
|
||
|
||
setField(worker, "oneClickProjectGenerationService", oneClickProjectGenerationService);
|
||
```
|
||
|
||
- [ ] 添加失败测试 `runOneClickProjectTaskDelegatesToOrchestrator`。
|
||
|
||
```java
|
||
AiGenerationTask task = new AiGenerationTask();
|
||
task.setTaskId(7L);
|
||
task.setProjectId(20L);
|
||
task.setUserId(10L);
|
||
task.setGenerateType("one_click_project");
|
||
task.setRequestPayload("{\"generateType\":\"one_click_project\",\"projectName\":\"客户关系管理系统\"}");
|
||
task.setAttempts(0);
|
||
task.setMaxAttempts(3);
|
||
|
||
OneClickProjectGenerationResult result = new OneClickProjectGenerationResult();
|
||
result.setProjectId(20L);
|
||
result.setStage(OneClickProjectGenerationResult.STAGE_COMPLETED);
|
||
result.setPreviewStatus("RUNNING");
|
||
result.setDownloadReady(true);
|
||
|
||
when(oneClickProjectGenerationService.generate(eq(task), any(AiGenerationTaskCreateRequest.class))).thenReturn(result);
|
||
|
||
worker.runTask(task);
|
||
|
||
verify(oneClickProjectGenerationService).generate(eq(task), any(AiGenerationTaskCreateRequest.class));
|
||
verify(taskMapper).update(argThat(updated ->
|
||
"SUCCEEDED".equals(updated.getStatus())
|
||
&& Integer.valueOf(100).equals(updated.getProgress())
|
||
&& updated.getResultPayload().contains("\"previewStatus\":\"RUNNING\"")));
|
||
```
|
||
|
||
- [ ] 添加失败测试 `runOneClickProjectTaskStoresPartialResultOnFailure`。
|
||
|
||
```java
|
||
AiGenerationTask task = new AiGenerationTask();
|
||
task.setTaskId(8L);
|
||
task.setProjectId(20L);
|
||
task.setUserId(10L);
|
||
task.setGenerateType("one_click_project");
|
||
task.setRequestPayload("{\"generateType\":\"one_click_project\",\"projectName\":\"客户关系管理系统\"}");
|
||
task.setAttempts(2);
|
||
task.setMaxAttempts(3);
|
||
|
||
OneClickProjectGenerationResult partial = new OneClickProjectGenerationResult();
|
||
partial.setProjectId(20L);
|
||
partial.setStage(OneClickProjectGenerationResult.STAGE_SOURCE_PREVIEW);
|
||
partial.setFailedStage(OneClickProjectGenerationResult.STAGE_RUN_PREVIEW);
|
||
partial.setDownloadReady(true);
|
||
partial.setPreviewStatus("FAILED");
|
||
partial.setErrorMessage("运行预览启动失败");
|
||
|
||
when(oneClickProjectGenerationService.generate(eq(task), any(AiGenerationTaskCreateRequest.class)))
|
||
.thenThrow(new OneClickProjectGenerationException("运行预览启动失败", partial, null));
|
||
when(retryPolicy.shouldRetry(eq(task), any(RuntimeException.class))).thenReturn(false);
|
||
|
||
worker.runTask(task);
|
||
|
||
verify(taskMapper).update(argThat(updated ->
|
||
"FAILED".equals(updated.getStatus())
|
||
&& "RUN_PREVIEW".equals(updated.getErrorCode())
|
||
&& updated.getResultPayload().contains("\"downloadReady\":true")
|
||
&& updated.getResultPayload().contains("\"failedStage\":\"RUN_PREVIEW\"")));
|
||
```
|
||
|
||
- [ ] 在 `AiGenerationTaskWorker` 增加字段。
|
||
|
||
```java
|
||
@Autowired
|
||
private IOneClickProjectGenerationService oneClickProjectGenerationService;
|
||
```
|
||
|
||
- [ ] 在任务执行分支中加入一键任务。
|
||
|
||
```java
|
||
if ("one_click_project".equals(task.getGenerateType())) {
|
||
return oneClickProjectGenerationService.generate(task, request);
|
||
}
|
||
```
|
||
|
||
- [ ] 在最终失败写库前处理 `OneClickProjectGenerationException`。
|
||
|
||
```java
|
||
if (e instanceof OneClickProjectGenerationException) {
|
||
OneClickProjectGenerationResult result = ((OneClickProjectGenerationException) e).getResult();
|
||
task.setResultPayload(JSON.toJSONString(result));
|
||
task.setErrorCode(result.getFailedStage());
|
||
}
|
||
```
|
||
|
||
- [ ] 运行测试。
|
||
|
||
```powershell
|
||
mvn -q -pl ruoyi-generator -Dtest=AiGenerationTaskWorkerTest test
|
||
```
|
||
|
||
Expected result: worker 的既有测试、新增委派测试、新增失败结果测试全部通过。
|
||
|
||
---
|
||
|
||
## Task 3: One-Click Orchestrator Happy Path
|
||
|
||
**Files:**
|
||
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java`
|
||
- `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java`
|
||
|
||
**Intent:** 新增核心编排服务,把已存在的 AI 生成、页面初始化、源码预览、运行预览串成默认成功链路。
|
||
|
||
- [ ] 新建失败测试类 `OneClickProjectGenerationServiceImplTest`,使用 JUnit4 + Mockito。mock 字段固定为:`IAiGenerateService`、`FrontendPageDesignService`、`IFrontProjectPreviewService`、`IFrontProjectRunPreviewService`、`IFrontProjectService`、`AiGenerationTaskMapper`。
|
||
|
||
- [ ] 添加失败测试 `generateRunsEveryStageAndReturnsRunningPreview`。测试准备一个 `frontendEnabled=1` 的 `FrontProject`,一个 `AppBlueprintDesign`,一个包含 1 张表的 `DatabaseDesignResponse`,一个包含 1 个动作的 `List<BusinessActionDesign>`,前台和后台各 1 个页面初始化结果,以及状态为 `RUNNING` 的 `ProjectRunPreviewStatus`。
|
||
|
||
- [ ] 在该测试中断言返回结果。
|
||
|
||
```java
|
||
OneClickProjectGenerationResult result = service.generate(task, request);
|
||
|
||
assertEquals(Long.valueOf(20L), result.getProjectId());
|
||
assertEquals(OneClickProjectGenerationResult.STAGE_COMPLETED, result.getStage());
|
||
assertEquals(ProjectRunPreviewStatus.RUNNING, result.getPreviewStatus());
|
||
assertTrue(result.isDownloadReady());
|
||
assertEquals("http://localhost:18080", result.getBackendUrl());
|
||
assertEquals("http://localhost:18081", result.getFrontendUrl());
|
||
assertEquals("http://localhost:18082", result.getAdminFrontendUrl());
|
||
assertEquals(1, result.getReport().getTableCount());
|
||
assertEquals(1, result.getReport().getBusinessActionCount());
|
||
assertEquals(1, result.getReport().getFrontendPageCount());
|
||
assertEquals(1, result.getReport().getAdminPageCount());
|
||
```
|
||
|
||
- [ ] 在该测试中断言调用顺序。
|
||
|
||
```java
|
||
InOrder inOrder = inOrder(aiGenerateService, frontendPageDesignService, frontProjectPreviewService, frontProjectRunPreviewService);
|
||
inOrder.verify(aiGenerateService).generateAppBlueprint(eq(10L), eq(20L), any(GenerateAppBlueprintRequest.class));
|
||
inOrder.verify(aiGenerateService).generateDatabase(eq(10L), eq(20L), any(GenerateDatabaseRequest.class));
|
||
inOrder.verify(aiGenerateService).generateBusinessBlueprint(eq(10L), eq(20L), any(GenerateBusinessBlueprintRequest.class));
|
||
inOrder.verify(frontendPageDesignService).initializePageDesigns(10L, 20L, "frontend");
|
||
inOrder.verify(frontendPageDesignService).initializePageDesigns(10L, 20L, "admin");
|
||
inOrder.verify(frontProjectPreviewService).markPreviewReady(10L, 20L);
|
||
inOrder.verify(frontProjectRunPreviewService).start(10L, 20L);
|
||
verify(taskMapper, atLeast(6)).update(any(AiGenerationTask.class));
|
||
```
|
||
|
||
- [ ] 实现 `OneClickProjectGenerationServiceImpl` 的主流程。
|
||
|
||
```java
|
||
@Service
|
||
public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGenerationService {
|
||
private static final long DEFAULT_RUN_PREVIEW_TIMEOUT_MILLIS = 120000L;
|
||
private static final long DEFAULT_RUN_PREVIEW_POLL_INTERVAL_MILLIS = 1000L;
|
||
|
||
@Autowired
|
||
private IAiGenerateService aiGenerateService;
|
||
@Autowired
|
||
private FrontendPageDesignService frontendPageDesignService;
|
||
@Autowired
|
||
private IFrontProjectPreviewService frontProjectPreviewService;
|
||
@Autowired
|
||
private IFrontProjectRunPreviewService frontProjectRunPreviewService;
|
||
@Autowired
|
||
private IFrontProjectService frontProjectService;
|
||
@Autowired
|
||
private AiGenerationTaskMapper taskMapper;
|
||
|
||
private long runPreviewTimeoutMillis = DEFAULT_RUN_PREVIEW_TIMEOUT_MILLIS;
|
||
private long runPreviewPollIntervalMillis = DEFAULT_RUN_PREVIEW_POLL_INTERVAL_MILLIS;
|
||
|
||
@Override
|
||
public OneClickProjectGenerationResult generate(AiGenerationTask task, AiGenerationTaskCreateRequest request) {
|
||
OneClickProjectGenerationResult result = new OneClickProjectGenerationResult();
|
||
result.setProjectId(task.getProjectId());
|
||
OneClickProjectGenerationReport report = new OneClickProjectGenerationReport();
|
||
result.setReport(report);
|
||
|
||
try {
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_PROJECT_PREPARE, 5, "正在准备项目");
|
||
FrontProject project = frontProjectService.getProject(task.getUserId(), task.getProjectId());
|
||
boolean frontendEnabled = project == null || !"0".equals(project.getFrontendEnabled());
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_APP_BLUEPRINT, 15, "正在规划应用蓝图");
|
||
AppBlueprintDesign appBlueprint = aiGenerateService.generateAppBlueprint(task.getUserId(), task.getProjectId(), toAppBlueprintRequest(request));
|
||
String appBlueprintJson = JSON.toJSONString(appBlueprint);
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_DATABASE, 35, "正在设计数据库和SQL");
|
||
DatabaseDesignResponse database = aiGenerateService.generateDatabase(task.getUserId(), task.getProjectId(), toDatabaseRequest(request, appBlueprintJson));
|
||
report.setTableCount(database == null || database.getTables() == null ? 0 : database.getTables().size());
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_BUSINESS_BLUEPRINT, 50, "正在生成业务流程");
|
||
List<BusinessActionDesign> businessActions = aiGenerateService.generateBusinessBlueprint(task.getUserId(), task.getProjectId(), toBusinessBlueprintRequest(request, appBlueprintJson));
|
||
report.setBusinessActionCount(businessActions == null ? 0 : businessActions.size());
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_PAGE_INITIALIZE, 65, "正在初始化前端页面");
|
||
if (frontendEnabled) {
|
||
FrontendPageDesignInitResponse frontendInit = frontendPageDesignService.initializePageDesigns(task.getUserId(), task.getProjectId(), "frontend");
|
||
report.setFrontendPageCount(countPageDesigns(frontendInit));
|
||
}
|
||
FrontendPageDesignInitResponse adminInit = frontendPageDesignService.initializePageDesigns(task.getUserId(), task.getProjectId(), "admin");
|
||
report.setAdminPageCount(countPageDesigns(adminInit));
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_SOURCE_PREVIEW, 78, "正在生成源码预览");
|
||
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "backend");
|
||
if (frontendEnabled) {
|
||
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "frontend");
|
||
}
|
||
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "admin_frontend");
|
||
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "sql");
|
||
frontProjectPreviewService.markPreviewReady(task.getUserId(), task.getProjectId());
|
||
result.setDownloadReady(true);
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_RUN_PREVIEW, 88, "正在启动在线运行预览");
|
||
ProjectRunPreviewStatus status = waitForRunningPreview(task.getUserId(), task.getProjectId());
|
||
applyPreviewStatus(result, status);
|
||
|
||
updateStage(task, result, OneClickProjectGenerationResult.STAGE_COMPLETED, 100, "完整项目已生成并启动");
|
||
return result;
|
||
} catch (RuntimeException e) {
|
||
throw wrapFailure(result, e);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] 添加请求转换、阶段更新、计数 helper。
|
||
|
||
```java
|
||
private void updateStage(AiGenerationTask task, OneClickProjectGenerationResult result, String stage, int progress, String currentStep) {
|
||
result.setStage(stage);
|
||
task.setCurrentStep(currentStep);
|
||
task.setProgress(progress);
|
||
task.setResultPayload(JSON.toJSONString(result));
|
||
taskMapper.update(task);
|
||
}
|
||
|
||
private GenerateAppBlueprintRequest toAppBlueprintRequest(AiGenerationTaskCreateRequest source) {
|
||
GenerateAppBlueprintRequest request = new GenerateAppBlueprintRequest();
|
||
request.setProjectName(source.getProjectName());
|
||
request.setProjectDesc(source.getProjectDesc());
|
||
request.setIndustryTemplate(source.getIndustryTemplate());
|
||
request.setExtraRequirements(source.getExtraRequirements());
|
||
return request;
|
||
}
|
||
|
||
private GenerateDatabaseRequest toDatabaseRequest(AiGenerationTaskCreateRequest source, String appBlueprintJson) {
|
||
GenerateDatabaseRequest request = new GenerateDatabaseRequest();
|
||
request.setProjectName(source.getProjectName());
|
||
request.setProjectDesc(source.getProjectDesc());
|
||
request.setIndustryTemplate(source.getIndustryTemplate());
|
||
request.setExtraRequirements(source.getExtraRequirements());
|
||
request.setAppBlueprint(appBlueprintJson);
|
||
return request;
|
||
}
|
||
|
||
private GenerateBusinessBlueprintRequest toBusinessBlueprintRequest(AiGenerationTaskCreateRequest source, String appBlueprintJson) {
|
||
GenerateBusinessBlueprintRequest request = new GenerateBusinessBlueprintRequest();
|
||
request.setProjectName(source.getProjectName());
|
||
request.setProjectDesc(source.getProjectDesc());
|
||
request.setIndustryTemplate(source.getIndustryTemplate());
|
||
request.setExtraRequirements(source.getExtraRequirements());
|
||
request.setAppBlueprint(appBlueprintJson);
|
||
return request;
|
||
}
|
||
|
||
private int countPageDesigns(FrontendPageDesignInitResponse response) {
|
||
return response == null || response.getDesigns() == null ? 0 : response.getDesigns().size();
|
||
}
|
||
```
|
||
|
||
- [ ] 运行测试。
|
||
|
||
```powershell
|
||
mvn -q -pl ruoyi-generator -Dtest=OneClickProjectGenerationServiceImplTest test
|
||
```
|
||
|
||
Expected result: 快乐路径测试通过,并验证阶段顺序。
|
||
|
||
---
|
||
|
||
## Task 4: Run Preview Polling, Disabled Frontend, and Failure Rules
|
||
|
||
**Files:**
|
||
|
||
- `ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java`
|
||
- `ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImplTest.java`
|
||
|
||
**Intent:** 补齐边界行为:运行预览等到 `RUNNING` 才算成功;用户前台关闭时跳过用户前台;失败时返回可恢复结果。
|
||
|
||
- [ ] 添加测试 `generatePollsRunPreviewUntilRunning`,让 `start` 返回 `STARTING`,让 `status` 依次返回 `STARTING` 和 `RUNNING`。断言最终 `previewStatus=RUNNING`,并断言 `status` 被调用 2 次。
|
||
|
||
- [ ] 添加测试 `generateSkipsUserFrontendWhenDisabled`。设置 `project.setFrontendEnabled("0")`,断言不会调用 `initializePageDesigns(10L, 20L, "frontend")` 和 `getStructure(10L, 20L, "frontend")`,并断言管理前台仍初始化和生成结构。
|
||
|
||
- [ ] 添加测试 `generateThrowsOneClickExceptionWithDownloadReadyResultWhenRunPreviewFails`。让源码预览完成,让运行预览返回 `FAILED` 和错误信息 `后端端口启动失败`。断言异常结果包含 `failedStage=RUN_PREVIEW`、`previewStatus=FAILED`、`downloadReady=true`、建议动作 `源码预览已生成,可以先下载项目或进入高级调整调整后重试运行预览`。
|
||
|
||
- [ ] 添加运行预览等待 helper。
|
||
|
||
```java
|
||
private ProjectRunPreviewStatus waitForRunningPreview(Long userId, Long projectId) {
|
||
ProjectRunPreviewStatus status = frontProjectRunPreviewService.start(userId, projectId);
|
||
if (ProjectRunPreviewStatus.RUNNING.equals(status.getStatus()) || ProjectRunPreviewStatus.FAILED.equals(status.getStatus())) {
|
||
return status;
|
||
}
|
||
|
||
long deadline = System.currentTimeMillis() + runPreviewTimeoutMillis;
|
||
ProjectRunPreviewStatus latest = status;
|
||
while (System.currentTimeMillis() <= deadline) {
|
||
sleepBeforeNextPoll();
|
||
latest = frontProjectRunPreviewService.status(userId, projectId);
|
||
if (ProjectRunPreviewStatus.RUNNING.equals(latest.getStatus()) || ProjectRunPreviewStatus.FAILED.equals(latest.getStatus())) {
|
||
return latest;
|
||
}
|
||
}
|
||
latest.setStatus(ProjectRunPreviewStatus.FAILED);
|
||
latest.setErrorMessage("运行预览启动超时");
|
||
return latest;
|
||
}
|
||
|
||
private void sleepBeforeNextPoll() {
|
||
if (runPreviewPollIntervalMillis <= 0L) {
|
||
return;
|
||
}
|
||
try {
|
||
Thread.sleep(runPreviewPollIntervalMillis);
|
||
} catch (InterruptedException e) {
|
||
Thread.currentThread().interrupt();
|
||
throw new RuntimeException("运行预览等待被中断", e);
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] 添加预览状态处理和失败包装 helper。
|
||
|
||
```java
|
||
private void applyPreviewStatus(OneClickProjectGenerationResult result, ProjectRunPreviewStatus status) {
|
||
result.setPreviewStatus(status.getStatus());
|
||
result.setBackendUrl(status.getBackendUrl());
|
||
result.setFrontendUrl(status.getFrontendUrl());
|
||
result.setAdminFrontendUrl(status.getAdminFrontendUrl());
|
||
if (!ProjectRunPreviewStatus.RUNNING.equals(status.getStatus())) {
|
||
result.setFailedStage(OneClickProjectGenerationResult.STAGE_RUN_PREVIEW);
|
||
result.setErrorMessage(status.getErrorMessage());
|
||
result.getSuggestedActions().add("源码预览已生成,可以先下载项目或进入高级调整调整后重试运行预览");
|
||
throw new OneClickProjectGenerationException(status.getErrorMessage(), result, null);
|
||
}
|
||
}
|
||
|
||
private OneClickProjectGenerationException wrapFailure(OneClickProjectGenerationResult result, RuntimeException e) {
|
||
if (e instanceof OneClickProjectGenerationException) {
|
||
return (OneClickProjectGenerationException) e;
|
||
}
|
||
String failedStage = result.getStage() == null ? OneClickProjectGenerationResult.STAGE_PROJECT_PREPARE : result.getStage();
|
||
result.setFailedStage(failedStage);
|
||
result.setErrorMessage(e.getMessage());
|
||
result.getSuggestedActions().add("进入高级调整检查当前阶段输入和生成结果后重试");
|
||
return new OneClickProjectGenerationException(e.getMessage(), result, e);
|
||
}
|
||
```
|
||
|
||
- [ ] 在测试 setup 中把等待时间改短。
|
||
|
||
```java
|
||
setField(service, "runPreviewPollIntervalMillis", 0L);
|
||
setField(service, "runPreviewTimeoutMillis", 10L);
|
||
```
|
||
|
||
- [ ] 运行测试。
|
||
|
||
```powershell
|
||
mvn -q -pl ruoyi-generator -Dtest=OneClickProjectGenerationServiceImplTest test
|
||
```
|
||
|
||
Expected result: 快乐路径、轮询、关闭用户前台、运行预览失败测试全部通过。
|
||
|
||
---
|
||
|
||
## Task 5: Frontend API Helper and Source-Level Tests
|
||
|
||
**Files:**
|
||
|
||
- `easycode-web/src/api/project.js`
|
||
- `easycode-web/src/views/generateView.test.mjs`
|
||
|
||
**Intent:** 前端先拥有清晰的一键任务 API 调用点,测试锁定“项目名称即可触发一键任务”的关键行为。
|
||
|
||
- [ ] 在 `generateView.test.mjs` 添加失败测试。
|
||
|
||
```javascript
|
||
test('GenerateView exposes one-click generation as the default flow', () => {
|
||
assert.match(source, /one_click_project/)
|
||
assert.match(source, /handleGenerateCompleteProject/)
|
||
assert.match(source, /validateOneClickInput/)
|
||
assert.match(source, /oneClickResult/)
|
||
assert.match(source, /expertMode/)
|
||
})
|
||
|
||
test('GenerateView one-click validation only requires project name', () => {
|
||
assert.match(source, /validateOneClickInput[\s\S]*projectForm\.projectName/)
|
||
assert.doesNotMatch(source, /validateOneClickInput[\s\S]*projectForm\.keyword/)
|
||
})
|
||
```
|
||
|
||
- [ ] 在 `project.js` 添加 API helper。
|
||
|
||
```javascript
|
||
export function createOneClickProjectTask(projectId, data) {
|
||
return createAiTask(projectId, Object.assign({}, data, {
|
||
generateType: 'one_click_project'
|
||
}))
|
||
}
|
||
```
|
||
|
||
- [ ] 保持 `createAiTask`、`retryAiTask`、`generateProject`、`startRunPreview`、`getRunPreviewStatus` 的既有导出不变。
|
||
|
||
- [ ] 运行前端测试并确认此时 `GenerateView.vue` 相关断言失败。
|
||
|
||
```powershell
|
||
cd easycode-web
|
||
node --test src/views/generateView.test.mjs
|
||
```
|
||
|
||
Expected result: 新增断言因页面尚未改造而失败。
|
||
|
||
---
|
||
|
||
## Task 6: GenerateView Default One-Click UX and Expert Mode
|
||
|
||
**Files:**
|
||
|
||
- `easycode-web/src/views/GenerateView.vue`
|
||
- `easycode-web/src/views/generateView.test.mjs`
|
||
|
||
**Intent:** 让第一屏回到用户初心:输入项目名称后直接生成完整项目。原有分步工具保留在高级调整中。
|
||
|
||
- [ ] 修改 `GenerateView.vue` 的 API import,加入 `createOneClickProjectTask`。
|
||
|
||
- [ ] 在 `<script setup>` 中添加状态。
|
||
|
||
```javascript
|
||
const ONE_CLICK_GENERATE_TYPE = 'one_click_project'
|
||
const oneClickGenerating = ref(false)
|
||
const oneClickResult = ref(null)
|
||
const expertMode = ref(false)
|
||
```
|
||
|
||
- [ ] 添加只校验项目名称的一键校验函数。
|
||
|
||
```javascript
|
||
function validateOneClickInput() {
|
||
if (!projectForm.projectName || !projectForm.projectName.trim()) {
|
||
ElMessage.warning('请输入项目名称')
|
||
return false
|
||
}
|
||
return true
|
||
}
|
||
```
|
||
|
||
- [ ] 添加一键提交函数。
|
||
|
||
```javascript
|
||
async function handleGenerateCompleteProject() {
|
||
if (!validateOneClickInput()) return
|
||
|
||
oneClickGenerating.value = true
|
||
oneClickResult.value = null
|
||
try {
|
||
await ensureProject()
|
||
const response = await createOneClickProjectTask(currentProjectId.value, {
|
||
projectName: projectForm.projectName,
|
||
projectDesc: projectForm.projectDesc,
|
||
industryTemplate: projectForm.industryTemplate,
|
||
extraRequirements: projectForm.extraRequirements
|
||
})
|
||
aiTask.value = response.data || response
|
||
await waitForAiTask(aiTask.value.taskId)
|
||
ElMessage.success('完整项目已生成')
|
||
} catch (error) {
|
||
ElMessage.error(error.msg || error.message || '完整项目生成失败')
|
||
} finally {
|
||
oneClickGenerating.value = false
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] 在 `applyAiTaskResult` 的现有分支前加入一键结果处理。
|
||
|
||
```javascript
|
||
if (task.generateType === ONE_CLICK_GENERATE_TYPE) {
|
||
oneClickResult.value = parseAiTaskResult(task.resultPayload)
|
||
return
|
||
}
|
||
```
|
||
|
||
- [ ] 添加结果解析函数。
|
||
|
||
```javascript
|
||
function parseAiTaskResult(payload) {
|
||
if (!payload) return null
|
||
if (typeof payload === 'object') return payload
|
||
try {
|
||
return JSON.parse(payload)
|
||
} catch (error) {
|
||
return { errorMessage: payload }
|
||
}
|
||
}
|
||
```
|
||
|
||
- [ ] 在模板顶部新增 `生成完整项目` 卡片,字段包括项目名称、项目描述、用户前台开关、高级调整开关、主按钮 `生成完整项目`。主按钮绑定 `handleGenerateCompleteProject`,loading 绑定 `oneClickGenerating`。
|
||
|
||
- [ ] 在一键卡片下方新增结果卡片。展示 `backendUrl`、`frontendUrl`、`adminFrontendUrl`、`downloadReady`,当 `errorMessage` 存在时展示 warning alert。
|
||
|
||
- [ ] 把原有应用蓝图、数据库设计、业务蓝图、页面设计、旧预览操作区整体包在 `v-if="expertMode"` 区域内。保留原有 handler 和文案,避免破坏高级调整。
|
||
|
||
- [ ] 保留 `validateRequirementInput` 给高级调整使用。它可以继续要求关键词或项目描述;一键生成使用新的 `validateOneClickInput`。
|
||
|
||
- [ ] 添加布局 CSS。
|
||
|
||
```css
|
||
.one-click-card :deep(.el-form-item__content) {
|
||
min-width: 0;
|
||
}
|
||
|
||
.result-card {
|
||
margin-top: 16px;
|
||
}
|
||
```
|
||
|
||
- [ ] 运行前端测试。
|
||
|
||
```powershell
|
||
cd easycode-web
|
||
node --test src/views/generateView.test.mjs src/views/previewView.test.mjs
|
||
```
|
||
|
||
Expected result: GenerateView 和 PreviewView 的 source-level tests 全部通过。
|
||
|
||
---
|
||
|
||
## Task 7: Verification and Regression Sweep
|
||
|
||
**Files:**
|
||
|
||
- Tasks 1-6 修改过的所有文件。
|
||
|
||
**Intent:** 确认后端编排、前端入口、旧高级调整、源码预览、运行预览没有互相冲突。
|
||
|
||
- [ ] 运行后端 focused tests。
|
||
|
||
```powershell
|
||
mvn -q -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest,AiGenerationTaskWorkerTest,OneClickProjectGenerationServiceImplTest test
|
||
```
|
||
|
||
Expected result: focused backend tests 全部通过。
|
||
|
||
- [ ] 运行后端模块测试。
|
||
|
||
```powershell
|
||
mvn -q -pl ruoyi-generator,ruoyi-admin -am test
|
||
```
|
||
|
||
Expected result: generator/admin 相关测试全部通过。
|
||
|
||
- [ ] 运行前端测试。
|
||
|
||
```powershell
|
||
cd easycode-web
|
||
node --test src/views/generateView.test.mjs src/views/previewView.test.mjs
|
||
```
|
||
|
||
Expected result: 前端 source-level tests 全部通过。
|
||
|
||
- [ ] 运行前端构建。
|
||
|
||
```powershell
|
||
cd easycode-web
|
||
npm run build
|
||
```
|
||
|
||
Expected result: Vite build 成功完成。
|
||
|
||
- [ ] 在浏览器做手动冒烟验证。
|
||
|
||
1. 打开生成页。
|
||
2. 确认第一屏默认是 `生成完整项目`。
|
||
3. 只输入 `客户关系管理系统`。
|
||
4. 点击 `生成完整项目`。
|
||
5. 后端服务可用时,确认运行预览进入 `RUNNING`。
|
||
6. 打开 `高级调整`。
|
||
7. 确认原有应用蓝图、数据库、业务蓝图、页面设计、源码预览控件仍可访问。
|
||
|
||
- [ ] 检查 diff。
|
||
|
||
```powershell
|
||
git diff --stat
|
||
git diff --check
|
||
```
|
||
|
||
Expected result: diff 范围只包含一键生成项目相关改动,`git diff --check` 无 whitespace 错误。
|
||
|
||
---
|
||
|
||
## Acceptance Criteria
|
||
|
||
- [ ] 用户只输入项目名称即可创建一键任务。
|
||
- [ ] 一键任务使用 `generateType=one_click_project`,由现有 AI 任务中心排队、查询、重试、取消。
|
||
- [ ] 后端按固定阶段执行:项目准备、应用蓝图、数据库、业务蓝图、页面初始化、源码预览、运行预览。
|
||
- [ ] `RUN_PREVIEW` 进入 `RUNNING` 才算任务成功。
|
||
- [ ] 源码预览完成但运行预览失败时,任务失败结果仍包含 `downloadReady=true` 和 `failedStage=RUN_PREVIEW`。
|
||
- [ ] 用户前台关闭时跳过用户前台页面初始化和用户前台源码结构生成,管理前台仍生成。
|
||
- [ ] 前端默认显示一键入口、阶段状态、运行地址、源码预览入口。
|
||
- [ ] 原有分步设计器保留在高级调整中。
|
||
- [ ] 后端 focused tests、后端模块测试、前端 source-level tests、前端 build 全部通过。
|
||
|
||
---
|
||
|
||
## Implementation Notes
|
||
|
||
- 不新增数据库字段。第一版使用现有 `current_step`、`progress`、`result_payload`、`error_code` 表达阶段和失败原因。
|
||
- 不引入模板库。模板库属于已确认的方案三,是当前版本之后的增强方向。
|
||
- 不移除旧的 `app_blueprint`、`database`、`business_blueprint` 任务类型。它们继续服务高级调整和调试场景。
|
||
- 一键入口和高级调整共享同一个项目记录,因此 `frontendEnabled`、模块选择、项目描述仍通过现有项目保存逻辑持久化。
|