feat: expand EasyCode software factory workflows

This commit is contained in:
王鹏
2026-07-15 12:48:50 +08:00
parent fcfa4374d7
commit 79dea897bc
1108 changed files with 163774 additions and 21593 deletions

View File

@@ -3,6 +3,7 @@ package com.ruoyi.web.controller.front;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@@ -13,6 +14,8 @@ import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.generator.domain.front.dto.FrontLoginBody;
import com.ruoyi.generator.domain.front.dto.FrontPasswordUpdateBody;
import com.ruoyi.generator.domain.front.dto.FrontProfileUpdateBody;
import com.ruoyi.generator.domain.front.dto.FrontRegisterBody;
import com.ruoyi.generator.service.front.IFrontUserService;
import com.ruoyi.web.service.front.FrontAuthService;
@@ -53,6 +56,18 @@ public class FrontAuthController extends BaseController
return AjaxResult.success(frontAuthService.profile(SecurityUtils.getUserId()));
}
@PutMapping("/profile")
public AjaxResult updateProfile(@RequestBody FrontProfileUpdateBody body)
{
return AjaxResult.success(frontAuthService.updateProfile(SecurityUtils.getUserId(), body));
}
@PutMapping("/password")
public AjaxResult updatePassword(@RequestBody FrontPasswordUpdateBody body)
{
return toAjax(frontAuthService.updatePassword(SecurityUtils.getUserId(), body));
}
@PostMapping("/logout")
public AjaxResult logout()
{

View File

@@ -0,0 +1,142 @@
package com.ruoyi.web.controller.front;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.generator.factory.dsl.ProjectSpecQueryService;
import com.ruoyi.generator.factory.dsl.model.ProjectSpecV1;
import com.ruoyi.generator.factory.diff.ProjectSpecVersionDiffService;
import com.ruoyi.generator.factory.generation.ProjectSpecGenerationService;
import com.ruoyi.generator.factory.generation.ProjectSpecGenerationRunService;
import com.ruoyi.generator.factory.patch.ProjectSpecPatchService;
import com.ruoyi.generator.factory.patch.model.ProjectSpecPatchRequest;
import com.ruoyi.generator.factory.validation.ProjectSpecValidationService;
import com.ruoyi.generator.factory.version.ProjectSpecVersionService;
import com.ruoyi.generator.factory.version.model.ProjectSpecRollbackRequest;
import com.ruoyi.generator.factory.version.model.ProjectSpecSnapshotRequest;
@RestController
@RequestMapping("/front/factory/projects")
public class FrontFactoryProjectController extends BaseController
{
@Autowired
private ProjectSpecQueryService projectSpecQueryService;
@Autowired
private ProjectSpecVersionService projectSpecVersionService;
@Autowired
private ProjectSpecValidationService projectSpecValidationService;
@Autowired
private ProjectSpecPatchService projectSpecPatchService;
@Autowired
private ProjectSpecVersionDiffService projectSpecVersionDiffService;
@Autowired
private ProjectSpecGenerationService projectSpecGenerationService;
@Autowired
private ProjectSpecGenerationRunService projectSpecGenerationRunService;
@GetMapping("/{projectId}/spec")
public AjaxResult getCurrentSpec(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecQueryService.getCurrentSpec(userId, projectId));
}
@PostMapping("/{projectId}/spec/validate")
public AjaxResult validateSpec(@PathVariable Long projectId, @RequestBody(required = false) ProjectSpecV1 spec)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecValidationService.validate(userId, projectId, spec));
}
@PostMapping("/{projectId}/spec/patch/preview")
public AjaxResult previewSpecPatch(@PathVariable Long projectId, @RequestBody ProjectSpecPatchRequest request)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecPatchService.preview(userId, projectId, request));
}
@PostMapping("/{projectId}/spec/patch")
public AjaxResult applySpecPatch(@PathVariable Long projectId, @RequestBody ProjectSpecPatchRequest request)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecPatchService.apply(userId, projectId, request));
}
@PostMapping("/{projectId}/versions")
public AjaxResult snapshotCurrentSpec(@PathVariable Long projectId,
@RequestBody(required = false) ProjectSpecSnapshotRequest request)
{
Long userId = SecurityUtils.getUserId();
String summary = request == null ? null : request.getChangeSummary();
return AjaxResult.success(projectSpecVersionService.snapshotCurrent(userId, projectId, summary));
}
@GetMapping("/{projectId}/versions")
public AjaxResult listSpecVersions(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecVersionService.listVersions(userId, projectId));
}
@GetMapping("/{projectId}/versions/{specVersionId}")
public AjaxResult getSpecVersion(@PathVariable Long projectId, @PathVariable Long specVersionId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecVersionService.getVersion(userId, projectId, specVersionId));
}
@PostMapping("/{projectId}/versions/{specVersionId}/generate")
public AjaxResult generateSpecVersion(@PathVariable Long projectId, @PathVariable Long specVersionId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecGenerationService.generate(userId, projectId, specVersionId));
}
@GetMapping("/{projectId}/generation-runs")
public AjaxResult listGenerationRuns(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecGenerationRunService.list(userId, projectId));
}
@GetMapping("/{projectId}/generation-runs/{generationRunId}")
public AjaxResult getGenerationRun(@PathVariable Long projectId, @PathVariable Long generationRunId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecGenerationRunService.get(userId, projectId, generationRunId));
}
@GetMapping("/{projectId}/generation-runs/{leftRunId}/diff/{rightRunId}")
public AjaxResult diffGenerationRuns(@PathVariable Long projectId, @PathVariable Long leftRunId,
@PathVariable Long rightRunId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecGenerationRunService.diff(userId, projectId,
leftRunId, rightRunId));
}
@PostMapping("/{projectId}/versions/{specVersionId}/rollback")
public AjaxResult rollbackSpecVersion(@PathVariable Long projectId, @PathVariable Long specVersionId,
@RequestBody(required = false) ProjectSpecRollbackRequest request)
{
Long userId = SecurityUtils.getUserId();
String summary = request == null ? null : request.getChangeSummary();
return AjaxResult.success(projectSpecVersionService.rollback(userId, projectId, specVersionId, summary));
}
@GetMapping("/{projectId}/versions/{leftVersionId}/diff/{rightVersionId}")
public AjaxResult diffSpecVersions(@PathVariable Long projectId, @PathVariable Long leftVersionId,
@PathVariable Long rightVersionId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(projectSpecVersionDiffService.diff(userId, projectId,
leftVersionId, rightVersionId));
}
}

View File

