Files
yidaima/RuoYi-Vue/docs/superpowers/plans/2026-07-07-code-template-bundles.md

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 for sys_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: add bundleCode.
  • Modify ruoyi-generator/src/main/resources/mapper/generator/TemplateMapper.xml: select, filter, insert, and update bundle_code.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/FrontProject.java: add codeTemplate.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectCreateRequest.java: add codeTemplate.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/FrontProjectUpdateRequest.java: add codeTemplate.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/domain/front/dto/AiGenerationTaskCreateRequest.java: add codeTemplate.
  • Modify ruoyi-generator/src/main/java/com/ruoyi/generator/domain/GenProject.java: add transient codeTemplate.
  • Modify ruoyi-generator/src/main/resources/mapper/front/FrontProjectMapper.xml: map and persist code_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: copy codeTemplate into GenProject.
  • 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: set bundle_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 submit codeTemplate.
  • 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_code and code_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:

  1. Add sys_template_bundle table near sys_template.
  2. Add bundle_code varchar(64) default 'qing' comment 'template bundle code' to sys_template.
  3. Add code_template varchar(64) default 'qing' comment 'code template bundle' to front_project.
  4. Insert the qing bundle seed before qing templates.
  5. Update all qing sys_template inserts with bundle_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.sql contains table and columns.

  • sql/code_template_bundles.sql contains table and qing seed.

  • sql/qing_templates.sql contains qing bundle_code.

  • TemplateMapper.xml maps bundle_code.

  • FrontProjectMapper.xml maps code_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 codeTemplate in Java and JavaScript, persisted as code_template, and the template metadata field is consistently named bundleCode / bundle_code.