feat: 完善 Prompt 管理与生成流程配置

This commit is contained in:
王鹏
2026-07-15 17:31:13 +08:00
parent 84d3c8fc05
commit de98ea59b1
93 changed files with 3474 additions and 980 deletions

View File

@@ -1,19 +0,0 @@
package com.ruoyi.generator.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "factory.prompt-registry")
public class PromptRegistryProperties
{
private Boolean databaseEnabled = false;
private Boolean builtinFallbackEnabled = false;
public Boolean getDatabaseEnabled() { return databaseEnabled; }
public void setDatabaseEnabled(Boolean databaseEnabled) { this.databaseEnabled = databaseEnabled; }
public boolean isDatabaseEnabled() { return Boolean.TRUE.equals(databaseEnabled); }
public Boolean getBuiltinFallbackEnabled() { return builtinFallbackEnabled; }
public void setBuiltinFallbackEnabled(Boolean builtinFallbackEnabled) { this.builtinFallbackEnabled = builtinFallbackEnabled; }
public boolean isBuiltinFallbackEnabled() { return Boolean.TRUE.equals(builtinFallbackEnabled); }
}

View File

@@ -91,12 +91,4 @@ public class PromptTemplateController extends BaseController
promptVersionId, getUsername()));
}
@PreAuthorize("@ss.hasPermi('generator:prompt:version')")
@Log(title = "Prompt V1 初始化", businessType = BusinessType.INSERT)
@PostMapping("/initialize-v1")
public AjaxResult initializeV1()
{
int count = promptTemplateService.initializeBuiltinV1(getUsername());
return AjaxResult.success("已初始化或修复 " + count + " 个 Prompt V1", count);
}
}

View File

@@ -1,169 +0,0 @@
package com.ruoyi.generator.factory.ai.prompt;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StreamUtils;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
@Service
public class BuiltinPromptRegistry implements PromptRegistry
{
public static final String DEFAULT_SYSTEM_PROMPT = load("default", "system");
public static final String REQUIREMENT_SYSTEM_PROMPT = load("requirement", "system");
public static final String REQUIREMENT_USER_PROMPT_TEMPLATE = load("requirement", "user");
public static final String APP_BLUEPRINT_SYSTEM_PROMPT = load("app_blueprint", "system");
public static final String APP_BLUEPRINT_USER_PROMPT_TEMPLATE = load("app_blueprint", "user");
public static final String FLOW_CONFIG_SYSTEM_PROMPT = load("flow_config", "system");
public static final String FLOW_CONFIG_USER_PROMPT_TEMPLATE = load("flow_config", "user");
public static final String DATABASE_SYSTEM_PROMPT = load("database", "system");
public static final String DATABASE_USER_PROMPT_TEMPLATE = load("database", "user");
public static final String DIAGRAM_SYSTEM_PROMPT = load("diagram_spec", "system");
public static final String DIAGRAM_USER_PROMPT_TEMPLATE = load("diagram_spec", "user");
public static final String PAPER_OUTLINE_SYSTEM_PROMPT = load("paper_outline", "system");
public static final String PAPER_OUTLINE_USER_PROMPT_TEMPLATE = load("paper_outline", "user");
public static final String PAPER_DRAFT_SYSTEM_PROMPT = load("paper_draft", "system");
public static final String PAPER_DRAFT_USER_PROMPT_TEMPLATE = load("paper_draft", "user");
public static final String CODE_ANALYSIS_SYSTEM_PROMPT = load("code_analysis", "system");
public static final String CODE_ANALYSIS_USER_PROMPT_TEMPLATE = load("code_analysis", "user");
public static final String DATABASE_CHANGE_SYNC_SYSTEM_PROMPT = load("database_change_sync", "system");
public static final String DATABASE_CHANGE_SYNC_USER_PROMPT_TEMPLATE = load("database_change_sync", "user");
public static final String ONE_CLICK_PROJECT_SYSTEM_PROMPT = load("one_click_project", "system");
public static final String ONE_CLICK_PROJECT_USER_PROMPT_TEMPLATE = load("one_click_project", "user");
public static final String VERSION = "1.0.0";
public static final String PIPELINE_RELEASE_CODE = "builtin-one-click-1.0.0";
private final Map<String, PromptRelease> byGenerateType;
private final Map<String, PromptRelease> byRelease;
@Autowired
public BuiltinPromptRegistry(PromptFingerprintService fingerprintService)
{
Map<String, PromptRelease> types = new LinkedHashMap<String, PromptRelease>();
register(types, release(fingerprintService, "requirement", REQUIREMENT_SYSTEM_PROMPT,
"context-json:requirement:v1", REQUIREMENT_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "app_blueprint", APP_BLUEPRINT_SYSTEM_PROMPT,
"context-json:app_blueprint:v1", APP_BLUEPRINT_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "flow_config", FLOW_CONFIG_SYSTEM_PROMPT,
"context-json:flow_config:v1", FLOW_CONFIG_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "database", DATABASE_SYSTEM_PROMPT,
"context-json:database:v1", DATABASE_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "diagram_spec", DIAGRAM_SYSTEM_PROMPT,
"context-json:diagram_spec:v1", DIAGRAM_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "paper_outline", PAPER_OUTLINE_SYSTEM_PROMPT,
"context-json:paper_outline:v1", PAPER_OUTLINE_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "paper_draft", PAPER_DRAFT_SYSTEM_PROMPT,
"context-json:paper_draft:v1", PAPER_DRAFT_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "code_analysis", CODE_ANALYSIS_SYSTEM_PROMPT,
"context-json:code_analysis:v1", CODE_ANALYSIS_USER_PROMPT_TEMPLATE));
register(types, release(fingerprintService, "database_change_sync", DATABASE_CHANGE_SYNC_SYSTEM_PROMPT,
"context-json:database_change_sync:v1", DATABASE_CHANGE_SYNC_USER_PROMPT_TEMPLATE));
register(types, pipelineRelease(fingerprintService));
byGenerateType = Collections.unmodifiableMap(types);
Map<String, PromptRelease> releases = new LinkedHashMap<String, PromptRelease>();
for (PromptRelease item : types.values())
{
releases.put(key(item.getPromptCode(), item.getVersion()), item);
}
byRelease = Collections.unmodifiableMap(releases);
}
@Override
public PromptRelease resolve(String generateType)
{
PromptRelease release = byGenerateType.get(StringUtils.defaultString(generateType));
if (release == null)
{
throw new ServiceException("No published Prompt release for generation type: "
+ StringUtils.defaultString(generateType));
}
return release;
}
@Override
public PromptRelease resolve(String promptCode, String version)
{
PromptRelease release = byRelease.get(key(promptCode, version));
if (release == null)
{
throw new ServiceException("Prompt release does not exist: "
+ StringUtils.defaultString(promptCode) + "@" + StringUtils.defaultString(version));
}
return release;
}
public String managementSystemPrompt(String generateType)
{
return resolve(generateType).getSystemPrompt();
}
public String managementUserPromptTemplate(String generateType)
{
return resolve(generateType).getUserPromptTemplate();
}
private PromptRelease pipelineRelease(PromptFingerprintService fingerprintService)
{
return release(fingerprintService, "one_click_project", ONE_CLICK_PROJECT_SYSTEM_PROMPT,
"pipeline:app_blueprint,flow_config,database,generate,preview:v2",
ONE_CLICK_PROJECT_USER_PROMPT_TEMPLATE);
}
private PromptRelease release(PromptFingerprintService fingerprintService,
String generateType, String systemPrompt, String userPromptContract,
String userPromptTemplate)
{
PromptRelease release = new PromptRelease();
release.setGenerateType(generateType);
release.setPromptCode("front." + generateType);
release.setVersion(VERSION);
release.setSystemPrompt(systemPrompt);
release.setUserPromptContract(userPromptContract);
release.setUserPromptTemplate(userPromptTemplate);
release.setProviderCode("deepseek");
release.setPipelineReleaseCode(PIPELINE_RELEASE_CODE);
release.setFingerprint(fingerprintService.fingerprint(release.getGenerateType(),
release.getPromptCode(), release.getVersion(), release.getSystemPrompt(),
release.getUserPromptContract(), release.getUserPromptTemplate()));
return release;
}
private void register(Map<String, PromptRelease> releases, PromptRelease release)
{
if (releases.put(release.getGenerateType(), release) != null)
{
throw new ServiceException("Duplicate built-in Prompt generation type: " + release.getGenerateType());
}
}
private String key(String promptCode, String version)
{
return StringUtils.defaultString(promptCode) + "@" + StringUtils.defaultString(version);
}
private static String load(String generateType, String role)
{
String path = "/factory-prompts/" + generateType + "/" + role + ".txt";
try (InputStream input = BuiltinPromptRegistry.class.getResourceAsStream(path))
{
if (input == null)
{
throw new ServiceException("Built-in Prompt resource does not exist: " + path);
}
return StreamUtils.copyToString(input, StandardCharsets.UTF_8).trim();
}
catch (IOException e)
{
throw new ServiceException("Unable to read built-in Prompt resource: " + path)
.setDetailMessage(e.getMessage());
}
}
}

View File

@@ -5,7 +5,6 @@ import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.config.PromptRegistryProperties;
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
@@ -14,33 +13,23 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
public class DatabasePromptRegistry implements PromptRegistry
{
private final PromptRegistryMapper mapper;
private final BuiltinPromptRegistry builtinRegistry;
private final PromptFingerprintService fingerprintService;
private final PromptRegistryProperties properties;
@Autowired
public DatabasePromptRegistry(PromptRegistryMapper mapper,
BuiltinPromptRegistry builtinRegistry,
PromptFingerprintService fingerprintService,
PromptRegistryProperties properties)
PromptFingerprintService fingerprintService)
{
this.mapper = mapper;
this.builtinRegistry = builtinRegistry;
this.fingerprintService = fingerprintService;
this.properties = properties;
}
@Override
public PromptRelease resolve(String generateType)
{
if (!properties.isDatabaseEnabled())
{
return builtinRegistry.resolve(generateType);
}
PromptVersion version = mapper.selectPublishedByGenerateType(generateType);
if (version == null)
{
return missingDatabaseRelease(generateType, null);
throw missingDatabaseRelease(generateType, null);
}
return toRelease(version);
}
@@ -48,27 +37,16 @@ public class DatabasePromptRegistry implements PromptRegistry
@Override
public PromptRelease resolve(String promptCode, String version)
{
if (!properties.isDatabaseEnabled())
{
return builtinRegistry.resolve(promptCode, version);
}
PromptVersion stored = mapper.selectExecutableByCodeAndVersion(promptCode, version);
if (stored == null)
{
return missingDatabaseRelease(promptCode, version);
throw missingDatabaseRelease(promptCode, version);
}
return toRelease(stored);
}
private PromptRelease toRelease(PromptVersion version)
{
PromptRelease supported = builtinRegistry.resolve(version.getGenerateType());
if (!StringUtils.equals(supported.getUserPromptContract(),
version.getUserPromptContract()))
{
throw new ServiceException("Stored User Prompt contract is not implemented: "
+ version.getUserPromptContract());
}
String actualHash = fingerprintService.fingerprint(version.getGenerateType(),
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
version.getUserPromptContract(), version.getUserPromptTemplate());
@@ -91,15 +69,10 @@ public class DatabasePromptRegistry implements PromptRegistry
return release;
}
private PromptRelease missingDatabaseRelease(String codeOrType, String version)
private ServiceException missingDatabaseRelease(String codeOrType, String version)
{
if (properties.isBuiltinFallbackEnabled())
{
return version == null ? builtinRegistry.resolve(codeOrType)
: builtinRegistry.resolve(codeOrType, version);
}
String identity = version == null ? StringUtils.defaultString(codeOrType)
: StringUtils.defaultString(codeOrType) + "@" + StringUtils.defaultString(version);
throw new ServiceException("Database Prompt release does not exist: " + identity);
return new ServiceException("Published managed Prompt release does not exist: " + identity);
}
}

View File