@@ -1,6 +1,12 @@
package com.ruoyi.web.controller.front;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.StandardCharsets;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -13,28 +19,60 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.FrontProjectDiagram;
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
import com.ruoyi.generator.domain.front.FrontProjectPaper;
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest;
import com.ruoyi.generator.domain.front.dto.BusinessLoopAuditResult;
import com.ruoyi.generator.domain.front.dto.CodeAnalysisResponse;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.FrontProjectCreateRequest;
import com.ruoyi.generator.domain.front.dto.FrontProjectUpdateRequest;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateBusinessBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateFlowConfigRequest;
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
import com.ruoyi.generator.domain.front.dto.GenerateCodeAnalysisRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDatabaseRequest;
import com.ruoyi.generator.domain.front.dto.GenerateDiagramRequest;
import com.ruoyi.generator.domain.front.dto.GeneratePaperDraftRequest;
import com.ruoyi.generator.domain.front.dto.GeneratePaperOutlineRequest;
import com.ruoyi.generator.domain.front.dto.GenerateRequirementRequest;
import com.ruoyi.generator.domain.front.dto.ParseErSqlRequest;
import com.ruoyi.generator.domain.front.dto.FrontendPageDesignRequest;
import com.ruoyi.generator.domain.front.dto.PreviewFileRequest;
import com.ruoyi.generator.domain.front.dto.ProjectDeliveryReadiness;
import com.ruoyi.generator.factory.quality.ProjectQualityRunService;
import com.ruoyi.generator.factory.artifact.CertifiedArtifactService;
import com.ruoyi.generator.factory.artifact.model.ProjectCertifiedArtifact;
import com.ruoyi.generator.factory.acceptance.FactoryV1AcceptanceService;
import com.ruoyi.generator.service.ITemplateBundleService;
import com.ruoyi.generator.service.front.IAiGenerateService;
import com.ruoyi.generator.service.front.IAiGenerationTaskService;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
import com.ruoyi.generator.service.front.BusinessLoopCoverageValidator;
import com.ruoyi.generator.service.front.BusinessRequirementCoverageValidator;
import com.ruoyi.generator.service.front.FlowConfigContractService;
import com.ruoyi.generator.service.front.FrontProjectAssetService;
import com.ruoyi.generator.service.front.FrontendPageDesignService;
import com.ruoyi.generator.service.front.IFrontProjectPreviewService;
import com.ruoyi.generator.service.front.IFrontProjectRunPreviewService;
import com.ruoyi.generator.service.front.IFrontProjectService;
import com.ruoyi.generator.service.front.IProjectDeliveryReadinessService;
import com.ruoyi.generator.service.front.SqlDdlParseService;
import com.ruoyi.generator.factory.flow.FlowConfigCompilation;
import com.ruoyi.generator.factory.flow.FlowConfigProjector;
import com.ruoyi.generator.service.front.PageLayoutTemplateRegistry;
import com.ruoyi.generator.service.front.PageStylePresetRegistry;
@RestController
@RequestMapping("/front/project")
@@ -47,6 +85,14 @@ public class FrontProjectController extends BaseController
@Autowired
private IAiGenerationTaskService aiGenerationTaskService;
@Autowired
private BusinessLoopCoverageValidator businessLoopCoverageValidator;
@Autowired
private BusinessRequirementCoverageValidator businessRequirementCoverageValidator;
@Autowired
private FlowConfigContractService flowConfigContractService;
@Autowired
private FrontProjectAssetService frontProjectAssetService;
@Autowired
private FrontendPageDesignService frontendPageDesignService;
@Autowired
private BusinessBlockRegistryService businessBlockRegistryService;
@@ -54,6 +100,22 @@ public class FrontProjectController extends BaseController
private IFrontProjectPreviewService frontProjectPreviewService;
@Autowired
private IFrontProjectRunPreviewService frontProjectRunPreviewService;
@Autowired
private SqlDdlParseService sqlDdlParseService;
@Autowired
private ITemplateBundleService templateBundleService;
@Autowired
private IProjectDeliveryReadinessService projectDeliveryReadinessService;
@Autowired
private ProjectQualityRunService projectQualityRunService;
@Autowired
private CertifiedArtifactService certifiedArtifactService;
@Autowired
private FactoryV1AcceptanceService factoryV1AcceptanceService;
@Autowired
private PageStylePresetRegistry pageStylePresetRegistry;
@Autowired
private PageLayoutTemplateRegistry pageLayoutTemplateRegistry;
@PostMapping("/create")
public AjaxResult create(@RequestBody FrontProjectCreateRequest request)
@@ -75,6 +137,31 @@ public class FrontProjectController extends BaseController
return AjaxResult.success(frontProjectService.listAvailableModules());
}
@GetMapping("/options/code-templates")
public AjaxResult codeTemplates()
{
return AjaxResult.success(templateBundleService.listEnabledBundles());
}
@GetMapping("/options/page-templates")
public AjaxResult pageTemplates()
{
pageStylePresetRegistry.refreshFromDatabase();
pageLayoutTemplateRegistry.refreshFromDatabase();
Map<String, Object> options = new LinkedHashMap<String, Object>();
options.put("themes", pageStylePresetRegistry.options());
options.put("solutions", pageStylePresetRegistry.options());
options.put("layouts", pageLayoutTemplateRegistry.options());
return AjaxResult.success(options);
}
@PostMapping("/generate-requirement")
public AjaxResult generateRequirement(@RequestBody GenerateRequirementRequest request)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(aiGenerateService.generateRequirementSuggestion(userId, request));
}
@GetMapping("/{projectId}")
public AjaxResult get(@PathVariable Long projectId)
{
@@ -110,6 +197,14 @@ public class FrontProjectController extends BaseController
return AjaxResult.success(frontProjectService.saveDatabase(userId, projectId, request));
}
@PostMapping("/{projectId}/er/parse-sql")
public AjaxResult parseErSql(@PathVariable Long projectId, @RequestBody ParseErSqlRequest request)
{
Long userId = SecurityUtils.getUserId();
frontProjectService.getProject(userId, projectId);
return AjaxResult.success(sqlDdlParseService.parse(request == null ? "" : request.getSql()));
}
@GetMapping("/{projectId}/page-designs")
public AjaxResult pageDesigns(@PathVariable Long projectId,
@RequestParam(defaultValue = "frontend") String scope)
@@ -127,6 +222,41 @@ public class FrontProjectController extends BaseController
return AjaxResult.success(businessBlockRegistryService.listDefinitions(scope));
}
@GetMapping("/{projectId}/diagrams")
public AjaxResult diagrams(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontProjectAssetService.listDiagrams(userId, projectId));
}
@PostMapping("/{projectId}/diagrams")
public AjaxResult saveDiagram(@PathVariable Long projectId, @RequestBody FrontProjectDiagram request)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontProjectAssetService.saveDiagram(userId, projectId, request));
}
@DeleteMapping("/{projectId}/diagrams/{diagramId}")
public AjaxResult deleteDiagram(@PathVariable Long projectId, @PathVariable Long diagramId)
{
Long userId = SecurityUtils.getUserId();
return toAjax(frontProjectAssetService.deleteDiagram(userId, projectId, diagramId));
}
@GetMapping("/{projectId}/paper")
public AjaxResult paper(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontProjectAssetService.getPaper(userId, projectId));
}
@PutMapping("/{projectId}/paper")
public AjaxResult savePaper(@PathVariable Long projectId, @RequestBody FrontProjectPaper request)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontProjectAssetService.savePaper(userId, projectId, request));
}
@PostMapping("/{projectId}/page-designs/init")
public AjaxResult initPageDesigns(@PathVariable Long projectId,
@RequestParam(defaultValue = "frontend") String scope)
@@ -135,6 +265,17 @@ public class FrontProjectController extends BaseController
return AjaxResult.success(frontendPageDesignService.initializePageDesigns(userId, projectId, scope));
}
@PostMapping("/{projectId}/page-designs/apply-business-loop-bindings")
public AjaxResult applyBusinessLoopBindings(@PathVariable Long projectId,
@RequestParam(defaultValue = "frontend") String scope)
{
Long userId = SecurityUtils.getUserId();
FrontProject project = frontProjectService.getProject(userId, projectId);
frontendPageDesignService.applyBusinessLoopBindings(userId, projectId,
FlowConfigProjector.compile(project).getLoopPlan(), scope);
return AjaxResult.success(frontendPageDesignService.listPageDesigns(userId, projectId, scope));
}
@PostMapping("/{projectId}/page-designs")
public AjaxResult createPageDesign(@PathVariable Long projectId,
@RequestParam(defaultValue = "frontend") String scope,
@@ -152,6 +293,23 @@ public class FrontProjectController extends BaseController
return AjaxResult.success(frontendPageDesignService.getPageDesign(userId, projectId, designId, scope));
}
@GetMapping("/{projectId}/page-designs/{designId}/dsl")
public AjaxResult pageDsl(@PathVariable Long projectId, @PathVariable Long designId,
@RequestParam(defaultValue = "frontend") String scope)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontendPageDesignService.getPageDsl(userId, projectId, designId, scope));
}
@PostMapping("/{projectId}/page-designs/{designId}/compose")
public AjaxResult composePageDesign(@PathVariable Long projectId, @PathVariable Long designId,
@RequestParam(defaultValue = "frontend") String scope,
@RequestBody(required = false) FrontendPageDesignRequest request)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontendPageDesignService.composePageDesign(userId, projectId, designId, request, scope));
}
@PutMapping("/{projectId}/page-designs/{designId}")
public AjaxResult savePageDesign(@PathVariable Long projectId, @PathVariable Long designId,
@RequestParam(defaultValue = "frontend") String scope,
@@ -184,11 +342,132 @@ public class FrontProjectController extends BaseController
return success(aiGenerateService.generateAppBlueprint(userId, projectId, request));
}
@PostMapping("/{projectId}/generate-business-blueprint")
public AjaxResult generateBusinessBlueprint(@PathVariable Long projectId, @RequestBody GenerateBusinessBlueprintRequest request)
@PostMapping("/{projectId}/generate-flow-config")
public AjaxResult generateFlowConfig(@PathVariable Long projectId, @RequestBody GenerateFlowConfigRequest request)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerateService.generateBusinessBlueprint(userId, projectId, request));
return success(aiGenerateService.generateFlowConfig(userId, projectId, request));
}
@PutMapping("/{projectId}/flow-config")
public AjaxResult saveFlowConfig(@PathVariable Long projectId, @RequestBody SimpleFlowConfig flowConfig)
{
Long userId = SecurityUtils.getUserId();
return toAjax(frontProjectService.updateFlowConfig(userId, projectId, JSON.toJSONString(flowConfig)));
}
@PostMapping("/{projectId}/diagram")
public AjaxResult generateDiagram(@PathVariable Long projectId, @RequestBody GenerateDiagramRequest request)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerateService.generateDiagram(userId, projectId, request));
}
@PostMapping("/{projectId}/paper-outline")
public AjaxResult generatePaperOutline(@PathVariable Long projectId, @RequestBody GeneratePaperOutlineRequest request)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerateService.generatePaperOutline(userId, projectId, request));
}
@PostMapping("/{projectId}/paper-draft")
public AjaxResult generatePaperDraft(@PathVariable Long projectId, @RequestBody GeneratePaperDraftRequest request)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerateService.generatePaperDraft(userId, projectId, request));
}
@PostMapping(value = "/{projectId}/paper-draft/stream", produces = "text/plain;charset=UTF-8")
public StreamingResponseBody streamPaperDraft(@PathVariable Long projectId,
@RequestBody GeneratePaperDraftRequest request)
{
final Long userId = SecurityUtils.getUserId();
return outputStream ->
{
final Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
aiGenerateService.streamPaperDraft(userId, projectId, request, content ->
{
writer.write(content);
writer.flush();
});
};
}
@PostMapping("/{projectId}/code-analysis")
public AjaxResult createCodeAnalysis(@PathVariable Long projectId,
@RequestBody(required = false) GenerateCodeAnalysisRequest request)
{
Long userId = SecurityUtils.getUserId();
assertCodeAnalysisPreviewReady(userId, projectId);
return success(aiGenerationTaskService.createTask(userId, projectId, toCodeAnalysisTaskRequest(request)));
}
@PostMapping(value = "/{projectId}/code-analysis/stream", produces = "text/plain;charset=UTF-8")
public StreamingResponseBody streamCodeAnalysis(@PathVariable Long projectId,
@RequestBody(required = false) GenerateCodeAnalysisRequest request)
{
final Long userId = SecurityUtils.getUserId();
assertCodeAnalysisPreviewReady(userId, projectId);
return outputStream ->
{
final Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
aiGenerateService.streamCodeAnalysis(userId, projectId, request, content ->
{
writer.write(content);
writer.flush();
});
};
}
@GetMapping("/{projectId}/code-analysis/history")
public AjaxResult codeAnalysisHistory(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerationTaskService.listGenerations(userId, projectId, "code_analysis"));
}
@PostMapping("/{projectId}/code-analysis/{generationId}/continue")
public AjaxResult continueCodeAnalysis(@PathVariable Long projectId, @PathVariable Long generationId,
@RequestBody(required = false) GenerateCodeAnalysisRequest request)
{
Long userId = SecurityUtils.getUserId();
assertCodeAnalysisPreviewReady(userId, projectId);
FrontProjectGeneration previous = aiGenerationTaskService.getGeneration(userId, projectId, generationId);
if (!"code_analysis".equals(previous.getGenerateType()))
{
throw new ServiceException("只能基于代码解读历史继续生成");
}
GenerateCodeAnalysisRequest nextRequest = request == null ? new GenerateCodeAnalysisRequest() : request;
nextRequest.setMode("continue");
nextRequest.setPreviousGenerationId(generationId);
nextRequest.setPreviousMarkdown(readCodeAnalysisMarkdown(previous.getResponsePayload()));
return success(aiGenerationTaskService.createTask(userId, projectId, toCodeAnalysisTaskRequest(nextRequest)));
}
@PostMapping(value = "/{projectId}/code-analysis/{generationId}/continue/stream", produces = "text/plain;charset=UTF-8")
public StreamingResponseBody continueCodeAnalysisStream(@PathVariable Long projectId, @PathVariable Long generationId,
@RequestBody(required = false) GenerateCodeAnalysisRequest request)
{
final Long userId = SecurityUtils.getUserId();
assertCodeAnalysisPreviewReady(userId, projectId);
final GenerateCodeAnalysisRequest nextRequest = toContinueCodeAnalysisRequest(userId, projectId, generationId,
request);
return outputStream ->
{
final Writer writer = new OutputStreamWriter(outputStream, StandardCharsets.UTF_8);
aiGenerateService.streamCodeAnalysis(userId, projectId, nextRequest, content ->
{
writer.write(content);
writer.flush();
});
};
}
@GetMapping("/ai-tasks/one-click-overview")
public AjaxResult oneClickTaskOverview()
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerationTaskService.listOneClickTaskOverview(userId));
}
@PostMapping("/{projectId}/ai-tasks")
@@ -198,6 +477,62 @@ public class FrontProjectController extends BaseController
return success(aiGenerationTaskService.createTask(userId, projectId, request));
}
@GetMapping("/{projectId}/ai-tasks/one-click-history")
public AjaxResult oneClickTaskHistory(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(aiGenerationTaskService.listOneClickTaskHistory(userId, projectId));
}
@GetMapping("/{projectId}/delivery-readiness")
public AjaxResult deliveryReadiness(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(projectDeliveryReadinessService.get(userId, projectId));
}
@PostMapping("/{projectId}/quality-runs")
public AjaxResult createQualityRun(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(projectQualityRunService.createSummary(userId, projectId));
}
@GetMapping("/{projectId}/quality-runs")
public AjaxResult qualityRuns(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(projectQualityRunService.listSummaries(userId, projectId));
}
@GetMapping("/{projectId}/quality-runs/latest")
public AjaxResult latestQualityRun(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(projectQualityRunService.latestSummary(userId, projectId));
}
@PostMapping("/{projectId}/v1-acceptance-runs")
public AjaxResult startV1Acceptance(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(factoryV1AcceptanceService.start(userId, projectId));
}
@GetMapping("/{projectId}/v1-acceptance-runs/latest")
public AjaxResult latestV1Acceptance(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(factoryV1AcceptanceService.latest(userId, projectId));
}
@GetMapping("/{projectId}/v1-acceptance-runs")
public AjaxResult v1Acceptances(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(factoryV1AcceptanceService.list(userId, projectId));
}
@GetMapping("/{projectId}/ai-tasks/{taskId}")
public AjaxResult getAiTask(@PathVariable Long projectId, @PathVariable Long taskId)
{
@@ -226,6 +561,35 @@ public class FrontProjectController extends BaseController
return success(aiGenerationTaskService.listGenerations(userId, projectId));
}
@GetMapping("/{projectId}/business-loop-audit")
public AjaxResult businessLoopAudit(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
FrontProject project = frontProjectService.getProject(userId, projectId);
SimpleFlowConfig flowConfig = FlowConfigProjector.read(project);
FlowConfigCompilation flow = flowConfig == null
? new FlowConfigCompilation() : flowConfigContractService.compile(flowConfig);
BusinessLoopPlan plan = flow.getLoopPlan();
DatabaseDesignResponse database = frontProjectService.getDatabase(userId, projectId);
Map<String, Object> result = new LinkedHashMap<String, Object>();
BusinessLoopAuditResult databaseAudit = flowConfig == null
? businessLoopCoverageValidator.auditDatabase(plan, database)
: flowConfigContractService.auditDatabase(flowConfig, database);
BusinessLoopAuditResult actionAudit = businessLoopCoverageValidator.auditBusinessActions(plan,
flow.getBusinessActions());
String requirement = StringUtils.defaultString(project.getProjectDesc());
if (StringUtils.isNotBlank(project.getRequirementKeyword()))
{
requirement += "\n补充要求" + project.getRequirementKeyword();
}
BusinessLoopAuditResult requirementAudit = businessRequirementCoverageValidator.audit(requirement, flowConfig);
result.put("requirements", requirementAudit);
result.put("database", databaseAudit);
result.put("businessActions", actionAudit);
result.put("complete", requirementAudit.isComplete() && databaseAudit.isComplete() && actionAudit.isComplete());
return success(result);
}
@GetMapping("/ai-quota")
public AjaxResult aiQuota()
{
@@ -237,14 +601,10 @@ public class FrontProjectController extends BaseController
public AjaxResult preview(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
FrontProject project = frontProjectService.getProject(userId, projectId);
frontProjectPreviewService.getStructure(userId, projectId, "backend");
if (!"0".equals(project.getFrontendEnabled()))
for (String type : frontProjectPreviewService.getSupportedTemplateTypes(userId, projectId))
{
frontProjectPreviewService.getStructure(userId, projectId, "frontend");
frontProjectPreviewService.getStructure(userId, projectId, type);
}
frontProjectPreviewService.getStructure(userId, projectId, "admin_frontend");
frontProjectPreviewService.getStructure(userId, projectId, "sql");
return toAjax(frontProjectPreviewService.markPreviewReady(userId, projectId));
}
@@ -269,6 +629,18 @@ public class FrontProjectController extends BaseController
return success(frontProjectRunPreviewService.stop(userId, projectId));
}
@GetMapping("/{projectId}/run-preview/{previewJobId}/screenshot")
public void runPreviewScreenshot(@PathVariable Long projectId, @PathVariable Long previewJobId,
HttpServletResponse response) throws IOException
{
Long userId = SecurityUtils.getUserId();
byte[] data = frontProjectRunPreviewService.screenshot(userId, projectId, previewJobId);
response.reset();
response.setContentType("image/png");
response.addHeader("Content-Length", String.valueOf(data.length));
IOUtils.write(data, response.getOutputStream());
}
@GetMapping("/{projectId}/structure")
public AjaxResult structure(@PathVariable Long projectId, @RequestParam String templateType)
{
@@ -276,6 +648,13 @@ public class FrontProjectController extends BaseController
return success(frontProjectPreviewService.getStructure(userId, projectId, templateType));
}
@GetMapping("/{projectId}/template-types")
public AjaxResult templateTypes(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return success(frontProjectPreviewService.getSupportedTemplateTypes(userId, projectId));
}
@GetMapping("/{projectId}/file-content")
public AjaxResult fileContent(@PathVariable Long projectId, PreviewFileRequest request)
{
@@ -284,15 +663,50 @@ public class FrontProjectController extends BaseController
}
@GetMapping("/{projectId}/download")
public void download(@PathVariable Long projectId, @RequestParam(required = false) String templateType, HttpServletResponse response) throws IOException
public void download(@PathVariable Long projectId,
@RequestParam(required = false) String templateType,
@RequestParam(required = false, defaultValue = ProjectDeliveryReadiness.ARTIFACT_DRAFT) String artifactType,
HttpServletResponse response) throws IOException
{
Long userId = SecurityUtils.getUserId();
String normalizedArtifactType = StringUtils.isEmpty(artifactType)
? ProjectDeliveryReadiness.ARTIFACT_DRAFT
: artifactType.trim().toUpperCase(Locale.ROOT);
ProjectDeliveryReadiness readiness = projectDeliveryReadinessService
.assertDownloadAllowed(userId, projectId, normalizedArtifactType);
FrontProject project = frontProjectService.getProject(userId, projectId);
byte[] data = StringUtils.isNotEmpty(templateType)
? frontProjectPreviewService.download(userId, projectId, templateType)
: frontProjectPreviewService.downloadAll(userId, projectId);
ProjectCertifiedArtifact certifiedArtifact = null;
byte[] data;
if (ProjectDeliveryReadiness.ARTIFACT_CERTIFIED.equals(normalizedArtifactType))
{
if (StringUtils.isNotEmpty(templateType))
{
throw new ServiceException("认证制品只支持下载完整冻结源码");
}
if (readiness.getQualityRun() == null || readiness.getQualityRun().getQualityRunId() == null)
{
throw new ServiceException("认证制品缺少质量运行身份");
}
certifiedArtifact = certifiedArtifactService.getReady(userId, projectId,
readiness.getQualityRun().getQualityRunId());
data = certifiedArtifactService.read(certifiedArtifact);
}
else
{
data = StringUtils.isNotEmpty(templateType)
? frontProjectPreviewService.download(userId, projectId, templateType)
: frontProjectPreviewService.downloadAll(userId, projectId);
}
response.reset();
FileUtils.setAttachmentResponseHeader(response, sourceZipName(project));
response.addHeader("X-Factory-Artifact-Type", normalizedArtifactType);
response.addHeader("X-Factory-Certification-Level", readiness.getCertificationLevel());
if (certifiedArtifact != null)
{
response.addHeader("X-Factory-Artifact-Id", String.valueOf(certifiedArtifact.getArtifactId()));
response.addHeader("X-Factory-Zip-SHA256", certifiedArtifact.getZipSha256());
response.addHeader("X-Factory-Report-SHA256", certifiedArtifact.getReportSha256());
}
response.addHeader("Content-Length", "" + data.length);
response.setContentType("application/octet-stream; charset=UTF-8");
IOUtils.write(data, response.getOutputStream());
@@ -311,4 +725,76 @@ public class FrontProjectController extends BaseController
}
return safeName + ".zip";
}
private AiGenerationTaskCreateRequest toCodeAnalysisTaskRequest(GenerateCodeAnalysisRequest source)
{
AiGenerationTaskCreateRequest request = new AiGenerationTaskCreateRequest();
request.setGenerateType("code_analysis");
if (source != null)
{
request.setMode(source.getMode());
request.setPreviousGenerationId(source.getPreviousGenerationId());
request.setPreviousMarkdown(source.getPreviousMarkdown());
request.setExtraRequirements(source.getExtraRequirements());
}
return request;
}
private void assertCodeAnalysisPreviewReady(Long userId, Long projectId)
{
FrontProject project = frontProjectService.getProject(userId, projectId);
if (project == null || !"1".equals(project.getPreviewStatus()))
{
throw new ServiceException("请先生成源码预览后再进行代码解读");
}
}
private GenerateCodeAnalysisRequest toContinueCodeAnalysisRequest(Long userId, Long projectId, Long generationId,
GenerateCodeAnalysisRequest request)
{
FrontProjectGeneration previous = aiGenerationTaskService.getGeneration(userId, projectId, generationId);
if (!"code_analysis".equals(previous.getGenerateType()))
{
throw new ServiceException("只能基于代码解读历史继续生成");
}
GenerateCodeAnalysisRequest nextRequest = request == null ? new GenerateCodeAnalysisRequest() : request;
nextRequest.setMode("continue");
nextRequest.setPreviousGenerationId(generationId);
if (StringUtils.isBlank(nextRequest.getPreviousMarkdown()))
{
nextRequest.setPreviousMarkdown(readCodeAnalysisMarkdown(previous.getResponsePayload()));
}
return nextRequest;
}
private String readCodeAnalysisMarkdown(String responsePayload)
{
if (StringUtils.isEmpty(responsePayload))
{
return "";
}
try
{
CodeAnalysisResponse response = JSON.parseObject(responsePayload, CodeAnalysisResponse.class);
if (response != null && StringUtils.isNotEmpty(response.getMarkdown()))
{
return response.getMarkdown();
}
}
catch (RuntimeException ignored)
{
return responsePayload;
}
return responsePayload;
}
private BusinessLoopPlan readBusinessLoopPlan(String json)
{
if (StringUtils.isEmpty(json))
{
return new BusinessLoopPlan();
}
BusinessLoopPlan plan = JSON.parseObject(json, BusinessLoopPlan.class);
return plan == null ? new BusinessLoopPlan() : plan;
}
}

