Files
yidaima/RuoYi-Vue/docs/superpowers/plans/2026-06-27-one-click-project-generation.md

757 lines
32 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`、模块选择、项目描述仍通过现有项目保存逻辑持久化。