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

32 KiB
Raw Permalink Blame History

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_stepprogressresult_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

    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,字段为 tableCountbusinessActionCountfrontendPageCountadminPageCount,全部使用 int,并提供标准 getter 和 setter。

  • 新建 OneClickProjectGenerationResult,字段和常量如下。

    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

    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

    public interface IOneClickProjectGenerationService {
        OneClickProjectGenerationResult generate(AiGenerationTask task, AiGenerationTaskCreateRequest request);
    }
    
  • 修改 AiGenerationTaskServiceImpl.validateRequest,允许新类型。

    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生成类型");
    }
    
  • 运行测试。

    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 并注入。

    @Mock
    private IOneClickProjectGenerationService oneClickProjectGenerationService;
    
    setField(worker, "oneClickProjectGenerationService", oneClickProjectGenerationService);
    
  • 添加失败测试 runOneClickProjectTaskDelegatesToOrchestrator

    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

    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 增加字段。

    @Autowired
    private IOneClickProjectGenerationService oneClickProjectGenerationService;
    
  • 在任务执行分支中加入一键任务。

    if ("one_click_project".equals(task.getGenerateType())) {
        return oneClickProjectGenerationService.generate(task, request);
    }
    
  • 在最终失败写库前处理 OneClickProjectGenerationException

    if (e instanceof OneClickProjectGenerationException) {
        OneClickProjectGenerationResult result = ((OneClickProjectGenerationException) e).getResult();
        task.setResultPayload(JSON.toJSONString(result));
        task.setErrorCode(result.getFailedStage());
    }
    
  • 运行测试。

    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 字段固定为:IAiGenerateServiceFrontendPageDesignServiceIFrontProjectPreviewServiceIFrontProjectRunPreviewServiceIFrontProjectServiceAiGenerationTaskMapper

  • 添加失败测试 generateRunsEveryStageAndReturnsRunningPreview。测试准备一个 frontendEnabled=1FrontProject,一个 AppBlueprintDesign,一个包含 1 张表的 DatabaseDesignResponse,一个包含 1 个动作的 List<BusinessActionDesign>,前台和后台各 1 个页面初始化结果,以及状态为 RUNNINGProjectRunPreviewStatus

  • 在该测试中断言返回结果。

    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());
    
  • 在该测试中断言调用顺序。

    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 的主流程。

    @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。

    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();
    }
    
  • 运行测试。

    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 依次返回 STARTINGRUNNING。断言最终 previewStatus=RUNNING,并断言 status 被调用 2 次。

  • 添加测试 generateSkipsUserFrontendWhenDisabled。设置 project.setFrontendEnabled("0"),断言不会调用 initializePageDesigns(10L, 20L, "frontend")getStructure(10L, 20L, "frontend"),并断言管理前台仍初始化和生成结构。

  • 添加测试 generateThrowsOneClickExceptionWithDownloadReadyResultWhenRunPreviewFails。让源码预览完成,让运行预览返回 FAILED 和错误信息 后端端口启动失败。断言异常结果包含 failedStage=RUN_PREVIEWpreviewStatus=FAILEDdownloadReady=true、建议动作 源码预览已生成,可以先下载项目或进入高级调整调整后重试运行预览

  • 添加运行预览等待 helper。

    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。

    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 中把等待时间改短。

    setField(service, "runPreviewPollIntervalMillis", 0L);
    setField(service, "runPreviewTimeoutMillis", 10L);
    
  • 运行测试。

    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 添加失败测试。

    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。

    export function createOneClickProjectTask(projectId, data) {
      return createAiTask(projectId, Object.assign({}, data, {
        generateType: 'one_click_project'
      }))
    }
    
  • 保持 createAiTaskretryAiTaskgenerateProjectstartRunPreviewgetRunPreviewStatus 的既有导出不变。

  • 运行前端测试并确认此时 GenerateView.vue 相关断言失败。

    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> 中添加状态。

    const ONE_CLICK_GENERATE_TYPE = 'one_click_project'
    const oneClickGenerating = ref(false)
    const oneClickResult = ref(null)
    const expertMode = ref(false)
    
  • 添加只校验项目名称的一键校验函数。

    function validateOneClickInput() {
      if (!projectForm.projectName || !projectForm.projectName.trim()) {
        ElMessage.warning('请输入项目名称')
        return false
      }
      return true
    }
    
  • 添加一键提交函数。

    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 的现有分支前加入一键结果处理。

    if (task.generateType === ONE_CLICK_GENERATE_TYPE) {
      oneClickResult.value = parseAiTaskResult(task.resultPayload)
      return
    }
    
  • 添加结果解析函数。

    function parseAiTaskResult(payload) {
      if (!payload) return null
      if (typeof payload === 'object') return payload
      try {
        return JSON.parse(payload)
      } catch (error) {
        return { errorMessage: payload }
      }
    }
    
  • 在模板顶部新增 生成完整项目 卡片,字段包括项目名称、项目描述、用户前台开关、高级调整开关、主按钮 生成完整项目。主按钮绑定 handleGenerateCompleteProjectloading 绑定 oneClickGenerating

  • 在一键卡片下方新增结果卡片。展示 backendUrlfrontendUrladminFrontendUrldownloadReady,当 errorMessage 存在时展示 warning alert。

  • 把原有应用蓝图、数据库设计、业务蓝图、页面设计、旧预览操作区整体包在 v-if="expertMode" 区域内。保留原有 handler 和文案,避免破坏高级调整。

  • 保留 validateRequirementInput 给高级调整使用。它可以继续要求关键词或项目描述;一键生成使用新的 validateOneClickInput

  • 添加布局 CSS。

    .one-click-card :deep(.el-form-item__content) {
      min-width: 0;
    }
    
    .result-card {
      margin-top: 16px;
    }
    
  • 运行前端测试。

    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。

    mvn -q -pl ruoyi-generator -Dtest=AiGenerationTaskServiceImplTest,AiGenerationTaskWorkerTest,OneClickProjectGenerationServiceImplTest test
    

    Expected result: focused backend tests 全部通过。

  • 运行后端模块测试。

    mvn -q -pl ruoyi-generator,ruoyi-admin -am test
    

    Expected result: generator/admin 相关测试全部通过。

  • 运行前端测试。

    cd easycode-web
    node --test src/views/generateView.test.mjs src/views/previewView.test.mjs
    

    Expected result: 前端 source-level tests 全部通过。

  • 运行前端构建。

    cd easycode-web
    npm run build
    

    Expected result: Vite build 成功完成。

  • 在浏览器做手动冒烟验证。

    1. 打开生成页。
    2. 确认第一屏默认是 生成完整项目
    3. 只输入 客户关系管理系统
    4. 点击 生成完整项目
    5. 后端服务可用时,确认运行预览进入 RUNNING
    6. 打开 高级调整
    7. 确认原有应用蓝图、数据库、业务蓝图、页面设计、源码预览控件仍可访问。
  • 检查 diff。

    git diff --stat
    git diff --check
    

    Expected result: diff 范围只包含一键生成项目相关改动,git diff --check 无 whitespace 错误。