View File

@@ -12,6 +12,8 @@ import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.generator.domain.front.FrontUser;
import com.ruoyi.generator.domain.front.dto.FrontPasswordUpdateBody;
import com.ruoyi.generator.domain.front.dto.FrontProfileUpdateBody;
import com.ruoyi.generator.service.front.IFrontUserService;
@Component
@@ -68,6 +70,79 @@ public class FrontAuthService
return frontUser;
}
public FrontUser updateProfile(Long userId, FrontProfileUpdateBody body)
{
if (userId == null)
{
throw new ServiceException("登录状态异常,请重新登录");
}
if (body == null)
{
throw new ServiceException("用户资料不能为空");
}
String nickname = trimToEmpty(body.getNickname());
String email = trimToEmpty(body.getEmail());
String phone = trimToEmpty(body.getPhone());
validateProfile(nickname, email, phone);
FrontUser user = new FrontUser();
user.setUserId(userId);
user.setNickname(nickname);
user.setEmail(email);
user.setPhone(phone);
if (frontUserService.updateProfile(user) <= 0)
{
throw new ServiceException("更新个人资料失败");
}
return profile(userId);
}
public int updatePassword(Long userId, FrontPasswordUpdateBody body)
{
if (userId == null)
{
throw new ServiceException("登录状态异常,请重新登录");
}
if (body == null || StringUtils.isEmpty(body.getOldPassword()))
{
throw new ServiceException("旧密码不能为空");
}
if (StringUtils.isEmpty(body.getNewPassword()))
{
throw new ServiceException("新密码不能为空");
}
if (!body.getNewPassword().equals(body.getConfirmPassword()))
{
throw new ServiceException("两次输入的新密码不一致");
}
if (body.getNewPassword().length() < 5 || body.getNewPassword().length() > 50)
{
throw new ServiceException("新密码长度必须在5到50个字符之间");
}
FrontUser existing = frontUserService.selectById(userId);
if (existing == null || StringUtils.isEmpty(existing.getPassword()))
{
throw new ServiceException("用户不存在");
}
if (!matchesPassword(body.getOldPassword(), existing.getPassword()))
{
throw new ServiceException("旧密码错误");
}
FrontUser user = new FrontUser();
user.setUserId(userId);
user.setPassword(SecurityUtils.encryptPassword(body.getNewPassword()));
int rows = frontUserService.updatePassword(user);
if (rows <= 0)
{
throw new ServiceException("修改密码失败");
}
return rows;
}
private boolean matchesPassword(String password, String encodedPassword)
{
try
@@ -80,6 +155,35 @@ public class FrontAuthService
}
}
private void validateProfile(String nickname, String email, String phone)
{
if (nickname.length() > 50)
{
throw new ServiceException("昵称长度不能超过50个字符");
}
if (email.length() > 100)
{
throw new ServiceException("邮箱长度不能超过100个字符");
}
if (StringUtils.isNotEmpty(email) && !email.matches("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$"))
{
throw new ServiceException("邮箱格式不正确");
}
if (phone.length() > 20)
{
throw new ServiceException("手机号长度不能超过20个字符");
}
if (StringUtils.isNotEmpty(phone) && !phone.matches("^[0-9+\\- ]+$"))
{
throw new ServiceException("手机号格式不正确");
}
}
private String trimToEmpty(String value)
{
return value == null ? "" : value.trim();
}
private ServiceException credentialException()
{
return new ServiceException("用户名或密码错误");

View File

@@ -17,12 +17,61 @@ ruoyi:
deepseek:
apiKey: sk-2aef3231d45048a9a0ba6f0367a072ca
baseUrl: https://api.deepseek.com
model: deepseek-v4-flash
model: ${DEEPSEEK_MODEL:deepseek-v4-flash}
timeout: ${DEEPSEEK_TIMEOUT:180000}
maxTables: 12
maxColumnsPerTable: 30
skipSslVerify: true
easycode:
one-click:
run-preview-timeout-ms: ${EASYCODE_ONE_CLICK_RUN_PREVIEW_TIMEOUT_MS:300000}
run-preview-poll-interval-ms: ${EASYCODE_ONE_CLICK_RUN_PREVIEW_POLL_INTERVAL_MS:1000}
preview:
execution-mode: ${EASYCODE_PREVIEW_EXECUTION_MODE:host}
ttl-minutes: ${EASYCODE_PREVIEW_TTL_MINUTES:15}
worker-enabled: ${EASYCODE_PREVIEW_WORKER_ENABLED:false}
worker-root: ${EASYCODE_PREVIEW_WORKER_ROOT:preview-worker-data}
docker-image: ${EASYCODE_PREVIEW_DOCKER_IMAGE:easycode/preview-worker:v1}
docker-network: ${EASYCODE_PREVIEW_DOCKER_NETWORK:bridge}
bind-host: ${EASYCODE_PREVIEW_BIND_HOST:127.0.0.1}
cpu-limit: ${EASYCODE_PREVIEW_CPU_LIMIT:1.0}
memory-limit-mb: ${EASYCODE_PREVIEW_MEMORY_LIMIT_MB:1024}
pids-limit: ${EASYCODE_PREVIEW_PIDS_LIMIT:256}
factory:
prompt-registry:
database-enabled: ${FACTORY_PROMPT_REGISTRY_DATABASE_ENABLED:true}
builtin-fallback-enabled: ${FACTORY_PROMPT_REGISTRY_BUILTIN_FALLBACK_ENABLED:false}
plugin-registry:
database-enabled: ${FACTORY_PLUGIN_REGISTRY_DATABASE_ENABLED:false}
plugin-execution:
executor-code: ${FACTORY_PLUGIN_EXECUTOR_CODE:dry-run:v1}
environment-code: ${FACTORY_PLUGIN_EXECUTION_ENVIRONMENT_CODE:development}
approval-required-for-non-dry-run: ${FACTORY_PLUGIN_APPROVAL_REQUIRED_FOR_NON_DRY_RUN:true}
approval-separation-enabled: ${FACTORY_PLUGIN_APPROVAL_SEPARATION_ENABLED:true}
approval-request-ttl-minutes: ${FACTORY_PLUGIN_APPROVAL_REQUEST_TTL_MINUTES:1440}
approval-validity-minutes: ${FACTORY_PLUGIN_APPROVAL_VALIDITY_MINUTES:30}
approval-requester-role-keys: ${FACTORY_PLUGIN_APPROVAL_REQUESTER_ROLE_KEYS:}
approval-approver-role-keys: ${FACTORY_PLUGIN_APPROVAL_APPROVER_ROLE_KEYS:}
executor-routes:
database-migration-sql: ${FACTORY_PLUGIN_DATABASE_MIGRATION_SQL_EXECUTOR_CODE:}
permission-backend: ${FACTORY_PLUGIN_PERMISSION_BACKEND_EXECUTOR_CODE:}
menu-admin-frontend: ${FACTORY_PLUGIN_MENU_ADMIN_FRONTEND_EXECUTOR_CODE:}
outbox-polling-enabled: ${FACTORY_PLUGIN_OUTBOX_POLLING_ENABLED:true}
outbox-poll-interval-seconds: ${FACTORY_PLUGIN_OUTBOX_POLL_INTERVAL_SECONDS:5}
outbox-batch-size: ${FACTORY_PLUGIN_OUTBOX_BATCH_SIZE:5}
outbox-lease-seconds: ${FACTORY_PLUGIN_OUTBOX_LEASE_SECONDS:60}
sql-target:
enabled: ${FACTORY_PLUGIN_SQL_TARGET_ENABLED:false}
environment-code: ${FACTORY_PLUGIN_SQL_TARGET_ENVIRONMENT_CODE:}
jdbc-url: ${FACTORY_PLUGIN_SQL_TARGET_JDBC_URL:}
username: ${FACTORY_PLUGIN_SQL_TARGET_USERNAME:}
password: ${FACTORY_PLUGIN_SQL_TARGET_PASSWORD:}
driver-class-name: ${FACTORY_PLUGIN_SQL_TARGET_DRIVER_CLASS_NAME:com.mysql.cj.jdbc.Driver}
allowed-catalog: ${FACTORY_PLUGIN_SQL_TARGET_ALLOWED_CATALOG:}
receipt-lease-seconds: ${FACTORY_PLUGIN_SQL_TARGET_RECEIPT_LEASE_SECONDS:300}
# 开发环境配置
server:
# 服务器的HTTP端口默认为8080
@@ -64,13 +113,16 @@ spring:
basename: i18n/messages
profiles:
active: druid
mvc:
async:
request-timeout: ${SPRING_MVC_ASYNC_REQUEST_TIMEOUT:600000}
# 文件上传
servlet:
multipart:
# 单个文件大小
max-file-size: 10MB
max-file-size: ${SPRING_SERVLET_MULTIPART_MAX_FILE_SIZE:100MB}
# 设置总上传的文件大小
max-request-size: 20MB
max-request-size: ${SPRING_SERVLET_MULTIPART_MAX_REQUEST_SIZE:120MB}
# 服务模块
devtools:
restart:

View File

@@ -0,0 +1,117 @@
package com.ruoyi.web.config;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import java.util.Locale;
import java.util.Properties;
import org.junit.Test;
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
import org.springframework.core.io.ClassPathResource;
public class ApplicationYamlTimeoutTest
{
@Test
public void mvcAsyncRequestTimeoutCoversDeepSeekTimeout()
{
Properties properties = loadApplicationProperties();
String asyncTimeout = properties.getProperty("spring.mvc.async.request-timeout");
String deepSeekTimeout = properties.getProperty("deepseek.timeout");
assertNotNull("spring.mvc.async.request-timeout must be configured for streaming responses", asyncTimeout);
assertNotNull("deepseek.timeout must be configured", deepSeekTimeout);
assertTrue("spring.mvc.async.request-timeout should be at least deepseek.timeout",
toMillis(asyncTimeout) >= toMillis(deepSeekTimeout));
}
@Test
public void multipartUploadLimitSupportsTemplateImportZip()
{
Properties properties = loadApplicationProperties();
String maxFileSize = properties.getProperty("spring.servlet.multipart.max-file-size");
String maxRequestSize = properties.getProperty("spring.servlet.multipart.max-request-size");
assertNotNull("spring.servlet.multipart.max-file-size must be configured", maxFileSize);
assertNotNull("spring.servlet.multipart.max-request-size must be configured", maxRequestSize);
assertTrue("template import needs to accept project ZIP files up to 100MB",
toBytes(maxFileSize) >= 100L * 1024L * 1024L);
assertTrue("template import request limit must cover a 100MB ZIP plus form fields",
toBytes(maxRequestSize) >= 100L * 1024L * 1024L);
}
@Test
public void oneClickRunPreviewTimeoutAllowsSlowLocalStartup()
{
Properties properties = loadApplicationProperties();
String runPreviewTimeout = properties.getProperty("easycode.one-click.run-preview-timeout-ms");
String pollInterval = properties.getProperty("easycode.one-click.run-preview-poll-interval-ms");
assertNotNull("one-click run preview timeout must be configurable", runPreviewTimeout);
assertNotNull("one-click run preview polling interval must be configurable", pollInterval);
assertTrue("one-click run preview timeout should allow slow Maven and frontend startup",
toMillis(runPreviewTimeout) >= 300000L);
assertTrue("one-click run preview polling interval must be positive",
toMillis(pollInterval) > 0L);
}
private Properties loadApplicationProperties()
{
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
factory.setResources(new ClassPathResource("application.yml"));
return factory.getObject();
}
private long toMillis(String rawValue)
{
String value = defaultValue(rawValue).trim().toLowerCase(Locale.ROOT);
if (value.endsWith("ms"))
{
return Long.parseLong(value.substring(0, value.length() - 2));
}
if (value.endsWith("s"))
{
return Long.parseLong(value.substring(0, value.length() - 1)) * 1000L;
}
if (value.endsWith("m"))
{
return Long.parseLong(value.substring(0, value.length() - 1)) * 60L * 1000L;
}
if (value.endsWith("h"))
{
return Long.parseLong(value.substring(0, value.length() - 1)) * 60L * 60L * 1000L;
}
return Long.parseLong(value);
}
private long toBytes(String rawValue)
{
String value = defaultValue(rawValue).trim().toLowerCase(Locale.ROOT);
if (value.endsWith("kb"))
{
return Long.parseLong(value.substring(0, value.length() - 2)) * 1024L;
}
if (value.endsWith("mb"))
{
return Long.parseLong(value.substring(0, value.length() - 2)) * 1024L * 1024L;
}
if (value.endsWith("gb"))
{
return Long.parseLong(value.substring(0, value.length() - 2)) * 1024L * 1024L * 1024L;
}
return Long.parseLong(value);
}
private String defaultValue(String rawValue)
{
String value = rawValue == null ? "" : rawValue.trim();
if (value.startsWith("${") && value.endsWith("}"))
{
int colon = value.indexOf(':');
if (colon >= 0)
{
return value.substring(colon + 1, value.length() - 1);
}
}
return value;
}
}

View File

@@ -0,0 +1,282 @@
package com.ruoyi.web.controller.front;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Collections;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.generator.factory.dsl.ProjectSpecQueryService;
import com.ruoyi.generator.factory.dsl.model.ProjectSpecDocument;
import com.ruoyi.generator.factory.dsl.model.ProjectSpecV1;
import com.ruoyi.generator.factory.diff.ProjectSpecVersionDiffService;
import com.ruoyi.generator.factory.diff.model.ProjectSpecDiffResult;
import com.ruoyi.generator.factory.generation.ProjectSpecGenerationService;
import com.ruoyi.generator.factory.generation.ProjectSpecGenerationRunService;
import com.ruoyi.generator.factory.generation.model.ProjectSpecGenerationResult;
import com.ruoyi.generator.factory.generation.model.ProjectSpecGenerationRunDiffResult;
import com.ruoyi.generator.factory.patch.ProjectSpecPatchService;
import com.ruoyi.generator.factory.patch.model.ProjectSpecPatchRequest;
import com.ruoyi.generator.factory.patch.model.ProjectSpecPatchResult;
import com.ruoyi.generator.factory.validation.ProjectSpecValidationService;
import com.ruoyi.generator.factory.validation.model.ProjectSpecValidationResult;
import com.ruoyi.generator.factory.version.ProjectSpecVersionService;
import com.ruoyi.generator.factory.version.model.ProjectSpecRollbackRequest;
import com.ruoyi.generator.factory.version.model.ProjectSpecSnapshotRequest;
import com.ruoyi.generator.factory.version.model.ProjectSpecVersionView;
public class FrontFactoryProjectControllerTest
{
@Mock
private ProjectSpecQueryService projectSpecQueryService;
@Mock
private ProjectSpecVersionService projectSpecVersionService;
@Mock
private ProjectSpecValidationService projectSpecValidationService;
@Mock
private ProjectSpecPatchService projectSpecPatchService;
@Mock
private ProjectSpecVersionDiffService projectSpecVersionDiffService;
@Mock
private ProjectSpecGenerationService projectSpecGenerationService;
@Mock
private ProjectSpecGenerationRunService projectSpecGenerationRunService;
private FrontFactoryProjectController controller;
@Before
public void setUp() throws Exception
{
MockitoAnnotations.initMocks(this);
controller = new FrontFactoryProjectController();
Field field = FrontFactoryProjectController.class.getDeclaredField("projectSpecQueryService");
field.setAccessible(true);
field.set(controller, projectSpecQueryService);
field = FrontFactoryProjectController.class.getDeclaredField("projectSpecVersionService");
field.setAccessible(true);
field.set(controller, projectSpecVersionService);
field = FrontFactoryProjectController.class.getDeclaredField("projectSpecValidationService");
field.setAccessible(true);
field.set(controller, projectSpecValidationService);
field = FrontFactoryProjectController.class.getDeclaredField("projectSpecPatchService");
field.setAccessible(true);
field.set(controller, projectSpecPatchService);
field = FrontFactoryProjectController.class.getDeclaredField("projectSpecVersionDiffService");
field.setAccessible(true);
field.set(controller, projectSpecVersionDiffService);
field = FrontFactoryProjectController.class.getDeclaredField("projectSpecGenerationService");
field.setAccessible(true);
field.set(controller, projectSpecGenerationService);
field = FrontFactoryProjectController.class.getDeclaredField("projectSpecGenerationRunService");
field.setAccessible(true);
field.set(controller, projectSpecGenerationRunService);
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken(loginUser(), null));
}
@After
public void tearDown()
{
SecurityContextHolder.clearContext();
}
@Test
public void readsCurrentUsersProjectSpec()
{
ProjectSpecDocument document = new ProjectSpecDocument();
document.setContentHash("hash");
when(projectSpecQueryService.getCurrentSpec(7L, 20L)).thenReturn(document);
AjaxResult result = controller.getCurrentSpec(20L);
assertEquals(document, result.get(AjaxResult.DATA_TAG));
verify(projectSpecQueryService).getCurrentSpec(7L, 20L);
}
@Test
public void validatesSubmittedProjectSpecForCurrentUser()
{
ProjectSpecV1 spec = new ProjectSpecV1();
ProjectSpecValidationResult validation = new ProjectSpecValidationResult();
when(projectSpecValidationService.validate(7L, 20L, spec)).thenReturn(validation);
AjaxResult result = controller.validateSpec(20L, spec);
assertEquals(validation, result.get(AjaxResult.DATA_TAG));
verify(projectSpecValidationService).validate(7L, 20L, spec);
}
@Test
public void previewsProjectSpecPatchForCurrentUser()
{
ProjectSpecPatchRequest request = new ProjectSpecPatchRequest();
ProjectSpecPatchResult preview = new ProjectSpecPatchResult();
when(projectSpecPatchService.preview(7L, 20L, request)).thenReturn(preview);
AjaxResult result = controller.previewSpecPatch(20L, request);
assertEquals(preview, result.get(AjaxResult.DATA_TAG));
verify(projectSpecPatchService).preview(7L, 20L, request);
}
@Test
public void appliesProjectSpecPatchForCurrentUser()
{
ProjectSpecPatchRequest request = new ProjectSpecPatchRequest();
ProjectSpecPatchResult applied = new ProjectSpecPatchResult();
applied.setApplied(true);
when(projectSpecPatchService.apply(7L, 20L, request)).thenReturn(applied);
AjaxResult result = controller.applySpecPatch(20L, request);
assertEquals(applied, result.get(AjaxResult.DATA_TAG));
verify(projectSpecPatchService).apply(7L, 20L, request);
}
@Test
public void snapshotsCurrentUsersProjectSpec()
{
ProjectSpecSnapshotRequest request = new ProjectSpecSnapshotRequest();
request.setChangeSummary("Before generator migration");
ProjectSpecVersionView version = version(101L, 1);
when(projectSpecVersionService.snapshotCurrent(7L, 20L, "Before generator migration"))
.thenReturn(version);
AjaxResult result = controller.snapshotCurrentSpec(20L, request);
assertEquals(version, result.get(AjaxResult.DATA_TAG));
verify(projectSpecVersionService).snapshotCurrent(7L, 20L, "Before generator migration");
}
@Test
public void listsCurrentUsersProjectSpecVersions()
{
ProjectSpecVersionView first = version(102L, 2);
ProjectSpecVersionView second = version(101L, 1);
when(projectSpecVersionService.listVersions(7L, 20L)).thenReturn(Arrays.asList(first, second));
AjaxResult result = controller.listSpecVersions(20L);
assertEquals(Arrays.asList(first, second), result.get(AjaxResult.DATA_TAG));
verify(projectSpecVersionService).listVersions(7L, 20L);
}
@Test
public void readsCurrentUsersProjectSpecVersion()
{
ProjectSpecVersionView version = version(101L, 1);
when(projectSpecVersionService.getVersion(7L, 20L, 101L)).thenReturn(version);
AjaxResult result = controller.getSpecVersion(20L, 101L);
assertEquals(version, result.get(AjaxResult.DATA_TAG));
verify(projectSpecVersionService).getVersion(7L, 20L, 101L);
}
@Test
public void diffsCurrentUsersProjectSpecVersions()
{
ProjectSpecDiffResult diff = new ProjectSpecDiffResult();
when(projectSpecVersionDiffService.diff(7L, 20L, 101L, 102L)).thenReturn(diff);
AjaxResult result = controller.diffSpecVersions(20L, 101L, 102L);
assertEquals(diff, result.get(AjaxResult.DATA_TAG));
verify(projectSpecVersionDiffService).diff(7L, 20L, 101L, 102L);
}
@Test
public void generatesCurrentUsersProjectSpecVersion()
{
ProjectSpecGenerationResult generation = new ProjectSpecGenerationResult();
generation.setSpecVersionId(101L);
when(projectSpecGenerationService.generate(7L, 20L, 101L)).thenReturn(generation);
AjaxResult result = controller.generateSpecVersion(20L, 101L);
assertEquals(generation, result.get(AjaxResult.DATA_TAG));
verify(projectSpecGenerationService).generate(7L, 20L, 101L);
}
@Test
public void rollsBackCurrentUsersProjectSpecVersion()
{
ProjectSpecRollbackRequest request = new ProjectSpecRollbackRequest();
request.setChangeSummary("Restore stable design");
ProjectSpecVersionView version = version(103L, 6);
when(projectSpecVersionService.rollback(7L, 20L, 101L, "Restore stable design"))
.thenReturn(version);
AjaxResult result = controller.rollbackSpecVersion(20L, 101L, request);
assertEquals(version, result.get(AjaxResult.DATA_TAG));
verify(projectSpecVersionService).rollback(7L, 20L, 101L, "Restore stable design");
}
@Test
public void listsCurrentUsersPersistedGenerationRuns()
{
ProjectSpecGenerationResult run = new ProjectSpecGenerationResult();
run.setGenerationRunId(301L);
when(projectSpecGenerationRunService.list(7L, 20L))
.thenReturn(Collections.singletonList(run));
AjaxResult result = controller.listGenerationRuns(20L);
assertEquals(Collections.singletonList(run), result.get(AjaxResult.DATA_TAG));
verify(projectSpecGenerationRunService).list(7L, 20L);
}
@Test
public void readsCurrentUsersPersistedGenerationRun()
{
ProjectSpecGenerationResult run = new ProjectSpecGenerationResult();
run.setGenerationRunId(301L);
when(projectSpecGenerationRunService.get(7L, 20L, 301L)).thenReturn(run);
AjaxResult result = controller.getGenerationRun(20L, 301L);
assertEquals(run, result.get(AjaxResult.DATA_TAG));
verify(projectSpecGenerationRunService).get(7L, 20L, 301L);
}
@Test
public void diffsCurrentUsersPersistedGenerationRuns()
{
ProjectSpecGenerationRunDiffResult diff = new ProjectSpecGenerationRunDiffResult();
when(projectSpecGenerationRunService.diff(7L, 20L, 301L, 302L)).thenReturn(diff);
AjaxResult result = controller.diffGenerationRuns(20L, 301L, 302L);
assertEquals(diff, result.get(AjaxResult.DATA_TAG));
verify(projectSpecGenerationRunService).diff(7L, 20L, 301L, 302L);
}
private ProjectSpecVersionView version(Long id, int versionNo)
{
ProjectSpecVersionView version = new ProjectSpecVersionView();
version.setSpecVersionId(id);
version.setVersionNo(versionNo);
return version;
}
private LoginUser loginUser()
{
SysUser user = new SysUser();
user.setUserName("front");
user.setPassword("password");
LoginUser loginUser = new LoginUser(7L, 1L, user, Collections.<String>emptySet());
loginUser.setLoginType("front");
return loginUser;
}
}