@@ -2,13 +2,11 @@ package com.ruoyi.generator.factory.ai.prompt.model;
import java.util.ArrayList;
import java.util.List;
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
public class PromptTemplateDetail
{
private PromptTemplate template;
private List<PromptVersion> versions = new ArrayList<PromptVersion>();
private PromptRelease builtinRelease;
public PromptTemplate getTemplate() { return template; }
public void setTemplate(PromptTemplate template) { this.template = template; }
@@ -17,6 +15,4 @@ public class PromptTemplateDetail
{
this.versions = versions == null ? new ArrayList<PromptVersion>() : versions;
}
public PromptRelease getBuiltinRelease() { return builtinRelease; }
public void setBuiltinRelease(PromptRelease builtinRelease) { this.builtinRelease = builtinRelease; }
}

View File

@@ -9,6 +9,7 @@ import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateFlowConfigRequest;
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationResult;
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
import com.ruoyi.generator.service.front.FlowConfigPageBindingReconciler;
import com.ruoyi.generator.service.front.IAiGenerateService;
/** One visible design step: application blueprint plus the small flow_config. */
@@ -16,11 +17,14 @@ import com.ruoyi.generator.service.front.IAiGenerateService;
public class DesignStageHandler implements OneClickGenerationStageHandler
{
private final IAiGenerateService aiGenerateService;
private final FlowConfigPageBindingReconciler pageBindingReconciler;
@Autowired
public DesignStageHandler(IAiGenerateService aiGenerateService)
public DesignStageHandler(IAiGenerateService aiGenerateService,
FlowConfigPageBindingReconciler pageBindingReconciler)
{
this.aiGenerateService = aiGenerateService;
this.pageBindingReconciler = pageBindingReconciler;
}
public String stageCode() { return OneClickProjectGenerationResult.STAGE_DESIGN; }
@@ -49,6 +53,8 @@ public class DesignStageHandler implements OneClickGenerationStageHandler
flowRequest.setAppBlueprint(context.getAppBlueprintJson());
SimpleFlowConfig flowConfig = aiGenerateService.generateFlowConfig(context.getTask().getUserId(),
context.getTask().getProjectId(), flowRequest);
pageBindingReconciler.reconcile(flowConfig, app);
context.setAppBlueprintJson(JSON.toJSONString(app));
context.setFlowConfig(flowConfig);
}
}

View File

@@ -39,11 +39,51 @@ public class FlowConfigSanitizer
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
{
repairMetricStatuses(flow);
repairSelfUpdateRelationFields(flow);
repairEffectValues(flow);
}
return config;
}
/**
* An update_related effect aimed at its owner row still needs the owner's
* primary-key field so the renderer can reload that row. AI responses often
* omit it because no cross-table relationship is involved. Infer it only
* from the action's request fields and only when target and owner are the
* same table; cross-table relationships must remain explicit.
*/
private void repairSelfUpdateRelationFields(SimpleFlowConfig config)
{
for (SimpleFlowConfig.Action action : config.getActions())
{
if (action == null) continue;
String ownerTable = StringUtils.defaultIfBlank(action.getOwnerTable(), config.getMainTable());
for (SimpleFlowConfig.Effect effect : action.getEffects())
{
if (effect == null || !"update_related".equals(effect.getType())
|| StringUtils.isNotBlank(effect.getRelationField())
|| !StringUtils.equals(ownerTable, effect.getTable()))
{
continue;
}
String relationField = ownerRequestId(action);
if (StringUtils.isNotBlank(relationField)) effect.setRelationField(relationField);
}
}
}
private String ownerRequestId(SimpleFlowConfig.Action action)
{
List<String> requestFields = action.getRequestFields();
if (requestFields == null || requestFields.isEmpty()) return null;
for (String requestField : requestFields)
{
if ("id".equals(requestField)) return requestField;
}
if (requestFields.size() != 1) return null;
return StringUtils.trim(requestFields.get(0));
}
private void repairEffectValues(SimpleFlowConfig config)
{
for (SimpleFlowConfig.Action action : config.getActions())

View File

@@ -32,8 +32,6 @@ public interface PromptRegistryMapper
int insertPromptVersion(PromptVersion version);
int updateInitializedV1(PromptVersion version);
int retirePublishedVersions(@Param("promptTemplateId") Long promptTemplateId,
@Param("excludeVersionId") Long excludeVersionId);

View File

@@ -20,6 +20,4 @@ public interface IPromptTemplateService
int publishPromptVersion(Long promptTemplateId, Long promptVersionId, String operator);
int rollbackPromptVersion(Long promptTemplateId, Long promptVersionId, String operator);
int initializeBuiltinV1(String operator);
}

View File