Acceptance Criteria

  • 用户只输入项目名称即可创建一键任务。
  • 一键任务使用 generateType=one_click_project,由现有 AI 任务中心排队、查询、重试、取消。
  • 后端按固定阶段执行:项目准备、应用蓝图、数据库、业务蓝图、页面初始化、源码预览、运行预览。
  • RUN_PREVIEW 进入 RUNNING 才算任务成功。
  • 源码预览完成但运行预览失败时,任务失败结果仍包含 downloadReady=truefailedStage=RUN_PREVIEW
  • 用户前台关闭时跳过用户前台页面初始化和用户前台源码结构生成,管理前台仍生成。
  • 前端默认显示一键入口、阶段状态、运行地址、源码预览入口。
  • 原有分步设计器保留在高级调整中。
  • 后端 focused tests、后端模块测试、前端 source-level tests、前端 build 全部通过。

Implementation Notes

  • 不新增数据库字段。第一版使用现有 current_stepprogressresult_payloaderror_code 表达阶段和失败原因。
  • 不引入模板库。模板库属于已确认的方案三,是当前版本之后的增强方向。
  • 不移除旧的 app_blueprintdatabasebusiness_blueprint 任务类型。它们继续服务高级调整和调试场景。
  • 一键入口和高级调整共享同一个项目记录,因此 frontendEnabled、模块选择、项目描述仍通过现有项目保存逻辑持久化。