View File

@@ -3,23 +3,55 @@ package com.ruoyi.web.controller.front;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.TemplateBundle;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.FrontProjectGeneration;
import com.ruoyi.generator.domain.front.dto.GenerateCodeAnalysisRequest;
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskOverview;
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskHistoryItem;
import com.ruoyi.generator.domain.front.dto.block.BusinessBlockDefinition;
import com.ruoyi.generator.service.ITemplateBundleService;
import com.ruoyi.generator.service.front.BusinessBlockRegistryService;
import com.ruoyi.generator.service.front.IAiGenerateService;
import com.ruoyi.generator.service.front.IAiGenerationTaskService;
import com.ruoyi.generator.service.front.IDeepSeekClient;
import com.ruoyi.generator.domain.front.dto.ProjectRunPreviewStatus;
import com.ruoyi.generator.domain.front.dto.ProjectDeliveryReadiness;
import com.ruoyi.generator.domain.front.dto.ProjectQualityRunSummary;
import com.ruoyi.generator.factory.quality.ProjectQualityRunService;
import com.ruoyi.generator.factory.artifact.CertifiedArtifactService;
import com.ruoyi.generator.factory.artifact.model.ProjectCertifiedArtifact;
import com.ruoyi.generator.factory.acceptance.FactoryV1AcceptanceService;
import com.ruoyi.generator.factory.acceptance.model.FactoryV1AcceptanceRun;
import com.ruoyi.generator.service.front.IFrontProjectPreviewService;
import com.ruoyi.generator.service.front.IFrontProjectService;
import com.ruoyi.generator.service.front.IFrontProjectRunPreviewService;
import com.ruoyi.generator.service.front.IProjectDeliveryReadinessService;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
import java.io.ByteArrayOutputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collections;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -33,6 +65,26 @@ public class FrontProjectControllerRunPreviewTest
private BusinessBlockRegistryService businessBlockRegistryService;
@Mock
private IFrontProjectService frontProjectService;
@Mock
private IFrontProjectPreviewService frontProjectPreviewService;
@Mock
private ITemplateBundleService templateBundleService;
@Mock
private IAiGenerationTaskService aiGenerationTaskService;
@Mock
private IAiGenerateService aiGenerateService;
@Mock
private IProjectDeliveryReadinessService projectDeliveryReadinessService;
@Mock
private ProjectQualityRunService projectQualityRunService;
@Mock
private CertifiedArtifactService certifiedArtifactService;
@Mock
private FactoryV1AcceptanceService factoryV1AcceptanceService;
@Mock
private HttpServletResponse httpServletResponse;
@Mock
private ServletOutputStream servletOutputStream;
@Before
public void setUp() throws Exception
@@ -42,15 +94,142 @@ public class FrontProjectControllerRunPreviewTest
setField("frontProjectRunPreviewService", frontProjectRunPreviewService);
setField("businessBlockRegistryService", businessBlockRegistryService);
setField("frontProjectService", frontProjectService);
setField("frontProjectPreviewService", frontProjectPreviewService);
setField("templateBundleService", templateBundleService);
setField("aiGenerationTaskService", aiGenerationTaskService);
setField("aiGenerateService", aiGenerateService);
setField("projectDeliveryReadinessService", projectDeliveryReadinessService);
setField("projectQualityRunService", projectQualityRunService);
setField("certifiedArtifactService", certifiedArtifactService);
setField("factoryV1AcceptanceService", factoryV1AcceptanceService);
SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(loginUser(), null));
}
@Test
public void deliveryReadinessDelegatesToCurrentFrontUser()
{
ProjectDeliveryReadiness readiness = new ProjectDeliveryReadiness();
readiness.setProjectId(20L);
readiness.setStatus(ProjectDeliveryReadiness.STATUS_SOURCE_READY);
when(projectDeliveryReadinessService.get(7L, 20L)).thenReturn(readiness);
AjaxResult result = controller.deliveryReadiness(20L);
assertEquals(readiness, result.get(AjaxResult.DATA_TAG));
verify(projectDeliveryReadinessService).get(7L, 20L);
}
@Test
public void certifiedDownloadChecksGateBeforeWritingArchive() throws Exception
{
FrontProject project = new FrontProject();
project.setProjectId(20L);
project.setProjectName("客户中心");
ProjectDeliveryReadiness readiness = new ProjectDeliveryReadiness();
readiness.setCertificationLevel(ProjectDeliveryReadiness.CERTIFICATION_BASELINE);
readiness.setCertifiedDownloadAllowed(true);
readiness.setGenerationRunId(41L);
ProjectQualityRunSummary qualityRun = new ProjectQualityRunSummary();
qualityRun.setQualityRunId(51L);
readiness.setQualityRun(qualityRun);
ProjectCertifiedArtifact artifact = new ProjectCertifiedArtifact();
artifact.setArtifactId(61L);
artifact.setQualityRunId(51L);
artifact.setProjectId(20L);
artifact.setUserId(7L);
artifact.setGenerationRunId(41L);
artifact.setStatus(ProjectCertifiedArtifact.STATUS_READY);
artifact.setZipSha256("zip-hash");
artifact.setReportSha256("report-hash");
when(projectDeliveryReadinessService.assertDownloadAllowed(7L, 20L, "CERTIFIED"))
.thenReturn(readiness);
when(frontProjectService.getProject(7L, 20L)).thenReturn(project);
when(certifiedArtifactService.getReady(7L, 20L, 51L)).thenReturn(artifact);
when(certifiedArtifactService.read(artifact)).thenReturn(new byte[] { 1, 2, 3 });
when(httpServletResponse.getOutputStream()).thenReturn(servletOutputStream);
controller.download(20L, null, "CERTIFIED", httpServletResponse);
verify(projectDeliveryReadinessService).assertDownloadAllowed(7L, 20L, "CERTIFIED");
verify(certifiedArtifactService).read(artifact);
verify(frontProjectPreviewService, never()).downloadAll(7L, 20L);
verify(httpServletResponse).addHeader("X-Factory-Artifact-Type", "CERTIFIED");
verify(httpServletResponse).addHeader("X-Factory-Certification-Level", "BASELINE");
verify(httpServletResponse).addHeader("X-Factory-Artifact-Id", "61");
verify(httpServletResponse).addHeader("X-Factory-Zip-SHA256", "zip-hash");
verify(httpServletResponse).addHeader("X-Factory-Report-SHA256", "report-hash");
}
@Test
public void qualityRunEndpointsUseCurrentFrontUser()
{
ProjectQualityRunSummary summary = new ProjectQualityRunSummary();
summary.setQualityRunId(301L);
summary.setStatus("QUEUED");
when(projectQualityRunService.createSummary(7L, 20L)).thenReturn(summary);
when(projectQualityRunService.listSummaries(7L, 20L))
.thenReturn(Collections.singletonList(summary));
when(projectQualityRunService.latestSummary(7L, 20L)).thenReturn(summary);
assertEquals(summary, controller.createQualityRun(20L).get(AjaxResult.DATA_TAG));
assertEquals(Collections.singletonList(summary),
controller.qualityRuns(20L).get(AjaxResult.DATA_TAG));
assertEquals(summary, controller.latestQualityRun(20L).get(AjaxResult.DATA_TAG));
verify(projectQualityRunService).createSummary(7L, 20L);
verify(projectQualityRunService).listSummaries(7L, 20L);
verify(projectQualityRunService).latestSummary(7L, 20L);
}
@Test
public void v1AcceptanceEndpointsUseCurrentFrontUser()
{
FactoryV1AcceptanceRun run = new FactoryV1AcceptanceRun();
run.setAcceptanceRunId(71L);
run.setStatus(FactoryV1AcceptanceRun.STATUS_PASSED);
when(factoryV1AcceptanceService.start(7L, 20L)).thenReturn(run);
when(factoryV1AcceptanceService.latest(7L, 20L)).thenReturn(run);
when(factoryV1AcceptanceService.list(7L, 20L)).thenReturn(Collections.singletonList(run));
assertEquals(run, controller.startV1Acceptance(20L).get(AjaxResult.DATA_TAG));
assertEquals(run, controller.latestV1Acceptance(20L).get(AjaxResult.DATA_TAG));
assertEquals(Collections.singletonList(run), controller.v1Acceptances(20L).get(AjaxResult.DATA_TAG));
}
@After
public void tearDown()
{
SecurityContextHolder.clearContext();
}
@Test
public void oneClickTaskOverviewDelegatesToCurrentFrontUser()
{
AiGenerationTaskOverview overview = new AiGenerationTaskOverview();
overview.setTaskId(88L);
when(aiGenerationTaskService.listOneClickTaskOverview(7L))
.thenReturn(Collections.singletonList(overview));
AjaxResult result = controller.oneClickTaskOverview();
assertEquals(Collections.singletonList(overview),
result.get(AjaxResult.DATA_TAG));
verify(aiGenerationTaskService).listOneClickTaskOverview(7L);
}
@Test
public void oneClickTaskHistoryDelegatesToCurrentFrontUser()
{
AiGenerationTaskHistoryItem item = new AiGenerationTaskHistoryItem();
item.setTaskId(99L);
when(aiGenerationTaskService.listOneClickTaskHistory(7L, 20L))
.thenReturn(Collections.singletonList(item));
AjaxResult result = controller.oneClickTaskHistory(20L);
assertEquals(Collections.singletonList(item), result.get(AjaxResult.DATA_TAG));
verify(aiGenerationTaskService).listOneClickTaskHistory(7L, 20L);
}
@Test
public void startRunPreviewDelegatesToCurrentFrontUser()
{
@@ -102,6 +281,145 @@ public class FrontProjectControllerRunPreviewTest
verify(businessBlockRegistryService).listDefinitions("admin");
}
@Test
public void codeTemplateOptionsReturnsEnabledBundles()
{
TemplateBundle qing = new TemplateBundle();
qing.setBundleCode("qing");
qing.setBundleName("Qing Default Template");
when(templateBundleService.listEnabledBundles()).thenReturn(Collections.singletonList(qing));
AjaxResult result = controller.codeTemplates();
assertEquals(Collections.singletonList(qing), result.get(AjaxResult.DATA_TAG));
verify(templateBundleService).listEnabledBundles();
}
@Test
public void projectTemplateTypesDelegatesToPreviewService()
{
when(frontProjectPreviewService.getSupportedTemplateTypes(7L, 20L))
.thenReturn(Arrays.asList("backend", "sql"));
AjaxResult result = controller.templateTypes(20L);
assertEquals(Arrays.asList("backend", "sql"), result.get(AjaxResult.DATA_TAG));
verify(frontProjectPreviewService).getSupportedTemplateTypes(7L, 20L);
}
@Test
public void previewGeneratesOnlySupportedTemplateTypes()
{
FrontProject project = new FrontProject();
project.setFrontendEnabled("1");
when(frontProjectService.getProject(7L, 20L)).thenReturn(project);
when(frontProjectPreviewService.getSupportedTemplateTypes(7L, 20L))
.thenReturn(Arrays.asList("backend", "sql"));
when(frontProjectPreviewService.markPreviewReady(7L, 20L)).thenReturn(1);
controller.preview(20L);
verify(frontProjectPreviewService).getSupportedTemplateTypes(7L, 20L);
verify(frontProjectPreviewService).getStructure(7L, 20L, "backend");
verify(frontProjectPreviewService).getStructure(7L, 20L, "sql");
verify(frontProjectPreviewService, never()).getStructure(7L, 20L, "frontend");
verify(frontProjectPreviewService, never()).getStructure(7L, 20L, "admin_frontend");
}
@Test
public void createCodeAnalysisRequiresSourcePreviewReady()
{
FrontProject project = new FrontProject();
project.setPreviewStatus("0");
when(frontProjectService.getProject(7L, 20L)).thenReturn(project);
try
{
controller.createCodeAnalysis(20L, null);
fail("Expected ServiceException");
}
catch (ServiceException e)
{
assertEquals("请先生成源码预览后再进行代码解读", e.getMessage());
}
verify(aiGenerationTaskService, never()).createTask(any(Long.class), any(Long.class), any());
}
@Test
public void continueCodeAnalysisRequiresSourcePreviewReady()
{
FrontProject project = new FrontProject();
project.setPreviewStatus("0");
when(frontProjectService.getProject(7L, 20L)).thenReturn(project);
try
{
controller.continueCodeAnalysis(20L, 99L, null);
fail("Expected ServiceException");
}
catch (ServiceException e)
{
assertEquals("请先生成源码预览后再进行代码解读", e.getMessage());
}
verify(aiGenerationTaskService, never()).getGeneration(any(Long.class), any(Long.class), any(Long.class));
verify(aiGenerationTaskService, never()).createTask(any(Long.class), any(Long.class), any());
}
@Test
public void streamCodeAnalysisWritesChunks() throws Exception
{
FrontProject project = new FrontProject();
project.setPreviewStatus("1");
when(frontProjectService.getProject(7L, 20L)).thenReturn(project);
doAnswer(invocation ->
{
IDeepSeekClient.StreamHandler handler = invocation.getArgument(3);
handler.onContent("# 报告\n\n");
handler.onContent("正文");
return null;
}).when(aiGenerateService).streamCodeAnalysis(eq(7L), eq(20L), any(GenerateCodeAnalysisRequest.class),
any(IDeepSeekClient.StreamHandler.class));
StreamingResponseBody body = controller.streamCodeAnalysis(20L, new GenerateCodeAnalysisRequest());
ByteArrayOutputStream output = new ByteArrayOutputStream();
body.writeTo(output);
assertEquals("# 报告\n\n正文", new String(output.toByteArray(), StandardCharsets.UTF_8));
verify(aiGenerateService).streamCodeAnalysis(eq(7L), eq(20L), any(GenerateCodeAnalysisRequest.class),
any(IDeepSeekClient.StreamHandler.class));
}
@Test
public void continueCodeAnalysisStreamUsesCurrentEditedMarkdown() throws Exception
{
FrontProject project = new FrontProject();
project.setPreviewStatus("1");
when(frontProjectService.getProject(7L, 20L)).thenReturn(project);
FrontProjectGeneration previous = new FrontProjectGeneration();
previous.setGenerateType("code_analysis");
previous.setResponsePayload("{\"markdown\":\"old\"}");
when(aiGenerationTaskService.getGeneration(7L, 20L, 99L)).thenReturn(previous);
doAnswer(invocation ->
{
IDeepSeekClient.StreamHandler handler = invocation.getArgument(3);
handler.onContent("续写");
return null;
}).when(aiGenerateService).streamCodeAnalysis(eq(7L), eq(20L), any(GenerateCodeAnalysisRequest.class),
any(IDeepSeekClient.StreamHandler.class));
GenerateCodeAnalysisRequest request = new GenerateCodeAnalysisRequest();
request.setPreviousMarkdown("edited");
StreamingResponseBody body = controller.continueCodeAnalysisStream(20L, 99L, request);
body.writeTo(new ByteArrayOutputStream());
ArgumentCaptor<GenerateCodeAnalysisRequest> requestCaptor = ArgumentCaptor.forClass(GenerateCodeAnalysisRequest.class);
verify(aiGenerateService).streamCodeAnalysis(eq(7L), eq(20L), requestCaptor.capture(),
any(IDeepSeekClient.StreamHandler.class));
assertEquals("continue", requestCaptor.getValue().getMode());
assertEquals(Long.valueOf(99L), requestCaptor.getValue().getPreviousGenerationId());
assertEquals("edited", requestCaptor.getValue().getPreviousMarkdown());
}
private ProjectRunPreviewStatus status(String value)
{
ProjectRunPreviewStatus status = new ProjectRunPreviewStatus();

View File

@@ -0,0 +1,27 @@
package com.ruoyi.web.exception;
import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;
import org.springframework.web.multipart.MaxUploadSizeExceededException;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.framework.web.exception.GlobalExceptionHandler;
public class GlobalExceptionHandlerTest
{
@Test
public void maxUploadSizeExceededReturnsReadableMessage()
{
GlobalExceptionHandler handler = new GlobalExceptionHandler();
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getRequestURI()).thenReturn("/generator/templateImport/analyze");
AjaxResult result = handler.handleMaxUploadSizeExceededException(
new MaxUploadSizeExceededException(100L * 1024L * 1024L), request);
assertEquals("上传文件大小不能超过 100MB", result.get(AjaxResult.MSG_TAG));
}
}

