60 KiB
Code Template Bundles Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Add selectable code template bundles so qing remains the default template while future templates can be added through data and metadata instead of hardcoded IDs.
Architecture: Introduce a sys_template_bundle aggregate that owns the enabled target types for a code template bundle. Persist the selected bundle on front projects as codeTemplate, carry it into GenProject, and update generation, preview, download, and one-click flows to resolve templates by bundleCode + templateType.
Tech Stack: Java 8, Spring Boot 2.5, MyBatis XML mappers, JUnit 4, Mockito, Vue 3, Element Plus, Node node:test, MySQL SQL seed scripts.
File Map
- Create
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/TemplateBundle.java: domain object forsys_template_bundle. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/TemplateBundleMapper.java: MyBatis mapper contract. - Create
ruoyi-generator/src/main/resources/mapper/generator/TemplateBundleMapper.xml: SQL queries for bundles. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/service/ITemplateBundleService.java: service contract for bundle lookup and supported target types. - Create
ruoyi-generator/src/main/java/com/ruoyi/generator/service/TemplateBundleServiceImpl.java: default bundle fallback and enabled type logic. - Create
ruoyi-generator/src/test/java/com/ruoyi/generator/service/TemplateBundleServiceImplTest.java: bundle service unit coverage. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/Template.java: addbundleCode. - Modify
ruoyi-generator/src/main/resources/mapper/generator/TemplateMapper.xml: select, filter, insert, and updatebundle_code. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java: addcodeTemplate. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java: addcodeTemplate. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java: addcodeTemplate. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/AiGenerationTaskCreateRequest.java: addcodeTemplate. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java: add transientcodeTemplate. - Modify
ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml: map and persistcode_template. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java: validate/default selected code template. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java: copycodeTemplateintoGenProject. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java: resolve active template by bundle code and supported type. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectPreviewService.java: expose supported template types. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java: use supported types for validation and download-all. - Modify
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java: preview only supported types. - Modify
ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java: expose bundle options and project template types. - Modify
sql/code_template_bundles.sql: create template bundle migration and qing seed. - Modify
sql/db.sql: fold migration into full initialization. - Modify
sql/qing_templates.sql: setbundle_code = 'qing'for qing templates. - Modify
sql/front_project_upgrade.sql: add front project column for older databases. - Modify
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java: project default and persistence tests. - Modify
ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java: bundle selection tests. - Modify
ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java: SQL seed assertions. - Modify
easycode-web/src/api/project.js: add bundle and supported-type APIs. - Modify
easycode-web/src/views/GenerateView.vue: show code template select and submitcodeTemplate. - Modify
easycode-web/src/views/PreviewView.vue: fetch and render supported template types dynamically. - Modify
easycode-web/src/views/generateView.test.mjs: source tests for create form payload. - Modify
easycode-web/src/views/previewView.test.mjs: source tests for dynamic preview tabs.
Task 1: Add Template Bundle Domain And Service
Files:
-
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/TemplateBundle.java -
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/TemplateBundleMapper.java -
Create:
ruoyi-generator/src/main/resources/mapper/generator/TemplateBundleMapper.xml -
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/ITemplateBundleService.java -
Create:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/TemplateBundleServiceImpl.java -
Create:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/TemplateBundleServiceImplTest.java -
Step 1: Write failing service tests
Create ruoyi-generator/src/test/java/com/ruoyi/generator/service/TemplateBundleServiceImplTest.java:
package com.ruoyi.generator.service;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.when;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.generator.domain.TemplateBundle;
import com.ruoyi.generator.mapper.TemplateBundleMapper;
public class TemplateBundleServiceImplTest
{
private TemplateBundleServiceImpl service;
@Mock
private TemplateBundleMapper templateBundleMapper;
@Before
public void setUp() throws Exception
{
MockitoAnnotations.initMocks(this);
service = new TemplateBundleServiceImpl();
Field field = TemplateBundleServiceImpl.class.getDeclaredField("templateBundleMapper");
field.setAccessible(true);
field.set(service, templateBundleMapper);
}
@Test
public void getDefaultBundleReturnsEnabledDefault()
{
TemplateBundle qing = bundle("qing", "0", "1", true, true, true, true);
when(templateBundleMapper.selectDefaultTemplateBundle()).thenReturn(qing);
assertEquals("qing", service.getDefaultBundle().getBundleCode());
}
@Test
public void getDefaultBundleFallsBackToQingWhenDatabaseIsEmpty()
{
when(templateBundleMapper.selectDefaultTemplateBundle()).thenReturn(null);
TemplateBundle fallback = service.getDefaultBundle();
assertEquals("qing", fallback.getBundleCode());
assertEquals("Qing Default Template", fallback.getBundleName());
assertEquals("0", fallback.getStatus());
}
@Test
public void getRequiredEnabledBundleRejectsDisabledBundle()
{
when(templateBundleMapper.selectTemplateBundleByCode("vue3-plus"))
.thenReturn(bundle("vue3-plus", "1", "0", true, true, true, true));
try
{
service.getRequiredEnabledBundle("vue3-plus");
fail("Expected disabled bundle to fail");
}
catch (ServiceException e)
{
assertEquals("Code template bundle is disabled: vue3-plus", e.getMessage());
}
}
@Test
public void getSupportedTemplateTypesHonorsFrontendSwitch()
{
TemplateBundle qing = bundle("qing", "0", "1", true, true, true, true);
List<String> enabled = service.getSupportedTemplateTypes(qing, "1");
List<String> frontendOff = service.getSupportedTemplateTypes(qing, "0");
assertEquals(Arrays.asList("backend", "frontend", "admin_frontend", "sql"), enabled);
assertEquals(Arrays.asList("backend", "admin_frontend", "sql"), frontendOff);
}
@Test
public void assertTemplateTypeSupportedRejectsUnsupportedType()
{
TemplateBundle backendOnly = bundle("backend-only", "0", "0", true, false, false, true);
try
{
service.assertTemplateTypeSupported(backendOnly, "frontend", "1");
fail("Expected unsupported type to fail");
}
catch (ServiceException e)
{
assertEquals("Code template bundle backend-only does not support template type: frontend", e.getMessage());
}
}
private TemplateBundle bundle(String code, String status, String isDefault,
boolean backend, boolean frontend, boolean adminFrontend, boolean sql)
{
TemplateBundle bundle = new TemplateBundle();
bundle.setBundleCode(code);
bundle.setBundleName("Qing Default Template");
bundle.setStatus(status);
bundle.setIsDefault(isDefault);
bundle.setSupportBackend(backend ? "1" : "0");
bundle.setSupportFrontend(frontend ? "1" : "0");
bundle.setSupportAdminFrontend(adminFrontend ? "1" : "0");
bundle.setSupportSql(sql ? "1" : "0");
return bundle;
}
}
- Step 2: Run test to verify it fails
Run: mvn -pl ruoyi-generator -Dtest=TemplateBundleServiceImplTest test
Expected: compilation fails because TemplateBundle, TemplateBundleMapper, and TemplateBundleServiceImpl do not exist.
- Step 3: Add domain object
Create ruoyi-generator/src/main/java/com/ruoyi/generator/domain/TemplateBundle.java:
package com.ruoyi.generator.domain;
import com.ruoyi.common.core.domain.BaseEntity;
public class TemplateBundle extends BaseEntity
{
private static final long serialVersionUID = 1L;
private Long bundleId;
private String bundleCode;
private String bundleName;
private String description;
private String status;
private String isDefault;
private String supportBackend;
private String supportFrontend;
private String supportAdminFrontend;
private String supportSql;
private Integer sortOrder;
public Long getBundleId() { return bundleId; }
public void setBundleId(Long bundleId) { this.bundleId = bundleId; }
public String getBundleCode() { return bundleCode; }
public void setBundleCode(String bundleCode) { this.bundleCode = bundleCode; }
public String getBundleName() { return bundleName; }
public void setBundleName(String bundleName) { this.bundleName = bundleName; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public String getIsDefault() { return isDefault; }
public void setIsDefault(String isDefault) { this.isDefault = isDefault; }
public String getSupportBackend() { return supportBackend; }
public void setSupportBackend(String supportBackend) { this.supportBackend = supportBackend; }
public String getSupportFrontend() { return supportFrontend; }
public void setSupportFrontend(String supportFrontend) { this.supportFrontend = supportFrontend; }
public String getSupportAdminFrontend() { return supportAdminFrontend; }
public void setSupportAdminFrontend(String supportAdminFrontend) { this.supportAdminFrontend = supportAdminFrontend; }
public String getSupportSql() { return supportSql; }
public void setSupportSql(String supportSql) { this.supportSql = supportSql; }
public Integer getSortOrder() { return sortOrder; }
public void setSortOrder(Integer sortOrder) { this.sortOrder = sortOrder; }
}
- Step 4: Add mapper contract and XML
Create ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/TemplateBundleMapper.java:
package com.ruoyi.generator.mapper;
import java.util.List;
import com.ruoyi.generator.domain.TemplateBundle;
public interface TemplateBundleMapper
{
TemplateBundle selectTemplateBundleByCode(String bundleCode);
TemplateBundle selectDefaultTemplateBundle();
List<TemplateBundle> selectTemplateBundleList(TemplateBundle templateBundle);
}
Create ruoyi-generator/src/main/resources/mapper/generator/TemplateBundleMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.generator.mapper.TemplateBundleMapper">
<resultMap type="com.ruoyi.generator.domain.TemplateBundle" id="TemplateBundleResult">
<id property="bundleId" column="bundle_id" />
<result property="bundleCode" column="bundle_code" />
<result property="bundleName" column="bundle_name" />
<result property="description" column="description" />
<result property="status" column="status" />
<result property="isDefault" column="is_default" />
<result property="supportBackend" column="support_backend" />
<result property="supportFrontend" column="support_frontend" />
<result property="supportAdminFrontend" column="support_admin_frontend" />
<result property="supportSql" column="support_sql" />
<result property="sortOrder" column="sort_order" />
<result property="createBy" column="create_by" />
<result property="createTime" column="create_time" />
<result property="updateBy" column="update_by" />
<result property="updateTime" column="update_time" />
<result property="remark" column="remark" />
</resultMap>
<sql id="selectTemplateBundleVo">
select bundle_id, bundle_code, bundle_name, description, status, is_default,
support_backend, support_frontend, support_admin_frontend, support_sql,
sort_order, create_by, create_time, update_by, update_time, remark
from sys_template_bundle
</sql>
<select id="selectTemplateBundleByCode" parameterType="String" resultMap="TemplateBundleResult">
<include refid="selectTemplateBundleVo"/>
where bundle_code = #{bundleCode}
limit 1
</select>
<select id="selectDefaultTemplateBundle" resultMap="TemplateBundleResult">
<include refid="selectTemplateBundleVo"/>
where status = '0' and is_default = '1'
order by sort_order asc, bundle_id asc
limit 1
</select>
<select id="selectTemplateBundleList" parameterType="com.ruoyi.generator.domain.TemplateBundle" resultMap="TemplateBundleResult">
<include refid="selectTemplateBundleVo"/>
<where>
<if test="bundleCode != null and bundleCode != ''">and bundle_code = #{bundleCode}</if>
<if test="bundleName != null and bundleName != ''">and bundle_name like concat('%', #{bundleName}, '%')</if>
<if test="status != null and status != ''">and status = #{status}</if>
</where>
order by sort_order asc, bundle_id asc
</select>
</mapper>
- Step 5: Add service contract and implementation
Create ruoyi-generator/src/main/java/com/ruoyi/generator/service/ITemplateBundleService.java:
package com.ruoyi.generator.service;
import java.util.List;
import com.ruoyi.generator.domain.TemplateBundle;
public interface ITemplateBundleService
{
String DEFAULT_BUNDLE_CODE = "qing";
List<TemplateBundle> listEnabledBundles();
TemplateBundle getDefaultBundle();
TemplateBundle getRequiredEnabledBundle(String bundleCode);
List<String> getSupportedTemplateTypes(TemplateBundle bundle, String frontendEnabled);
List<String> getSupportedTemplateTypes(String bundleCode, String frontendEnabled);
void assertTemplateTypeSupported(TemplateBundle bundle, String templateType, String frontendEnabled);
}
Create ruoyi-generator/src/main/java/com/ruoyi/generator/service/TemplateBundleServiceImpl.java:
package com.ruoyi.generator.service;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.generator.domain.TemplateBundle;
import com.ruoyi.generator.mapper.TemplateBundleMapper;
@Service
public class TemplateBundleServiceImpl implements ITemplateBundleService
{
@Autowired
private TemplateBundleMapper templateBundleMapper;
@Override
public List<TemplateBundle> listEnabledBundles()
{
TemplateBundle query = new TemplateBundle();
query.setStatus("0");
return templateBundleMapper.selectTemplateBundleList(query);
}
@Override
public TemplateBundle getDefaultBundle()
{
TemplateBundle bundle = templateBundleMapper.selectDefaultTemplateBundle();
return bundle == null ? qingFallbackBundle() : bundle;
}
@Override
public TemplateBundle getRequiredEnabledBundle(String bundleCode)
{
String code = StringUtils.defaultIfEmpty(bundleCode, getDefaultBundle().getBundleCode());
TemplateBundle bundle = templateBundleMapper.selectTemplateBundleByCode(code);
if (bundle == null && DEFAULT_BUNDLE_CODE.equals(code))
{
bundle = qingFallbackBundle();
}
if (bundle == null)
{
throw new ServiceException("Code template bundle does not exist: " + code);
}
if (!"0".equals(bundle.getStatus()))
{
throw new ServiceException("Code template bundle is disabled: " + code);
}
return bundle;
}
@Override
public List<String> getSupportedTemplateTypes(String bundleCode, String frontendEnabled)
{
return getSupportedTemplateTypes(getRequiredEnabledBundle(bundleCode), frontendEnabled);
}
@Override
public List<String> getSupportedTemplateTypes(TemplateBundle bundle, String frontendEnabled)
{
List<String> types = new ArrayList<String>();
if (isSupported(bundle == null ? null : bundle.getSupportBackend()))
{
types.add("backend");
}
if (!"0".equals(frontendEnabled) && isSupported(bundle == null ? null : bundle.getSupportFrontend()))
{
types.add("frontend");
}
if (isSupported(bundle == null ? null : bundle.getSupportAdminFrontend()))
{
types.add("admin_frontend");
}
if (isSupported(bundle == null ? null : bundle.getSupportSql()))
{
types.add("sql");
}
return types;
}
@Override
public void assertTemplateTypeSupported(TemplateBundle bundle, String templateType, String frontendEnabled)
{
if (!getSupportedTemplateTypes(bundle, frontendEnabled).contains(templateType))
{
String code = bundle == null ? DEFAULT_BUNDLE_CODE : bundle.getBundleCode();
throw new ServiceException("Code template bundle " + code
+ " does not support template type: " + templateType);
}
}
private boolean isSupported(String value)
{
return !"0".equals(value);
}
private TemplateBundle qingFallbackBundle()
{
TemplateBundle bundle = new TemplateBundle();
bundle.setBundleCode(DEFAULT_BUNDLE_CODE);
bundle.setBundleName("Qing Default Template");
bundle.setStatus("0");
bundle.setIsDefault("1");
bundle.setSupportBackend("1");
bundle.setSupportFrontend("1");
bundle.setSupportAdminFrontend("1");
bundle.setSupportSql("1");
bundle.setSortOrder(0);
return bundle;
}
}
- Step 6: Run test to verify it passes
Run: mvn -pl ruoyi-generator -Dtest=TemplateBundleServiceImplTest test
Expected: PASS with all 5 tests green.
- Step 7: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/domain/TemplateBundle.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/mapper/TemplateBundleMapper.java \
ruoyi-generator/src/main/resources/mapper/generator/TemplateBundleMapper.xml \
ruoyi-generator/src/main/java/com/ruoyi/generator/service/ITemplateBundleService.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/service/TemplateBundleServiceImpl.java \
ruoyi-generator/src/test/java/com/ruoyi/generator/service/TemplateBundleServiceImplTest.java
git commit -m "feat: add template bundle service"
Task 2: Persist Bundle Codes On Templates And Projects
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/Template.java -
Modify:
ruoyi-generator/src/main/resources/mapper/generator/TemplateMapper.xml -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/AiGenerationTaskCreateRequest.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java -
Modify:
ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml -
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java -
Step 1: Write failing project persistence tests
Add these tests to FrontProjectServiceImplTest:
@Mock
private com.ruoyi.generator.service.ITemplateBundleService templateBundleService;
In setUp, add:
setField("templateBundleService", templateBundleService);
when(templateBundleService.getDefaultBundle()).thenReturn(templateBundle("qing"));
when(templateBundleService.getRequiredEnabledBundle("qing")).thenReturn(templateBundle("qing"));
when(templateBundleService.getRequiredEnabledBundle("vue3-plus")).thenReturn(templateBundle("vue3-plus"));
Add tests:
@Test
public void createProjectDefaultsToQingCodeTemplate()
{
FrontProjectCreateRequest request = new FrontProjectCreateRequest();
request.setProjectName("Demo");
FrontProject project = service.createProject(7L, request);
assertEquals("qing", project.getCodeTemplate());
verify(frontProjectMapper).insertFrontProject(project);
}
@Test
public void createProjectPersistsSelectedCodeTemplate()
{
FrontProjectCreateRequest request = new FrontProjectCreateRequest();
request.setProjectName("Demo");
request.setCodeTemplate("vue3-plus");
FrontProject project = service.createProject(7L, request);
assertEquals("vue3-plus", project.getCodeTemplate());
verify(templateBundleService).getRequiredEnabledBundle("vue3-plus");
}
@Test
public void updateProjectPersistsSelectedCodeTemplate()
{
when(frontProjectMapper.selectFrontProjectByUserAndId(7L, 10L)).thenReturn(project());
FrontProjectUpdateRequest request = new FrontProjectUpdateRequest();
request.setProjectName("Demo");
request.setCodeTemplate("vue3-plus");
service.updateProject(7L, 10L, request);
ArgumentCaptor<FrontProject> projectCaptor = ArgumentCaptor.forClass(FrontProject.class);
verify(frontProjectMapper).updateFrontProject(projectCaptor.capture());
assertEquals("vue3-plus", projectCaptor.getValue().getCodeTemplate());
}
private com.ruoyi.generator.domain.TemplateBundle templateBundle(String code)
{
com.ruoyi.generator.domain.TemplateBundle bundle = new com.ruoyi.generator.domain.TemplateBundle();
bundle.setBundleCode(code);
bundle.setBundleName(code);
bundle.setStatus("0");
bundle.setSupportBackend("1");
bundle.setSupportFrontend("1");
bundle.setSupportAdminFrontend("1");
bundle.setSupportSql("1");
return bundle;
}
- Step 2: Run tests to verify they fail
Run: mvn -pl ruoyi-generator -Dtest=FrontProjectServiceImplTest test
Expected: compilation fails because codeTemplate accessors and templateBundleService field do not exist.
- Step 3: Add fields and accessors
In Template.java, add after templateType:
private String bundleCode;
public String getBundleCode()
{
return bundleCode;
}
public void setBundleCode(String bundleCode)
{
this.bundleCode = bundleCode;
}
Also add to toString():
.append("bundleCode", getBundleCode())
In FrontProject.java, add after industryTemplate:
private String codeTemplate;
public String getCodeTemplate() { return codeTemplate; }
public void setCodeTemplate(String codeTemplate) { this.codeTemplate = codeTemplate; }
In FrontProjectCreateRequest.java, FrontProjectUpdateRequest.java, and AiGenerationTaskCreateRequest.java, add:
private String codeTemplate;
public String getCodeTemplate() { return codeTemplate; }
public void setCodeTemplate(String codeTemplate) { this.codeTemplate = codeTemplate; }
In GenProject.java, add near transient generation fields:
private transient String codeTemplate;
public String getCodeTemplate() {
return codeTemplate;
}
public void setCodeTemplate(String codeTemplate) {
this.codeTemplate = codeTemplate;
}
- Step 4: Map
bundle_codeandcode_template
In TemplateMapper.xml, update TemplateResult:
<result property="bundleCode" column="bundle_code" />
Update selectTemplateVo:
select t.template_id, t.template_name, t.template_path, t.template_desc, t.template_type, t.bundle_code, t.template_status
from sys_template t
Add filter:
<if test="bundleCode != null and bundleCode != ''">
AND t.bundle_code = #{bundleCode}
</if>
Update selectTemplateByTemplateId selected columns:
select t.template_id, t.template_name, t.template_path, t.template_desc, t.template_type, t.bundle_code, t.template_status,
tf.template_file_id, tf.file_name, tf.file_path, tf.file_content, tf.create_by, tf.create_time, tf.update_by, tf.update_time
Add bundle_code to insert and update trims:
<if test="bundleCode != null and bundleCode != ''">bundle_code,</if>
<if test="bundleCode != null and bundleCode != ''">#{bundleCode},</if>
<if test="bundleCode != null and bundleCode != ''">bundle_code = #{bundleCode},</if>
In FrontProjectMapper.xml, add result:
<result property="codeTemplate" column="code_template" />
Update selectFrontProjectVo to include code_template after industry_template:
select project_id, user_id, project_name, project_file_name, package_name, version, project_desc,
requirement_keyword, industry_template, code_template, style_preset, front_framework, back_framework,
frontend_enabled, generate_status, preview_status, app_blueprint, business_loop_plan, er_diagram,
business_blueprint, status, create_time, update_time, remark
from front_project
Add insert column/value:
<if test="codeTemplate != null">code_template,</if>
<if test="codeTemplate != null">#{codeTemplate},</if>
Add update:
<if test="codeTemplate != null">code_template = #{codeTemplate},</if>
- Step 5: Default and validate project
codeTemplate
In FrontProjectServiceImpl, add import:
import com.ruoyi.generator.service.ITemplateBundleService;
Add field:
@Autowired
private ITemplateBundleService templateBundleService;
In createProject, after project.setIndustryTemplate(request.getIndustryTemplate());, add:
project.setCodeTemplate(resolveCodeTemplate(request.getCodeTemplate()));
In updateProject, after project.setIndustryTemplate(request.getIndustryTemplate());, add:
project.setCodeTemplate(resolveNullableCodeTemplate(request.getCodeTemplate()));
Add helper methods:
private String resolveCodeTemplate(String codeTemplate)
{
String code = StringUtils.defaultIfEmpty(codeTemplate, templateBundleService.getDefaultBundle().getBundleCode());
return templateBundleService.getRequiredEnabledBundle(code).getBundleCode();
}
private String resolveNullableCodeTemplate(String codeTemplate)
{
if (codeTemplate == null)
{
return null;
}
return resolveCodeTemplate(codeTemplate);
}
In FrontProjectConverter.toGenProject, after genProject.setStylePreset(project.getStylePreset());, add:
genProject.setCodeTemplate(project.getCodeTemplate());
- Step 6: Run tests to verify they pass
Run: mvn -pl ruoyi-generator -Dtest=FrontProjectServiceImplTest test
Expected: PASS.
- Step 7: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/domain/Template.java \
ruoyi-generator/src/main/resources/mapper/generator/TemplateMapper.xml \
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/AiGenerationTaskCreateRequest.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java \
ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml \
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectServiceImpl.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectConverter.java \
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectServiceImplTest.java
git commit -m "feat: persist selected code template bundle"
Task 3: Resolve Generation Templates By Bundle
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java -
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java -
Step 1: Write failing bundle selection tests
In GenProjectServiceImplTest, add mock field:
@Mock
private ITemplateBundleService templateBundleService;
In setUp, add:
setField("templateBundleService", templateBundleService);
when(templateBundleService.getDefaultBundle()).thenReturn(bundle("qing"));
when(templateBundleService.getRequiredEnabledBundle("qing")).thenReturn(bundle("qing"));
when(templateBundleService.getRequiredEnabledBundle("vue3-plus")).thenReturn(bundle("vue3-plus"));
Add tests:
@Test
public void frontDraftTemplateSelectionUsesProjectCodeTemplate()
{
GenProject project = project();
project.setFrontDraft(true);
project.setCodeTemplate("vue3-plus");
Template vue3Backend = template(9301L, 0L);
vue3Backend.setBundleCode("vue3-plus");
Template qingBackend = template(9201L, 0L);
qingBackend.setBundleCode("qing");
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(qingBackend, vue3Backend));
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
structure(1L, 0L, "{projectName}", "folder", null, null)
));
service.getProjectStructure(project, "backend");
ArgumentCaptor<Template> filterCaptor = ArgumentCaptor.forClass(Template.class);
verify(templateService).selectTemplateList(filterCaptor.capture());
assertEquals("backend", filterCaptor.getValue().getTemplateType());
assertEquals("vue3-plus", filterCaptor.getValue().getBundleCode());
}
@Test
public void frontDraftTemplateSelectionFallsBackToDefaultBundle()
{
GenProject project = project();
project.setFrontDraft(true);
project.setCodeTemplate("");
Template qingBackend = template(9201L, 0L);
qingBackend.setBundleCode("qing");
when(templateService.selectTemplateList(any(Template.class))).thenReturn(Arrays.asList(qingBackend));
when(sysProjectStructureService.selectSysProjectStructureList(any(SysProjectStructure.class))).thenReturn(Arrays.asList(
structure(1L, 0L, "{projectName}", "folder", null, null)
));
service.getProjectStructure(project, "backend");
ArgumentCaptor<Template> filterCaptor = ArgumentCaptor.forClass(Template.class);
verify(templateService).selectTemplateList(filterCaptor.capture());
assertEquals("qing", filterCaptor.getValue().getBundleCode());
}
@Test
public void boundProjectTemplateStillWinsOverBundleDefault()
{
GenProject project = project();
project.setFrontDraft(false);
project.setCodeTemplate("vue3-plus");
SysProjectTemplate bound = projectTemplate(100L);
when(sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(1L, "backend")).thenReturn(bound);
when(sysProjectStructureService.selectProjectStructureListByModule(1L, 100L)).thenReturn(Arrays.asList(
structure(1L, 0L, "{projectName}", "folder", null, null)
));
service.getProjectStructure(project, "backend");
verify(templateService, never()).selectTemplateList(any(Template.class));
}
private com.ruoyi.generator.domain.TemplateBundle bundle(String code)
{
com.ruoyi.generator.domain.TemplateBundle bundle = new com.ruoyi.generator.domain.TemplateBundle();
bundle.setBundleCode(code);
bundle.setStatus("0");
bundle.setSupportBackend("1");
bundle.setSupportFrontend("1");
bundle.setSupportAdminFrontend("1");
bundle.setSupportSql("1");
return bundle;
}
- Step 2: Run tests to verify they fail
Run: mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test
Expected: compilation fails until Task 2 exists, then assertion fails because resolveActiveTemplate ignores bundleCode.
- Step 3: Inject template bundle service
In GenProjectServiceImpl, add imports:
import com.ruoyi.generator.domain.TemplateBundle;
Add field:
@Autowired
private ITemplateBundleService templateBundleService;
- Step 4: Replace hardcoded runnable template preference
Remove constants:
private static final Long RUNNABLE_BACKEND_TEMPLATE_ID = 9201L;
private static final Long RUNNABLE_ADMIN_FRONTEND_TEMPLATE_ID = 9202L;
private static final Long RUNNABLE_FRONTEND_TEMPLATE_ID = 9203L;
Replace resolveActiveTemplate and remove preferredRunnableTemplateId:
private SysProjectTemplate resolveActiveTemplate(Long projectId, String type) {
GenProject project = null;
if (projectId != null) {
project = genProjectMapper.selectGenProjectById(projectId);
}
String bundleCode = project == null ? null : project.getCodeTemplate();
return resolveActiveTemplate(projectId, type, bundleCode);
}
private SysProjectTemplate resolveActiveTemplate(Long projectId, String type, String bundleCode) {
TemplateBundle bundle = templateBundleService.getRequiredEnabledBundle(
StringUtils.defaultIfEmpty(bundleCode, templateBundleService.getDefaultBundle().getBundleCode()));
templateBundleService.assertTemplateTypeSupported(bundle, type, null);
Template templateFilter = new Template();
templateFilter.setTemplateType(type);
templateFilter.setBundleCode(bundle.getBundleCode());
List<Template> templates = templateService.selectTemplateList(templateFilter);
if (StringUtils.isNotEmpty(templates)) {
for (Template template : templates) {
if (template != null && template.getTemplateId() != null
&& Long.valueOf(0L).equals(template.getTemplateStatus())) {
return toProjectTemplate(projectId, type, template);
}
}
}
throw new ServiceException("No enabled template configured for bundle "
+ bundle.getBundleCode() + " and type " + type);
}
Then update resolveProjectTemplate fallback:
return resolveActiveTemplate(project.getProjectId(), type, project.getCodeTemplate());
Keep the existing bound project-template branch before this fallback.
- Step 5: Avoid database reload for front draft projects
If the Step 4 overload still calls genProjectMapper during front draft tests, adjust the method so the active fallback receives project.getCodeTemplate() directly:
private SysProjectTemplate resolveProjectTemplate(GenProject project, String type) {
if (!project.isFrontDraft()) {
SysProjectTemplate projectTemplate = sysProjectTemplateService.selectSysProjectTemplateByProjectAndType(project.getProjectId(), type);
if (projectTemplate != null && projectTemplate.getTemplateId() != null) {
return projectTemplate;
}
}
return resolveActiveTemplate(project.getProjectId(), type, project.getCodeTemplate());
}
- Step 6: Run tests to verify they pass
Run: mvn -pl ruoyi-generator -Dtest=GenProjectServiceImplTest test
Expected: PASS.
- Step 7: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/GenProjectServiceImpl.java \
ruoyi-generator/src/test/java/com/ruoyi/generator/service/GenProjectServiceImplTest.java
git commit -m "feat: resolve generation templates by bundle"
Task 4: Expose Supported Types And Use Them In Preview Downloads
Files:
-
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectPreviewService.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java -
Modify:
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java -
Modify:
ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java -
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java -
Step 1: Write failing preview service test
In FrontProjectPreviewServiceImplTest, add a mock:
@Mock
private com.ruoyi.generator.service.ITemplateBundleService templateBundleService;
In setUp, inject it:
setField("templateBundleService", templateBundleService);
Add test:
@Test
public void supportedTemplateTypesComeFromSelectedBundleAndFrontendSwitch()
{
FrontProject project = project();
project.setCodeTemplate("backend-only");
project.setFrontendEnabled("0");
when(frontProjectService.getProject(7L, 10L)).thenReturn(project);
when(templateBundleService.getSupportedTemplateTypes("backend-only", "0"))
.thenReturn(Arrays.asList("backend", "sql"));
List<String> types = service.getSupportedTemplateTypes(7L, 10L);
assertEquals(Arrays.asList("backend", "sql"), types);
}
- Step 2: Run test to verify it fails
Run: mvn -pl ruoyi-generator -Dtest=FrontProjectPreviewServiceImplTest test
Expected: compilation fails because getSupportedTemplateTypes is missing.
- Step 3: Add service method
In IFrontProjectPreviewService, add:
public List<String> getSupportedTemplateTypes(Long userId, Long projectId);
In FrontProjectPreviewServiceImpl, add field:
@Autowired
private com.ruoyi.generator.service.ITemplateBundleService templateBundleService;
Add implementation:
@Override
public List<String> getSupportedTemplateTypes(Long userId, Long projectId)
{
FrontProject frontProject = frontProjectService.getProject(userId, projectId);
return templateBundleService.getSupportedTemplateTypes(frontProject.getCodeTemplate(),
frontProject.getFrontendEnabled());
}
Update assertTemplateTypeEnabled:
private void assertTemplateTypeEnabled(GenProject project, String templateType)
{
com.ruoyi.generator.domain.TemplateBundle bundle = templateBundleService.getRequiredEnabledBundle(
project == null ? null : project.getCodeTemplate());
templateBundleService.assertTemplateTypeSupported(bundle, templateType,
project == null ? null : project.getFrontendEnabled());
}
- Step 4: Update download-all
Replace fixed calls in downloadAll:
appendZip(zip, genProjectService.downloadStructure(genProject, "backend"), zipEntries);
if (isFrontendEnabled(genProject))
{
appendZip(zip, genProjectService.downloadStructure(genProject, "frontend"), zipEntries);
}
appendZip(zip, genProjectService.downloadStructure(genProject, "admin_frontend"), zipEntries);
appendZip(zip, genProjectService.downloadStructure(genProject, "sql"), zipEntries);
with:
List<String> types = templateBundleService.getSupportedTemplateTypes(genProject.getCodeTemplate(),
genProject.getFrontendEnabled());
for (String type : types)
{
appendZip(zip, genProjectService.downloadStructure(genProject, type), zipEntries);
}
- Step 5: Update controller endpoints
In FrontProjectController, add field:
@Autowired
private com.ruoyi.generator.service.ITemplateBundleService templateBundleService;
Add endpoints near /options/modules:
@GetMapping("/options/code-templates")
public AjaxResult codeTemplates()
{
return AjaxResult.success(templateBundleService.listEnabledBundles());
}
@GetMapping("/{projectId}/template-types")
public AjaxResult templateTypes(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
return AjaxResult.success(frontProjectPreviewService.getSupportedTemplateTypes(userId, projectId));
}
Update preview action to use supported types:
@PostMapping("/{projectId}/preview")
public AjaxResult preview(@PathVariable Long projectId)
{
Long userId = SecurityUtils.getUserId();
for (String type : frontProjectPreviewService.getSupportedTemplateTypes(userId, projectId))
{
frontProjectPreviewService.getStructure(userId, projectId, type);
}
return toAjax(frontProjectPreviewService.markPreviewReady(userId, projectId));
}
- Step 6: Update one-click source preview
In OneClickProjectGenerationServiceImpl, replace fixed calls:
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "backend");
if (frontendEnabled)
{
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "frontend");
}
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "admin_frontend");
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), "sql");
with:
for (String type : frontProjectPreviewService.getSupportedTemplateTypes(task.getUserId(), task.getProjectId()))
{
frontProjectPreviewService.getStructure(task.getUserId(), task.getProjectId(), type);
}
- Step 7: Run tests
Run:
mvn -pl ruoyi-generator -Dtest=FrontProjectPreviewServiceImplTest test
mvn -pl ruoyi-generator -Dtest=OneClickProjectGenerationServiceImplTest test
Expected: PASS.
- Step 8: Commit
git add ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/IFrontProjectPreviewService.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImpl.java \
ruoyi-generator/src/main/java/com/ruoyi/generator/service/front/OneClickProjectGenerationServiceImpl.java \
ruoyi-admin/src/main/java/com/ruoyi/web/controller/front/FrontProjectController.java \
ruoyi-generator/src/test/java/com/ruoyi/generator/service/front/FrontProjectPreviewServiceImplTest.java
git commit -m "feat: expose supported code template types"
Task 5: Add SQL Migration And Qing Seed Updates
Files:
-
Create:
sql/code_template_bundles.sql -
Modify:
sql/front_project_upgrade.sql -
Modify:
sql/qing_templates.sql -
Modify:
sql/db.sql -
Modify:
ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java -
Step 1: Write failing SQL seed test
Add to QingTemplateSupportTest:
@Test
public void databaseSeedsCodeTemplateBundleMetadata() throws Exception
{
String dbSql = read(repoRoot().resolve("sql/db.sql"));
String qingSql = read(repoRoot().resolve("sql/qing_templates.sql"));
String bundleSql = read(repoRoot().resolve("sql/code_template_bundles.sql"));
assertTrue(dbSql.contains("create table sys_template_bundle"));
assertTrue(dbSql.contains("bundle_code"));
assertTrue(dbSql.contains("'qing', 'Qing Default Template'"));
assertTrue(dbSql.contains("alter table sys_template add column bundle_code"));
assertTrue(dbSql.contains("alter table front_project add column code_template"));
assertTrue(qingSql.contains("bundle_code"));
assertTrue(qingSql.contains("'qing'"));
assertTrue(bundleSql.contains("sys_template_bundle"));
}
- Step 2: Run test to verify it fails
Run: mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest#databaseSeedsCodeTemplateBundleMetadata test
Expected: FAIL because sql/code_template_bundles.sql is missing and SQL files do not contain bundle metadata.
- Step 3: Create standalone migration
Create sql/code_template_bundles.sql:
drop table if exists sys_template_bundle;
create table sys_template_bundle (
bundle_id bigint(20) not null auto_increment comment 'template bundle id',
bundle_code varchar(64) not null comment 'template bundle code',
bundle_name varchar(100) not null comment 'template bundle name',
description varchar(500) default null comment 'description',
status char(1) not null default '0' comment 'status 0 normal 1 disabled',
is_default char(1) not null default '0' comment 'default bundle 1 yes 0 no',
support_backend char(1) not null default '1' comment 'supports backend',
support_frontend char(1) not null default '1' comment 'supports frontend',
support_admin_frontend char(1) not null default '1' comment 'supports admin frontend',
support_sql char(1) not null default '1' comment 'supports sql',
sort_order int default 0 comment 'sort order',
create_by varchar(64) default '' comment 'creator',
create_time datetime comment 'create time',
update_by varchar(64) default '' comment 'updater',
update_time datetime comment 'update time',
remark varchar(500) default null comment 'remark',
primary key (bundle_id),
unique key uk_template_bundle_code (bundle_code)
) engine=innodb auto_increment=100 comment='code template bundle table';
insert into sys_template_bundle
(bundle_id, bundle_code, bundle_name, description, status, is_default, support_backend,
support_frontend, support_admin_frontend, support_sql, sort_order, create_by, create_time, remark)
values
(1, 'qing', 'Qing Default Template', 'Spring Boot + MyBatis Plus + Vue2 runnable template', '0', '1',
'1', '1', '1', '1', 1, 'admin', sysdate(), 'default code template bundle');
alter table sys_template add column bundle_code varchar(64) default 'qing' comment 'template bundle code';
update sys_template set bundle_code = 'qing' where template_id in (9201, 9202, 9203);
alter table front_project add column code_template varchar(64) default 'qing' comment 'code template bundle';
update front_project set code_template = 'qing' where code_template is null or code_template = '';
For MySQL scripts that may run repeatedly, if the project already uses non-idempotent migration scripts, keep this style. If existing scripts use guarded changes, wrap alter table with the repository's established guard pattern.
- Step 4: Update qing template inserts
In sql/qing_templates.sql, change the INSERT INTO sys_template column list from:
INSERT INTO sys_template (template_id, template_name, template_path, template_desc, template_type, template_status, create_by, create_time, remark) VALUES
to:
INSERT INTO sys_template (template_id, template_name, template_path, template_desc, template_type, bundle_code, template_status, create_by, create_time, remark) VALUES
Change row values:
(9201, 'Runnable Backend Template', '/runnable/backend', 'Spring Boot + MyBatis Plus runnable backend', 'backend', 'qing', 0, 'admin', sysdate(), 'three-tier runnable template'),
(9202, 'Runnable Admin Frontend Template', '/runnable/admin_frontend', 'Vue2 + Element UI runnable admin frontend', 'admin_frontend', 'qing', 0, 'admin', sysdate(), 'three-tier runnable template'),
(9203, 'Runnable Portal Frontend Template', '/runnable/frontend', 'Vue2 + Element UI runnable portal frontend', 'frontend', 'qing', 0, 'admin', sysdate(), 'three-tier runnable template');
- Step 5: Update full initialization SQL
In sql/db.sql:
- Add
sys_template_bundletable nearsys_template. - Add
bundle_code varchar(64) default 'qing' comment 'template bundle code'tosys_template. - Add
code_template varchar(64) default 'qing' comment 'code template bundle'tofront_project. - Insert the qing bundle seed before qing templates.
- Update all qing
sys_templateinserts withbundle_code = 'qing'.
- Step 6: Update front project upgrade script
In sql/front_project_upgrade.sql, add:
alter table front_project add column code_template varchar(64) default 'qing' comment 'code template bundle';
update front_project set code_template = 'qing' where code_template is null or code_template = '';
- Step 7: Run SQL seed test
Run: mvn -pl ruoyi-generator -Dtest=QingTemplateSupportTest#databaseSeedsCodeTemplateBundleMetadata test
Expected: PASS.
- Step 8: Commit
git add sql/code_template_bundles.sql \
sql/front_project_upgrade.sql \
sql/qing_templates.sql \
sql/db.sql \
ruoyi-generator/src/test/java/com/ruoyi/generator/util/QingTemplateSupportTest.java
git commit -m "feat: seed qing code template bundle"
Task 6: Add Frontend Bundle APIs And Project Form Selection
Files:
-
Modify:
easycode-web/src/api/project.js -
Modify:
easycode-web/src/views/GenerateView.vue -
Modify:
easycode-web/src/views/generateView.test.mjs -
Step 1: Write failing frontend source tests
Add to easycode-web/src/views/generateView.test.mjs:
test('generate page exposes code template bundle selection and submits it', () => {
const source = readSource('views/GenerateView.vue')
const apiSource = readSource('api/project.js')
const projectPayload = source.match(/function projectPayload\(\) \{([\s\S]*?)\n\}/)
const oneClickPayload = source.match(/function oneClickTaskPayload\(\) \{([\s\S]*?)\n\}/)
assert.match(apiSource, /export function listCodeTemplateBundles\(\)/)
assert.match(apiSource, /url: '\/front\/project\/options\/code-templates'/)
assert.match(apiSource, /export function getProjectSupportedTemplateTypes\(projectId\)/)
assert.match(source, /const CODE_TEMPLATE_FALLBACK = 'qing'/)
assert.match(source, /const codeTemplateBundles = ref\(\[\]\)/)
assert.match(source, /projectForm\.codeTemplate/)
assert.match(source, /loadCodeTemplateBundles/)
assert.notEqual(projectPayload, null)
assert.match(projectPayload[1], /codeTemplate: projectForm\.codeTemplate \|\| CODE_TEMPLATE_FALLBACK/)
assert.notEqual(oneClickPayload, null)
assert.match(oneClickPayload[1], /codeTemplate: projectForm\.codeTemplate \|\| CODE_TEMPLATE_FALLBACK/)
})
- Step 2: Run frontend test to verify it fails
Run:
cd easycode-web
node --test src/views/generateView.test.mjs
Expected: FAIL because APIs and form state are missing.
- Step 3: Add API functions
In easycode-web/src/api/project.js, add:
export function listCodeTemplateBundles() {
return request({
url: '/front/project/options/code-templates',
method: 'get'
}).then(unwrap)
}
export function getProjectSupportedTemplateTypes(projectId) {
return request({
url: `/front/project/${projectId}/template-types`,
method: 'get'
}).then(unwrap)
}
- Step 4: Add GenerateView state and loading
In GenerateView.vue, add imports:
import { listCodeTemplateBundles } from '@/api/project'
If there is already a grouped import from @/api/project, add listCodeTemplateBundles to that list.
Add constants and state:
const CODE_TEMPLATE_FALLBACK = 'qing'
const codeTemplateBundles = ref([])
Add codeTemplate to projectForm:
codeTemplate: CODE_TEMPLATE_FALLBACK,
Add loader:
async function loadCodeTemplateBundles() {
try {
const result = await listCodeTemplateBundles()
const bundles = Array.isArray(result) ? result : []
codeTemplateBundles.value = bundles
if (!projectForm.codeTemplate && bundles.length) {
const defaultBundle = bundles.find((bundle) => bundle.isDefault === '1')
projectForm.codeTemplate = defaultBundle?.bundleCode || bundles[0].bundleCode || CODE_TEMPLATE_FALLBACK
}
} catch (error) {
codeTemplateBundles.value = [{
bundleCode: CODE_TEMPLATE_FALLBACK,
bundleName: 'Qing Default Template',
description: 'Spring Boot + Vue2 runnable template'
}]
projectForm.codeTemplate = projectForm.codeTemplate || CODE_TEMPLATE_FALLBACK
}
}
Call it from onMounted next to existing option loaders:
await loadCodeTemplateBundles()
- Step 5: Add project payload fields
In projectPayload(), add:
codeTemplate: projectForm.codeTemplate || CODE_TEMPLATE_FALLBACK,
In oneClickTaskPayload(), add:
codeTemplate: projectForm.codeTemplate || CODE_TEMPLATE_FALLBACK,
If aiTaskPayload(generateType) sends common project metadata, add the same field there too:
codeTemplate: projectForm.codeTemplate || CODE_TEMPLATE_FALLBACK,
- Step 6: Add select control
Place this near the visual theme selector in the one-click form:
<el-form-item label="代码模板">
<el-select v-model="projectForm.codeTemplate">
<el-option
v-for="bundle in codeTemplateBundles"
:key="bundle.bundleCode"
:label="bundle.bundleName || bundle.bundleCode"
:value="bundle.bundleCode"
>
<div class="code-template-option">
<strong>{{ bundle.bundleName || bundle.bundleCode }}</strong>
<span>{{ bundle.description || bundle.bundleCode }}</span>
</div>
</el-option>
</el-select>
</el-form-item>
Add scoped style if the file already has style blocks:
.code-template-option {
display: grid;
gap: 2px;
line-height: 1.35;
}
.code-template-option strong {
color: #303133;
font-size: 13px;
}
.code-template-option span {
color: #909399;
font-size: 12px;
}
- Step 7: Run frontend test
Run:
cd easycode-web
node --test src/views/generateView.test.mjs
Expected: PASS.
- Step 8: Commit
git add easycode-web/src/api/project.js \
easycode-web/src/views/GenerateView.vue \
easycode-web/src/views/generateView.test.mjs
git commit -m "feat: choose code template bundle in generator"
Task 7: Make Preview Tabs Dynamic
Files:
-
Modify:
easycode-web/src/views/PreviewView.vue -
Modify:
easycode-web/src/views/previewView.test.mjs -
Step 1: Write failing preview source test
Add to previewView.test.mjs:
test('preview page loads template types from backend', () => {
const source = readView('PreviewView.vue')
const apiSource = readSource('api', 'project.js')
assert.match(apiSource, /getProjectSupportedTemplateTypes/)
assert.match(source, /getProjectSupportedTemplateTypes/)
assert.match(source, /const supportedTemplateTypes = ref\(\[\]\)/)
assert.match(source, /function normalizeTemplateTypes/)
assert.match(source, /await getProjectSupportedTemplateTypes\(projectId\.value\)/)
assert.doesNotMatch(source, /const types = \[\{ label: '.*', value: 'backend' \}\]/)
})
- Step 2: Run test to verify it fails
Run:
cd easycode-web
node --test src/views/previewView.test.mjs
Expected: FAIL because preview tabs are still locally fixed.
- Step 3: Import backend API and add state
In PreviewView.vue, add getProjectSupportedTemplateTypes to the existing API import:
import { downloadProject, getFileContent, getProject, getProjectStructure, getProjectSupportedTemplateTypes, getRunPreviewStatus, startRunPreview, stopRunPreview } from '@/api/project'
Add:
const supportedTemplateTypes = ref([])
Replace fixed templateTypes computed with:
const TEMPLATE_TYPE_LABELS = {
backend: '后端',
frontend: '前台前端',
admin_frontend: '后台前端',
sql: 'SQL'
}
const templateTypes = computed(() => normalizeTemplateTypes(supportedTemplateTypes.value))
function normalizeTemplateTypes(types) {
const values = Array.isArray(types) && types.length ? types : ['backend', 'frontend', 'admin_frontend']
return values
.filter((value) => value !== 'frontend' || projectConfig.value.frontendEnabled !== '0')
.map((value) => ({
label: TEMPLATE_TYPE_LABELS[value] || value,
value
}))
}
- Step 4: Load supported types with project config
In loadProjectConfig, after setting projectConfig.value, add:
const supportedResult = await getProjectSupportedTemplateTypes(projectId.value)
supportedTemplateTypes.value = unwrap(supportedResult) || []
Replace hard reset:
structures.backend = []
structures.frontend = []
structures.admin_frontend = []
with:
Object.keys(structures).forEach((key) => {
structures[key] = []
})
templateTypes.value.forEach((type) => {
if (!Object.prototype.hasOwnProperty.call(structures, type.value)) {
structures[type.value] = []
}
})
Initialize structures with known defaults and allow dynamic keys:
const structures = reactive({
backend: [],
frontend: [],
admin_frontend: [],
sql: []
})
- Step 5: Keep active tab valid
Keep existing active-type correction, but make sure it runs after supported types are loaded:
if (!templateTypes.value.some((type) => type.value === activeType.value)) {
activeType.value = templateTypes.value[0]?.value || 'backend'
}
- Step 6: Run frontend preview tests
Run:
cd easycode-web
node --test src/views/previewView.test.mjs
Expected: PASS.
- Step 7: Commit
git add easycode-web/src/views/PreviewView.vue easycode-web/src/views/previewView.test.mjs
git commit -m "feat: load preview template types dynamically"
Task 8: Full Verification
Files:
-
No new source files.
-
Use prior task files only if failures reveal missed integration points.
-
Step 1: Run backend focused tests
Run:
mvn -pl ruoyi-generator -Dtest=TemplateBundleServiceImplTest,FrontProjectServiceImplTest,GenProjectServiceImplTest,FrontProjectPreviewServiceImplTest,OneClickProjectGenerationServiceImplTest,QingTemplateSupportTest test
Expected: BUILD SUCCESS.
- Step 2: Run frontend focused tests
Run:
cd easycode-web
node --test src/views/generateView.test.mjs src/views/previewView.test.mjs
Expected: both test files pass.
- Step 3: Run SQL text checks
Run:
rg -n "sys_template_bundle|bundle_code|code_template" sql ruoyi-generator/src/main/resources/mapper
Expected:
-
sql/db.sqlcontains table and columns. -
sql/code_template_bundles.sqlcontains table and qing seed. -
sql/qing_templates.sqlcontains qingbundle_code. -
TemplateMapper.xmlmapsbundle_code. -
FrontProjectMapper.xmlmapscode_template. -
Step 4: Run git diff review
Run:
git diff --stat
git diff --check
Expected: no whitespace errors. Stat should show only files listed in this plan plus already existing unrelated worktree changes that were not staged by these tasks.
- Step 5: Commit verification fixes if any
If Step 1 through Step 4 required small fixes, commit only those files:
git add <files-fixed-during-verification>
git commit -m "test: verify code template bundle integration"
If no fixes were needed, do not create an empty commit.
Self-Review
- Spec coverage: The plan covers bundle metadata, qing defaulting, project-level selection, generation template resolution, preview/download supported types, SQL migration, frontend create form, preview tabs, and verification.
- Red-flag scan: No task leaves an unnamed implementation step. Each code-changing step names concrete files and snippets.
- Type consistency: The selected project field is consistently named
codeTemplatein Java and JavaScript, persisted ascode_template, and the template metadata field is consistently namedbundleCode/bundle_code.