@@ -10,8 +10,6 @@ import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
import com.ruoyi.generator.factory.ai.prompt.PromptTemplateRenderer;
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplate;
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplateDetail;
@@ -22,23 +20,18 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
public class PromptTemplateServiceImpl implements IPromptTemplateService
{
private static final Pattern PROMPT_CODE = Pattern.compile("^[a-z0-9._-]+$");
private static final String INITIAL_V1_REMARK = "由当前代码内置 Prompt 初始化";
private static final String BUSINESS_V1_REMARK = "由当前代码内置业务 Prompt 初始化";
private final PromptRegistryMapper mapper;
private final PromptFingerprintService fingerprintService;
private final ModelGatewayRouter gatewayRouter;
private final BuiltinPromptRegistry builtinPromptRegistry;
@Autowired
public PromptTemplateServiceImpl(PromptRegistryMapper mapper,
PromptFingerprintService fingerprintService, ModelGatewayRouter gatewayRouter,
BuiltinPromptRegistry builtinPromptRegistry)
PromptFingerprintService fingerprintService, ModelGatewayRouter gatewayRouter)
{
this.mapper = mapper;
this.fingerprintService = fingerprintService;
this.gatewayRouter = gatewayRouter;
this.builtinPromptRegistry = builtinPromptRegistry;
}
@Override
@@ -55,7 +48,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
PromptTemplateDetail detail = new PromptTemplateDetail();
detail.setTemplate(template);
detail.setVersions(versions == null ? Collections.<PromptVersion>emptyList() : versions);
detail.setBuiltinRelease(builtinPromptRegistry.resolve(template.getGenerateType()));
return detail;
}
@@ -64,7 +56,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
public int insertPromptTemplate(PromptTemplate template)
{
normalizeTemplate(template, true);
builtinPromptRegistry.resolve(template.getGenerateType());
if (mapper.selectPromptTemplateByCode(template.getPromptCode()) != null)
{
throw new ServiceException("Prompt code already exists: " + template.getPromptCode());
@@ -148,96 +139,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
return changePublication(promptTemplateId, promptVersionId, operator, true);
}
@Override
@Transactional
public int initializeBuiltinV1(String operator)
{
List<PromptTemplate> templates = mapper.selectPromptTemplateList(new PromptTemplate());
if (templates == null || templates.isEmpty())
{
return 0;
}
int initialized = 0;
String initializedBy = StringUtils.defaultIfEmpty(operator, "system");
for (PromptTemplate item : templates)
{
PromptTemplate template = requireLockedTemplate(item.getPromptTemplateId());
int maxVersion = safe(mapper.selectMaxVersionNo(template.getPromptTemplateId()));
if (maxVersion > 0)
{
List<PromptVersion> versions = mapper.selectPromptVersions(
template.getPromptTemplateId());
if (isRepairableInitializedV1(versions))
{
PromptVersion version = versions.get(0);
applyBuiltinV1(template, version, initializedBy);
version.setRemark(BUSINESS_V1_REMARK);
if (mapper.updateInitializedV1(version) != 1)
{
throw new ServiceException("Unable to repair initialized Prompt V1: "
+ template.getPromptCode());
}
initialized++;
}
continue;
}
PromptVersion version = new PromptVersion();
version.setPromptTemplateId(template.getPromptTemplateId());
version.setVersionNo(1);
version.setVersion("V1");
version.setStatus(PromptVersion.STATUS_DRAFT);
applyBuiltinV1(template, version, initializedBy);
version.setRemark(BUSINESS_V1_REMARK);
if (mapper.insertPromptVersion(version) != 1 || version.getPromptVersionId() == null)
{
throw new ServiceException("Unable to initialize Prompt V1: "
+ template.getPromptCode());
}
if (mapper.publishPromptVersion(version.getPromptVersionId(), initializedBy) != 1
|| mapper.updateCurrentVersion(template.getPromptTemplateId(),
version.getPromptVersionId(), initializedBy) != 1)
{
throw new ServiceException("Unable to publish initialized Prompt V1: "
+ template.getPromptCode());
}
initialized++;
}
return initialized;
}
private void applyBuiltinV1(PromptTemplate template, PromptVersion version,
String initializedBy)
{
PromptRelease builtin = builtinPromptRegistry.resolve(template.getGenerateType());
gatewayRouter.resolve(builtin.getProviderCode());
version.setSystemPrompt(builtinPromptRegistry.managementSystemPrompt(
template.getGenerateType()));
version.setUserPromptContract(builtin.getUserPromptContract());
version.setUserPromptTemplate(builtinPromptRegistry.managementUserPromptTemplate(
template.getGenerateType()));
version.setProviderCode(builtin.getProviderCode());
version.setModel(StringUtils.defaultString(builtin.getModel()));
version.setPipelineReleaseCode(builtin.getPipelineReleaseCode());
version.setCreateBy(initializedBy);
version.setContentHash(fingerprintService.fingerprint(template.getGenerateType(),
template.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
version.getUserPromptContract(), version.getUserPromptTemplate()));
}
private boolean isRepairableInitializedV1(List<PromptVersion> versions)
{
if (versions == null || versions.size() != 1)
{
return false;
}
PromptVersion version = versions.get(0);
return Integer.valueOf(1).equals(version.getVersionNo())
&& "V1".equals(version.getVersion())
&& StringUtils.defaultString(version.getUserPromptContract()).startsWith("legacy-builder:")
&& (INITIAL_V1_REMARK.equals(version.getRemark())
|| BUSINESS_V1_REMARK.equals(version.getRemark()));
}
private int changePublication(Long promptTemplateId, Long promptVersionId,
String operator, boolean rollback)
{
@@ -257,13 +158,6 @@ public class PromptTemplateServiceImpl implements IPromptTemplateService
throw new ServiceException("Only a draft Prompt version can be published");
}
verifyContentHash(template, version);
PromptRelease supportedPrompt = builtinPromptRegistry.resolve(template.getGenerateType());
if (!StringUtils.equals(supportedPrompt.getUserPromptContract(),
version.getUserPromptContract()))
{
throw new ServiceException("User Prompt contract is not implemented by this application: "
+ version.getUserPromptContract());
}
gatewayRouter.resolve(version.getProviderCode());
mapper.retirePublishedVersions(promptTemplateId, promptVersionId);
if (mapper.publishPromptVersion(promptVersionId, StringUtils.defaultString(operator)) != 1)

View File

@@ -186,10 +186,11 @@ public class AiGenerateServiceImpl implements IAiGenerateService
String aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
modelAttempts.add(aiContent);
SimpleFlowConfig config;
AppBlueprintDesign reconciledBlueprint;
try
{
config = parseFlowConfigResponse(aiContent);
validateGeneratedFlowConfig(project, request, config);
reconciledBlueprint = validateGeneratedFlowConfig(project, request, config);
}
catch (RuntimeException firstFailure)
{
@@ -197,9 +198,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
aiContent = chatWithUsage(userId, projectId, "flow_config", prompt);
modelAttempts.add(aiContent);
config = parseFlowConfigResponse(aiContent);
validateGeneratedFlowConfig(project, request, config);
reconciledBlueprint = validateGeneratedFlowConfig(project, request, config);
}
markFlowConfigGenerated(projectId, project.getDraftRevision(), config, request);
markFlowConfigGenerated(projectId, project.getDraftRevision(), config, reconciledBlueprint, request);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
@@ -253,7 +254,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
flowConfig, flowCompilation, true);
}
persistDatabase(userId, projectId, response);
markDatabaseGenerated(projectId, project.getDraftRevision(), request, flowConfig);
markDatabaseGenerated(projectId, project.getDraftRevision(), request, appBlueprint, flowConfig);
generation.setResponsePayload(aiContent);
generation.setSuccess("1");
generation.setElapsedMs(System.currentTimeMillis() - start);
@@ -920,7 +921,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
return flowConfigSanitizer().sanitize(config);
}
private void validateGeneratedFlowConfig(FrontProject project, GenerateFlowConfigRequest request,
private AppBlueprintDesign validateGeneratedFlowConfig(FrontProject project, GenerateFlowConfigRequest request,
SimpleFlowConfig config)
{
String blueprintJson = firstNonBlank(request == null ? null : request.getAppBlueprint(),
@@ -934,6 +935,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
flowConfigContractService().validateDatabase(loadSavedDatabaseDesign(project.getProjectId()),
config, compilation);
}
return blueprint;
}
private FlowConfigSanitizer flowConfigSanitizer()
@@ -946,8 +948,9 @@ public class AiGenerateServiceImpl implements IAiGenerateService
String appBlueprintJson = resolveConfirmedAppBlueprint(project, request);
SimpleFlowConfig flowConfig = readFlowConfig(project,
request == null ? null : request.getFlowConfig());
FlowConfigCompilation compilation = flowConfigContractService().canonicalize(flowConfig,
parseConfirmedAppBlueprint(appBlueprintJson));
AppBlueprintDesign appBlueprint = parseConfirmedAppBlueprint(appBlueprintJson);
FlowConfigCompilation compilation = flowConfigContractService().canonicalize(flowConfig, appBlueprint);
appBlueprintJson = JSON.toJSONString(appBlueprint);
String flowConfigJson = flowConfig == null ? "" : JSON.toJSONString(flowConfig);
String businessLoopPlanJson = JSON.toJSONString(compilation.getLoopPlan());
Map<String, Object> context = new LinkedHashMap<String, Object>();
@@ -4140,7 +4143,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
}
private void markDatabaseGenerated(Long projectId, Long draftRevision, GenerateDatabaseRequest request,
SimpleFlowConfig flowConfig)
AppBlueprintDesign blueprint, SimpleFlowConfig flowConfig)
{
FrontProject project = new FrontProject();
project.setProjectId(projectId);
@@ -4150,6 +4153,7 @@ public class AiGenerateServiceImpl implements IAiGenerateService
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
project.setProjectDesc(request == null ? null : request.getProjectDesc());
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());
if (blueprint != null) project.setAppBlueprint(JSON.toJSONString(blueprint));
if (flowConfig != null) project.setFlowConfig(JSON.toJSONString(flowConfig));
applyStylePreset(project, request == null ? null : request.getStylePreset());
if (frontProjectMapper.updateFrontProjectIfRevision(project) == 0)
@@ -4160,13 +4164,14 @@ public class AiGenerateServiceImpl implements IAiGenerateService
}
private void markFlowConfigGenerated(Long projectId, Long draftRevision, SimpleFlowConfig config,
GenerateFlowConfigRequest request)
AppBlueprintDesign blueprint, GenerateFlowConfigRequest request)
{
FrontProject project = new FrontProject();
project.setProjectId(projectId);
project.setDraftRevision(draftRevision);
project.setPreviewStatus("0");
project.setFlowConfig(JSON.toJSONString(config));
project.setAppBlueprint(JSON.toJSONString(blueprint));
project.setRequirementKeyword(request == null ? null : request.getExtraRequirements());
project.setProjectDesc(request == null ? null : request.getProjectDesc());
project.setIndustryTemplate(request == null ? null : request.getIndustryTemplate());

View File

@@ -24,22 +24,34 @@ public class FlowConfigContractService
private final FlowConfigCompiler flowCompiler;
private final FlowConfigDatabaseContractValidator databaseValidator;
private final BusinessLoopCoverageValidator coverageValidator;
private final FlowConfigPageBindingReconciler pageBindingReconciler;
public FlowConfigContractService()
{
this(new FlowConfigValidator(), new FlowConfigCompiler(),
new FlowConfigDatabaseContractValidator(), new BusinessLoopCoverageValidator());
new FlowConfigDatabaseContractValidator(), new BusinessLoopCoverageValidator(),
new FlowConfigPageBindingReconciler());
}
public FlowConfigContractService(FlowConfigValidator flowValidator, FlowConfigCompiler flowCompiler,
FlowConfigDatabaseContractValidator databaseValidator,
BusinessLoopCoverageValidator coverageValidator)
{
this(flowValidator, flowCompiler, databaseValidator, coverageValidator,
new FlowConfigPageBindingReconciler());
}
@Autowired
public FlowConfigContractService(FlowConfigValidator flowValidator, FlowConfigCompiler flowCompiler,
FlowConfigDatabaseContractValidator databaseValidator,
BusinessLoopCoverageValidator coverageValidator)
BusinessLoopCoverageValidator coverageValidator,
FlowConfigPageBindingReconciler pageBindingReconciler)
{
this.flowValidator = flowValidator;
this.flowCompiler = flowCompiler;
this.databaseValidator = databaseValidator;
this.coverageValidator = coverageValidator;
this.pageBindingReconciler = pageBindingReconciler;
}
public FlowConfigCompilation compile(SimpleFlowConfig config)
@@ -58,8 +70,9 @@ public class FlowConfigContractService
first.getLoopPlan(), blueprint);
if (aliases.isEmpty())
{
boolean bindingChanged = pageBindingReconciler.reconcile(config, blueprint);
validatePageBindings(config, blueprint);
return first;
return bindingChanged ? compile(config) : first;
}
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
@@ -83,6 +96,7 @@ public class FlowConfigContractService
if (metric != null) metric.setTable(rename(metric.getTable(), aliases));
}
}
pageBindingReconciler.reconcile(config, blueprint);
validatePageBindings(config, blueprint);
return compile(config);
}
@@ -156,19 +170,25 @@ public class FlowConfigContractService
private void validatePageBindings(SimpleFlowConfig config, AppBlueprintDesign blueprint)
{
Map<String, String> pages = new java.util.LinkedHashMap<String, String>();
Map<String, PageTarget> pages = new java.util.LinkedHashMap<String, PageTarget>();
if (blueprint.getFrontendPages() != null)
{
for (com.ruoyi.generator.domain.front.dto.AppPageDesign page : blueprint.getFrontendPages())
{
if (page != null && StringUtils.isNotBlank(page.getCode())) pages.put(page.getCode(), page.getTableName());
if (page != null && StringUtils.isNotBlank(page.getCode()))
{
pages.put(page.getCode(), new PageTarget(page.getTableName(), page.getPageType()));
}
}
}
if (blueprint.getAdminMenus() != null)
{
for (com.ruoyi.generator.domain.front.dto.AppMenuDesign menu : blueprint.getAdminMenus())
{
if (menu != null && StringUtils.isNotBlank(menu.getPageCode())) pages.put(menu.getPageCode(), menu.getTableName());
if (menu != null && StringUtils.isNotBlank(menu.getPageCode()))
{
pages.put(menu.getPageCode(), new PageTarget(menu.getTableName(), menu.getPageType()));
}
}
}
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
@@ -183,16 +203,45 @@ public class FlowConfigContractService
}
String ownerTable = StringUtils.defaultIfBlank(action.getOwnerTable(), flow.getMainTable());
String bindingTable = StringUtils.defaultIfBlank(action.getButton().getTableName(), ownerTable);
String pageTable = pages.get(pageCode);
PageTarget page = pages.get(pageCode);
String pageTable = page.tableName;
if (StringUtils.isNotBlank(pageTable) && !StringUtils.equals(pageTable, bindingTable))
{
throw new ServiceException("flow_config button table does not match page: " + pageCode
+ " -> " + bindingTable + ", expected " + pageTable);
}
validateSlotPageType(action, pageCode, action.getButton().getSlot(), page.pageType);
}
}
}
private void validateSlotPageType(SimpleFlowConfig.Action action, String pageCode, String slot,
String pageType)
{
if (StringUtils.isBlank(slot) || StringUtils.isBlank(pageType)) return;
boolean valid = slot.startsWith("detail.") ? "detail".equals(pageType)
: slot.startsWith("form.") ? "form".equals(pageType)
: slot.startsWith("list.") ? StringUtils.equalsAny(pageType, "list", "current_user_list")
: true;
if (!valid)
{
throw new ServiceException("flow_config button slot does not match page type: "
+ action.getCode() + " -> " + pageCode + " (" + pageType + ") / " + slot);
}
}
private static class PageTarget
{
private final String tableName;
private final String pageType;
private PageTarget(String tableName, String pageType)
{
this.tableName = tableName;
this.pageType = pageType;
}
}
private <T> List<T> safe(List<T> items)
{
return items == null ? java.util.Collections.<T>emptyList() : items;

View File

@@ -0,0 +1,163 @@
package com.ruoyi.generator.service.front;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;
import org.springframework.stereotype.Component;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
import com.ruoyi.generator.factory.flow.FlowConfigSupport;
/**
* Keeps record-scoped business entry actions on a resource detail page.
*
* <p>An initiating action that loads one resource record and creates the main
* business record is different from a transition on an existing business
* record. The former belongs on the resource detail page; the latter may stay
* in a list row.</p>
*/
@Component
public class FlowConfigPageBindingReconciler
{
public boolean reconcile(SimpleFlowConfig config, AppBlueprintDesign blueprint)
{
if (config == null || blueprint == null || blueprint.getFrontendPages() == null) return false;
boolean changed = false;
for (SimpleFlowConfig flow : FlowConfigSupport.leafFlows(config))
{
for (SimpleFlowConfig.Action action : flow.getActions())
{
if (!requiresResourceDetail(flow, action)) continue;
String ownerTable = action.getOwnerTable();
AppPageDesign detailPage = findPage(blueprint, ownerTable, "detail", null);
AppPageDesign sourcePage = findSourceListPage(blueprint, ownerTable,
action.getButton().getPageCode());
if (detailPage == null && sourcePage != null)
{
detailPage = createDetailPage(blueprint, sourcePage, ownerTable);
blueprint.getFrontendPages().add(detailPage);
changed = true;
}
if (detailPage == null) continue;
SimpleFlowConfig.Button button = action.getButton();
if (!StringUtils.equals(button.getPageCode(), detailPage.getCode()))
{
button.setPageCode(detailPage.getCode());
changed = true;
}
if (!StringUtils.equals(button.getTableName(), ownerTable))
{
button.setTableName(ownerTable);
changed = true;
}
if (!"detail.primaryActions".equals(button.getSlot()))
{
button.setSlot("detail.primaryActions");
changed = true;
}
}
}
return changed;
}
boolean requiresResourceDetail(SimpleFlowConfig flow, SimpleFlowConfig.Action action)
{
if (flow == null || action == null || action.getButton() == null) return false;
if (StringUtils.isBlank(action.getOwnerTable())
|| StringUtils.equals(action.getOwnerTable(), flow.getMainTable())) return false;
if (StringUtils.isNotBlank(action.getFrom()) || StringUtils.isNotBlank(action.getTo())) return false;
if (isAdmin(action.getActor()) || isAdmin(action.getButton().getScope())) return false;
for (SimpleFlowConfig.Effect effect : action.getEffects())
{
if (effect != null && "create_record".equals(effect.getType())
&& StringUtils.equals(flow.getMainTable(), effect.getTable())) return true;
}
return false;
}
private AppPageDesign findSourceListPage(AppBlueprintDesign blueprint, String tableName, String pageCode)
{
AppPageDesign bound = findPage(blueprint, tableName, "list", pageCode);
return bound == null ? findPage(blueprint, tableName, "list", null) : bound;
}
private AppPageDesign findPage(AppBlueprintDesign blueprint, String tableName, String pageType,
String preferredCode)
{
AppPageDesign fallback = null;
for (AppPageDesign page : blueprint.getFrontendPages())
{
if (page == null || !StringUtils.equals(tableName, page.getTableName())
|| !StringUtils.equals(pageType, page.getPageType())) continue;
if (StringUtils.isNotBlank(preferredCode) && StringUtils.equals(preferredCode, page.getCode()))
{
return page;
}
if (fallback == null) fallback = page;
}
return fallback;
}
private AppPageDesign createDetailPage(AppBlueprintDesign blueprint, AppPageDesign sourcePage,
String tableName)
{
AppPageDesign detail = new AppPageDesign();
detail.setCode(uniquePageCode(blueprint, detailPageCode(tableName)));
detail.setName(detailPageName(sourcePage.getName()));
detail.setMenuCode(sourcePage.getMenuCode());
detail.setParentPageCode(sourcePage.getCode());
detail.setPath(detailPath(sourcePage.getPath(), tableName));
detail.setPageType("detail");
detail.setTableName(tableName);
return detail;
}
private String uniquePageCode(AppBlueprintDesign blueprint, String baseCode)
{
Set<String> used = new HashSet<String>();
for (AppPageDesign page : blueprint.getFrontendPages())
{
if (page != null && StringUtils.isNotBlank(page.getCode())) used.add(page.getCode());
}
if (!used.contains(baseCode)) return baseCode;
int suffix = 2;
while (used.contains(baseCode + "_" + suffix)) suffix++;
return baseCode + "_" + suffix;
}
private String detailPageCode(String tableName)
{
String base = StringUtils.defaultString(tableName).toLowerCase(Locale.ENGLISH)
.replaceAll("(_info|_records|_record|_table|_data|_list)$", "");
if (StringUtils.isBlank(base)) base = "resource";
return base + "_detail_page";
}
private String detailPageName(String sourceName)
{
String name = StringUtils.defaultIfBlank(sourceName, "资源").trim();
String replaced = name.replaceFirst("(目录|列表)$", "详情")
.replaceFirst("(?i)(catalog|list)$", "Detail");
return StringUtils.equals(replaced, name) ? name + "详情" : replaced;
}
private String detailPath(String sourcePath, String tableName)
{
String path = StringUtils.defaultString(sourcePath).trim().replaceAll("/+$", "");
if (StringUtils.isBlank(path))
{
String resource = detailPageCode(tableName).replaceFirst("_detail_page$", "").replace('_', '-');
path = "/" + resource;
}
return path + "/:id";
}
private boolean isAdmin(String value)
{
String normalized = StringUtils.defaultString(value).trim().toLowerCase(Locale.ENGLISH);
return "admin".equals(normalized) || "admin_frontend".equals(normalized);
}
}

View File

@@ -31,7 +31,6 @@ import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.config.DeepSeekProperties;
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
@Component
public class HttpDeepSeekClient implements IDeepSeekClient
@@ -64,19 +63,11 @@ public class HttpDeepSeekClient implements IDeepSeekClient
}
};
private static final String DEFAULT_SYSTEM_PROMPT = BuiltinPromptRegistry.DEFAULT_SYSTEM_PROMPT;
@Autowired
private DeepSeekProperties deepSeekProperties;
@Autowired(required = false)
private AiUsageRecorder aiUsageRecorder;
@Override
public String chat(String prompt)
{
return chat(DEFAULT_SYSTEM_PROMPT, prompt);
}
@Override
public String chat(String systemPrompt, String prompt)
{
@@ -86,6 +77,7 @@ public class HttpDeepSeekClient implements IDeepSeekClient
@Override
public String chatModel(String model, String systemPrompt, String prompt)
{
requireManagedSystemPrompt(systemPrompt);
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
{
throw new ServiceException("DeepSeek API Key 未配置");
@@ -100,12 +92,6 @@ public class HttpDeepSeekClient implements IDeepSeekClient
return parseContent(response.getBody(), model);
}
@Override
public void chatStream(String prompt, final StreamHandler handler)
{
chatStream(DEFAULT_SYSTEM_PROMPT, prompt, handler);
}
@Override
public void chatStream(String systemPrompt, String prompt, final StreamHandler handler)
{
@@ -116,6 +102,7 @@ public class HttpDeepSeekClient implements IDeepSeekClient
public void chatStreamModel(String model, String systemPrompt, String prompt,
final StreamHandler handler)
{
requireManagedSystemPrompt(systemPrompt);
if (StringUtils.isBlank(deepSeekProperties.getApiKey()))
{
throw new ServiceException("DeepSeek API Key 未配置");
@@ -140,11 +127,6 @@ public class HttpDeepSeekClient implements IDeepSeekClient
});
}
private JSONObject requestBody(String prompt, boolean stream)
{
return requestBody(DEFAULT_SYSTEM_PROMPT, prompt, stream);
}
private JSONObject requestBody(String systemPrompt, String prompt, boolean stream)
{
return requestBody(deepSeekProperties.getModel(), systemPrompt, prompt, stream);
@@ -152,10 +134,11 @@ public class HttpDeepSeekClient implements IDeepSeekClient
private JSONObject requestBody(String model, String systemPrompt, String prompt, boolean stream)
{
requireManagedSystemPrompt(systemPrompt);
JSONObject request = new JSONObject();
request.put("model", StringUtils.defaultIfEmpty(model, deepSeekProperties.getModel()));
JSONArray messages = new JSONArray();
messages.add(message("system", StringUtils.defaultIfEmpty(systemPrompt, DEFAULT_SYSTEM_PROMPT)));
messages.add(message("system", systemPrompt));
messages.add(message("user", prompt));
request.put("messages", messages);
request.put("temperature", 0.2);
@@ -169,6 +152,14 @@ public class HttpDeepSeekClient implements IDeepSeekClient
return request;
}
private void requireManagedSystemPrompt(String systemPrompt)
{
if (StringUtils.isBlank(systemPrompt))
{
throw new ServiceException("System Prompt must come from a published managed Prompt version");
}
}
private JSONObject message(String role, String content)
{
JSONObject message = new JSONObject();

View File

@@ -9,14 +9,10 @@ public interface IDeepSeekClient
void onContent(String content) throws IOException;
}
String chat(String prompt);
String chat(String systemPrompt, String prompt);
String chatModel(String model, String systemPrompt, String prompt);
void chatStream(String prompt, StreamHandler handler);
void chatStream(String systemPrompt, String prompt, StreamHandler handler);
void chatStreamModel(String model, String systemPrompt, String prompt, StreamHandler handler);

View File

@@ -8,6 +8,7 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.front.AiGenerationTask;
import com.ruoyi.generator.domain.front.FrontProject;
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.DatabaseDesignResponse;
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationException;
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationReport;
@@ -38,6 +39,8 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
private FlowConfigDatabaseContractValidator flowConfigDatabaseContractValidator;
@Autowired(required = false)
private BusinessRequirementCoverageValidator businessRequirementCoverageValidator;
@Autowired(required = false)
private FlowConfigPageBindingReconciler pageBindingReconciler;
@Override
public OneClickProjectGenerationResult generate(AiGenerationTask task, AiGenerationTaskCreateRequest request)
@@ -106,6 +109,11 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
{
throw new ServiceException("Flow config is missing in the design checkpoint");
}
AppBlueprintDesign blueprint = JSON.parseObject(context.getAppBlueprintJson(), AppBlueprintDesign.class);
if (pageBindingReconciler().reconcile(context.getFlowConfig(), blueprint))
{
throw new ServiceException("Flow config resource actions require regenerated detail pages");
}
requirementCoverageValidator().validate(requirement(context.getRequest()), context.getFlowConfig());
return true;
}
@@ -204,6 +212,11 @@ public class OneClickProjectGenerationServiceImpl implements IOneClickProjectGen
? new BusinessRequirementCoverageValidator() : businessRequirementCoverageValidator;
}
private FlowConfigPageBindingReconciler pageBindingReconciler()
{
return pageBindingReconciler == null ? new FlowConfigPageBindingReconciler() : pageBindingReconciler;
}
private String requirement(AiGenerationTaskCreateRequest request)
{
if (request == null) return "";

View File

@@ -1 +0,0 @@
你是应用蓝图架构师。只输出符合用户消息结构的 JSON规划业务角色、前台菜单与页面、后台菜单和运营看板。保持 pageCode、tableName、角色数据范围和登录要求前后一致只描述静态蓝图不生成数据库、权限表、菜单表或代码。角色名称使用简体中文技术标识使用稳定的小写 snake_case。

View File

@@ -1,22 +0,0 @@
根据项目输入 JSON 生成应用蓝图,只返回 JSON不要 Markdown 围栏或解释。
输出结构:{"roles":[{"code":"reader","name":"读者","description":""}],"frontendMenus":[{"code":"book_catalog","name":"图书目录","path":"/books","pageCode":"book_catalog_page","requiresLogin":false,"visibleRoles":[],"dataScope":"PUBLIC"}],"frontendPages":[{"code":"book_catalog_page","name":"图书目录","menuCode":"book_catalog","path":"/books","pageType":"list","tableName":"book_info"}],"adminMenus":[{"code":"book_admin","name":"图书管理","path":"/admin/books","pageCode":"book_admin_page","tableName":"book_info","pageType":"list","requiresLogin":true,"visibleRoles":["admin"],"dataScope":"ROLE"}],"adminHomeDashboard":{"title":"运营总览","description":"核心业务数据","metrics":[],"charts":[]}}。
蓝图描述目标生成项目,而不是当前 EasyCode/RuoYi 平台。不要生成数据库表、菜单表、角色表、权限表或代码。
role.name 使用简体中文role.code 使用小写 snake_case。只有 role.code=admin 是系统管理员;名称含“管理员”或“后台”的业务角色仍保持独立。
role.name must be Simplified Chinese. Only role.code=admin is the system administrator.
每个菜单必须包含 requiresLogin、visibleRoles、dataScopedataScope 只能是 PUBLIC、CURRENT_USER 或 ROLE。
adminMenus 必须包含 pageCode、tableName、pageType个人资料使用 form普通管理使用 list。
adminMenus must include pageCode, tableName, and pageType.
frontendMenus 顶层菜单应指向 pageCode详情页作为子页面不创建顶层详情菜单。
不要为轮播图、banner、公告滚动条等首页装饰创建顶层菜单可放在首页业务块或作为后台维护数据。
frontendPages.pageType 只能是 list、current_user_list、detail、formtableName 必须是页面展示的业务表。
同一业务对象只使用一个稳定 tableName后续数据库、闭环、页面、看板、动作必须复用不能使用同义表名。
adminHomeDashboard 尽量包含 4 个业务指标和至少 2 张图表趋势图与分布图所有表字段必须可由后续数据库步骤创建。semantic 只能使用 money、order、user、task、warning、inventory、content、default。
adminHomeDashboard.metrics 每项结构为 {"code":"total_books","title":"总图书数","tableName":"book_info","aggregate":"count","field":"book_id","semantic":"inventory"}。
adminHomeDashboard.charts 每项结构为 {"code":"borrow_trend","title":"借阅趋势","type":"line","tableName":"borrow_record","dimension":{"field":"borrow_date","timeUnit":"day"},"metrics":[{"field":"borrow_id","aggregate":"count","alias":"borrowCount","label":"借阅量"}],"semantic":"order"}。dimension 必须是对象metrics 必须是数组;不要输出旧版 xField、yField、metric 或字符串 dimension。
每个非 admin 角色使用表名等于 role.code 的独立账号表,不得创建共享 user、user_info、member 或 account 表。
Never invent a shared user, user_info, member, or account table for role accounts.
需要个人资料页时,为每个非 admin 角色创建独立 form 页面和菜单tableName 绑定 role.codepageCode/path 带角色前缀visibleRoles 只包含该角色。
Create one form page and one menu per non-admin role when profile pages are needed.
个人订单、借阅、资料、预约、支付等页面必须 requiresLogin=true 且 dataScope=CURRENT_USER。
项目输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是资深 Java/Vue 毕设项目代码解读专家。只依据用户消息中提供的源码、文件清单和项目元信息生成正式 Markdown 报告,引用真实类名、方法名、组件、表和字段;无法确认的内容必须明确说明,不得臆造模块或流程。报告应覆盖项目概述、技术栈、架构模块、核心业务、数据库设计和答辩准备。

View File

@@ -1,12 +0,0 @@
根据输入 JSON 中的精选核心源码和项目元信息生成正式 Markdown 代码解读报告,不要代码围栏或 JSON。
只能依据已展开源码和文件清单,不要编造未出现的模块、接口、表或流程;无法确认时明确说明。引用真实类名、方法名、组件名、表名和字段名。
无法从精选源码确认的内容必须明确标注“无法从精选源码确认”。
必须按顺序包含:
## 1. 项目概述与技术栈
## 2. 系统架构与核心模块
## 3. 核心业务流程
## 4. 数据库设计
## 5. 答辩准备
若 input.mode=continue在保留主题和章节结构的前提下根据 previousMarkdown 补充、修正和续写,避免重复无变化的长段。
输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是 MySQL 业务数据库设计专家。只输出符合用户消息结构的 JSON根据已确认的应用蓝图和业务闭环生成可直接落地的表、字段、主外键、字典和表操作策略。字段必须业务化且类型合法所有蓝图与闭环引用必须有真实表字段承载不得生成菜单、权限关系、代码或业务动作。flowConfig.actions[].audit 中非空的 userField 和 timeField 必须是 mainTable 的真实字段userField 使用 bigint(20)timeField 使用 datetime。

View File

@@ -1,30 +0,0 @@
根据项目输入 JSON 生成 MySQL 数据库设计 JSON只返回 JSON不要 Markdown 围栏或解释。
Confirmed app blueprint JSON and Confirmed business loop plan JSON are supplied in the input.
Database generation constraints from the business loop plan must be enforced.
Every confirmedBusinessLoopPlan.metrics tableName, field, and filters.field must exist in the generated schema. Numeric sum/avg/min/max metric fields must use a compatible numeric column type. Status-filter metric values must remain compatible with the matching stateMachines.states.
If input.mode is correction, repair input.originalResponse according to input.validationError and return the complete corrected database JSON. Do not drop unrelated valid tables or fields.
输出结构:{"tables":[{"tableName":"car_info","tableComment":"车辆信息表","adminMenuName":"车辆管理","operations":{"add":true,"edit":true,"remove":true,"removeType":"physical","reason":""},"columns":[{"columnName":"id","columnLabel":"ID","columnComment":"主键ID","columnType":"bigint(20)","isPk":"1","isIncrement":"1","isRequired":"1"}]}]}。
不要使用 field_1 等占位字段;每个表必须包含非空 columns以及 adminMenuName 和 operations.add/edit/remove/removeType/reason。
adminMenuName 必须来自本表 tableComment/tableName不得复用其他表或宽泛模块名称。
adminMenuName must come from that table's own tableComment or tableName.
字段数组名称只能是 columns不能使用 fields、fieldDesign、fieldList、columnList。
Every table must include a non-empty columns array; use columns exactly.
每个字段必须包含 columnName、columnLabel、columnComment、columnType、isPk、isIncrement、isRequired。columnLabel 是短 UI 标签columnComment 是完整数据库说明。
可提供 javaType、htmlTypehtmlType 只能是 input、textarea、select、radio、checkbox、datetime、imageUpload、fileUpload、editor。
select/radio/checkbox 应提供 dictType 和 dictOptions。关联选择使用 remote:target_table:value_field:label_field且目标表和值/标签字段必须都存在于本次 tables JSON。
非主键外键字段应在目标表存在时使用 remote select无目标表的分类、类型、状态使用本地 dictOptions 或普通输入。
普通主数据表除唯一主键外至少包含 4 个业务字段。
订单、支付、借还、审批、流程、任务、库存记录等动作记录表使用仅新增;关系、审计、日志、历史表使用只读;普通主数据表使用完整 CRUD。
不要生成 businessActions、业务动作、工作流、代码、菜单表、user_role、角色权限表或权限表。
Do not generate menu tables, user_role tables, role-permission tables, or permission tables.
表名字段名只使用小写字母、数字、下划线;每表必须且只能有一个主键。字段类型只能使用 bigint(20)、int(11)、varchar(n)、char(n)、decimal(p,s)、datetime、date、text、longtext。
为 confirmedAppBlueprint.roles 中每个角色创建独立账号表admin 使用 admin其他角色使用 role.code。不得使用共享用户表或 role_id/role_code/user_role。
角色账号表包含 `<role_code>_id` 自增主键、username、password_hash、nickname、status、create_time。admin 只能登录后台,非 admin 角色登录前台。
生成 frontendPages.tableName 引用的全部表;个人资料页必须绑定对应非 admin 角色账号表。requiresLogin=true 且 dataScope=CURRENT_USER 的业务数据通常包含 user_id。
canonicalTableRegistry 中的名称是最终规范表名。confirmedBusinessLoopPlan 若出现与其指向同一业务对象的同义表名,必须使用 canonicalTableRegistry 中的名称,不得同时生成两张同义表。
根据 confirmedBusinessLoopPlan 创建 coreObjects.tableName、actions.ownerTable补齐 stateMachines.statusField、quantityRules.quantityField 以及 recordRules 和动作前置条件需要的字段。状态值不得超出 stateMachines.states。
严格实现 confirmedBusinessLoopPlan.businessPoliciesTIME_LIMIT 的 startField、dueField 使用 datetimeOVERDUE_FEE 的 dueField 使用 datetime、daysTargetField 使用 int(11)、targetField 使用 decimal(10,2)QUANTITY_LIMIT 的 targetField 使用 int(11) 且默认值为 0。所有政策字段必须位于政策指定的 tableName禁止只把期限或公式写进 columnComment 而不创建字段。
Do not invent status values outside stateMachines.states.
严格遵守 input.limits.maxTables 和 maxColumnsPerTable。
项目输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是数据库变更同步专家。根据已确认的数据库变更分析受影响的蓝图、业务闭环、页面、动作和生成产物,保持表字段引用及业务语义一致。只提出可验证的同步结果,不虚构不存在的表、字段或模块。

View File

@@ -1,3 +0,0 @@
根据数据库变更和项目上下文同步受影响的生成结果。严格遵守输入中的阶段契约。
输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是企业管理系统生成助手。严格遵守用户消息中的任务、输出格式和语言要求。

View File

@@ -1 +0,0 @@
你是软件设计图专家。只输出符合用户消息结构的 JSON同时提供有效 Mermaid、节点和连线数据。根据指定图类型生成简体中文、结构清晰且规模适中的流程图、用例图、时序图、活动图、状态图、数据流图或架构图不得虚构与当前项目无关的系统。

View File

@@ -1,6 +0,0 @@
根据输入 JSON 生成软件设计图,只返回 JSON不要 Markdown 围栏或解释。
输出结构:{"diagramType":"flowchart","title":"...","description":"...","mermaid":"flowchart TD...","markdown":"...","nodes":[{"id":"start","label":"开始","type":"start","lane":"用户","description":""}],"edges":[{"source":"start","target":"next","label":""}]}。
支持 flowchart、use_case、sequence、activity、state、data_flow、architecture。用例图使用 actor/use_case/system_boundary流程、活动、状态图使用过程和判断节点架构图使用 frontend/backend/database/deployment 节点。
标签使用简体中文,节点 id 只使用小写字母、数字、下划线。保持 6 到 14 个节点、6 到 18 条边,不虚构无关系统。
输入 JSON
{{input}}

View File

@@ -1,55 +0,0 @@
你是业务流程配置设计器。只输出一个合法 JSON 对象,不输出 Markdown、解释或 SQL。
目标:用最小 flow_config 描述业务发起、状态流转、显式 Java 业务动作、多表数量联动、按钮和统计。不要输出普通 CRUD不要输出通用规则 DSL。创建主业务记录的“借书/预约/申请/下单”等发起动作属于业务闭环,必须输出。项目存在预约、借阅等多个独立状态对象时,必须分别放入 flows禁止只选择其中一个局部流程。
固定结构:
{
"version":"1.1",
"code":"library_circulation",
"name":"图书流通",
"flows":[{
"code":"reservation_flow","name":"图书预约","mainTable":"reservation","statusField":"status",
"notFoundMessage":"预约记录不存在",
"states":[{"code":"pending","label":"待处理"},{"code":"cancelled","label":"已取消"}],
"actions":[{
"code":"reserve_book","name":"预约","actor":"reader","ownerTable":"book_info","requestFields":["id"],
"method":"POST","path":"/book-info/reserve","successMessage":"预约成功",
"effects":[{"type":"create_record","table":"reservation","values":{"book_id":"record.id","reader_id":"currentUserId","status":"pending","reserve_date":"now","create_time":"now"}}],
"button":{"scope":"reader","pageCode":"book_detail_page","tableName":"book_info","slot":"detail.primaryActions","type":"primary","confirm":"确认预约吗?","order":10}
},{
"code":"cancel_reservation","name":"取消预约","from":"pending","to":"cancelled","actor":"reader",
"ownerTable":"reservation","requestFields":["id"],"method":"POST","path":"/reservation/cancel",
"button":{"scope":"reader","pageCode":"my_reservations_page","tableName":"reservation","slot":"list.rowActions","type":"danger","confirm":"确认取消预约吗?","order":10}
}],
"metrics":[]
},{
"code":"borrowing_flow","name":"图书借阅","mainTable":"borrowing","statusField":"status",
"notFoundMessage":"借阅记录不存在",
"states":[{"code":"borrowing","label":"借阅中"},{"code":"returned","label":"已归还"}],
"actions":[{
"code":"borrow_book","name":"借书","actor":"reader","ownerTable":"book_info","requestFields":["id"],
"method":"POST","path":"/book-info/borrow","successMessage":"借书成功",
"effects":[{"type":"create_record","table":"borrowing","values":{"book_id":"record.id","reader_id":"currentUserId","status":"borrowing","borrow_date":"now","create_time":"now"}},
{"type":"decrease","table":"book_info","relationField":"id","field":"stock","amount":"1","min":"0","errorMessage":"图书库存不足"}],
"button":{"scope":"reader","pageCode":"book_detail_page","tableName":"book_info","slot":"detail.primaryActions","type":"success","confirm":"确认借书吗?","order":20}
},{
"code":"return_book","name":"还书","from":"borrowing","to":"returned","actor":"reader","ownerTable":"borrowing","requestFields":["id"],
"method":"POST","path":"/borrowing/return","effects":[{"type":"increase","table":"book_info","relationField":"book_id","field":"stock","amount":"1"}],
"button":{"scope":"reader","pageCode":"my_borrowings_page","tableName":"borrowing","slot":"list.rowActions","type":"primary","confirm":"确认还书吗?","order":10}
}],
"metrics":[{"code":"borrowing_count","name":"借阅中数量","table":"borrowing","aggregate":"count","field":"id","status":"borrowing","unit":"条"}]
}]
}
约束:
1. 根对象固定使用 version=1.1 和 flows 数组;每个独立状态主表对应一个 flow。code、表名、字段名全部使用小写 snake_case状态 code 使用稳定英文编码label 使用中文。
2. 每个 action 必须给 ownerTablerequestFields 只列页面传入字段,通常为 ownerTable 主键 idcurrentUserId 不能作为前端可传字段。
3. 除发起型 action 外,每个 action 必须有 from 和 to二者存在于 states 中且不相同ownerTable 必须为 mainTable。
4. 创建主业务记录的发起型 action 必须省略 from/to并 create_record 到 mainTablevalues 必须给 statusField 写入 states 中的初始状态。借书、预约、申请、报名、下单、提交工单等需求不得误判为普通 CRUD 而省略。
5. effects.type 只能是 decrease、increase、update_related、create_record。
6. decrease/increase 必须给 table、relationField、field、amountdecrease 还要给 min。
7. update_related/create_record 使用 values值可为 currentUserId、now、record.字段名或常量。record 表示 ownerTable 当前记录。create_record 必须覆盖目标表全部非自增、无默认值的必填字段create_time、created_at 等创建时间字段必须写为 now。
8. 审核人等状态动作身份通过 audit.userField 表达;发起型动作的用户和时间通过 create_record.values 的 currentUserId、now 表达,均由后端登录态注入。
9. requiredActionCodes 中的每个动作都必须在 actions 中出现,并绑定到 appBlueprint 已有页面。覆盖需求中的完整正向闭环、业务发起动作和关键回退/拒绝动作,但不要为无业务语义的普通 CRUD 创建 action。
10. metrics.status 如果填写,必须完全等于 states 中某个 code统计全状态数据或不能确定状态时省略 status。
11. 每个面向用户或管理员的显式动作必须有 button。button.pageCode 必须引用 appBlueprint 中已有页面button.tableName 必须是该页面绑定表,省略时等于 ownerTablebutton.slot 只能是 list.toolbarActions、list.rowActions、detail.primaryActions、detail.secondaryActions、form.footerActions。详情页主操作使用 detail.primaryActions列表记录操作使用 list.rowActions禁止使用 page.actions 等自定义值。

View File

@@ -1,2 +0,0 @@
请根据以下 JSON 上下文生成 flow_config。appBlueprint 是已确认的页面、角色和菜单范围,必须与它保持一致:
{{input}}

View File

@@ -1 +0,0 @@
你是一键项目生成流水线编排专家。按照应用蓝图、业务闭环、数据库和业务动作的固定阶段顺序工作,保证各阶段 Prompt 身份、输入输出和引用关系可追踪、可恢复、可复现。不跨阶段生成不属于当前阶段的内容。

View File

@@ -1,3 +0,0 @@
按照一键生成阶段契约处理项目输入,当前阶段只输出该阶段要求的内容。
输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是中文软件工程本科毕业论文写作助手。只输出论文正文 Markdown首个非空字符必须是 #不要确认语、解释、JSON 或代码围栏。必须围绕当前项目上下文写作,不套用无关行业示例。

View File

@@ -1,13 +0,0 @@
根据输入 JSON 撰写软件工程本科论文 Markdown 初稿。
Return Markdown only. Do not include acknowledgements, confirmations, explanations, or preamble before the thesis content. The first non-blank character of the response must be #.
首个非空字符必须是 #,正文前不要确认、解释或前言。使用简体中文,篇幅参考 input.targetWords以章节连贯性优先。
严格按以下大纲生成,不得跳过或调换:系统设定、第 1 至第 7 章、参考文献、致谢。
必须严格按照以下大纲生成论文初稿:系统设定、第 1 章 绪论、第 2 章 相关技术介绍、第 3 章 系统需求分析、第 4 章 系统设计、第 5 章 系统实现、第 6 章 系统测试、第 7 章 总结与展望、参考文献与致谢。
系统设定应结合项目名称、需求、应用蓝图、闭环和数据库,说明模块、角色、核心表与流程。
第 1 章 绪论;第 2 章 相关技术;第 3 章 系统需求分析;第 4 章 系统设计;第 5 章 系统实现;第 6 章 系统测试;第 7 章 总结与展望;参考文献;致谢。
模块、角色、表和流程必须来自当前项目。除非项目确实包含,否则不得套用图书、读者、借阅等示例。
Use module names, roles, and tables from the current project context. Do not use unrelated sample domain terms.
图表位置使用“[此处插入 XX 图]”占位。不得虚构不可核验引用,明确提醒学生核验参考文献。
合理使用标题、段落和表格;除配置片段外避免代码块。
输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是中文软件工程本科毕业论文导师。只输出用户要求的 JSON不要 Markdown 代码块或解释。

View File

@@ -1,16 +0,0 @@
根据输入 JSON 生成软件工程本科论文提纲,只返回 JSON不要 Markdown 围栏或解释。
输出结构:{"title":"...","abstractDraft":"...","keywords":["Spring Boot","Vue"],"chapters":[{"title":"系统设定","goal":"...","sections":["模块设定","角色设定","数据表设定"]}]}。
使用简体中文,不虚构参考文献;模块、角色、表名必须来自当前项目,不使用无关行业示例。
chapters 必须严格按以下顺序,每项包含 2 到 5 个具体小节:
系统设定:结合项目名称、需求描述、应用蓝图、业务闭环和数据库表,推导系统模块、用户角色、核心数据表与关键业务流程。
第 1 章 绪论:研究背景、研究意义、国内外研究现状、研究内容与论文结构。
第 2 章 相关技术介绍:围绕实际技术栈介绍关键技术。
第 3 章 系统需求分析:可行性、用户角色、功能需求、非功能需求和业务流程。
第 4 章 系统设计:总体架构、功能模块、数据库、接口和安全设计。
第 5 章 系统实现:围绕当前项目实际模块展开。
第 6 章 系统测试:测试环境、方法、功能测试、异常测试、兼容性测试和结论。
第 7 章 总结与展望:成果、不足和后续优化。
参考文献与致谢:提醒学生人工核验参考文献真实性与学校格式。
Target a later draft of about 10000 Chinese characters.
输入 JSON
{{input}}

View File

@@ -1 +0,0 @@
你是资深软件需求分析师。面向本科毕设或课程设计规模在保留用户原始描述的前提下补全真实业务闭环、角色、核心对象和可执行业务规则。规则必须给出明确的时间、数量、金额、状态、公式、上限和异常边界用户未提供关键数值时应给出合理默认值并标注“AI假设”不得使用“按实际情况”“超时后处理”等模糊表述。不要把需求扩张成大型企业平台也不要用增删改查或运维功能代替业务需求。只输出用户消息要求的中文纯文本不要 Markdown、JSON 或解释。

View File

@@ -1,17 +0,0 @@
请根据以下 JSON 项目输入为低代码生成器补全一份可直接驱动代码生成的项目需求。
需求复杂度控制在本科毕设或课程设计级别,功能要完整但不要设计成大型企业平台或运维平台。
currentDescription 是用户原始描述,必须作为事实来源保留其约束;输出补充内容,不要改写或否定用户已经明确的规则。
只返回中文纯文本,不要 Markdown不要 JSON不要解释。格式必须严格如下
业务目标:一句话说明系统解决的问题
角色:用顿号分隔 2 到 4 个用户角色
核心对象:列出流程涉及的主要业务对象和记录
核心流程:按先后顺序写出完整正向流程、逆向动作和补偿动作,不要只写增删改查
业务规则:用分号分隔明确规则;必须覆盖适用的时间期限、起算点、状态判定、数量限制、金额公式、计费单位、取整方式、封顶值、重复操作限制
异常边界:说明库存不足、状态不符、重复提交、取消、超时、重复支付等适用场景
验收场景:至少给出一个包含边界数值和预期结果的完整场景
如果输入没有给出期限、费率、上限等关键数值必须选择合理默认值并在该条前标注“AI假设禁止输出“待定”“按实际情况”“超时后罚款”等不可执行描述。
对于借阅、租赁、预约等有期限的流程,必须明确起算时间、期限、到期判定和逾期处理;存在罚款或费用时必须明确公式、日费率、取整和上限。
其他模块:用顿号分隔 2 到 4 个前台展示或内容维护模块,优先从轮播图、公告、新闻资讯、友情链接、留言反馈、帮助中心、系统简介中选择
不要出现通知提醒、统计报表、数据备份、操作日志、系统监控、定时任务等后台运维或企业管理平台标配模块。
项目输入 JSON
{{input}}

View File

@@ -152,24 +152,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{createBy}, sysdate(), #{remark})
</insert>
<update id="updateInitializedV1">
update factory_prompt_version
set system_prompt = #{systemPrompt},
user_prompt_contract = #{userPromptContract},
user_prompt_template = #{userPromptTemplate},
provider_code = #{providerCode},
model = #{model},
pipeline_release_code = #{pipelineReleaseCode},
content_hash = #{contentHash},
published_at = sysdate(),
remark = #{remark}
where prompt_version_id = #{promptVersionId}
and version_no = 1
and version_label = 'V1'
and user_prompt_contract like 'legacy-builder:%'
and remark in ('由当前代码内置 Prompt 初始化', '由当前代码内置业务 Prompt 初始化')
</update>
<update id="retirePublishedVersions">
update factory_prompt_version
set status = 'RETIRED'

View File

@@ -1,94 +0,0 @@
package com.ruoyi.generator.factory.ai.prompt;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import org.junit.Test;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
public class BuiltinPromptRegistryTest
{
private final BuiltinPromptRegistry registry = new BuiltinPromptRegistry(
new PromptFingerprintService(
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
@Test
public void resolvesLatestAndExactPublishedPromptIdentity()
{
PromptRelease latest = registry.resolve("database");
PromptRelease exact = registry.resolve(latest.getPromptCode(), latest.getVersion());
assertEquals("front.database", latest.getPromptCode());
assertEquals("1.0.0", latest.getVersion());
assertEquals(latest.getFingerprint(), exact.getFingerprint());
assertEquals(64, latest.getFingerprint().length());
assertTrue(latest.getUserPromptContract().contains("context-json:database:v1"));
}
@Test
public void paperPromptOwnsItsPublishedSystemInstruction()
{
PromptRelease paper = registry.resolve("paper_draft");
PromptRelease database = registry.resolve("database");
assertTrue(paper.getSystemPrompt().contains("论文正文 Markdown"));
assertFalse(paper.getFingerprint().equals(database.getFingerprint()));
}
@Test
public void eachBusinessTypeOwnsTaskSpecificSystemInstruction()
{
String requirement = registry.managementSystemPrompt("requirement");
String blueprint = registry.managementSystemPrompt("app_blueprint");
String flow = registry.managementSystemPrompt("flow_config");
String database = registry.managementSystemPrompt("database");
assertTrue(requirement.contains("AI假设"));
assertTrue(requirement.contains("时间、数量、金额、状态、公式"));
assertTrue(blueprint.contains("应用蓝图"));
assertTrue(flow.contains("最小 flow_config"));
assertTrue(flow.contains("decrease"));
assertTrue(flow.contains("currentUserId"));
assertTrue(flow.contains("statusField"));
assertTrue(database.contains("MySQL"));
assertTrue(registry.managementUserPromptTemplate("database").contains("QUANTITY_LIMIT"));
assertFalse(requirement.equals(database));
assertFalse(blueprint.equals(flow));
assertTrue(registry.managementUserPromptTemplate("database").contains("数据库设计 JSON"));
assertTrue(registry.managementUserPromptTemplate("database").contains("{{input}}"));
assertFalse(registry.managementUserPromptTemplate("database").equals(
registry.managementUserPromptTemplate("code_analysis")));
}
@Test
public void requirementReleaseOwnsTheCompleteManagedPrompt()
{
PromptRelease requirement = registry.resolve("requirement");
assertEquals(BuiltinPromptRegistry.REQUIREMENT_SYSTEM_PROMPT,
requirement.getSystemPrompt());
assertEquals(BuiltinPromptRegistry.REQUIREMENT_USER_PROMPT_TEMPLATE,
requirement.getUserPromptTemplate());
assertTrue(requirement.getUserPromptTemplate().contains("业务规则"));
assertTrue(requirement.getUserPromptTemplate().contains("验收场景"));
assertTrue(requirement.getUserPromptTemplate().contains("期限、费率、上限"));
assertTrue(requirement.getUserPromptTemplate().contains("{{input}}"));
}
@Test
public void rejectsUnknownPromptRelease()
{
try
{
registry.resolve("front.database", "9.9.9");
fail("Expected ServiceException");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("Prompt release does not exist"));
}
}
}

View File

@@ -8,7 +8,6 @@ import static org.mockito.Mockito.when;
import org.junit.Before;
import org.junit.Test;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.config.PromptRegistryProperties;
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
@@ -17,9 +16,7 @@ import com.ruoyi.generator.mapper.factory.PromptRegistryMapper;
public class DatabasePromptRegistryTest
{
private PromptRegistryMapper mapper;
private BuiltinPromptRegistry builtin;
private PromptFingerprintService fingerprints;
private PromptRegistryProperties properties;
private DatabasePromptRegistry registry;
@Before
@@ -28,23 +25,12 @@ public class DatabasePromptRegistryTest
mapper = mock(PromptRegistryMapper.class);
fingerprints = new PromptFingerprintService(
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
builtin = new BuiltinPromptRegistry(fingerprints);
properties = new PromptRegistryProperties();
registry = new DatabasePromptRegistry(mapper, builtin, fingerprints, properties);
}
@Test
public void disabledDatabaseUsesBuiltinRelease()
{
properties.setDatabaseEnabled(false);
assertEquals(builtin.resolve("database").getFingerprint(),
registry.resolve("database").getFingerprint());
registry = new DatabasePromptRegistry(mapper, fingerprints);
}
@Test
public void resolvesCurrentAndExactDatabaseRelease()
{
properties.setDatabaseEnabled(true);
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
when(mapper.selectExecutableByCodeAndVersion("front.database", "2.0.0"))
@@ -58,7 +44,6 @@ public class DatabasePromptRegistryTest
@Test
public void rejectsStoredContentHashDrift()
{
properties.setDatabaseEnabled(true);
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
version.setContentHash("tampered");
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
@@ -75,32 +60,22 @@ public class DatabasePromptRegistryTest
}
@Test
public void obsoleteLegacyBuilderReleaseIsRejectedInsteadOfSilentlyFallingBack()
public void acceptsContractOwnedByManagedVersion()
{
properties.setDatabaseEnabled(true);
PromptVersion version = version("1.0.0", PromptVersion.STATUS_PUBLISHED);
version.setUserPromptContract("legacy-builder:database:v1");
PromptVersion version = version("2.0.0", PromptVersion.STATUS_PUBLISHED);
version.setUserPromptContract("context-json:database:v2");
version.setContentHash(fingerprints.fingerprint(version.getGenerateType(),
version.getPromptCode(), version.getVersion(), version.getSystemPrompt(),
version.getUserPromptContract(), version.getUserPromptTemplate()));
when(mapper.selectPublishedByGenerateType("database")).thenReturn(version);
try
{
registry.resolve("database");
fail("Expected incompatible stored contract");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("not implemented"));
}
assertEquals("context-json:database:v2",
registry.resolve("database").getUserPromptContract());
}
@Test
public void missingDatabaseReleaseFailsClosedByDefault()
{
properties.setDatabaseEnabled(true);
try
{
registry.resolve("database");
@@ -112,16 +87,6 @@ public class DatabasePromptRegistryTest
}
}
@Test
public void explicitBootstrapFallbackCanUseBuiltinRelease()
{
properties.setDatabaseEnabled(true);
properties.setBuiltinFallbackEnabled(true);
assertEquals(builtin.resolve("database").getFingerprint(),
registry.resolve("database").getFingerprint());
}
private PromptVersion version(String label, String status)
{
PromptVersion version = new PromptVersion();
@@ -132,7 +97,7 @@ public class DatabasePromptRegistryTest
version.setVersion(label);
version.setSystemPrompt("database system v2");
version.setUserPromptContract("context-json:database:v1");
version.setUserPromptTemplate(BuiltinPromptRegistry.DATABASE_USER_PROMPT_TEMPLATE);
version.setUserPromptTemplate("{{input}}");
version.setProviderCode("deepseek");
version.setModel("deepseek-chat");
version.setStatus(status);

View File

@@ -23,17 +23,15 @@ public class PromptRegistrySchemaScriptTest
}
@Test
public void mapperKeepsVersionContentImmutableOutsideGuardedBootstrapRepair() throws Exception
public void mapperKeepsManagedVersionContentImmutable() throws Exception
{
String mapper = read(repoRoot().resolve(
"ruoyi-generator/src/main/resources/mapper/factory/PromptRegistryMapper.xml"));
assertTrue(mapper.contains("insertPromptVersion"));
assertTrue(mapper.contains("publishPromptVersion"));
assertTrue(mapper.contains("retirePublishedVersions"));
assertTrue(mapper.contains("updateInitializedV1"));
assertTrue(mapper.contains("version_label = 'V1'"));
assertTrue(mapper.contains("user_prompt_contract like 'legacy-builder:%'"));
assertTrue(mapper.contains("remark in ('由当前代码内置 Prompt 初始化', '由当前代码内置业务 Prompt 初始化')"));
assertFalse(mapper.contains("updateInitializedV1"));
assertFalse(mapper.contains("legacy-builder:"));
assertFalse(mapper.contains("updatePromptVersionContent"));
}
@@ -47,7 +45,7 @@ public class PromptRegistrySchemaScriptTest
String completeDatabase = read(root.resolve("sql/db.sql"));
assertTrue(controller.contains("/versions/{promptVersionId}/publish"));
assertTrue(controller.contains("/versions/{promptVersionId}/rollback"));
assertTrue(controller.contains("/initialize-v1"));
assertFalse(controller.contains("/initialize-v1"));
assertTrue(menu.contains("generator:prompt:publish"));
assertTrue(menu.contains("generator:prompt:rollback"));
assertTrue(completeDatabase.contains("'Prompt 中心'"));

View File

@@ -0,0 +1,86 @@
package com.ruoyi.generator.factory.ai.stage.oneclick;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Test;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.generator.domain.front.AiGenerationTask;
import com.ruoyi.generator.domain.front.dto.AiGenerationTaskCreateRequest;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
import com.ruoyi.generator.domain.front.dto.GenerateAppBlueprintRequest;
import com.ruoyi.generator.domain.front.dto.GenerateFlowConfigRequest;
import com.ruoyi.generator.domain.front.dto.OneClickProjectGenerationResult;
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
import com.ruoyi.generator.service.front.FlowConfigPageBindingReconciler;
import com.ruoyi.generator.service.front.IAiGenerateService;
public class DesignStageHandlerTest
{
@Test
public void publishesReconciledBlueprintToFollowingOneClickStages()
{
IAiGenerateService ai = mock(IAiGenerateService.class);
AppBlueprintDesign blueprint = blueprint();
SimpleFlowConfig flow = flow();
when(ai.generateAppBlueprint(eq(7L), eq(11L), any(GenerateAppBlueprintRequest.class)))
.thenReturn(blueprint);
when(ai.generateFlowConfig(eq(7L), eq(11L), any(GenerateFlowConfigRequest.class)))
.thenReturn(flow);
AiGenerationTask task = new AiGenerationTask();
task.setUserId(7L);
task.setProjectId(11L);
OneClickGenerationContext context = new OneClickGenerationContext(task,
new AiGenerationTaskCreateRequest(), new OneClickProjectGenerationResult());
new DesignStageHandler(ai, new FlowConfigPageBindingReconciler()).execute(context);
AppBlueprintDesign reconciled = JSON.parseObject(context.getAppBlueprintJson(), AppBlueprintDesign.class);
assertNotNull(findPage(reconciled, "event_detail_page"));
assertEquals("event_detail_page", context.getFlowConfig().getActions().get(0).getButton().getPageCode());
assertEquals("detail.primaryActions", context.getFlowConfig().getActions().get(0).getButton().getSlot());
}
private AppBlueprintDesign blueprint()
{
AppBlueprintDesign blueprint = new AppBlueprintDesign();
AppPageDesign page = new AppPageDesign();
page.setCode("event_catalog_page");
page.setName("活动列表");
page.setMenuCode("events");
page.setPath("/events");
page.setPageType("list");
page.setTableName("event_info");
blueprint.getFrontendPages().add(page);
return blueprint;
}
private SimpleFlowConfig flow()
{
return JSON.parseObject("{\"version\":\"1.1\",\"code\":\"event_signup\","
+ "\"name\":\"活动报名\",\"mainTable\":\"event_signup\",\"statusField\":\"status\","
+ "\"states\":[{\"code\":\"pending\",\"label\":\"待处理\"},"
+ "{\"code\":\"cancelled\",\"label\":\"已取消\"}],\"actions\":[{"
+ "\"code\":\"signup\",\"name\":\"报名\",\"actor\":\"member\","
+ "\"ownerTable\":\"event_info\",\"effects\":[{\"type\":\"create_record\","
+ "\"table\":\"event_signup\",\"values\":{\"status\":\"pending\"}}],"
+ "\"button\":{\"scope\":\"member\",\"pageCode\":\"event_catalog_page\","
+ "\"tableName\":\"event_info\",\"slot\":\"list.rowActions\"}}]}",
SimpleFlowConfig.class);
}
private AppPageDesign findPage(AppBlueprintDesign blueprint, String code)
{
for (AppPageDesign page : blueprint.getFrontendPages())
{
if (code.equals(page.getCode())) return page;
}
return null;
}
}

View File

@@ -209,6 +209,56 @@ public class FlowConfigCompilerTest
assertTrue(compilation.getLoopPlan().getMetrics().get(0).getFilters().isEmpty());
}
@Test
public void sanitizerDefaultsSelfUpdateRelationFieldToOwnerRequestId()
{
SimpleFlowConfig config = JSON.parseObject("{"
+ "\"version\":\"1.1\",\"code\":\"book_return\",\"name\":\"Book return\","
+ "\"mainTable\":\"borrow_record\",\"statusField\":\"status\","
+ "\"states\":[{\"code\":\"borrowing\",\"label\":\"Borrowing\"},"
+ "{\"code\":\"returned\",\"label\":\"Returned\"}],"
+ "\"actions\":[{\"code\":\"return_book\",\"name\":\"Return\","
+ "\"from\":\"borrowing\",\"to\":\"returned\",\"actor\":\"reader\","
+ "\"ownerTable\":\"borrow_record\",\"requestFields\":[\"id\"],"
+ "\"effects\":[{\"type\":\"update_related\",\"table\":\"borrow_record\","
+ "\"values\":{\"return_date\":\"now\"}}]}],\"metrics\":[]}",
SimpleFlowConfig.class);
new FlowConfigSanitizer().sanitize(config);
assertEquals("id", config.getActions().get(0).getEffects().get(0).getRelationField());
new FlowConfigValidator().validate(config);
}
@Test
public void sanitizerDoesNotGuessCrossTableRelationField()
{
SimpleFlowConfig config = JSON.parseObject("{"
+ "\"version\":\"1.1\",\"code\":\"book_return\",\"name\":\"Book return\","
+ "\"mainTable\":\"borrow_record\",\"statusField\":\"status\","
+ "\"states\":[{\"code\":\"borrowing\",\"label\":\"Borrowing\"},"
+ "{\"code\":\"returned\",\"label\":\"Returned\"}],"
+ "\"actions\":[{\"code\":\"return_book\",\"name\":\"Return\","
+ "\"from\":\"borrowing\",\"to\":\"returned\",\"actor\":\"reader\","
+ "\"ownerTable\":\"borrow_record\",\"requestFields\":[\"id\"],"
+ "\"effects\":[{\"type\":\"update_related\",\"table\":\"fine_record\","
+ "\"values\":{\"paid_at\":\"now\"}}]}],\"metrics\":[]}",
SimpleFlowConfig.class);
new FlowConfigSanitizer().sanitize(config);
assertNull(config.getActions().get(0).getEffects().get(0).getRelationField());
try
{
new FlowConfigValidator().validate(config);
fail("Expected cross-table relationField validation to fail");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("effect.relationField"));
}
}
@Test
public void compilesReservationAndBorrowingAsTwoIndependentFlows()
{

View File

@@ -7,9 +7,7 @@ import static org.junit.Assert.fail;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.Collections;
import org.junit.Before;
import org.junit.Test;
import org.mockito.InOrder;
@@ -17,7 +15,6 @@ import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.factory.ai.model.ModelGateway;
import com.ruoyi.generator.factory.ai.model.ModelGatewayRouter;
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
import com.ruoyi.generator.factory.ai.prompt.model.PromptTemplate;
import com.ruoyi.generator.factory.ai.prompt.model.PromptVersion;
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
@@ -38,8 +35,7 @@ public class PromptTemplateServiceImplTest
gatewayRouter = mock(ModelGatewayRouter.class);
fingerprints = new PromptFingerprintService(
new CanonicalFingerprintService(new ProjectSpecJsonCodec()));
service = new PromptTemplateServiceImpl(mapper, fingerprints, gatewayRouter,
new BuiltinPromptRegistry(fingerprints));
service = new PromptTemplateServiceImpl(mapper, fingerprints, gatewayRouter);
}
@Test
@@ -126,7 +122,7 @@ public class PromptTemplateServiceImplTest
}
@Test
public void rejectsUnimplementedUserPromptContractBeforePublication()
public void publishesContractDefinedByManagedVersion()
{
PromptVersion draft = version(202L, PromptVersion.STATUS_DRAFT);
draft.setUserPromptContract("context-json:database:v2");
@@ -134,88 +130,11 @@ public class PromptTemplateServiceImplTest
draft.getVersion(), draft.getSystemPrompt(), draft.getUserPromptContract()));
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template());
when(mapper.selectPromptVersionById(202L)).thenReturn(draft);
try
{
service.publishPromptVersion(101L, 202L, "admin");
fail("Expected unsupported builder contract");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("not implemented"));
}
}
@Test
public void initializesCurrentBuiltinPromptAsPublishedV1OnlyForEmptyTemplate()
{
PromptTemplate template = template();
when(mapper.selectPromptTemplateList(any(PromptTemplate.class)))
.thenReturn(Collections.singletonList(template));
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template);
when(mapper.selectMaxVersionNo(101L)).thenReturn(0);
when(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
when(mapper.insertPromptVersion(any(PromptVersion.class))).thenAnswer(invocation ->
{
PromptVersion value = invocation.getArgument(0);
value.setPromptVersionId(301L);
return 1;
});
when(mapper.publishPromptVersion(301L, "admin")).thenReturn(1);
when(mapper.updateCurrentVersion(101L, 301L, "admin")).thenReturn(1);
when(mapper.publishPromptVersion(202L, "admin")).thenReturn(1);
when(mapper.updateCurrentVersion(101L, 202L, "admin")).thenReturn(1);
assertEquals(1, service.initializeBuiltinV1("admin"));
InOrder order = inOrder(mapper);
order.verify(mapper).insertPromptVersion(any(PromptVersion.class));
order.verify(mapper).publishPromptVersion(301L, "admin");
order.verify(mapper).updateCurrentVersion(101L, 301L, "admin");
}
@Test
public void repairsOnlyTheOriginalGenericSystemInitializedV1()
{
PromptTemplate template = template();
PromptVersion oldV1 = version(301L, PromptVersion.STATUS_PUBLISHED);
oldV1.setVersionNo(1);
oldV1.setVersion("V1");
oldV1.setUserPromptContract("legacy-builder:database:v1");
oldV1.setRemark("由当前代码内置 Prompt 初始化");
when(mapper.selectPromptTemplateList(any(PromptTemplate.class)))
.thenReturn(Collections.singletonList(template));
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template);
when(mapper.selectMaxVersionNo(101L)).thenReturn(1);
when(mapper.selectPromptVersions(101L)).thenReturn(Collections.singletonList(oldV1));
when(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
when(mapper.updateInitializedV1(oldV1)).thenReturn(1);
assertEquals(1, service.initializeBuiltinV1("admin"));
assertTrue(oldV1.getSystemPrompt().contains("MySQL"));
assertEquals("由当前代码内置业务 Prompt 初始化", oldV1.getRemark());
verify(mapper).updateInitializedV1(oldV1);
}
@Test
public void repairsTheOriginalBusinessSystemInitializedLegacyV1()
{
PromptTemplate template = template();
PromptVersion oldV1 = version(301L, PromptVersion.STATUS_PUBLISHED);
oldV1.setVersionNo(1);
oldV1.setVersion("V1");
oldV1.setUserPromptContract("legacy-builder:database:v1");
oldV1.setRemark("由当前代码内置业务 Prompt 初始化");
when(mapper.selectPromptTemplateList(any(PromptTemplate.class)))
.thenReturn(Collections.singletonList(template));
when(mapper.selectPromptTemplateForUpdate(101L)).thenReturn(template);
when(mapper.selectMaxVersionNo(101L)).thenReturn(1);
when(mapper.selectPromptVersions(101L)).thenReturn(Collections.singletonList(oldV1));
when(gatewayRouter.resolve("deepseek")).thenReturn(mock(ModelGateway.class));
when(mapper.updateInitializedV1(oldV1)).thenReturn(1);
assertEquals(1, service.initializeBuiltinV1("admin"));
assertEquals("context-json:database:v1", oldV1.getUserPromptContract());
assertEquals("由当前代码内置业务 Prompt 初始化", oldV1.getRemark());
verify(mapper).updateInitializedV1(oldV1);
assertEquals(1, service.publishPromptVersion(101L, 202L, "admin"));
}
private PromptTemplate template()