View File

@@ -6,11 +6,14 @@ import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.generator.domain.front.FrontUser;
import com.ruoyi.generator.domain.front.dto.FrontPasswordUpdateBody;
import com.ruoyi.generator.domain.front.dto.FrontProfileUpdateBody;
import com.ruoyi.generator.service.front.IFrontUserService;
import com.ruoyi.web.controller.front.FrontAuthController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@@ -18,6 +21,7 @@ import java.lang.reflect.Field;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.any;
@@ -196,6 +200,93 @@ public class FrontAuthServiceTest
assertNull(profile.getPassword());
}
@Test
public void updateProfilePersistsEditableFieldsAndReturnsPasswordFreeProfile()
{
FrontProfileUpdateBody body = new FrontProfileUpdateBody();
body.setNickname(" Demo User ");
body.setEmail(" demo@example.com ");
body.setPhone(" 13800138000 ");
FrontUser refreshed = new FrontUser();
refreshed.setUserId(100L);
refreshed.setUsername("demo");
refreshed.setNickname("Demo User");
refreshed.setEmail("demo@example.com");
refreshed.setPhone("13800138000");
refreshed.setPassword("encoded-password");
when(frontUserService.updateProfile(any(FrontUser.class))).thenReturn(1);
when(frontUserService.selectById(100L)).thenReturn(refreshed);
FrontUser profile = frontAuthService.updateProfile(100L, body);
ArgumentCaptor<FrontUser> captor = ArgumentCaptor.forClass(FrontUser.class);
verify(frontUserService).updateProfile(captor.capture());
FrontUser saved = captor.getValue();
assertEquals(Long.valueOf(100L), saved.getUserId());
assertEquals("Demo User", saved.getNickname());
assertEquals("demo@example.com", saved.getEmail());
assertEquals("13800138000", saved.getPhone());
assertNull(saved.getUsername());
assertNull(saved.getPassword());
assertEquals("Demo User", profile.getNickname());
assertNull(profile.getPassword());
}
@Test
public void updatePasswordRejectsWrongOldPassword()
{
FrontUser user = new FrontUser();
user.setUserId(100L);
user.setUsername("demo");
user.setPassword(SecurityUtils.encryptPassword("old123"));
FrontPasswordUpdateBody body = new FrontPasswordUpdateBody();
body.setOldPassword("wrong");
body.setNewPassword("new123");
body.setConfirmPassword("new123");
when(frontUserService.selectById(100L)).thenReturn(user);
try
{
frontAuthService.updatePassword(100L, body);
fail("Expected ServiceException");
}
catch (ServiceException e)
{
assertEquals("旧密码错误", e.getMessage());
}
verify(frontUserService, never()).updatePassword(any(FrontUser.class));
}
@Test
public void updatePasswordEncryptsAcceptedNewPassword()
{
FrontUser user = new FrontUser();
user.setUserId(100L);
user.setUsername("demo");
user.setPassword(SecurityUtils.encryptPassword("old123"));
FrontPasswordUpdateBody body = new FrontPasswordUpdateBody();
body.setOldPassword("old123");
body.setNewPassword("new123");
body.setConfirmPassword("new123");
when(frontUserService.selectById(100L)).thenReturn(user);
when(frontUserService.updatePassword(any(FrontUser.class))).thenReturn(1);
frontAuthService.updatePassword(100L, body);
ArgumentCaptor<FrontUser> captor = ArgumentCaptor.forClass(FrontUser.class);
verify(frontUserService).updatePassword(captor.capture());
FrontUser saved = captor.getValue();
assertEquals(Long.valueOf(100L), saved.getUserId());
assertTrue(SecurityUtils.matchesPassword("new123", saved.getPassword()));
}
private void setField(Object target, String name, Object value) throws Exception
{
Field field = target.getClass().getDeclaredField(name);