View File

@@ -7,11 +7,6 @@ import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import org.junit.Test;
import com.ruoyi.generator.factory.ai.prompt.BuiltinPromptRegistry;
import com.ruoyi.generator.factory.ai.prompt.PromptFingerprintService;
import com.ruoyi.generator.factory.ai.prompt.PromptRelease;
import com.ruoyi.generator.factory.dsl.ProjectSpecJsonCodec;
import com.ruoyi.generator.factory.generation.CanonicalFingerprintService;
public class AiPromptExternalizationTest
{
@@ -30,35 +25,19 @@ public class AiPromptExternalizationTest
}
@Test
public void everyExecutableGenerationTypeOwnsManagedTemplates()
public void runtimeRegistryHasNoBuiltinPromptFallback() throws Exception
{
BuiltinPromptRegistry registry = new BuiltinPromptRegistry(new PromptFingerprintService(
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
String[] types = new String[] { "requirement", "app_blueprint", "flow_config", "database",
"diagram_spec", "paper_outline", "paper_draft", "code_analysis" };
Path source = resolve(
"ruoyi-generator/src/main/java/com/ruoyi/generator/factory/ai/prompt/DatabasePromptRegistry.java",
"src/main/java/com/ruoyi/generator/factory/ai/prompt/DatabasePromptRegistry.java");
String content = new String(Files.readAllBytes(source), StandardCharsets.UTF_8);
for (String type : types)
{
PromptRelease release = registry.resolve(type);
assertTrue(type, release.getSystemPrompt() != null && !release.getSystemPrompt().trim().isEmpty());
assertTrue(type, release.getUserPromptTemplate() != null
&& release.getUserPromptTemplate().contains("{{input}}"));
}
}
@Test
public void flowConfigPromptRequiresExplicitStateTransitions()
{
BuiltinPromptRegistry registry = new BuiltinPromptRegistry(new PromptFingerprintService(
new CanonicalFingerprintService(new ProjectSpecJsonCodec())));
PromptRelease release = registry.resolve("flow_config");
assertTrue(release.getSystemPrompt().contains("每个 action 必须有 from 和 to"));
assertTrue(release.getSystemPrompt().contains("不能作为前端可传字段"));
assertTrue(release.getSystemPrompt().contains("metrics.status 如果填写"));
assertTrue(release.getSystemPrompt().contains("create_time、created_at 等创建时间字段必须写为 now"));
assertTrue(release.getSystemPrompt().contains("\"create_time\":\"now\""));
assertTrue(content.contains("selectPublishedByGenerateType"));
assertFalse(content.contains("BuiltinPromptRegistry"));
assertFalse(content.contains("PromptRegistryProperties"));
Path resources = resolve("ruoyi-generator/src/main/resources/factory-prompts",
"src/main/resources/factory-prompts");
assertFalse(Files.exists(resources.resolve("default/system.txt")));
}
private Path resolve(String repositoryPath, String modulePath)

View File

@@ -0,0 +1,126 @@
package com.ruoyi.generator.service.front;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import com.alibaba.fastjson2.JSON;
import com.ruoyi.generator.domain.front.dto.AppBlueprintDesign;
import com.ruoyi.generator.domain.front.dto.AppPageDesign;
import com.ruoyi.generator.domain.front.dto.BusinessLoopPlan;
import com.ruoyi.generator.domain.front.dto.SimpleFlowConfig;
import com.ruoyi.generator.factory.flow.FlowConfigCompilation;
public class FlowConfigPageBindingReconcilerTest
{
@Test
public void createsGenericResourceDetailPageAndMovesInitiatingAction()
{
AppBlueprintDesign blueprint = blueprint(false);
SimpleFlowConfig config = flowConfig();
FlowConfigCompilation compilation = new FlowConfigContractService().canonicalize(config, blueprint);
SimpleFlowConfig.Action apply = config.getActions().get(0);
assertEquals("course_detail_page", apply.getButton().getPageCode());
assertEquals("detail.primaryActions", apply.getButton().getSlot());
AppPageDesign detail = page(blueprint, "course_detail_page");
assertNotNull(detail);
assertEquals("detail", detail.getPageType());
assertEquals("course_info", detail.getTableName());
assertEquals("course_catalog_page", detail.getParentPageCode());
assertEquals("/courses/:id", detail.getPath());
SimpleFlowConfig.Action cancel = config.getActions().get(1);
assertEquals("my_registrations_page", cancel.getButton().getPageCode());
assertEquals("list.rowActions", cancel.getButton().getSlot());
BusinessLoopPlan.BusinessLoopPageBinding binding = compilation.getLoopPlan().getPageBindings().get(0);
assertEquals("course_detail_page", binding.getPageCode());
assertEquals("detail.primaryActions", binding.getSlot());
}
@Test
public void reusesExistingDetailPageWithoutCreatingDuplicate()
{
AppBlueprintDesign blueprint = blueprint(true);
SimpleFlowConfig config = flowConfig();
boolean changed = new FlowConfigPageBindingReconciler().reconcile(config, blueprint);
assertTrue(changed);
assertEquals("custom_course_view", config.getActions().get(0).getButton().getPageCode());
assertEquals("detail.primaryActions", config.getActions().get(0).getButton().getSlot());
assertEquals(3, blueprint.getFrontendPages().size());
}
@Test(expected = com.ruoyi.common.exception.ServiceException.class)
public void rejectsSlotThatDoesNotMatchBoundPageType()
{
AppBlueprintDesign blueprint = blueprint(false);
SimpleFlowConfig config = flowConfig();
SimpleFlowConfig.Action cancel = config.getActions().get(1);
cancel.getButton().setSlot("detail.primaryActions");
new FlowConfigContractService().canonicalize(config, blueprint);
}
private AppBlueprintDesign blueprint(boolean includeDetail)
{
AppBlueprintDesign blueprint = new AppBlueprintDesign();
blueprint.getFrontendPages().add(page("course_catalog_page", "课程目录", "/courses", "list",
"course_info", "course_catalog", ""));
blueprint.getFrontendPages().add(page("my_registrations_page", "我的报名", "/my/registrations",
"current_user_list", "registration", "my_registrations", ""));
if (includeDetail)
{
blueprint.getFrontendPages().add(page("custom_course_view", "课程介绍", "/courses/:id",
"detail", "course_info", "course_catalog", "course_catalog_page"));
}
return blueprint;
}
private AppPageDesign page(String code, String name, String path, String type, String table,
String menuCode, String parentPageCode)
{
AppPageDesign page = new AppPageDesign();
page.setCode(code);
page.setName(name);
page.setPath(path);
page.setPageType(type);
page.setTableName(table);
page.setMenuCode(menuCode);
page.setParentPageCode(parentPageCode);
return page;
}
private AppPageDesign page(AppBlueprintDesign blueprint, String code)
{
for (AppPageDesign page : blueprint.getFrontendPages())
{
if (page != null && code.equals(page.getCode())) return page;
}
return null;
}
private SimpleFlowConfig flowConfig()
{
return JSON.parseObject("{"
+ "\"version\":\"1.1\",\"code\":\"course_registration\",\"name\":\"课程报名\","
+ "\"mainTable\":\"registration\",\"statusField\":\"status\","
+ "\"states\":[{\"code\":\"pending\",\"label\":\"待处理\"},"
+ "{\"code\":\"cancelled\",\"label\":\"已取消\"}],"
+ "\"actions\":[{\"code\":\"apply_course\",\"name\":\"报名\",\"actor\":\"student\","
+ "\"ownerTable\":\"course_info\",\"requestFields\":[\"id\"],"
+ "\"effects\":[{\"type\":\"create_record\",\"table\":\"registration\","
+ "\"values\":{\"course_id\":\"record.id\",\"student_id\":\"currentUserId\","
+ "\"status\":\"pending\"}}],\"button\":{\"scope\":\"student\","
+ "\"pageCode\":\"course_catalog_page\",\"tableName\":\"course_info\","
+ "\"slot\":\"list.rowActions\"}},{\"code\":\"cancel_registration\","
+ "\"name\":\"取消报名\",\"actor\":\"student\",\"ownerTable\":\"registration\","
+ "\"requestFields\":[\"id\"],\"from\":\"pending\",\"to\":\"cancelled\","
+ "\"effects\":[],\"button\":{\"scope\":\"student\","
+ "\"pageCode\":\"my_registrations_page\",\"tableName\":\"registration\","
+ "\"slot\":\"list.rowActions\"}}],\"metrics\":[]}", SimpleFlowConfig.class);
}
}

View File

@@ -21,6 +21,7 @@ import org.mockito.ArgumentCaptor;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.config.DeepSeekProperties;
public class HttpDeepSeekClientTest
@@ -130,6 +131,22 @@ public class HttpDeepSeekClientTest
assertTrue(request.getJSONObject("stream_options").getBooleanValue("include_usage"));
}
@Test
public void rejectsInvocationWithoutManagedSystemPrompt() throws Exception
{
HttpDeepSeekClient client = client(false);
try
{
client.chatModel("deepseek-chat", " ", "prompt");
fail("Expected an explicit managed system Prompt");
}
catch (ServiceException e)
{
assertTrue(e.getMessage().contains("published managed Prompt version"));
}
}
@Test
public void parseContentRecordsUsageFromNonStreamingResponse() throws Exception
{