Просмотр исходного кода

代码生成器:增加单元测试,🛫

zhijiantianya@gmail.com 1 год назад
Родитель
Сommit
067d53ac48
100 измененных файлов с 5897 добавлено и 1848 удалено
  1. 30 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
  2. 2 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm
  3. 0 23
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java
  4. 0 4
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/package-info.java
  5. 0 4
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/package-info.java
  6. 0 34
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenEngineTest.java
  7. 0 29
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenSQLParserTest.java
  8. 0 20
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImplTest.java
  9. 0 4
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/package-info.java
  10. 0 38
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseDbAndRedisIntegrationTest.java
  11. 0 23
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseRedisIntegrationTest.java
  12. 110 720
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineTest.java
  13. 53 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/category.json
  14. 144 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/contact.json
  15. 0 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/extension.json
  16. 131 4
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/student.json
  17. 144 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/teacher.json
  18. 0 3
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/ErrorCodeConstants
  19. 0 53
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserBaseVO
  20. 0 102
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserController
  21. 0 34
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserConvert
  22. 0 14
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserCreateReqVO
  23. 0 58
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserExcelVO
  24. 0 40
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserMapper
  25. 0 16
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserRespVO
  26. 0 70
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserService
  27. 0 88
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserServiceImpl
  28. 0 191
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserServiceImplTest
  29. 0 18
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserUpdateReqVO
  30. 0 18
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/sql/h2
  31. 0 45
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/vue/api
  32. 73 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/assert.json
  33. 6 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/ErrorCodeConstants_手动操作
  34. 23 25
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentContactDO
  35. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentContactMapper
  36. 183 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentController
  37. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentDO
  38. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentMapper
  39. 9 12
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentPageReqVO
  40. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentRespVO
  41. 52 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentSaveReqVO
  42. 139 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentService
  43. 180 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentServiceImpl
  44. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentServiceImplTest
  45. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentTeacherDO
  46. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentTeacherMapper
  47. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/sql/h2
  48. 8 8
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/sql/sql
  49. 95 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/ts/index
  50. 155 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentContactForm
  51. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentContactList
  52. 47 59
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentForm
  53. 155 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentTeacherForm
  54. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentTeacherList
  55. 278 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/index
  56. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/xml/InfraStudentMapper
  57. 73 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/assert.json
  58. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/ErrorCodeConstants_手动操作
  59. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentContactDO
  60. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentContactMapper
  61. 117 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentController
  62. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentDO
  63. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentMapper
  64. 12 13
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentPageReqVO
  65. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentRespVO
  66. 58 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentSaveReqVO
  67. 77 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentService
  68. 147 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentServiceImpl
  69. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentServiceImplTest
  70. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentTeacherDO
  71. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentTeacherMapper
  72. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/sql/h2
  73. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/sql/sql
  74. 57 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/ts/index
  75. 174 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentContactForm
  76. 72 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentContactList
  77. 184 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentForm
  78. 122 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentTeacherForm
  79. 76 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentTeacherList
  80. 72 72
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/index
  81. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/xml/InfraStudentMapper
  82. 67 4
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/assert.json
  83. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/ErrorCodeConstants_手动操作
  84. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentContactDO
  85. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentContactMapper
  86. 117 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentController
  87. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentDO
  88. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentMapper
  89. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentPageReqVO
  90. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentRespVO
  91. 58 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentSaveReqVO
  92. 77 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentService
  93. 147 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentServiceImpl
  94. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentServiceImplTest
  95. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentTeacherDO
  96. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentTeacherMapper
  97. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/sql/h2
  98. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/sql/sql
  99. 57 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/ts/index
  100. 174 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/vue/StudentContactForm

+ 30 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.framework.common.util.json;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.DeserializationFeature;
 import com.fasterxml.jackson.databind.JsonNode;
@@ -30,6 +31,7 @@ public class JsonUtils {
     static {
         objectMapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
         objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); // 忽略 null 值
         objectMapper.registerModules(new JavaTimeModule()); // 解决 LocalDateTime 的序列化
     }
 
@@ -71,6 +73,20 @@ public class JsonUtils {
         }
     }
 
+    public static <T> T parseObject(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), clazz);
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
     public static <T> T parseObject(String text, Type type) {
         if (StrUtil.isEmpty(text)) {
             return null;
@@ -132,6 +148,20 @@ public class JsonUtils {
         }
     }
 
+    public static <T> List<T> parseArray(String text, String path, Class<T> clazz) {
+        if (StrUtil.isEmpty(text)) {
+            return null;
+        }
+        try {
+            JsonNode treeNode = objectMapper.readTree(text);
+            JsonNode pathNode = treeNode.path(path);
+            return objectMapper.readValue(pathNode.toString(), objectMapper.getTypeFactory().constructCollectionType(List.class, clazz));
+        } catch (IOException e) {
+            log.error("json parse err,json:{}", text, e);
+            throw new RuntimeException(e);
+        }
+    }
+
     public static JsonNode parseTree(String text) {
         try {
             return objectMapper.readTree(text);

+ 2 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm

@@ -49,13 +49,14 @@ public class ${sceneEnum.prefixClass}${table.className}SaveReqVO {
 #end
 ## 特殊:主子表专属逻辑(非 ERP 模式)
 #if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
-
 #foreach ($subTable in $subTables)
 #set ($index = $foreach.count - 1)
     #if ( $subTable.subJoinMany)
+    @Schema(description = "${subTable.classComment}列表")
     private List<${subTable.className}DO> ${subClassNameVars.get($index)}s;
 
     #else
+    @Schema(description = "${subTable.classComment}")
     private ${subTable.className}DO ${subClassNameVars.get($index)};
 
     #end

+ 0 - 23
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.module.infra.dal.mysql.codegen;
-
-import cn.iocoder.yudao.module.tool.dal.dataobject.codegen.SchemaColumnDO;
-import cn.iocoder.yudao.module.tool.test.BaseDbUnitTest;
-import org.junit.jupiter.api.Test;
-
-import javax.annotation.Resource;
-import java.util.List;
-
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-public class SchemaColumnMapperTest extends BaseDbUnitTest {
-
-    @Resource
-    private SchemaColumnMapper schemaColumnMapper;
-
-    @Test
-    public void testSelectListByTableName() {
-        List<SchemaColumnDO> columns = schemaColumnMapper.selectListByTableName("", "inf_config");
-        assertTrue(columns.size() > 0);
-    }
-
-}

+ 0 - 4
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 占位
- */
-package cn.iocoder.yudao.module.infra.dal.mysql;

+ 0 - 4
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 占位
- */
-package cn.iocoder.yudao.module.infra.dal;

+ 0 - 34
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenEngineTest.java

@@ -1,34 +0,0 @@
-package cn.iocoder.yudao.module.infra.service.codegen;
-
-import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
-import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
-import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
-import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
-import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
-import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
-import org.junit.jupiter.api.Test;
-
-import javax.annotation.Resource;
-import java.util.List;
-import java.util.Map;
-
-public class CodegenEngineTest extends BaseDbUnitTest {
-
-    @Resource
-    private CodegenTableMapper codegenTableMapper;
-    @Resource
-    private CodegenColumnMapper codegenColumnMapper;
-
-    @Resource
-    private CodegenEngine codegenEngine;
-
-    @Test
-    public void testExecute() {
-        CodegenTableDO table = codegenTableMapper.selectById(20);
-        List<CodegenColumnDO> columns = codegenColumnMapper.selectListByTableId(table.getId());
-        Map<String, String> result = codegenEngine.execute(table, columns);
-        result.forEach((s, s2) -> System.out.println(s2));
-//        System.out.println(result.get("vue/views/system/test/index.vue"));
-    }
-
-}

+ 0 - 29
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenSQLParserTest.java

@@ -1,29 +0,0 @@
-package cn.iocoder.yudao.module.infra.service.codegen;
-
-import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenSQLParser;
-import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
-import org.junit.jupiter.api.Test;
-
-public class CodegenSQLParserTest extends BaseDbUnitTest {
-
-    @Test
-    public void testParse() {
-        String sql = "CREATE TABLE `infra_test_demo` (\n" +
-                "  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '编号',\n" +
-                "  `name` varchar(100) NOT NULL DEFAULT '' COMMENT '名字',\n" +
-                "  `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态',\n" +
-                "  `type` tinyint(4) NOT NULL COMMENT '类型',\n" +
-                "  `category` tinyint(4) NOT NULL COMMENT '分类',\n" +
-                "  `remark` varchar(500) DEFAULT NULL COMMENT '备注',\n" +
-                "  `create_by` varchar(64) DEFAULT '' COMMENT '创建者',\n" +
-                "  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n" +
-                "  `update_by` varchar(64) DEFAULT '' COMMENT '更新者',\n" +
-                "  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',\n" +
-                "  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',\n" +
-                "  PRIMARY KEY (`id`) USING BTREE\n" +
-                ") ENGINE=InnoDB AUTO_INCREMENT=108 DEFAULT CHARSET=utf8mb4 COMMENT='字典类型表';";
-        CodegenSQLParser.parse(sql);
-        // TODO 芋艿:后续完善断言
-    }
-
-}

+ 0 - 20
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImplTest.java

@@ -1,20 +0,0 @@
-package cn.iocoder.yudao.module.infra.service.codegen;
-
-import cn.iocoder.yudao.module.infra.test.BaseDbUnitTest;
-import org.junit.jupiter.api.Test;
-
-import javax.annotation.Resource;
-
-class CodegenServiceImplTest extends BaseDbUnitTest {
-
-    @Resource
-    private CodegenServiceImpl codegenService;
-
-    @Test
-    public void tetCreateCodegenTable() {
-        codegenService.createCodegen(0L, "infra_test_demo");
-//        infraCodegenService.createCodegenTable("infra_codegen_table");
-//        infraCodegenService.createCodegen("infra_codegen_column");
-    }
-
-}

+ 0 - 4
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/service/package-info.java

@@ -1,4 +0,0 @@
-/**
- * 占位
- */
-package cn.iocoder.yudao.module.infra.service;

+ 0 - 38
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseDbAndRedisIntegrationTest.java

@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.infra.test;
-
-import cn.iocoder.yudao.framework.datasource.config.YudaoDataSourceAutoConfiguration;
-import cn.iocoder.yudao.framework.mybatis.config.YudaoMybatisAutoConfiguration;
-import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
-import com.baomidou.dynamic.datasource.spring.boot.autoconfigure.DynamicDataSourceAutoConfiguration;
-import com.baomidou.mybatisplus.autoconfigure.MybatisPlusAutoConfiguration;
-import org.redisson.spring.starter.RedissonAutoConfiguration;
-import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
-import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
-import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
-import org.springframework.test.context.ActiveProfiles;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseDbAndRedisIntegrationTest.Application.class)
-@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
-public class BaseDbAndRedisIntegrationTest {
-
-    @Import({
-            // DB 配置类
-            DynamicDataSourceAutoConfiguration.class, // Dynamic Datasource 配置类
-            YudaoDataSourceAutoConfiguration.class, // 自己的 DB 配置类
-            DataSourceAutoConfiguration.class, // Spring DB 自动配置类
-            DataSourceTransactionManagerAutoConfiguration.class, // Spring 事务自动配置类
-            // MyBatis 配置类
-            YudaoMybatisAutoConfiguration.class, // 自己的 MyBatis 配置类
-            MybatisPlusAutoConfiguration.class, // MyBatis 的自动配置类
-
-            // Redis 配置类
-            RedisAutoConfiguration.class, // Spring Redis 自动配置类
-            YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
-            RedissonAutoConfiguration.class, // Redisson 自动高配置类
-    })
-    public static class Application {
-    }
-
-}

+ 0 - 23
yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/test/BaseRedisIntegrationTest.java

@@ -1,23 +0,0 @@
-package cn.iocoder.yudao.module.infra.test;
-
-import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
-import org.redisson.spring.starter.RedissonAutoConfiguration;
-import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
-import org.springframework.boot.test.context.SpringBootTest;
-import org.springframework.context.annotation.Import;
-import org.springframework.test.context.ActiveProfiles;
-
-@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = BaseRedisIntegrationTest.Application.class)
-@ActiveProfiles("integration-test") // 设置使用 application-integration-test 配置文件
-public class BaseRedisIntegrationTest {
-
-    @Import({
-            // Redis 配置类
-            RedisAutoConfiguration.class, // Spring Redis 自动配置类
-            YudaoRedisAutoConfiguration.class, // 自己的 Redis 配置类
-            RedissonAutoConfiguration.class, // Redisson 自动高配置类
-    })
-    public static class Application {
-    }
-
-}

+ 110 - 720
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineTest.java

@@ -3,7 +3,10 @@ package cn.iocoder.yudao.module.infra.service.codegen.inner;
 import cn.hutool.core.io.FileUtil;
 import cn.hutool.core.io.IoUtil;
 import cn.hutool.core.io.resource.ResourceUtil;
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
 import cn.hutool.core.util.ZipUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
 import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO;
 import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
@@ -17,11 +20,10 @@ import org.mockito.Spy;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
  * {@link CodegenEngine} 的单元测试
@@ -43,759 +45,126 @@ public class CodegenEngineTest extends BaseMockitoUnitTest {
     }
 
     @Test
-    public void testExecute_vue3_tree() {
-        // 准备请求参数
-        CodegenTableDO table = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene()).setParentMenuId(888L)
-                .setTableName("infra_demo66_student").setTableComment("学生表")
-                .setModuleName("infra").setBusinessName("demo66").setClassName("InfraDemo66Student")
-                .setClassComment("学生").setAuthor("芋道源码")
-                .setTemplateType(CodegenTemplateTypeEnum.TREE.getType())
+    public void testExecute_vue3_one() {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
                 .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
-                .setTreeParentColumnId(22L).setTreeNameColumnId(11L);
-        CodegenColumnDO idColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO nameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setId(11L)
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO parentIdColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setId(22L)
-                .setColumnComment("父编号").setNullable(false)
-                .setJavaType("Long").setJavaField("parentId").setExample("2048")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        List<CodegenColumnDO> columns = Arrays.asList(idColumn, nameColumn, parentIdColumn);
+                .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
 
         // 调用
         Map<String, String> result = codegenEngine.execute(table, columns, null, null);
-
-        // 构建 zip 包
-        writeFile(result, "/Users/yunai/test/demo66.zip");
+        // 断言
+        assertResult(result, "codegen/vue3_one");
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_one");
     }
 
     @Test
-    public void testExecute_vue3_crud() {
-        // 准备请求参数
-        CodegenTableDO table = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene()).setParentMenuId(888L)
-                .setTableName("infra_demo01_student").setTableComment("学生表")
-                .setModuleName("infra").setBusinessName("demo01").setClassName("InfraDemo01Student")
-                .setClassComment("学生").setAuthor("芋道源码")
-                .setTemplateType(CodegenTemplateTypeEnum.ONE.getType())
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType());
-        CodegenColumnDO idColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO nameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO descriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO birthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO sexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO enabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO avatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO videoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO memoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO createTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> columns = Arrays.asList(idColumn, nameColumn, descriptionColumn, birthdayColumn,
-                sexColumn, enabledColumn, avatarColumn, videoColumn, memoColumn, createTimeColumn);
+    public void testExecute_vue3_tree() {
+        // 准备参数
+        CodegenTableDO table = getTable("category")
+                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
+                .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
+        List<CodegenColumnDO> columns = getColumnList("category");
 
         // 调用
         Map<String, String> result = codegenEngine.execute(table, columns, null, null);
-
-        // 构建 zip 包
-        writeFile(result, "/Users/yunai/test/demo01.zip");
-
         // 断言
-        assertEquals(21, result.size());
-        // 断言 vo 类
-        for (String vo : new String[]{"SystemUserBaseVO", "SystemUserCreateReqVO", "SystemUserUpdateReqVO", "SystemUserRespVO",
-                "SystemUserPageReqVO", "SystemUserExportReqVO", "SystemUserExcelVO"}) {
-            assertPathContentEquals("vue3_crud/java/" + vo,
-                    result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/vo/" + vo + ".java");
-        }
-        // 断言 controller 类
-        assertPathContentEquals("vue3_crud/java/SystemUserController",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/user/SystemUserController.java");
-        // 断言 service 类
-        assertPathContentEquals("vue3_crud/java/SystemUserService",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/SystemUserService.java");
-        assertPathContentEquals("vue3_crud/java/SystemUserServiceImpl",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/user/SystemUserServiceImpl.java");
-        // 断言 convert 类
-        assertPathContentEquals("vue3_crud/java/SystemUserConvert",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/user/SystemUserConvert.java");
-        // 断言 enums 类
-        assertPathContentEquals("vue3_crud/java/ErrorCodeConstants",
-                result, "yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants_手动操作.java");
-        // 断言 dal 类
-        assertPathContentEquals("vue3_crud/java/SystemUserDO",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/user/SystemUserDO.java");
-        assertPathContentEquals("vue3_crud/java/SystemUserMapper",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/user/SystemUserMapper.java");
-        assertPathContentEquals("vue3_crud/java/SystemUserMapper_xml",
-                result, "yudao-module-system/yudao-module-system-biz/src/main/resources/mapper/user/SystemUserMapper.xml");
-        // 断言 test 类
-        assertPathContentEquals("vue3_crud/java/SystemUserServiceImplTest",
-                result, "yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/user/SystemUserServiceImplTest.java");
-        // 断言 sql 语句
-        assertPathContentEquals("vue3_crud/sql/h2",
-                result, "sql/h2.sql");
-        assertPathContentEquals("vue3_crud/sql/sql",
-                result, "sql/sql.sql");
-        // 断言 vue 语句
-        assertPathContentEquals("vue3_crud/vue/index",
-                result, "yudao-ui-admin-vue3/src/views/system/user/index.vue");
-        assertPathContentEquals("vue3_crud/vue/form",
-                result, "yudao-ui-admin-vue3/src/views/system/user/UserForm.vue");
-        assertPathContentEquals("vue3_crud/vue/api",
-                result, "yudao-ui-admin-vue3/src/api/system/user/index.ts");
+        assertResult(result, "codegen/vue3_tree");
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_tree");
+//        writeFile(result, "/Users/yunai/test/demo66.zip");
     }
 
     @Test
-    public void testExecute_vue3_masterNormal() {
-        // 准备请求参数
-        // 主表
-        CodegenTableDO table = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene()).setParentMenuId(888L)
-                .setTableName("infra_demo11_student").setTableComment("学生表")
-                .setModuleName("infra").setBusinessName("demo11").setClassName("InfraDemo11Student")
-                .setClassComment("学生").setAuthor("芋道源码")
-                .setTemplateType(CodegenTemplateTypeEnum.MASTER_NORMAL.getType())
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType());
-        CodegenColumnDO idColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setId(100L)
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO nameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO descriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO birthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO sexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO enabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO avatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO videoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO memoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO createTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> columns = Arrays.asList(idColumn, nameColumn, descriptionColumn, birthdayColumn,
-                sexColumn, enabledColumn, avatarColumn, videoColumn, memoColumn, createTimeColumn);
-
-        // 子表(联系人)
-        CodegenTableDO contactTable = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene())
-                .setTableName("infra_demo11_student_contact").setTableComment("学生联系人表")
-                .setModuleName("infra").setBusinessName("demo11").setClassName("InfraDemo11StudentContact")
-                .setClassComment("学生联系人").setAuthor("芋道源码")
-                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
-                .setSubJoinColumnId(100L).setSubJoinMany(true);
-        CodegenColumnDO contactIdColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO contactStudentIdColumn = new CodegenColumnDO().setColumnName("student_id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("学生编号").setNullable(false).setPrimaryKey(false)
-                .setJavaType("Long").setJavaField("studentId").setExample("2048")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setId(100L);
-        CodegenColumnDO contactNameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO contactDescriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO contactBirthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO contactSexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO contactEnabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO contactAvatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO contactVideoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO contactMemoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO contactCreateTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> contactColumns = Arrays.asList(contactIdColumn, contactStudentIdColumn,
-                contactNameColumn, contactDescriptionColumn, contactBirthdayColumn,
-                contactSexColumn, contactEnabledColumn, contactAvatarColumn, contactVideoColumn, contactMemoColumn, contactCreateTimeColumn);
-
-        // 子表(班主任)
-        CodegenTableDO teacherTable = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene())
-                .setTableName("infra_demo11_student_teacher").setTableComment("学生班主任表")
-                .setModuleName("infra").setBusinessName("demo11").setClassName("InfraDemo11StudentTeacher")
-                .setClassComment("学生班主任").setAuthor("芋道源码")
-                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
-                .setSubJoinColumnId(200L).setSubJoinMany(false);
-        CodegenColumnDO teacherIdColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO teacherStudentIdColumn = new CodegenColumnDO().setColumnName("student_id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("学生编号").setNullable(false).setPrimaryKey(false)
-                .setJavaType("Long").setJavaField("studentId").setExample("2048")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setId(200L);
-        CodegenColumnDO teacherNameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO teacherDescriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO teacherBirthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO teacherSexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO teacherEnabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO teacherAvatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO teacherVideoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO teacherMemoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO teacherCreateTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> teacherColumns = Arrays.asList(teacherIdColumn, teacherStudentIdColumn,
-                teacherNameColumn, teacherDescriptionColumn, teacherBirthdayColumn,
-                teacherSexColumn, teacherEnabledColumn, teacherAvatarColumn, teacherVideoColumn, teacherMemoColumn, teacherCreateTimeColumn);
-
-        // 调用
-        Map<String, String> result = codegenEngine.execute(table, columns,
-                Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
-
-        // 构建 zip 包
-        writeFile(result, "/Users/yunai/test/demo11.zip");
-
-        // 断言
-        assertEquals(27, result.size());
+    public void testExecute_vue3_master_normal() {
+        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "codegen/vue3_master_normal");
+    }
 
-        for (Map.Entry<String, String> entry : result.entrySet()) {
-            System.out.println(entry.getKey());
-            System.out.println(entry.getValue());
-            System.out.println("");
-            System.out.println("");
-            System.out.println("");
-        }
+    @Test
+    public void testExecute_vue3_master_erp() {
+        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_ERP, "codegen/vue3_master_erp");
     }
 
     @Test
-    public void testExecute_vue3_masterInner() {
-        // 准备请求参数
-        // 主表
-        CodegenTableDO table = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene()).setParentMenuId(888L)
-                .setTableName("infra_demo12_student").setTableComment("学生表")
-                .setModuleName("infra").setBusinessName("demo12").setClassName("InfraDemo12Student")
-                .setClassComment("学生").setAuthor("芋道源码")
-//                .setTemplateType(CodegenTemplateTypeEnum.MASTER_INNER.getType())
-                .setTemplateType(CodegenTemplateTypeEnum.MASTER_ERP.getType())
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType());
-        CodegenColumnDO idColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setId(100L)
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO nameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO descriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO birthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO sexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO enabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO avatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO videoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO memoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO createTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> columns = Arrays.asList(idColumn, nameColumn, descriptionColumn, birthdayColumn,
-                sexColumn, enabledColumn, avatarColumn, videoColumn, memoColumn, createTimeColumn);
+    public void testExecute_vue3_master_inner() {
+        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_INNER, "codegen/vue3_master_inner");
+    }
 
-        // 子表(联系人)
-        CodegenTableDO contactTable = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene())
-                .setTableName("infra_demo12_student_contact").setTableComment("学生联系人表")
-                .setModuleName("infra").setBusinessName("demo12").setClassName("InfraDemo12StudentContact")
-                .setClassComment("学生联系人").setAuthor("芋道源码")
+    private void testExecute_vue3_master(CodegenTemplateTypeEnum templateType,
+                                         String path) {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
+                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
+                .setTemplateType(templateType.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
+        // 准备参数(子表)
+        CodegenTableDO contactTable = getTable("contact")
                 .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
                 .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
                 .setSubJoinColumnId(100L).setSubJoinMany(true);
-        CodegenColumnDO contactIdColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO contactStudentIdColumn = new CodegenColumnDO().setColumnName("student_id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("学生编号").setNullable(false).setPrimaryKey(false)
-                .setJavaType("Long").setJavaField("studentId").setExample("2048")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setId(100L);
-        CodegenColumnDO contactNameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO contactDescriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO contactBirthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO contactSexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO contactEnabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO contactAvatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO contactVideoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO contactMemoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO contactCreateTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> contactColumns = Arrays.asList(contactIdColumn, contactStudentIdColumn,
-                contactNameColumn, contactDescriptionColumn, contactBirthdayColumn,
-                contactSexColumn, contactEnabledColumn, contactAvatarColumn, contactVideoColumn, contactMemoColumn, contactCreateTimeColumn);
-
-        // 子表(班主任)
-        CodegenTableDO teacherTable = new CodegenTableDO().setScene(CodegenSceneEnum.ADMIN.getScene())
-                .setTableName("infra_demo12_student_teacher").setTableComment("学生班主任表")
-                .setModuleName("infra").setBusinessName("demo12").setClassName("InfraDemo12StudentTeacher")
-                .setClassComment("学生班主任").setAuthor("芋道源码")
+        List<CodegenColumnDO> contactColumns = getColumnList("contact");
+        // 准备参数(班主任)
+        CodegenTableDO teacherTable = getTable("teacher")
                 .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
                 .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
                 .setSubJoinColumnId(200L).setSubJoinMany(false);
-        CodegenColumnDO teacherIdColumn = new CodegenColumnDO().setColumnName("id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("编号").setNullable(false).setPrimaryKey(true).setAutoIncrement(true)
-                .setJavaType("Long").setJavaField("id").setExample("1024")
-                .setCreateOperation(false).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true);
-        CodegenColumnDO teacherStudentIdColumn = new CodegenColumnDO().setColumnName("student_id").setDataType(JdbcType.BIGINT.name())
-                .setColumnComment("学生编号").setNullable(false).setPrimaryKey(false)
-                .setJavaType("Long").setJavaField("studentId").setExample("2048")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setId(200L);
-        CodegenColumnDO teacherNameColumn = new CodegenColumnDO().setColumnName("name").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("名字").setNullable(false)
-                .setJavaType("String").setJavaField("name").setExample("芋头")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.LIKE.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());
-        CodegenColumnDO teacherDescriptionColumn = new CodegenColumnDO().setColumnName("description").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("简介").setNullable(false)
-                .setJavaType("String").setJavaField("description").setExample("我是介绍")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.TEXTAREA.getType());
-        CodegenColumnDO teacherBirthdayColumn = new CodegenColumnDO().setColumnName("birthday").setDataType(JdbcType.DATE.name())
-                .setColumnComment("出生日期").setNullable(false)
-                .setJavaType("LocalDateTime").setJavaField("birthday")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        CodegenColumnDO teacherSexColumn = new CodegenColumnDO().setColumnName("sex").setDataType(JdbcType.INTEGER.name())
-                .setColumnComment("性别").setNullable(false)
-                .setJavaType("Integer").setJavaField("sex").setExample("1")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.SELECT.getType())
-                .setDictType("system_user_sex");
-        CodegenColumnDO teacherEnabledColumn = new CodegenColumnDO().setColumnName("enabled").setDataType(JdbcType.BOOLEAN.name())
-                .setColumnComment("是否有效").setNullable(false)
-                .setJavaType("Boolean").setJavaField("enabled").setExample("true")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType())
-                .setDictType("infra_boolean_string");
-        CodegenColumnDO teacherAvatarColumn = new CodegenColumnDO().setColumnName("avatar").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("头像").setNullable(false)
-                .setJavaType("String").setJavaField("avatar").setExample("https://www.iocoder.cn/1.png")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD.getType());
-        CodegenColumnDO teacherVideoColumn = new CodegenColumnDO().setColumnName("video").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("附件").setNullable(true)
-                .setJavaType("String").setJavaField("video").setExample("https://www.iocoder.cn/1.mp4")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.FILE_UPLOAD.getType());
-        CodegenColumnDO teacherMemoColumn = new CodegenColumnDO().setColumnName("memo").setDataType(JdbcType.VARCHAR.name())
-                .setColumnComment("备注").setNullable(false)
-                .setJavaType("String").setJavaField("memo").setExample("我是备注")
-                .setCreateOperation(true).setUpdateOperation(true)
-                .setListOperation(false)
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.EDITOR.getType());
-        CodegenColumnDO teacherCreateTimeColumn = new CodegenColumnDO().setColumnName("create_time").setDataType(JdbcType.DATE.name())
-                .setColumnComment("创建时间").setNullable(true)
-                .setJavaType("LocalDateTime").setJavaField("createTime")
-                .setCreateOperation(false).setUpdateOperation(false)
-                .setListOperation(true).setListOperationCondition(CodegenColumnListConditionEnum.BETWEEN.getCondition())
-                .setListOperationResult(true)
-                .setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
-        List<CodegenColumnDO> teacherColumns = Arrays.asList(teacherIdColumn, teacherStudentIdColumn,
-                teacherNameColumn, teacherDescriptionColumn, teacherBirthdayColumn,
-                teacherSexColumn, teacherEnabledColumn, teacherAvatarColumn, teacherVideoColumn, teacherMemoColumn, teacherCreateTimeColumn);
+        List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
 
         // 调用
         Map<String, String> result = codegenEngine.execute(table, columns,
                 Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
-
-        // 构建 zip 包
-        writeFile(result, "/Users/yunai/test/demo12.zip");
-
         // 断言
-        assertEquals(27, result.size());
+        assertResult(result, path);
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/" + path);
+//        writeFile(result, "/Users/yunai/test/demo11.zip");
+    }
 
-        for (Map.Entry<String, String> entry : result.entrySet()) {
-            System.out.println(entry.getKey());
-            System.out.println(entry.getValue());
-            System.out.println("");
-            System.out.println("");
-            System.out.println("");
-        }
+    private static CodegenTableDO getTable(String name) {
+        String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
+        return JsonUtils.parseObject(content, "table", CodegenTableDO.class);
     }
 
-    private void assertPathContentEquals(String path, Map<String, String> result, String key) {
-        String pathContent = ResourceUtil.readUtf8Str("codegen/" + path);
-        String valueContent = result.get(key);
-        assertEquals(pathContent, valueContent);
+    private static List<CodegenColumnDO> getColumnList(String name) {
+        String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
+        List<CodegenColumnDO> list = JsonUtils.parseArray(content, "columns", CodegenColumnDO.class);
+        list.forEach(column -> {
+            if (column.getNullable() == null) {
+                column.setNullable(false);
+            }
+            if (column.getCreateOperation() == null) {
+                column.setCreateOperation(false);
+            }
+            if (column.getUpdateOperation() == null) {
+                column.setUpdateOperation(false);
+            }
+            if (column.getListOperation() == null) {
+                column.setListOperation(false);
+            }
+            if (column.getListOperationResult() == null) {
+                column.setListOperationResult(false);
+            }
+        });
+        return list;
     }
 
+    @SuppressWarnings("rawtypes")
+    private static void assertResult(Map<String, String> result, String path) {
+        String assertContent = ResourceUtil.readUtf8Str(path + "/assert.json");
+        List<HashMap> asserts = JsonUtils.parseArray(assertContent, HashMap.class);
+        assertEquals(asserts.size(), result.size());
+        // 校验每个文件
+        asserts.forEach(assertMap -> {
+            String contentPath = (String) assertMap.get("contentPath");
+            String filePath = (String) assertMap.get("filePath");
+            String content = ResourceUtil.readUtf8Str(path + "/" + contentPath);
+            assertEquals(content, result.get(filePath), filePath + ":不匹配");
+        });
+    }
+
+    // ==================== 调试专用 ====================
+
     /**
-     * 将生成的代码,写入到文件
-     *
-     * 用途:方便本地调试
+     * 【调试使用】将生成的代码,写入到文件
      *
      * @param result 生成的代码
      * @param path 写入文件的路径
@@ -810,4 +179,25 @@ public class CodegenEngineTest extends BaseMockitoUnitTest {
         FileUtil.writeBytes(outputStream.toByteArray(), path);
     }
 
+    /**
+     * 【调试使用】将生成的结果,写入到文件
+     *
+     * @param result 生成的代码
+     * @param basePath 写入文件的路径(绝对路径)
+     */
+    private void writeResult(Map<String, String> result, String basePath) {
+        // 写入文件内容
+        List<Map<String, String>> asserts = new ArrayList<>();
+        result.forEach((filePath, fileContent) -> {
+            String lastFilePath = StrUtil.subAfter(filePath, '/', true);
+            String contentPath = StrUtil.subAfter(lastFilePath, '.', true)
+                    + '/' + StrUtil.subBefore(lastFilePath, '.', true);
+            asserts.add(MapUtil.<String, String>builder().put("filePath", filePath)
+                    .put("contentPath", contentPath).build());
+            FileUtil.writeUtf8String(fileContent, basePath + "/" + contentPath);
+        });
+        // 写入 assert.json 文件
+        FileUtil.writeUtf8String(JsonUtils.toJsonPrettyString(asserts), basePath +"/assert.json");
+    }
+
 }

+ 53 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/category.json

@@ -0,0 +1,53 @@
+{
+  "table": {
+    "id": 10,
+    "scene" : 1,
+    "parentMenuId" : 888,
+    "tableName" : "infra_category",
+    "tableComment" : "分类表",
+    "moduleName" : "infra",
+    "businessName" : "demo",
+    "className" : "InfraCategory",
+    "classComment" : "分类",
+    "author" : "芋道源码",
+    "treeParentColumnId" : 22,
+    "treeNameColumnId" : 11
+  },
+  "columns": [ {
+    "columnName" : "id",
+    "dataType" : "BIGINT",
+    "columnComment" : "编号",
+    "primaryKey" : true,
+    "autoIncrement" : true,
+    "javaType" : "Long",
+    "javaField" : "id",
+    "example" : "1024",
+    "updateOperation" : true,
+    "listOperationResult" : true
+  }, {
+    "id" : 11,
+    "columnName" : "name",
+    "dataType" : "VARCHAR",
+    "columnComment" : "名字",
+    "javaType" : "String",
+    "javaField" : "name",
+    "example" : "芋头",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "LIKE",
+    "listOperationResult" : true,
+    "htmlType" : "input"
+  }, {
+    "id" : 22,
+    "columnName" : "description",
+    "dataType" : "VARCHAR",
+    "columnComment" : "父编号",
+    "javaType" : "Long",
+    "javaField" : "parentId",
+    "example" : "2048",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true
+  } ]
+}

+ 144 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/contact.json

@@ -0,0 +1,144 @@
+{
+  "table": {
+    "scene" : 1,
+    "tableName" : "infra_student_contact",
+    "tableComment" : "学生联系人表",
+    "moduleName" : "infra",
+    "businessName" : "demo",
+    "className" : "InfraStudentContact",
+    "classComment" : "学生联系人",
+    "author" : "芋道源码"
+  },
+  "columns": [ {
+    "columnName" : "id",
+    "dataType" : "BIGINT",
+    "columnComment" : "编号",
+    "primaryKey" : true,
+    "autoIncrement" : true,
+    "javaType" : "Long",
+    "javaField" : "id",
+    "example" : "1024",
+    "updateOperation" : true,
+    "listOperationResult" : true
+  }, {
+    "id" : 100,
+    "columnName" : "student_id",
+    "dataType" : "BIGINT",
+    "columnComment" : "学生编号",
+    "javaType" : "Long",
+    "javaField" : "studentId",
+    "example" : "2048",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true
+  }, {
+    "columnName" : "name",
+    "dataType" : "VARCHAR",
+    "columnComment" : "名字",
+    "javaType" : "String",
+    "javaField" : "name",
+    "example" : "芋头",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "LIKE",
+    "listOperationResult" : true,
+    "htmlType" : "input"
+  }, {
+    "columnName" : "description",
+    "dataType" : "VARCHAR",
+    "columnComment" : "简介",
+    "javaType" : "String",
+    "javaField" : "description",
+    "example" : "我是介绍",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "textarea"
+  }, {
+    "columnName" : "birthday",
+    "dataType" : "DATE",
+    "columnComment" : "出生日期",
+    "javaType" : "LocalDateTime",
+    "javaField" : "birthday",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "datetime"
+  }, {
+    "columnName" : "sex",
+    "dataType" : "INTEGER",
+    "columnComment" : "性别",
+    "javaType" : "Integer",
+    "javaField" : "sex",
+    "dictType" : "system_user_sex",
+    "example" : "1",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "select"
+  }, {
+    "columnName" : "enabled",
+    "dataType" : "BOOLEAN",
+    "columnComment" : "是否有效",
+    "javaType" : "Boolean",
+    "javaField" : "enabled",
+    "dictType" : "infra_boolean_string",
+    "example" : "true",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "radio"
+  }, {
+    "columnName" : "avatar",
+    "dataType" : "VARCHAR",
+    "columnComment" : "头像",
+    "javaType" : "String",
+    "javaField" : "avatar",
+    "example" : "https://www.iocoder.cn/1.png",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "imageUpload"
+  }, {
+    "columnName" : "video",
+    "dataType" : "VARCHAR",
+    "columnComment" : "附件",
+    "nullable" : true,
+    "javaType" : "String",
+    "javaField" : "video",
+    "example" : "https://www.iocoder.cn/1.mp4",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "fileUpload"
+  }, {
+    "columnName" : "memo",
+    "dataType" : "VARCHAR",
+    "columnComment" : "备注",
+    "javaType" : "String",
+    "javaField" : "memo",
+    "example" : "我是备注",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "editor"
+  }, {
+    "columnName" : "create_time",
+    "dataType" : "DATE",
+    "columnComment" : "创建时间",
+    "nullable" : true,
+    "javaType" : "LocalDateTime",
+    "javaField" : "createTime",
+    "listOperation" : true,
+    "listOperationCondition" : "BETWEEN",
+    "listOperationResult" : true,
+    "htmlType" : "datetime"
+  } ]
+}

+ 0 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/extension.json


+ 131 - 4
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/student.json

@@ -1,8 +1,135 @@
 {
   "table": {
-
+    "id": 1,
+    "scene" : 1,
+    "parentMenuId" : 888,
+    "tableName" : "infra_student",
+    "tableComment" : "学生表",
+    "moduleName" : "infra",
+    "businessName" : "demo",
+    "className" : "InfraStudent",
+    "classComment" : "学生",
+    "author" : "芋道源码"
   },
-  "columns": [{
-
-  }]
+  "columns": [ {
+    "id" : 100,
+    "columnName" : "id",
+    "dataType" : "BIGINT",
+    "columnComment" : "编号",
+    "primaryKey" : true,
+    "autoIncrement" : true,
+    "javaType" : "Long",
+    "javaField" : "id",
+    "example" : "1024",
+    "updateOperation" : true,
+    "listOperationResult" : true
+  }, {
+    "columnName" : "name",
+    "dataType" : "VARCHAR",
+    "columnComment" : "名字",
+    "javaType" : "String",
+    "javaField" : "name",
+    "example" : "芋头",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "LIKE",
+    "listOperationResult" : true,
+    "htmlType" : "input"
+  }, {
+    "columnName" : "description",
+    "dataType" : "VARCHAR",
+    "columnComment" : "简介",
+    "javaType" : "String",
+    "javaField" : "description",
+    "example" : "我是介绍",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "textarea"
+  }, {
+    "columnName" : "birthday",
+    "dataType" : "DATE",
+    "columnComment" : "出生日期",
+    "javaType" : "LocalDateTime",
+    "javaField" : "birthday",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "datetime"
+  }, {
+    "columnName" : "sex",
+    "dataType" : "INTEGER",
+    "columnComment" : "性别",
+    "javaType" : "Integer",
+    "javaField" : "sex",
+    "dictType" : "system_user_sex",
+    "example" : "1",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "select"
+  }, {
+    "columnName" : "enabled",
+    "dataType" : "BOOLEAN",
+    "columnComment" : "是否有效",
+    "javaType" : "Boolean",
+    "javaField" : "enabled",
+    "dictType" : "infra_boolean_string",
+    "example" : "true",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "radio"
+  }, {
+    "columnName" : "avatar",
+    "dataType" : "VARCHAR",
+    "columnComment" : "头像",
+    "javaType" : "String",
+    "javaField" : "avatar",
+    "example" : "https://www.iocoder.cn/1.png",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "imageUpload"
+  }, {
+    "columnName" : "video",
+    "dataType" : "VARCHAR",
+    "columnComment" : "附件",
+    "javaType" : "String",
+    "javaField" : "video",
+    "example" : "https://www.iocoder.cn/1.mp4",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "fileUpload"
+  }, {
+    "columnName" : "memo",
+    "dataType" : "VARCHAR",
+    "columnComment" : "备注",
+    "javaType" : "String",
+    "javaField" : "memo",
+    "example" : "我是备注",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "editor"
+  }, {
+    "columnName" : "create_time",
+    "dataType" : "DATE",
+    "columnComment" : "创建时间",
+    "nullable" : true,
+    "javaType" : "LocalDateTime",
+    "javaField" : "createTime",
+    "listOperation" : true,
+    "listOperationCondition" : "BETWEEN",
+    "listOperationResult" : true,
+    "htmlType" : "datetime"
+  } ]
 }

+ 144 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/table/teacher.json

@@ -0,0 +1,144 @@
+{
+  "table": {
+    "scene" : 1,
+    "tableName" : "infra_student_teacher",
+    "tableComment" : "学生班主任表",
+    "moduleName" : "infra",
+    "businessName" : "demo",
+    "className" : "InfraStudentTeacher",
+    "classComment" : "学生班主任",
+    "author" : "芋道源码"
+  },
+  "columns": [ {
+    "columnName" : "id",
+    "dataType" : "BIGINT",
+    "columnComment" : "编号",
+    "primaryKey" : true,
+    "autoIncrement" : true,
+    "javaType" : "Long",
+    "javaField" : "id",
+    "example" : "1024",
+    "updateOperation" : true,
+    "listOperationResult" : true
+  }, {
+    "id" : 200,
+    "columnName" : "student_id",
+    "dataType" : "BIGINT",
+    "columnComment" : "学生编号",
+    "javaType" : "Long",
+    "javaField" : "studentId",
+    "example" : "2048",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true
+  }, {
+    "columnName" : "name",
+    "dataType" : "VARCHAR",
+    "columnComment" : "名字",
+    "javaType" : "String",
+    "javaField" : "name",
+    "example" : "芋头",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "LIKE",
+    "listOperationResult" : true,
+    "htmlType" : "input"
+  }, {
+    "columnName" : "description",
+    "dataType" : "VARCHAR",
+    "columnComment" : "简介",
+    "javaType" : "String",
+    "javaField" : "description",
+    "example" : "我是介绍",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "textarea"
+  }, {
+    "columnName" : "birthday",
+    "dataType" : "DATE",
+    "columnComment" : "出生日期",
+    "javaType" : "LocalDateTime",
+    "javaField" : "birthday",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "datetime"
+  }, {
+    "columnName" : "sex",
+    "dataType" : "INTEGER",
+    "columnComment" : "性别",
+    "javaType" : "Integer",
+    "javaField" : "sex",
+    "dictType" : "system_user_sex",
+    "example" : "1",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "select"
+  }, {
+    "columnName" : "enabled",
+    "dataType" : "BOOLEAN",
+    "columnComment" : "是否有效",
+    "javaType" : "Boolean",
+    "javaField" : "enabled",
+    "dictType" : "infra_boolean_string",
+    "example" : "true",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperation" : true,
+    "listOperationCondition" : "=",
+    "listOperationResult" : true,
+    "htmlType" : "radio"
+  }, {
+    "columnName" : "avatar",
+    "dataType" : "VARCHAR",
+    "columnComment" : "头像",
+    "javaType" : "String",
+    "javaField" : "avatar",
+    "example" : "https://www.iocoder.cn/1.png",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "imageUpload"
+  }, {
+    "columnName" : "video",
+    "dataType" : "VARCHAR",
+    "columnComment" : "附件",
+    "nullable" : true,
+    "javaType" : "String",
+    "javaField" : "video",
+    "example" : "https://www.iocoder.cn/1.mp4",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "fileUpload"
+  }, {
+    "columnName" : "memo",
+    "dataType" : "VARCHAR",
+    "columnComment" : "备注",
+    "javaType" : "String",
+    "javaField" : "memo",
+    "example" : "我是备注",
+    "createOperation" : true,
+    "updateOperation" : true,
+    "listOperationResult" : true,
+    "htmlType" : "editor"
+  }, {
+    "columnName" : "create_time",
+    "dataType" : "DATE",
+    "columnComment" : "创建时间",
+    "nullable" : true,
+    "javaType" : "LocalDateTime",
+    "javaField" : "createTime",
+    "listOperation" : true,
+    "listOperationCondition" : "BETWEEN",
+    "listOperationResult" : true,
+    "htmlType" : "datetime"
+  } ]
+}

+ 0 - 3
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/ErrorCodeConstants

@@ -1,3 +0,0 @@
-// TODO 待办:请将下面的错误码复制到 yudao-module-system-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
-// ========== 用户 TODO 补充编号 ==========
-ErrorCode USER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "用户不存在");

+ 0 - 53
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserBaseVO

@@ -1,53 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import javax.validation.constraints.*;
-import org.springframework.format.annotation.DateTimeFormat;
-
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
-/**
- * 用户 Base VO,提供给添加、修改、详细的子 VO 使用
- * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
- */
-@Data
-public class SystemUserBaseVO {
-
-    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
-    @NotEmpty(message = "名字不能为空")
-    private String name;
-
-    @Schema(description = "头像", example = "https://www.iocoder.cn/1.png")
-    private String avatar;
-
-    @Schema(description = "视频", example = "https://www.iocoder.cn/1.mp4")
-    private String video;
-
-    @Schema(description = "个人简介", example = "我是介绍")
-    private String description;
-
-    @Schema(description = "性别 1", example = "男")
-    private String sex1;
-
-    @Schema(description = "性别 2", example = "1")
-    private Integer sex2;
-
-    @Schema(description = "性别 3", example = "true")
-    private Boolean sex3;
-
-    @Schema(description = "出生日期")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime birthday;
-
-    @Schema(description = "备注", example = "我是备注")
-    private String memo;
-
-    @Schema(description = "创建时间")
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    private LocalDateTime createTime;
-
-}

+ 0 - 102
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserController

@@ -1,102 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.admin.user;
-
-import org.springframework.web.bind.annotation.*;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.security.access.prepost.PreAuthorize;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.Operation;
-
-import javax.validation.constraints.*;
-import javax.validation.*;
-import javax.servlet.http.*;
-import java.util.*;
-import java.io.IOException;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-
-import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-
-import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
-
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.*;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.SystemUserDO;
-import cn.iocoder.yudao.module.system.convert.user.SystemUserConvert;
-import cn.iocoder.yudao.module.system.service.user.SystemUserService;
-
-@Tag(name = "管理后台 - 用户")
-@RestController
-@RequestMapping("/system/user")
-@Validated
-public class SystemUserController {
-
-    @Resource
-    private SystemUserService userService;
-
-    @PostMapping("/create")
-    @Operation(summary = "创建用户")
-    @PreAuthorize("@ss.hasPermission('system:user:create')")
-    public CommonResult<Long> createUser(@Valid @RequestBody SystemUserCreateReqVO createReqVO) {
-        return success(userService.createUser(createReqVO));
-    }
-
-    @PutMapping("/update")
-    @Operation(summary = "更新用户")
-    @PreAuthorize("@ss.hasPermission('system:user:update')")
-    public CommonResult<Boolean> updateUser(@Valid @RequestBody SystemUserUpdateReqVO updateReqVO) {
-        userService.updateUser(updateReqVO);
-        return success(true);
-    }
-
-    @DeleteMapping("/delete")
-    @Operation(summary = "删除用户")
-    @Parameter(name = "id", description = "编号", required = true)
-    @PreAuthorize("@ss.hasPermission('system:user:delete')")
-    public CommonResult<Boolean> deleteUser(@RequestParam("id") Long id) {
-        userService.deleteUser(id);
-        return success(true);
-    }
-
-    @GetMapping("/get")
-    @Operation(summary = "获得用户")
-    @Parameter(name = "id", description = "编号", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('system:user:query')")
-    public CommonResult<SystemUserRespVO> getUser(@RequestParam("id") Long id) {
-        SystemUserDO user = userService.getUser(id);
-        return success(SystemUserConvert.INSTANCE.convert(user));
-    }
-
-    @GetMapping("/list")
-    @Operation(summary = "获得用户列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('system:user:query')")
-    public CommonResult<List<SystemUserRespVO>> getUserList(@RequestParam("ids") Collection<Long> ids) {
-        List<SystemUserDO> list = userService.getUserList(ids);
-        return success(SystemUserConvert.INSTANCE.convertList(list));
-    }
-
-    @GetMapping("/page")
-    @Operation(summary = "获得用户分页")
-    @PreAuthorize("@ss.hasPermission('system:user:query')")
-    public CommonResult<PageResult<SystemUserRespVO>> getUserPage(@Valid SystemUserPageReqVO pageVO) {
-        PageResult<SystemUserDO> pageResult = userService.getUserPage(pageVO);
-        return success(SystemUserConvert.INSTANCE.convertPage(pageResult));
-    }
-
-    @GetMapping("/export-excel")
-    @Operation(summary = "导出用户 Excel")
-    @PreAuthorize("@ss.hasPermission('system:user:export')")
-    @OperateLog(type = EXPORT)
-    public void exportUserExcel(@Valid SystemUserExportReqVO exportReqVO,
-              HttpServletResponse response) throws IOException {
-        List<SystemUserDO> list = userService.getUserList(exportReqVO);
-        // 导出 Excel
-        List<SystemUserExcelVO> datas = SystemUserConvert.INSTANCE.convertList02(list);
-        ExcelUtils.write(response, "用户.xls", "数据", SystemUserExcelVO.class, datas);
-    }
-
-}

+ 0 - 34
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserConvert

@@ -1,34 +0,0 @@
-package cn.iocoder.yudao.module.system.convert.user;
-
-import java.util.*;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.*;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.SystemUserDO;
-
-/**
- * 用户 Convert
- *
- * @author 芋道源码
- */
-@Mapper
-public interface SystemUserConvert {
-
-    SystemUserConvert INSTANCE = Mappers.getMapper(SystemUserConvert.class);
-
-    SystemUserDO convert(SystemUserCreateReqVO bean);
-
-    SystemUserDO convert(SystemUserUpdateReqVO bean);
-
-    SystemUserRespVO convert(SystemUserDO bean);
-
-    List<SystemUserRespVO> convertList(List<SystemUserDO> list);
-
-    PageResult<SystemUserRespVO> convertPage(PageResult<SystemUserDO> page);
-
-    List<SystemUserExcelVO> convertList02(List<SystemUserDO> list);
-
-}

+ 0 - 14
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserCreateReqVO

@@ -1,14 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
-
-import lombok.*;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
-
-@Schema(description = "管理后台 - 用户创建 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class SystemUserCreateReqVO extends SystemUserBaseVO {
-
-}

+ 0 - 58
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserExcelVO

@@ -1,58 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-
-import com.alibaba.excel.annotation.ExcelProperty;
-import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
-import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
-
-
-/**
- * 用户 Excel VO
- *
- * @author 芋道源码
- */
-@Data
-public class SystemUserExcelVO {
-
-    @ExcelProperty("编号")
-    private Long id;
-
-    @ExcelProperty("名字")
-    private String name;
-
-    @ExcelProperty("头像")
-    private String avatar;
-
-    @ExcelProperty("视频")
-    private String video;
-
-    @ExcelProperty("个人简介")
-    private String description;
-
-    @ExcelProperty(value = "性别 1", converter = DictConvert.class)
-    @DictFormat("system_sex1") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
-    private String sex1;
-
-    @ExcelProperty(value = "性别 2", converter = DictConvert.class)
-    @DictFormat("system_sex2") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
-    private Integer sex2;
-
-    @ExcelProperty(value = "性别 3", converter = DictConvert.class)
-    @DictFormat("system_sex3") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
-    private Boolean sex3;
-
-    @ExcelProperty("出生日期")
-    private LocalDateTime birthday;
-
-    @ExcelProperty("备注")
-    private String memo;
-
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
-
-}

+ 0 - 40
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserMapper

@@ -1,40 +0,0 @@
-package cn.iocoder.yudao.module.system.dal.mysql.user;
-
-import java.util.*;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.SystemUserDO;
-import org.apache.ibatis.annotations.Mapper;
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.*;
-
-/**
- * 用户 Mapper
- *
- * @author 芋道源码
- */
-@Mapper
-public interface SystemUserMapper extends BaseMapperX<SystemUserDO> {
-
-    default PageResult<SystemUserDO> selectPage(SystemUserPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<SystemUserDO>()
-                .likeIfPresent(SystemUserDO::getName, reqVO.getName())
-                .eqIfPresent(SystemUserDO::getSex1, reqVO.getSex1())
-                .eqIfPresent(SystemUserDO::getSex2, reqVO.getSex2())
-                .eqIfPresent(SystemUserDO::getBirthday, reqVO.getBirthday())
-                .betweenIfPresent(SystemUserDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(SystemUserDO::getId));
-    }
-
-    default List<SystemUserDO> selectList(SystemUserExportReqVO reqVO) {
-        return selectList(new LambdaQueryWrapperX<SystemUserDO>()
-                .likeIfPresent(SystemUserDO::getName, reqVO.getName())
-                .eqIfPresent(SystemUserDO::getSex1, reqVO.getSex1())
-                .eqIfPresent(SystemUserDO::getSex2, reqVO.getSex2())
-                .eqIfPresent(SystemUserDO::getBirthday, reqVO.getBirthday())
-                .betweenIfPresent(SystemUserDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(SystemUserDO::getId));
-    }
-
-}

+ 0 - 16
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserRespVO

@@ -1,16 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.time.LocalDateTime;
-
-@Schema(description = "管理后台 - 用户 Response VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class SystemUserRespVO extends SystemUserBaseVO {
-
-    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
-    private Long id;
-
-}

+ 0 - 70
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserService

@@ -1,70 +0,0 @@
-package cn.iocoder.yudao.module.system.service.user;
-
-import java.util.*;
-import javax.validation.*;
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.*;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.SystemUserDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-/**
- * 用户 Service 接口
- *
- * @author 芋道源码
- */
-public interface SystemUserService {
-
-    /**
-     * 创建用户
-     *
-     * @param createReqVO 创建信息
-     * @return 编号
-     */
-    Long createUser(@Valid SystemUserCreateReqVO createReqVO);
-
-    /**
-     * 更新用户
-     *
-     * @param updateReqVO 更新信息
-     */
-    void updateUser(@Valid SystemUserUpdateReqVO updateReqVO);
-
-    /**
-     * 删除用户
-     *
-     * @param id 编号
-     */
-    void deleteUser(Long id);
-
-    /**
-     * 获得用户
-     *
-     * @param id 编号
-     * @return 用户
-     */
-    SystemUserDO getUser(Long id);
-
-    /**
-     * 获得用户列表
-     *
-     * @param ids 编号
-     * @return 用户列表
-     */
-    List<SystemUserDO> getUserList(Collection<Long> ids);
-
-    /**
-     * 获得用户分页
-     *
-     * @param pageReqVO 分页查询
-     * @return 用户分页
-     */
-    PageResult<SystemUserDO> getUserPage(SystemUserPageReqVO pageReqVO);
-
-    /**
-     * 获得用户列表, 用于 Excel 导出
-     *
-     * @param exportReqVO 查询条件
-     * @return 用户列表
-     */
-    List<SystemUserDO> getUserList(SystemUserExportReqVO exportReqVO);
-
-}

+ 0 - 88
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserServiceImpl

@@ -1,88 +0,0 @@
-package cn.iocoder.yudao.module.system.service.user;
-
-import org.springframework.stereotype.Service;
-import javax.annotation.Resource;
-import org.springframework.validation.annotation.Validated;
-
-import java.util.*;
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.*;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.SystemUserDO;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import cn.iocoder.yudao.module.system.convert.user.SystemUserConvert;
-import cn.iocoder.yudao.module.system.dal.mysql.user.SystemUserMapper;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
-
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.ListUtil;
-
-/**
- * 用户 Service 实现类
- *
- * @author 芋道源码
- */
-@Service
-@Validated
-public class SystemUserServiceImpl implements SystemUserService {
-
-    @Resource
-    private SystemUserMapper userMapper;
-
-    @Override
-    public Long createUser(SystemUserCreateReqVO createReqVO) {
-        // 插入
-        SystemUserDO user = SystemUserConvert.INSTANCE.convert(createReqVO);
-        userMapper.insert(user);
-        // 返回
-        return user.getId();
-    }
-
-    @Override
-    public void updateUser(SystemUserUpdateReqVO updateReqVO) {
-        // 校验存在
-        validateUserExists(updateReqVO.getId());
-        // 更新
-        SystemUserDO updateObj = SystemUserConvert.INSTANCE.convert(updateReqVO);
-        userMapper.updateById(updateObj);
-    }
-
-    @Override
-    public void deleteUser(Long id) {
-        // 校验存在
-        validateUserExists(id);
-        // 删除
-        userMapper.deleteById(id);
-    }
-
-    private void validateUserExists(Long id) {
-        if (userMapper.selectById(id) == null) {
-            throw exception(USER_NOT_EXISTS);
-        }
-    }
-
-    @Override
-    public SystemUserDO getUser(Long id) {
-        return userMapper.selectById(id);
-    }
-
-    @Override
-    public List<SystemUserDO> getUserList(Collection<Long> ids) {
-        if (CollUtil.isEmpty(ids)) {
-            return ListUtil.empty();
-        }
-        return userMapper.selectBatchIds(ids);
-    }
-
-    @Override
-    public PageResult<SystemUserDO> getUserPage(SystemUserPageReqVO pageReqVO) {
-        return userMapper.selectPage(pageReqVO);
-    }
-
-    @Override
-    public List<SystemUserDO> getUserList(SystemUserExportReqVO exportReqVO) {
-        return userMapper.selectList(exportReqVO);
-    }
-
-}

+ 0 - 191
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserServiceImplTest

@@ -1,191 +0,0 @@
-package cn.iocoder.yudao.module.system.service.user;
-
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.mock.mockito.MockBean;
-
-import javax.annotation.Resource;
-
-import cn.iocoder.yudao.module.framework.test.core.ut.BaseDbUnitTest;
-
-import cn.iocoder.yudao.module.system.controller.admin.user.vo.*;
-import cn.iocoder.yudao.module.system.dal.dataobject.user.SystemUserDO;
-import cn.iocoder.yudao.module.system.dal.mysql.user.SystemUserMapper;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-
-import javax.annotation.Resource;
-import org.springframework.context.annotation.Import;
-import java.util.*;
-import java.time.LocalDateTime;
-
-import static cn.hutool.core.util.RandomUtil.*;
-import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*;
-import static cn.iocoder.yudao.module.framework.test.core.util.AssertUtils.*;
-import static cn.iocoder.yudao.module.framework.test.core.util.RandomUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
-import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
-import static org.junit.jupiter.api.Assertions.*;
-import static org.mockito.Mockito.*;
-
-/**
- * {@link SystemUserServiceImpl} 的单元测试类
- *
- * @author 芋道源码
- */
-@Import(SystemUserServiceImpl.class)
-public class SystemUserServiceImplTest extends BaseDbUnitTest {
-
-    @Resource
-    private SystemUserServiceImpl userService;
-
-    @Resource
-    private SystemUserMapper userMapper;
-
-    @Test
-    public void testCreateUser_success() {
-        // 准备参数
-        SystemUserCreateReqVO reqVO = randomPojo(SystemUserCreateReqVO.class);
-
-        // 调用
-        Long userId = userService.createUser(reqVO);
-        // 断言
-        assertNotNull(userId);
-        // 校验记录的属性是否正确
-        SystemUserDO user = userMapper.selectById(userId);
-        assertPojoEquals(reqVO, user);
-    }
-
-    @Test
-    public void testUpdateUser_success() {
-        // mock 数据
-        SystemUserDO dbUser = randomPojo(SystemUserDO.class);
-        userMapper.insert(dbUser);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        SystemUserUpdateReqVO reqVO = randomPojo(SystemUserUpdateReqVO.class, o -> {
-            o.setId(dbUser.getId()); // 设置更新的 ID
-        });
-
-        // 调用
-        userService.updateUser(reqVO);
-        // 校验是否更新正确
-        SystemUserDO user = userMapper.selectById(reqVO.getId()); // 获取最新的
-        assertPojoEquals(reqVO, user);
-    }
-
-    @Test
-    public void testUpdateUser_notExists() {
-        // 准备参数
-        SystemUserUpdateReqVO reqVO = randomPojo(SystemUserUpdateReqVO.class);
-
-        // 调用, 并断言异常
-        assertServiceException(() -> userService.updateUser(reqVO), USER_NOT_EXISTS);
-    }
-
-    @Test
-    public void testDeleteUser_success() {
-        // mock 数据
-        SystemUserDO dbUser = randomPojo(SystemUserDO.class);
-        userMapper.insert(dbUser);// @Sql: 先插入出一条存在的数据
-        // 准备参数
-        Long id = dbUser.getId();
-
-        // 调用
-        userService.deleteUser(id);
-       // 校验数据不存在了
-       assertNull(userMapper.selectById(id));
-    }
-
-    @Test
-    public void testDeleteUser_notExists() {
-        // 准备参数
-        Long id = randomLongId();
-
-        // 调用, 并断言异常
-        assertServiceException(() -> userService.deleteUser(id), USER_NOT_EXISTS);
-    }
-
-    @Test
-    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
-    public void testGetUserPage() {
-       // mock 数据
-       SystemUserDO dbUser = randomPojo(SystemUserDO.class, o -> { // 等会查询到
-           o.setName(null);
-           o.setSex1(null);
-           o.setSex2(null);
-           o.setSex3(null);
-           o.setBirthday(null);
-           o.setCreateTime(null);
-       });
-       userMapper.insert(dbUser);
-       // 测试 name 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setName(null)));
-       // 测试 sex1 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setSex1(null)));
-       // 测试 sex2 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setSex2(null)));
-       // 测试 sex3 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setSex3(null)));
-       // 测试 birthday 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setBirthday(null)));
-       // 测试 createTime 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setCreateTime(null)));
-       // 准备参数
-       SystemUserPageReqVO reqVO = new SystemUserPageReqVO();
-       reqVO.setName(null);
-       reqVO.setSex1(null);
-       reqVO.setSex2(null);
-       reqVO.setSex3(null);
-       reqVO.setBirthday(null);
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-
-       // 调用
-       PageResult<SystemUserDO> pageResult = userService.getUserPage(reqVO);
-       // 断言
-       assertEquals(1, pageResult.getTotal());
-       assertEquals(1, pageResult.getList().size());
-       assertPojoEquals(dbUser, pageResult.getList().get(0));
-    }
-
-    @Test
-    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
-    public void testGetUserList() {
-       // mock 数据
-       SystemUserDO dbUser = randomPojo(SystemUserDO.class, o -> { // 等会查询到
-           o.setName(null);
-           o.setSex1(null);
-           o.setSex2(null);
-           o.setSex3(null);
-           o.setBirthday(null);
-           o.setCreateTime(null);
-       });
-       userMapper.insert(dbUser);
-       // 测试 name 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setName(null)));
-       // 测试 sex1 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setSex1(null)));
-       // 测试 sex2 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setSex2(null)));
-       // 测试 sex3 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setSex3(null)));
-       // 测试 birthday 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setBirthday(null)));
-       // 测试 createTime 不匹配
-       userMapper.insert(cloneIgnoreId(dbUser, o -> o.setCreateTime(null)));
-       // 准备参数
-       SystemUserExportReqVO reqVO = new SystemUserExportReqVO();
-       reqVO.setName(null);
-       reqVO.setSex1(null);
-       reqVO.setSex2(null);
-       reqVO.setSex3(null);
-       reqVO.setBirthday(null);
-       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
-
-       // 调用
-       List<SystemUserDO> list = userService.getUserList(reqVO);
-       // 断言
-       assertEquals(1, list.size());
-       assertPojoEquals(dbUser, list.get(0));
-    }
-
-}

+ 0 - 18
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserUpdateReqVO

@@ -1,18 +0,0 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
-
-@Schema(description = "管理后台 - 用户更新 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class SystemUserUpdateReqVO extends SystemUserBaseVO {
-
-    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
-    @NotNull(message = "编号不能为空")
-    private Long id;
-
-}

+ 0 - 18
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/sql/h2

@@ -1,18 +0,0 @@
--- 将该建表 SQL 语句,添加到 yudao-module-system-biz 模块的 test/resources/sql/create_tables.sql 文件里
-CREATE TABLE IF NOT EXISTS "system_user" (
-    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
-    "name" varchar NOT NULL,
-    "avatar" varchar,
-    "video" varchar,
-    "description" varchar,
-    "sex1" varchar,
-    "sex2" int,
-    "sex3" bit,
-    "birthday" varchar,
-    "memo" varchar,
-    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
-    PRIMARY KEY ("id")
-) COMMENT '用户表';
-
--- 将该删表 SQL 语句,添加到 yudao-module-system-biz 模块的 test/resources/sql/clean.sql 文件里
-DELETE FROM "system_user";

+ 0 - 45
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/vue/api

@@ -1,45 +0,0 @@
-import request from '@/config/axios'
-
-export interface UserVO {
-  id: number
-  name: string
-  avatar: string
-  video: string
-  description: string
-  sex1: string
-  sex2: number
-  sex3: boolean
-  birthday: Date
-  memo: string
-  createTime: Date
-}
-
-// 查询用户列表
-export const getUserPage = async (params) => {
-  return await request.get({ url: `/system/user/page`, params })
-}
-
-// 查询用户详情
-export const getUser = async (id: number) => {
-  return await request.get({ url: `/system/user/get?id=` + id })
-}
-
-// 新增用户
-export const createUser = async (data: UserVO) => {
-  return await request.post({ url: `/system/user/create`, data })
-}
-
-// 修改用户
-export const updateUser = async (data: UserVO) => {
-  return await request.put({ url: `/system/user/update`, data })
-}
-
-// 删除用户
-export const deleteUser = async (id: number) => {
-  return await request.delete({ url: `/system/user/delete?id=` + id })
-}
-
-// 导出用户 Excel
-export const exportUser = async (params) => {
-  return await request.download({ url: `/system/user/export-excel`, params })
-}

+ 73 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/assert.json

@@ -0,0 +1,73 @@
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentContactDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentContactMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactList",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentContactList.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherList",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentTeacherList.vue"
+}, {
+  "contentPath" : "ts/index",
+  "filePath" : "yudao-ui-admin-vue3/src/api/infra/demo/index.ts"
+} ]

+ 6 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,6 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");
+ErrorCode STUDENT_CONTACT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生联系人不存在");
+ErrorCode STUDENT_TEACHER_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任不存在");
+ErrorCode STUDENT_TEACHER_EXISTS = new ErrorCode(TODO 补充编号, "学生班主任已存在");

+ 23 - 25
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserDO → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentContactDO

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.dal.dataobject.user;
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
 
 import lombok.*;
 import java.util.*;
@@ -8,19 +8,19 @@ import com.baomidou.mybatisplus.annotation.*;
 import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
 
 /**
- * 用户 DO
+ * 学生联系人 DO
  *
  * @author 芋道源码
  */
-@TableName("system_user")
-@KeySequence("system_user_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@TableName("infra_student_contact")
+@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
 @Builder
 @NoArgsConstructor
 @AllArgsConstructor
-public class SystemUserDO extends BaseDO {
+public class InfraStudentContactDO extends BaseDO {
 
     /**
      * 编号
@@ -28,43 +28,41 @@ public class SystemUserDO extends BaseDO {
     @TableId
     private Long id;
     /**
-     * 名字
+     * 学生编号
      */
-    private String name;
+    private Long studentId;
     /**
-     * 头像
+     * 名字
      */
-    private String avatar;
+    private String name;
     /**
-     * 视频
+     * 简介
      */
-    private String video;
+    private String description;
     /**
-     * 个人简介
+     * 出生日期
      */
-    private String description;
+    private LocalDateTime birthday;
     /**
-     * 性别 1
+     * 性别
      *
-     * 枚举 {@link TODO system_sex1 对应的类}
+     * 枚举 {@link TODO system_user_sex 对应的类}
      */
-    private String sex1;
+    private Integer sex;
     /**
-     * 性别 2
+     * 是否有效
      *
-     * 枚举 {@link TODO system_sex2 对应的类}
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
      */
-    private Integer sex2;
+    private Boolean enabled;
     /**
-     * 性别 3
-     *
-     * 枚举 {@link TODO system_sex3 对应的类}
+     * 头像
      */
-    private Boolean sex3;
+    private String avatar;
     /**
-     * 出生日期
+     * 附件
      */
-    private LocalDateTime birthday;
+    private String video;
     /**
      * 备注
      */

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentContactMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
+
+    default PageResult<InfraStudentContactDO> selectPage(PageParam reqVO, Long studentId) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentContactDO>()
+            .eq(InfraStudentContactDO::getStudentId, studentId)
+            .orderByDesc(InfraStudentContactDO::getId));
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+}

+ 183 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentController

@@ -0,0 +1,183 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @GetMapping("/student-contact/page")
+    @Operation(summary = "获得学生联系人分页")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentContactDO>> getStudentContactPage(PageParam pageReqVO,
+                                                                                        @RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentContactPage(pageReqVO, studentId));
+    }
+
+    @PostMapping("/student-contact/create")
+    @Operation(summary = "创建学生联系人")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
+        return success(studentService.createStudentContact(studentContact));
+    }
+
+    @PutMapping("/student-contact/update")
+    @Operation(summary = "更新学生联系人")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudentContact(@Valid @RequestBody InfraStudentContactDO studentContact) {
+        studentService.updateStudentContact(studentContact);
+        return success(true);
+    }
+
+    @DeleteMapping("/student-contact/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除学生联系人")
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudentContact(@RequestParam("id") Long id) {
+        studentService.deleteStudentContact(id);
+        return success(true);
+    }
+
+	@GetMapping("/student-contact/get")
+	@Operation(summary = "获得学生联系人")
+	@Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+	public CommonResult<InfraStudentContactDO> getStudentContact(@RequestParam("id") Long id) {
+	    return success(studentService.getStudentContact(id));
+	}
+
+    // ==================== 子表(学生班主任) ====================
+
+    @GetMapping("/student-teacher/page")
+    @Operation(summary = "获得学生班主任分页")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentTeacherDO>> getStudentTeacherPage(PageParam pageReqVO,
+                                                                                        @RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentTeacherPage(pageReqVO, studentId));
+    }
+
+    @PostMapping("/student-teacher/create")
+    @Operation(summary = "创建学生班主任")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
+        return success(studentService.createStudentTeacher(studentTeacher));
+    }
+
+    @PutMapping("/student-teacher/update")
+    @Operation(summary = "更新学生班主任")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudentTeacher(@Valid @RequestBody InfraStudentTeacherDO studentTeacher) {
+        studentService.updateStudentTeacher(studentTeacher);
+        return success(true);
+    }
+
+    @DeleteMapping("/student-teacher/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除学生班主任")
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudentTeacher(@RequestParam("id") Long id) {
+        studentService.deleteStudentTeacher(id);
+        return success(true);
+    }
+
+	@GetMapping("/student-teacher/get")
+	@Operation(summary = "获得学生班主任")
+	@Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+	public CommonResult<InfraStudentTeacherDO> getStudentTeacher(@RequestParam("id") Long id) {
+	    return success(studentService.getStudentTeacher(id));
+	}
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 9 - 12
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserPageReqVO → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentPageReqVO

@@ -1,4 +1,4 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
 
 import lombok.*;
 import java.util.*;
@@ -9,27 +9,24 @@ import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - 用户分页 Request VO")
+@Schema(description = "管理后台 - 学生分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class SystemUserPageReqVO extends PageParam {
+public class InfraStudentPageReqVO extends PageParam {
 
     @Schema(description = "名字", example = "芋头")
     private String name;
 
-    @Schema(description = "性别 1", example = "男")
-    private String sex1;
-
-    @Schema(description = "性别 2", example = "1")
-    private Integer sex2;
-
-    @Schema(description = "性别 3", example = "true")
-    private Boolean sex3;
-
     @Schema(description = "出生日期")
     private LocalDateTime birthday;
 
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 52 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentSaveReqVO

@@ -0,0 +1,52 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+}

+ 139 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentService

@@ -0,0 +1,139 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+    // ==================== 子表(学生联系人) ====================
+
+    /**
+     * 获得学生联系人分页
+     *
+     * @param pageReqVO 分页查询
+     * @param studentId 学生编号
+     * @return 学生联系人分页
+     */
+    PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId);
+
+    /**
+     * 创建学生联系人
+     *
+     * @param studentContact 创建信息
+     * @return 编号
+     */
+    Long createStudentContact(@Valid InfraStudentContactDO studentContact);
+
+    /**
+     * 更新学生联系人
+     *
+     * @param studentContact 更新信息
+     */
+    void updateStudentContact(@Valid InfraStudentContactDO studentContact);
+
+    /**
+     * 删除学生联系人
+     *
+     * @param id 编号
+     */
+    void deleteStudentContact(Long id);
+
+	/**
+	 * 获得学生联系人
+	 *
+	 * @param id 编号
+     * @return 学生联系人
+	 */
+    InfraStudentContactDO getStudentContact(Long id);
+
+    // ==================== 子表(学生班主任) ====================
+
+    /**
+     * 获得学生班主任分页
+     *
+     * @param pageReqVO 分页查询
+     * @param studentId 学生编号
+     * @return 学生班主任分页
+     */
+    PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId);
+
+    /**
+     * 创建学生班主任
+     *
+     * @param studentTeacher 创建信息
+     * @return 编号
+     */
+    Long createStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
+
+    /**
+     * 更新学生班主任
+     *
+     * @param studentTeacher 更新信息
+     */
+    void updateStudentTeacher(@Valid InfraStudentTeacherDO studentTeacher);
+
+    /**
+     * 删除学生班主任
+     *
+     * @param id 编号
+     */
+    void deleteStudentTeacher(Long id);
+
+	/**
+	 * 获得学生班主任
+	 *
+	 * @param id 编号
+     * @return 学生班主任
+	 */
+    InfraStudentTeacherDO getStudentTeacher(Long id);
+
+}

+ 180 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentServiceImpl

@@ -0,0 +1,180 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+    @Resource
+    private InfraStudentContactMapper studentContactMapper;
+    @Resource
+    private InfraStudentTeacherMapper studentTeacherMapper;
+
+    @Override
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+
+        // 删除子表
+        deleteStudentContactByStudentId(id);
+        deleteStudentTeacherByStudentId(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @Override
+    public PageResult<InfraStudentContactDO> getStudentContactPage(PageParam pageReqVO, Long studentId) {
+        return studentContactMapper.selectPage(pageReqVO, studentId);
+    }
+
+    @Override
+    public Long createStudentContact(InfraStudentContactDO studentContact) {
+        studentContactMapper.insert(studentContact);
+        return studentContact.getId();
+    }
+
+    @Override
+    public void updateStudentContact(InfraStudentContactDO studentContact) {
+        // 校验存在
+        validateStudentContactExists(studentContact.getId());
+        // 更新
+        studentContactMapper.updateById(studentContact);
+    }
+
+    @Override
+    public void deleteStudentContact(Long id) {
+        // 校验存在
+        validateStudentContactExists(id);
+        // 删除
+        studentContactMapper.deleteById(id);
+    }
+
+    @Override
+    public InfraStudentContactDO getStudentContact(Long id) {
+        return studentContactMapper.selectById(id);
+    }
+
+    private void validateStudentContactExists(Long id) {
+        if (studentContactMapper.selectById(id) == null) {
+            throw exception(STUDENT_CONTACT_NOT_EXISTS);
+        }
+    }
+
+    private void deleteStudentContactByStudentId(Long studentId) {
+        studentContactMapper.deleteByStudentId(studentId);
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @Override
+    public PageResult<InfraStudentTeacherDO> getStudentTeacherPage(PageParam pageReqVO, Long studentId) {
+        return studentTeacherMapper.selectPage(pageReqVO, studentId);
+    }
+
+    @Override
+    public Long createStudentTeacher(InfraStudentTeacherDO studentTeacher) {
+        // 校验是否已经存在
+        if (studentTeacherMapper.selectByStudentId(studentTeacher.getStudentId()) != null) {
+            throw exception(STUDENT_TEACHER_EXISTS);
+        }
+        // 插入
+        studentTeacherMapper.insert(studentTeacher);
+        return studentTeacher.getId();
+    }
+
+    @Override
+    public void updateStudentTeacher(InfraStudentTeacherDO studentTeacher) {
+        // 校验存在
+        validateStudentTeacherExists(studentTeacher.getId());
+        // 更新
+        studentTeacherMapper.updateById(studentTeacher);
+    }
+
+    @Override
+    public void deleteStudentTeacher(Long id) {
+        // 校验存在
+        validateStudentTeacherExists(id);
+        // 删除
+        studentTeacherMapper.deleteById(id);
+    }
+
+    @Override
+    public InfraStudentTeacherDO getStudentTeacher(Long id) {
+        return studentTeacherMapper.selectById(id);
+    }
+
+    private void validateStudentTeacherExists(Long id) {
+        if (studentTeacherMapper.selectById(id) == null) {
+            throw exception(STUDENT_TEACHER_NOT_EXISTS);
+        }
+    }
+
+    private void deleteStudentTeacherByStudentId(Long studentId) {
+        studentTeacherMapper.deleteByStudentId(studentId);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentTeacherDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/java/InfraStudentTeacherMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
+
+    default PageResult<InfraStudentTeacherDO> selectPage(PageParam reqVO, Long studentId) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentTeacherDO>()
+            .eq(InfraStudentTeacherDO::getStudentId, studentId)
+            .orderByDesc(InfraStudentTeacherDO::getId));
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+}

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 8 - 8
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/sql/sql → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/sql/sql

@@ -4,8 +4,8 @@ INSERT INTO system_menu(
     path, icon, component, status, component_name
 )
 VALUES (
-    '用户管理', '', 2, 0, 10,
-    'user', '', 'system/user/index', 0, 'SystemUser'
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
 );
 
 -- 按钮父菜单ID
@@ -18,7 +18,7 @@ INSERT INTO system_menu(
     path, icon, component, status
 )
 VALUES (
-    '用户查询', 'system:user:query', 3, 1, @parentId,
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
     '', '', '', 0
 );
 INSERT INTO system_menu(
@@ -26,7 +26,7 @@ INSERT INTO system_menu(
     path, icon, component, status
 )
 VALUES (
-    '用户创建', 'system:user:create', 3, 2, @parentId,
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
     '', '', '', 0
 );
 INSERT INTO system_menu(
@@ -34,7 +34,7 @@ INSERT INTO system_menu(
     path, icon, component, status
 )
 VALUES (
-    '用户更新', 'system:user:update', 3, 3, @parentId,
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
     '', '', '', 0
 );
 INSERT INTO system_menu(
@@ -42,7 +42,7 @@ INSERT INTO system_menu(
     path, icon, component, status
 )
 VALUES (
-    '用户删除', 'system:user:delete', 3, 4, @parentId,
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
     '', '', '', 0
 );
 INSERT INTO system_menu(
@@ -50,6 +50,6 @@ INSERT INTO system_menu(
     path, icon, component, status
 )
 VALUES (
-    '用户导出', 'system:user:export', 3, 5, @parentId,
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
     '', '', '', 0
-);
+);

+ 95 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/ts/index

@@ -0,0 +1,95 @@
+import request from '@/config/axios'
+
+export interface StudentVO {
+  id: number
+  name: string
+  description: string
+  birthday: Date
+  sex: number
+  enabled: boolean
+  avatar: string
+  video: string
+  memo: string
+}
+
+// 查询学生分页
+export const getStudentPage = async (params) => {
+  return await request.get({ url: `/infra/student/page`, params })
+}
+
+// 查询学生详情
+export const getStudent = async (id: number) => {
+  return await request.get({ url: `/infra/student/get?id=` + id })
+}
+
+// 新增学生
+export const createStudent = async (data: StudentVO) => {
+  return await request.post({ url: `/infra/student/create`, data })
+}
+
+// 修改学生
+export const updateStudent = async (data: StudentVO) => {
+  return await request.put({ url: `/infra/student/update`, data })
+}
+
+// 删除学生
+export const deleteStudent = async (id: number) => {
+  return await request.delete({ url: `/infra/student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportStudent = async (params) => {
+  return await request.download({ url: `/infra/student/export-excel`, params })
+}
+
+// ==================== 子表(学生联系人) ====================
+
+// 获得学生联系人分页
+export const getStudentContactPage = async (params) => {
+  return await request.get({ url: `/infra/student/student-contact/page`, params })
+}
+// 新增学生联系人
+export const createStudentContact = async (data) => {
+  return await request.post({ url: `/infra/student/student-contact/create`, data })
+}
+
+// 修改学生联系人
+export const updateStudentContact = async (data) => {
+  return await request.put({ url: `/infra/student/student-contact/update`, data })
+}
+
+// 删除学生联系人
+export const deleteStudentContact = async (id: number) => {
+  return await request.delete({ url: `/infra/student/student-contact/delete?id=` + id })
+}
+
+// 获得学生联系人
+export const getStudentContact = async (id: number) => {
+  return await request.get({ url: `/infra/student/student-contact/get?id=` + id })
+}
+
+// ==================== 子表(学生班主任) ====================
+
+// 获得学生班主任分页
+export const getStudentTeacherPage = async (params) => {
+  return await request.get({ url: `/infra/student/student-teacher/page`, params })
+}
+// 新增学生班主任
+export const createStudentTeacher = async (data) => {
+  return await request.post({ url: `/infra/student/student-teacher/create`, data })
+}
+
+// 修改学生班主任
+export const updateStudentTeacher = async (data) => {
+  return await request.put({ url: `/infra/student/student-teacher/update`, data })
+}
+
+// 删除学生班主任
+export const deleteStudentTeacher = async (id: number) => {
+  return await request.delete({ url: `/infra/student/student-teacher/delete?id=` + id })
+}
+
+// 获得学生班主任
+export const getStudentTeacher = async (id: number) => {
+  return await request.get({ url: `/infra/student/student-teacher/get?id=` + id })
+}

+ 155 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentContactForm

@@ -0,0 +1,155 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+       <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, studentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.studentId = studentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await StudentApi.getStudentContact(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await StudentApi.createStudentContact(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await StudentApi.updateStudentContact(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentContactList

@@ -0,0 +1,146 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['infra:student:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <StudentContactForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as StudentApi from '@/api/infra/demo'
+import StudentContactForm from './StudentContactForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await StudentApi.getStudentContactPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.studentId) {
+    message.error('请选择一个学生')
+    return
+  }
+  formRef.value.open(type, id, props.studentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await StudentApi.deleteStudentContact(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+</script>

+ 47 - 59
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/vue/form → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentForm

@@ -10,34 +10,31 @@
       <el-form-item label="名字" prop="name">
         <el-input v-model="formData.name" placeholder="请输入名字" />
       </el-form-item>
-      <el-form-item label="个人简介">
-        <Editor v-model="formData.description" height="150px" />
+      <el-form-item label="简介" prop="description">
+        <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
       </el-form-item>
-      <el-form-item label="性别 1" prop="sex1">
-        <el-select v-model="formData.sex1" placeholder="请选择性别 1">
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
           <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SEX1)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="性别 2" prop="sex2">
-        <el-checkbox-group v-model="formData.sex2">
-          <el-checkbox
-            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_SEX2)"
-            :key="dict.value"
-            :label="dict.value"
-          >
-            {{ dict.label }}
-          </el-checkbox>
-        </el-checkbox-group>
-      </el-form-item>
-      <el-form-item label="性别 3" prop="sex3">
-        <el-radio-group v-model="formData.sex3">
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
           <el-radio
-            v-for="dict in getBoolDictOptions(DICT_TYPE.SYSTEM_SEX3)"
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
             :key="dict.value"
             :label="dict.value"
           >
@@ -45,24 +42,14 @@
           </el-radio>
         </el-radio-group>
       </el-form-item>
-      <el-form-item label="出生日期" prop="birthday">
-        <el-date-picker
-          v-model="formData.birthday"
-          type="date"
-          value-format="x"
-          placeholder="选择出生日期"
-        />
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" />
       </el-form-item>
-      <el-form-item label="备注" prop="memo">
-        <el-input v-model="formData.memo" type="textarea" placeholder="请输入备注" />
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
       </el-form-item>
-      <el-form-item label="创建时间" prop="createTime">
-        <el-date-picker
-          v-model="formData.createTime"
-          type="date"
-          value-format="x"
-          placeholder="选择创建时间"
-        />
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
       </el-form-item>
     </el-form>
     <template #footer>
@@ -72,8 +59,8 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-import { DICT_TYPE, getStrDictOptions, getIntDictOptions, getBoolDictOptions } from '@/utils/dict'
-import * as UserApi from '@/api/system/user'
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -85,18 +72,23 @@ const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
   id: undefined,
   name: undefined,
-  avatar: undefined,
-  video: undefined,
   description: undefined,
-  sex1: undefined,
-  sex2: [],
-  sex3: undefined,
   birthday: undefined,
-  memo: undefined,
-  createTime: undefined
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
 })
 const formRules = reactive({
-  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }]
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
 })
 const formRef = ref() // 表单 Ref
 
@@ -110,7 +102,7 @@ const open = async (type: string, id?: number) => {
   if (id) {
     formLoading.value = true
     try {
-      formData.value = await UserApi.getUser(id)
+      formData.value = await StudentApi.getStudent(id)
     } finally {
       formLoading.value = false
     }
@@ -122,18 +114,16 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
   // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
+  await formRef.value.validate()
   // 提交请求
   formLoading.value = true
   try {
-    const data = formData.value as unknown as UserApi.UserVO
+    const data = formData.value as unknown as StudentApi.StudentVO
     if (formType.value === 'create') {
-      await UserApi.createUser(data)
+      await StudentApi.createStudent(data)
       message.success(t('common.createSuccess'))
     } else {
-      await UserApi.updateUser(data)
+      await StudentApi.updateStudent(data)
       message.success(t('common.updateSuccess'))
     }
     dialogVisible.value = false
@@ -149,15 +139,13 @@ const resetForm = () => {
   formData.value = {
     id: undefined,
     name: undefined,
-    avatar: undefined,
-    video: undefined,
     description: undefined,
-    sex1: undefined,
-    sex2: [],
-    sex3: undefined,
     birthday: undefined,
-    memo: undefined,
-    createTime: undefined
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
   }
   formRef.value?.resetFields()
 }

+ 155 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentTeacherForm

@@ -0,0 +1,155 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+       <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  studentId: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, studentId: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.studentId = studentId
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await StudentApi.getStudentTeacher(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await StudentApi.createStudentTeacher(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await StudentApi.updateStudentTeacher(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/StudentTeacherList

@@ -0,0 +1,146 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['infra:student:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+    <!-- 表单弹窗:添加/修改 -->
+    <StudentTeacherForm ref="formRef" @success="getList" />
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as StudentApi from '@/api/infra/demo'
+import StudentTeacherForm from './StudentTeacherForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  studentId: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  (val) => {
+    queryParams.studentId = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await StudentApi.getStudentTeacherPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.studentId) {
+    message.error('请选择一个学生')
+    return
+  }
+  formRef.value.open(type, id, props.studentId)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await StudentApi.deleteStudentTeacher(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+</script>

+ 278 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/vue/index

@@ -0,0 +1,278 @@
+<template>
+  <ContentWrap>
+    <!-- 搜索工作栏 -->
+    <el-form
+      class="-mb-15px"
+      :model="queryParams"
+      ref="queryFormRef"
+      :inline="true"
+      label-width="68px"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入名字"
+          clearable
+          @keyup.enter="handleQuery"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="queryParams.birthday"
+          value-format="YYYY-MM-DD"
+          type="date"
+          placeholder="选择出生日期"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select
+          v-model="queryParams.enabled"
+          placeholder="请选择是否有效"
+          clearable
+          class="!w-240px"
+        >
+          <el-option
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker
+          v-model="queryParams.createTime"
+          value-format="YYYY-MM-DD HH:mm:ss"
+          type="daterange"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
+        <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:student:create']"
+        >
+          <Icon icon="ep:plus" class="mr-5px" /> 新增
+        </el-button>
+        <el-button
+          type="success"
+          plain
+          @click="handleExport"
+          :loading="exportLoading"
+          v-hasPermi="['infra:student:export']"
+        >
+          <Icon icon="ep:download" class="mr-5px" /> 导出
+        </el-button>
+      </el-form-item>
+    </el-form>
+  </ContentWrap>
+
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      highlight-current-row
+      @current-change="handleCurrentChange"
+    >
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['infra:student:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['infra:student:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </ContentWrap>
+
+  <!-- 表单弹窗:添加/修改 -->
+  <StudentForm ref="formRef" @success="getList" />
+  <!-- 子表的列表 -->
+  <ContentWrap>
+    <el-tabs model-value="studentContact">
+      <el-tab-pane label="学生联系人" name="studentContact">
+        <StudentContactList :student-id="currentRow.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班主任" name="studentTeacher">
+        <StudentTeacherList :student-id="currentRow.id" />
+      </el-tab-pane>
+    </el-tabs>
+  </ContentWrap>
+</template>
+
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import download from '@/utils/download'
+import * as StudentApi from '@/api/infra/demo'
+import StudentForm from './StudentForm.vue'
+import StudentContactList from './components/StudentContactList.vue'
+import StudentTeacherList from './components/StudentTeacherList.vue'
+
+defineOptions({ name: 'InfraStudent' })
+
+const message = useMessage() // 消息弹窗
+const { t } = useI18n() // 国际化
+
+const loading = ref(true) // 列表的加载中
+const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  name: null,
+  birthday: null,
+  birthday: [],
+  sex: null,
+  enabled: null,
+  createTime: []
+})
+const queryFormRef = ref() // 搜索的表单
+const exportLoading = ref(false) // 导出的加载中
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await StudentApi.getStudentPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 重置按钮操作 */
+const resetQuery = () => {
+  queryFormRef.value.resetFields()
+  handleQuery()
+}
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  formRef.value.open(type, id)
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await StudentApi.deleteStudent(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+
+/** 导出按钮操作 */
+const handleExport = async () => {
+  try {
+    // 导出的二次确认
+    await message.exportConfirm()
+    // 发起导出
+    exportLoading.value = true
+    const data = await StudentApi.exportStudent(queryParams)
+    download.excel(data, '学生.xls')
+  } catch {
+  } finally {
+    exportLoading.value = false
+  }
+}
+
+/** 选中行操作 */
+const currentRow = ref({}) // 选中行
+const handleCurrentChange = (row) => {
+  currentRow.value = row
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserMapper_xml → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_erp/xml/InfraStudentMapper

@@ -1,6 +1,6 @@
 <?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="cn.iocoder.yudao.module.system.dal.mysql.user.SystemUserMapper">
+<mapper namespace="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
 
     <!--
         一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。

+ 73 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/assert.json

@@ -0,0 +1,73 @@
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentContactDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentContactMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactList",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentContactList.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherList",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentTeacherList.vue"
+}, {
+  "contentPath" : "ts/index",
+  "filePath" : "yudao-ui-admin-vue3/src/api/infra/demo/index.ts"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,3 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentContactDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生联系人 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_contact")
+@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentContactDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentContactMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
+
+    default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
+        return selectList(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+}

+ 117 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentController

@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @GetMapping("/student-contact/list-by-student-id")
+    @Operation(summary = "获得学生联系人列表")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentContactListByStudentId(studentId));
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @GetMapping("/student-teacher/get-by-student-id")
+    @Operation(summary = "获得学生班主任")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentTeacherByStudentId(studentId));
+    }
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 12 - 13
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/java/SystemUserExportReqVO → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentPageReqVO

@@ -1,33 +1,32 @@
-package cn.iocoder.yudao.module.system.controller.admin.user.vo;
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
 
 import lombok.*;
 import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import cn.iocoder.yudao.framework.common.pojo.PageParam;
-import java.time.LocalDateTime;
 import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
 
 import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
 
-@Schema(description = "管理后台 - 用户 Excel 导出 Request VO,参数和 SystemUserPageReqVO 是一致的")
+@Schema(description = "管理后台 - 学生分页 Request VO")
 @Data
-public class SystemUserExportReqVO {
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfraStudentPageReqVO extends PageParam {
 
     @Schema(description = "名字", example = "芋头")
     private String name;
 
-    @Schema(description = "性别 1", example = "男")
-    private String sex1;
-
-    @Schema(description = "性别 2", example = "1")
-    private Integer sex2;
-
-    @Schema(description = "性别 3", example = "true")
-    private Boolean sex3;
-
     @Schema(description = "出生日期")
     private LocalDateTime birthday;
 
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
     @Schema(description = "创建时间")
     @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 58 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentSaveReqVO

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+    @Schema(description = "学生联系人列表")
+    private List<InfraStudentContactDO> studentContacts;
+
+    @Schema(description = "学生班主任")
+    private InfraStudentTeacherDO studentTeacher;
+
+}

+ 77 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentService

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+    // ==================== 子表(学生联系人) ====================
+
+    /**
+     * 获得学生联系人列表
+     *
+     * @param studentId 学生编号
+     * @return 学生联系人列表
+     */
+    List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
+
+    // ==================== 子表(学生班主任) ====================
+
+    /**
+     * 获得学生班主任
+     *
+     * @param studentId 学生编号
+     * @return 学生班主任
+     */
+    InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
+
+}

+ 147 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentServiceImpl

@@ -0,0 +1,147 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+    @Resource
+    private InfraStudentContactMapper studentContactMapper;
+    @Resource
+    private InfraStudentTeacherMapper studentTeacherMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+
+        // 插入子表
+        createStudentContactList(student.getId(), createReqVO.getStudentContacts());
+        createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+
+        // 更新子表
+        updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
+        updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+
+        // 删除子表
+        deleteStudentContactByStudentId(id);
+        deleteStudentTeacherByStudentId(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @Override
+    public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
+        return studentContactMapper.selectListByStudentId(studentId);
+    }
+
+    private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        list.forEach(o -> o.setStudentId(studentId));
+        studentContactMapper.insertBatch(list);
+    }
+
+    private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        deleteStudentContactByStudentId(studentId);
+		list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
+        createStudentContactList(studentId, list);
+    }
+
+    private void deleteStudentContactByStudentId(Long studentId) {
+        studentContactMapper.deleteByStudentId(studentId);
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @Override
+    public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
+        return studentTeacherMapper.selectByStudentId(studentId);
+    }
+
+    private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+            return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacherMapper.insert(studentTeacher);
+    }
+
+    private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+			return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacher.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
+        studentTeacherMapper.insertOrUpdate(studentTeacher);
+    }
+
+    private void deleteStudentTeacherByStudentId(Long studentId) {
+        studentTeacherMapper.deleteByStudentId(studentId);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentTeacherDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/java/InfraStudentTeacherMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
+
+    default InfraStudentTeacherDO selectByStudentId(Long studentId) {
+        return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+}

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/sql/sql

@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
+    '', '', '', 0
+);

+ 57 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/ts/index

@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+export interface StudentVO {
+  id: number
+  name: string
+  description: string
+  birthday: Date
+  sex: number
+  enabled: boolean
+  avatar: string
+  video: string
+  memo: string
+}
+
+// 查询学生分页
+export const getStudentPage = async (params) => {
+  return await request.get({ url: `/infra/student/page`, params })
+}
+
+// 查询学生详情
+export const getStudent = async (id: number) => {
+  return await request.get({ url: `/infra/student/get?id=` + id })
+}
+
+// 新增学生
+export const createStudent = async (data: StudentVO) => {
+  return await request.post({ url: `/infra/student/create`, data })
+}
+
+// 修改学生
+export const updateStudent = async (data: StudentVO) => {
+  return await request.put({ url: `/infra/student/update`, data })
+}
+
+// 删除学生
+export const deleteStudent = async (id: number) => {
+  return await request.delete({ url: `/infra/student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportStudent = async (params) => {
+  return await request.download({ url: `/infra/student/export-excel`, params })
+}
+
+// ==================== 子表(学生联系人) ====================
+
+// 获得学生联系人列表
+export const getStudentContactListByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/student/student-contact/list-by-student-id?studentId=` + studentId })
+}
+
+// ==================== 子表(学生班主任) ====================
+
+// 获得学生班主任
+export const getStudentTeacherByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/student/student-teacher/get-by-student-id?studentId=` + studentId })
+}

+ 174 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentContactForm

@@ -0,0 +1,174 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+       <el-table-column label="名字" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+            <el-input v-model="row.name" placeholder="请输入名字" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
+            <el-input v-model="row.description" type="textarea" placeholder="请输入简介" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+            <el-date-picker
+              v-model="row.birthday"
+              type="date"
+              value-format="x"
+              placeholder="选择出生日期"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
+            <el-select v-model="row.sex" placeholder="请选择性别">
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.enabled`" :rules="formRules.enabled" class="mb-0px!">
+            <el-radio-group v-model="row.enabled">
+                <el-radio
+                  v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                  :key="dict.value"
+                  :label="dict.value"
+                >
+                  {{ dict.label }}
+                </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+            <UploadImg v-model="row.avatar" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+            <UploadFile v-model="row.video" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" min-width="400">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+            <Editor v-model="row.memo" height="150px" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = []
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      formData.value = await StudentApi.getStudentContactListByStudentId(val)
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  row.studentId = props.studentId
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

+ 72 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentContactList

@@ -0,0 +1,72 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as StudentApi from '@/api/infra/demo'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    list.value = await StudentApi.getStudentContactListByStudentId(props.studentId)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 184 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentForm

@@ -0,0 +1,184 @@
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <el-form
+      ref="formRef"
+      :model="formData"
+      :rules="formRules"
+      label-width="100px"
+      v-loading="formLoading"
+    >
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="formData.name" placeholder="请输入名字" />
+      </el-form-item>
+      <el-form-item label="简介" prop="description">
+        <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="formData.birthday"
+          type="date"
+          value-format="x"
+          placeholder="选择出生日期"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="formData.sex" placeholder="请选择性别">
+          <el-option
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否有效" prop="enabled">
+        <el-radio-group v-model="formData.enabled">
+          <el-radio
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ dict.label }}
+          </el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="头像" prop="avatar">
+        <UploadImg v-model="formData.avatar" />
+      </el-form-item>
+      <el-form-item label="附件" prop="video">
+        <UploadFile v-model="formData.video" />
+      </el-form-item>
+      <el-form-item label="备注" prop="memo">
+        <Editor v-model="formData.memo" height="150px" />
+      </el-form-item>
+    </el-form>
+    <!-- 子表的表单 -->
+    <el-tabs v-model="subTabsName">
+      <el-tab-pane label="学生联系人" name="studentContact">
+        <StudentContactForm ref="studentContactFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+      <el-tab-pane label="学生班主任" name="studentTeacher">
+        <StudentTeacherForm ref="studentTeacherFormRef" :student-id="formData.id" />
+      </el-tab-pane>
+    </el-tabs>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
+import StudentContactForm from './components/StudentContactForm.vue'
+import StudentTeacherForm from './components/StudentTeacherForm.vue'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+  id: undefined,
+  name: undefined,
+  description: undefined,
+  birthday: undefined,
+  sex: undefined,
+  enabled: undefined,
+  avatar: undefined,
+  video: undefined,
+  memo: undefined
+})
+const formRules = reactive({
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  video: [{ required: true, message: '附件不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 子表的表单 */
+const subTabsName = ref('studentContact')
+const studentContactFormRef = ref()
+const studentTeacherFormRef = ref()
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await StudentApi.getStudent(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 校验子表单
+  try {
+    await studentContactFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'studentContact'
+    return
+  }
+  try {
+    await studentTeacherFormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = 'studentTeacher'
+    return
+  }
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value as unknown as StudentApi.StudentVO
+    // 拼接子表的数据
+    data.studentContacts = studentContactFormRef.value.getData()
+    data.studentTeacher = studentTeacherFormRef.value.getData()
+    if (formType.value === 'create') {
+      await StudentApi.createStudent(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await StudentApi.updateStudent(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+    id: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  formRef.value?.resetFields()
+}
+</script>

+ 122 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentTeacherForm

@@ -0,0 +1,122 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    label-width="100px"
+    v-loading="formLoading"
+  >
+     <el-form-item label="名字" prop="name">
+      <el-input v-model="formData.name" placeholder="请输入名字" />
+    </el-form-item>
+    <el-form-item label="简介" prop="description">
+      <el-input v-model="formData.description" type="textarea" placeholder="请输入简介" />
+    </el-form-item>
+    <el-form-item label="出生日期" prop="birthday">
+      <el-date-picker
+        v-model="formData.birthday"
+        type="date"
+        value-format="x"
+        placeholder="选择出生日期"
+      />
+    </el-form-item>
+    <el-form-item label="性别" prop="sex">
+      <el-select v-model="formData.sex" placeholder="请选择性别">
+        <el-option
+          v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+      </el-select>
+    </el-form-item>
+    <el-form-item label="是否有效" prop="enabled">
+      <el-radio-group v-model="formData.enabled">
+        <el-radio
+          v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+          :key="dict.value"
+          :label="dict.value"
+          >
+          {{ dict.label }}
+        </el-radio>
+      </el-radio-group>
+    </el-form-item>
+    <el-form-item label="头像" prop="avatar">
+      <UploadImg v-model="formData.avatar" />
+    </el-form-item>
+    <el-form-item label="附件" prop="video">
+      <UploadFile v-model="formData.video" />
+    </el-form-item>
+    <el-form-item label="备注" prop="memo">
+      <Editor v-model="formData.memo" height="150px" />
+    </el-form-item>
+  </el-form>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = {
+      id: undefined,
+      studentId: undefined,
+      name: undefined,
+      description: undefined,
+      birthday: undefined,
+      sex: undefined,
+      enabled: undefined,
+      avatar: undefined,
+      video: undefined,
+      memo: undefined,
+    }
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      const data = await StudentApi.getStudentTeacherByStudentId(val)
+      if (!data) {
+        return
+      }
+      formData.value = data
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

+ 76 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/StudentTeacherList

@@ -0,0 +1,76 @@
+<template>
+  <!-- 列表 -->
+  <ContentWrap>
+    <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      <el-table-column label="编号" align="center" prop="id" />
+       <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
+      <el-table-column
+        label="出生日期"
+        align="center"
+        prop="birthday"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
+      <el-table-column
+        label="创建时间"
+        align="center"
+        prop="createTime"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+    </el-table>
+  </ContentWrap>
+</template>
+<script setup lang="ts">
+import { DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as StudentApi from '@/api/infra/demo'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await StudentApi.getStudentTeacherByStudentId(props.studentId)
+    if (!data) {
+      return
+    }
+    list.value.push(data)
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+</script>

+ 72 - 72
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_crud/vue/index → yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/vue/index

@@ -17,36 +17,46 @@
           class="!w-240px"
         />
       </el-form-item>
-      <el-form-item label="性别 1" prop="sex1">
-        <el-select v-model="queryParams.sex1" placeholder="请选择性别 1" clearable class="!w-240px">
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker
+          v-model="queryParams.birthday"
+          value-format="YYYY-MM-DD"
+          type="date"
+          placeholder="选择出生日期"
+          clearable
+          class="!w-240px"
+        />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select
+          v-model="queryParams.sex"
+          placeholder="请选择性别"
+          clearable
+          class="!w-240px"
+        >
           <el-option
-            v-for="dict in getStrDictOptions(DICT_TYPE.SYSTEM_SEX1)"
+            v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="性别 3" prop="sex3">
-        <el-select v-model="queryParams.sex3" placeholder="请选择性别 3" clearable class="!w-240px">
+      <el-form-item label="是否有效" prop="enabled">
+        <el-select
+          v-model="queryParams.enabled"
+          placeholder="请选择是否有效"
+          clearable
+          class="!w-240px"
+        >
           <el-option
-            v-for="dict in getBoolDictOptions(DICT_TYPE.SYSTEM_SEX3)"
+            v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
             :key="dict.value"
             :label="dict.label"
             :value="dict.value"
           />
         </el-select>
       </el-form-item>
-      <el-form-item label="出生日期" prop="birthday">
-        <el-date-picker
-          v-model="queryParams.birthday"
-          value-format="YYYY-MM-DD"
-          type="date"
-          placeholder="选择出生日期"
-          clearable
-          class="!w-240px"
-        />
-      </el-form-item>
       <el-form-item label="创建时间" prop="createTime">
         <el-date-picker
           v-model="queryParams.createTime"
@@ -61,7 +71,12 @@
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        <el-button type="primary" @click="openForm('create')" v-hasPermi="['system:user:create']">
+        <el-button
+          type="primary"
+          plain
+          @click="openForm('create')"
+          v-hasPermi="['infra:student:create']"
+        >
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
@@ -69,7 +84,7 @@
           plain
           @click="handleExport"
           :loading="exportLoading"
-          v-hasPermi="['system:user:export']"
+          v-hasPermi="['infra:student:export']"
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
@@ -80,46 +95,22 @@
   <!-- 列表 -->
   <ContentWrap>
     <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
-      <el-table-column label="编号" align="center" prop="id">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.id" />
-        </template>
-      </el-table-column>
-      <el-table-column label="名字" align="center" prop="name">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
-        </template>
-      </el-table-column>
-      <el-table-column label="头像" align="center" prop="avatar">
+      <!-- 子表的列表 -->
+      <el-table-column type="expand">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.avatar" />
-        </template>
-      </el-table-column>
-      <el-table-column label="视频" align="center" prop="video">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
-        </template>
-      </el-table-column>
-      <el-table-column label="个人简介" align="center" prop="description">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
-        </template>
-      </el-table-column>
-      <el-table-column label="性别 1" align="center" prop="sex1">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_SEX1" :value="scope.row.sex1" />
-        </template>
-      </el-table-column>
-      <el-table-column label="性别 2" align="center" prop="sex2">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_SEX2" :value="scope.row.sex2" />
-        </template>
-      </el-table-column>
-      <el-table-column label="性别 3" align="center" prop="sex3">
-        <template #default="scope">
-          <dict-tag :type="DICT_TYPE.SYSTEM_SEX3" :value="scope.row.sex3" />
+          <el-tabs model-value="studentContact">
+            <el-tab-pane label="学生联系人" name="studentContact">
+              <StudentContactList :student-id="scope.row.id" />
+            </el-tab-pane>
+            <el-tab-pane label="学生班主任" name="studentTeacher">
+              <StudentTeacherList :student-id="scope.row.id" />
+            </el-tab-pane>
+          </el-tabs>
         </template>
       </el-table-column>
+      <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="名字" align="center" prop="name" />
+      <el-table-column label="简介" align="center" prop="description" />
       <el-table-column
         label="出生日期"
         align="center"
@@ -127,11 +118,19 @@
         :formatter="dateFormatter"
         width="180px"
       />
-      <el-table-column label="备注" align="center" prop="memo">
+      <el-table-column label="性别" align="center" prop="sex">
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.SYSTEM_USER_SEX" :value="scope.row.sex" />
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" align="center" prop="enabled">
         <template #default="scope">
-          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+          <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.enabled" />
         </template>
       </el-table-column>
+      <el-table-column label="头像" align="center" prop="avatar" />
+      <el-table-column label="附件" align="center" prop="video" />
+      <el-table-column label="备注" align="center" prop="memo" />
       <el-table-column
         label="创建时间"
         align="center"
@@ -145,7 +144,7 @@
             link
             type="primary"
             @click="openForm('update', scope.row.id)"
-            v-hasPermi="['system:user:update']"
+            v-hasPermi="['infra:student:update']"
           >
             编辑
           </el-button>
@@ -153,7 +152,7 @@
             link
             type="danger"
             @click="handleDelete(scope.row.id)"
-            v-hasPermi="['system:user:delete']"
+            v-hasPermi="['infra:student:delete']"
           >
             删除
           </el-button>
@@ -170,33 +169,34 @@
   </ContentWrap>
 
   <!-- 表单弹窗:添加/修改 -->
-  <UserForm ref="formRef" @success="getList" />
+  <StudentForm ref="formRef" @success="getList" />
 </template>
 
 <script setup lang="ts">
-import { DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
 import download from '@/utils/download'
-import * as UserApi from '@/api/system/user'
-import UserForm from './UserForm.vue'
+import * as StudentApi from '@/api/infra/demo'
+import StudentForm from './StudentForm.vue'
+import StudentContactList from './components/StudentContactList.vue'
+import StudentTeacherList from './components/StudentTeacherList.vue'
 
-defineOptions({ name: 'SystemUser' })
+defineOptions({ name: 'InfraStudent' })
 
 const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
+const total = ref(0) // 列表的总页数
 const queryParams = reactive({
   pageNo: 1,
   pageSize: 10,
   name: null,
-  sex1: null,
-  sex2: null,
-  sex3: null,
   birthday: null,
   birthday: [],
+  sex: null,
+  enabled: null,
   createTime: []
 })
 const queryFormRef = ref() // 搜索的表单
@@ -206,7 +206,7 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
-    const data = await UserApi.getUserPage(queryParams)
+    const data = await StudentApi.getStudentPage(queryParams)
     list.value = data.list
     total.value = data.total
   } finally {
@@ -238,7 +238,7 @@ const handleDelete = async (id: number) => {
     // 删除的二次确认
     await message.delConfirm()
     // 发起删除
-    await UserApi.deleteUser(id)
+    await StudentApi.deleteStudent(id)
     message.success(t('common.delSuccess'))
     // 刷新列表
     await getList()
@@ -252,8 +252,8 @@ const handleExport = async () => {
     await message.exportConfirm()
     // 发起导出
     exportLoading.value = true
-    const data = await UserApi.exportUser(queryParams)
-    download.excel(data, '用户.xls')
+    const data = await StudentApi.exportStudent(queryParams)
+    download.excel(data, '学生.xls')
   } catch {
   } finally {
     exportLoading.value = false

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_inner/xml/InfraStudentMapper

@@ -0,0 +1,12 @@
+<?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="cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 67 - 4
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/assert.json

@@ -1,4 +1,67 @@
-[{
-  "filePath": "",
-  "contentPath": ""
-}]
+[ {
+  "contentPath" : "java/InfraStudentPageReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentPageReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentRespVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentRespVO.java"
+}, {
+  "contentPath" : "java/InfraStudentSaveReqVO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/vo/InfraStudentSaveReqVO.java"
+}, {
+  "contentPath" : "java/InfraStudentController",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/InfraStudentController.java"
+}, {
+  "contentPath" : "java/InfraStudentDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentDO.java"
+}, {
+  "contentPath" : "java/InfraStudentContactDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentContactDO.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherDO",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/InfraStudentTeacherDO.java"
+}, {
+  "contentPath" : "java/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentContactMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentContactMapper.java"
+}, {
+  "contentPath" : "java/InfraStudentTeacherMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentTeacherMapper.java"
+}, {
+  "contentPath" : "xml/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/demo/InfraStudentMapper.xml"
+}, {
+  "contentPath" : "java/InfraStudentServiceImpl",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImpl.java"
+}, {
+  "contentPath" : "java/InfraStudentService",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentService.java"
+}, {
+  "contentPath" : "java/InfraStudentServiceImplTest",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/demo/InfraStudentServiceImplTest.java"
+}, {
+  "contentPath" : "java/ErrorCodeConstants_手动操作",
+  "filePath" : "yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants_手动操作.java"
+}, {
+  "contentPath" : "sql/sql",
+  "filePath" : "sql/sql.sql"
+}, {
+  "contentPath" : "sql/h2",
+  "filePath" : "sql/h2.sql"
+}, {
+  "contentPath" : "vue/index",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue3/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "ts/index",
+  "filePath" : "yudao-ui-admin-vue3/src/api/infra/demo/index.ts"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/ErrorCodeConstants_手动操作

@@ -0,0 +1,3 @@
+// TODO 待办:请将下面的错误码复制到 yudao-module-infra-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
+// ========== 学生 TODO 补充编号 ==========
+ErrorCode STUDENT_NOT_EXISTS = new ErrorCode(TODO 补充编号, "学生不存在");

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentContactDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生联系人 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_contact")
+@KeySequence("infra_student_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentContactDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentContactMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentContactMapper extends BaseMapperX<InfraStudentContactDO> {
+
+    default List<InfraStudentContactDO> selectListByStudentId(Long studentId) {
+        return selectList(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentContactDO::getStudentId, studentId);
+    }
+
+}

+ 117 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentController

@@ -0,0 +1,117 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo;
+
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.Operation;
+
+import javax.validation.constraints.*;
+import javax.validation.*;
+import javax.servlet.http.*;
+import java.util.*;
+import java.io.IOException;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.module.infra.service.demo.InfraStudentService;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/student")
+@Validated
+public class InfraStudentController {
+
+    @Resource
+    private InfraStudentService studentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:create')")
+    public CommonResult<Long> createStudent(@Valid @RequestBody InfraStudentSaveReqVO createReqVO) {
+        return success(studentService.createStudent(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:student:update')")
+    public CommonResult<Boolean> updateStudent(@Valid @RequestBody InfraStudentSaveReqVO updateReqVO) {
+        studentService.updateStudent(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:student:delete')")
+    public CommonResult<Boolean> deleteStudent(@RequestParam("id") Long id) {
+        studentService.deleteStudent(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentRespVO> getStudent(@RequestParam("id") Long id) {
+        InfraStudentDO student = studentService.getStudent(id);
+        return success(BeanUtils.toBean(student, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<PageResult<InfraStudentRespVO>> getStudentPage(@Valid InfraStudentPageReqVO pageReqVO) {
+        PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, InfraStudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportStudentExcel(@Valid InfraStudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<InfraStudentDO> list = studentService.getStudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", InfraStudentRespVO.class,
+                        BeanUtils.toBean(list, InfraStudentRespVO.class));
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @GetMapping("/student-contact/list-by-student-id")
+    @Operation(summary = "获得学生联系人列表")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<List<InfraStudentContactDO>> getStudentContactListByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentContactListByStudentId(studentId));
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @GetMapping("/student-teacher/get-by-student-id")
+    @Operation(summary = "获得学生班主任")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:student:query')")
+    public CommonResult<InfraStudentTeacherDO> getStudentTeacherByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(studentService.getStudentTeacherByStudentId(studentId));
+    }
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentDO

@@ -0,0 +1,67 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student")
+@KeySequence("infra_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentMapper

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentMapper extends BaseMapperX<InfraStudentDO> {
+
+    default PageResult<InfraStudentDO> selectPage(InfraStudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<InfraStudentDO>()
+                .likeIfPresent(InfraStudentDO::getName, reqVO.getName())
+                .eqIfPresent(InfraStudentDO::getBirthday, reqVO.getBirthday())
+                .eqIfPresent(InfraStudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(InfraStudentDO::getEnabled, reqVO.getEnabled())
+                .betweenIfPresent(InfraStudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(InfraStudentDO::getId));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentPageReqVO

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - 学生分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class InfraStudentPageReqVO extends PageParam {
+
+    @Schema(description = "名字", example = "芋头")
+    private String name;
+
+    @Schema(description = "出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "是否有效", example = "true")
+    private Boolean enabled;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 60 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentRespVO

@@ -0,0 +1,60 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import com.alibaba.excel.annotation.*;
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+
+@Schema(description = "管理后台 - 学生 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class InfraStudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @ExcelProperty(value = "是否有效", converter = DictConvert.class)
+    @DictFormat("infra_boolean_string") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @ExcelProperty("附件")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @ExcelProperty("备注")
+    private String memo;
+
+    @Schema(description = "创建时间")
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 58 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentSaveReqVO

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.util.*;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class InfraStudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋头")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是介绍")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "是否有效", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
+    @NotNull(message = "是否有效不能为空")
+    private Boolean enabled;
+
+    @Schema(description = "头像", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.png")
+    @NotEmpty(message = "头像不能为空")
+    private String avatar;
+
+    @Schema(description = "附件", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/1.mp4")
+    @NotEmpty(message = "附件不能为空")
+    private String video;
+
+    @Schema(description = "备注", requiredMode = Schema.RequiredMode.REQUIRED, example = "我是备注")
+    @NotEmpty(message = "备注不能为空")
+    private String memo;
+
+    @Schema(description = "学生联系人列表")
+    private List<InfraStudentContactDO> studentContacts;
+
+    @Schema(description = "学生班主任")
+    private InfraStudentTeacherDO studentTeacher;
+
+}

+ 77 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentService

@@ -0,0 +1,77 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import java.util.*;
+import javax.validation.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface InfraStudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createStudent(@Valid InfraStudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateStudent(@Valid InfraStudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteStudent(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    InfraStudentDO getStudent(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO);
+
+    // ==================== 子表(学生联系人) ====================
+
+    /**
+     * 获得学生联系人列表
+     *
+     * @param studentId 学生编号
+     * @return 学生联系人列表
+     */
+    List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId);
+
+    // ==================== 子表(学生班主任) ====================
+
+    /**
+     * 获得学生班主任
+     *
+     * @param studentId 学生编号
+     * @return 学生班主任
+     */
+    InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId);
+
+}

+ 147 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentServiceImpl

@@ -0,0 +1,147 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentContactDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentContactMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentTeacherMapper;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+
+/**
+ * 学生 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+public class InfraStudentServiceImpl implements InfraStudentService {
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+    @Resource
+    private InfraStudentContactMapper studentContactMapper;
+    @Resource
+    private InfraStudentTeacherMapper studentTeacherMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createStudent(InfraStudentSaveReqVO createReqVO) {
+        // 插入
+        InfraStudentDO student = BeanUtils.toBean(createReqVO, InfraStudentDO.class);
+        studentMapper.insert(student);
+
+        // 插入子表
+        createStudentContactList(student.getId(), createReqVO.getStudentContacts());
+        createStudentTeacher(student.getId(), createReqVO.getStudentTeacher());
+        // 返回
+        return student.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateStudent(InfraStudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateStudentExists(updateReqVO.getId());
+        // 更新
+        InfraStudentDO updateObj = BeanUtils.toBean(updateReqVO, InfraStudentDO.class);
+        studentMapper.updateById(updateObj);
+
+        // 更新子表
+        updateStudentContactList(updateReqVO.getId(), updateReqVO.getStudentContacts());
+        updateStudentTeacher(updateReqVO.getId(), updateReqVO.getStudentTeacher());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(id);
+
+        // 删除子表
+        deleteStudentContactByStudentId(id);
+        deleteStudentTeacherByStudentId(id);
+    }
+
+    private void validateStudentExists(Long id) {
+        if (studentMapper.selectById(id) == null) {
+            throw exception(STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public InfraStudentDO getStudent(Long id) {
+        return studentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<InfraStudentDO> getStudentPage(InfraStudentPageReqVO pageReqVO) {
+        return studentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生联系人) ====================
+
+    @Override
+    public List<InfraStudentContactDO> getStudentContactListByStudentId(Long studentId) {
+        return studentContactMapper.selectListByStudentId(studentId);
+    }
+
+    private void createStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        list.forEach(o -> o.setStudentId(studentId));
+        studentContactMapper.insertBatch(list);
+    }
+
+    private void updateStudentContactList(Long studentId, List<InfraStudentContactDO> list) {
+        deleteStudentContactByStudentId(studentId);
+		list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
+        createStudentContactList(studentId, list);
+    }
+
+    private void deleteStudentContactByStudentId(Long studentId) {
+        studentContactMapper.deleteByStudentId(studentId);
+    }
+
+    // ==================== 子表(学生班主任) ====================
+
+    @Override
+    public InfraStudentTeacherDO getStudentTeacherByStudentId(Long studentId) {
+        return studentTeacherMapper.selectByStudentId(studentId);
+    }
+
+    private void createStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+            return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacherMapper.insert(studentTeacher);
+    }
+
+    private void updateStudentTeacher(Long studentId, InfraStudentTeacherDO studentTeacher) {
+        if (studentTeacher == null) {
+			return;
+        }
+        studentTeacher.setStudentId(studentId);
+        studentTeacher.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
+        studentTeacherMapper.insertOrUpdate(studentTeacher);
+    }
+
+    private void deleteStudentTeacherByStudentId(Long studentId) {
+        studentTeacherMapper.deleteByStudentId(studentId);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentServiceImplTest

@@ -0,0 +1,146 @@
+package cn.iocoder.yudao.module.infra.service.demo;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.mock.mockito.MockBean;
+
+import javax.annotation.Resource;
+
+import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.vo.*;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.InfraStudentMapper;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+import javax.annotation.Resource;
+import org.springframework.context.annotation.Import;
+import java.util.*;
+import java.time.LocalDateTime;
+
+import static cn.hutool.core.util.RandomUtil.*;
+import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.*;
+import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
+import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
+import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.*;
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+/**
+ * {@link InfraStudentServiceImpl} 的单元测试类
+ *
+ * @author 芋道源码
+ */
+@Import(InfraStudentServiceImpl.class)
+public class InfraStudentServiceImplTest extends BaseDbUnitTest {
+
+    @Resource
+    private InfraStudentServiceImpl studentService;
+
+    @Resource
+    private InfraStudentMapper studentMapper;
+
+    @Test
+    public void testCreateStudent_success() {
+        // 准备参数
+        InfraStudentSaveReqVO createReqVO = randomPojo(InfraStudentSaveReqVO.class).setId(null);
+
+        // 调用
+        Long studentId = studentService.createStudent(createReqVO);
+        // 断言
+        assertNotNull(studentId);
+        // 校验记录的属性是否正确
+        InfraStudentDO student = studentMapper.selectById(studentId);
+        assertPojoEquals(createReqVO, student, "id");
+    }
+
+    @Test
+    public void testUpdateStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class, o -> {
+            o.setId(dbStudent.getId()); // 设置更新的 ID
+        });
+
+        // 调用
+        studentService.updateStudent(updateReqVO);
+        // 校验是否更新正确
+        InfraStudentDO student = studentMapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, student);
+    }
+
+    @Test
+    public void testUpdateStudent_notExists() {
+        // 准备参数
+        InfraStudentSaveReqVO updateReqVO = randomPojo(InfraStudentSaveReqVO.class);
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.updateStudent(updateReqVO), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    public void testDeleteStudent_success() {
+        // mock 数据
+        InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class);
+        studentMapper.insert(dbStudent);// @Sql: 先插入出一条存在的数据
+        // 准备参数
+        Long id = dbStudent.getId();
+
+        // 调用
+        studentService.deleteStudent(id);
+       // 校验数据不存在了
+       assertNull(studentMapper.selectById(id));
+    }
+
+    @Test
+    public void testDeleteStudent_notExists() {
+        // 准备参数
+        Long id = randomLongId();
+
+        // 调用, 并断言异常
+        assertServiceException(() -> studentService.deleteStudent(id), STUDENT_NOT_EXISTS);
+    }
+
+    @Test
+    @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
+    public void testGetStudentPage() {
+       // mock 数据
+       InfraStudentDO dbStudent = randomPojo(InfraStudentDO.class, o -> { // 等会查询到
+           o.setName(null);
+           o.setBirthday(null);
+           o.setSex(null);
+           o.setEnabled(null);
+           o.setCreateTime(null);
+       });
+       studentMapper.insert(dbStudent);
+       // 测试 name 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setName(null)));
+       // 测试 birthday 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setBirthday(null)));
+       // 测试 sex 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setSex(null)));
+       // 测试 enabled 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setEnabled(null)));
+       // 测试 createTime 不匹配
+       studentMapper.insert(cloneIgnoreId(dbStudent, o -> o.setCreateTime(null)));
+       // 准备参数
+       InfraStudentPageReqVO reqVO = new InfraStudentPageReqVO();
+       reqVO.setName(null);
+       reqVO.setBirthday(null);
+       reqVO.setSex(null);
+       reqVO.setEnabled(null);
+       reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
+
+       // 调用
+       PageResult<InfraStudentDO> pageResult = studentService.getStudentPage(reqVO);
+       // 断言
+       assertEquals(1, pageResult.getTotal());
+       assertEquals(1, pageResult.getList().size());
+       assertPojoEquals(dbStudent, pageResult.getList().get(0));
+    }
+
+}

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentTeacherDO

@@ -0,0 +1,71 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+import java.time.LocalDateTime;
+import com.baomidou.mybatisplus.annotation.*;
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+
+/**
+ * 学生班主任 DO
+ *
+ * @author 芋道源码
+ */
+@TableName("infra_student_teacher")
+@KeySequence("infra_student_teacher_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class InfraStudentTeacherDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 是否有效
+     *
+     * 枚举 {@link TODO infra_boolean_string 对应的类}
+     */
+    private Boolean enabled;
+    /**
+     * 头像
+     */
+    private String avatar;
+    /**
+     * 附件
+     */
+    private String video;
+    /**
+     * 备注
+     */
+    private String memo;
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/java/InfraStudentTeacherMapper

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.InfraStudentTeacherDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 学生班主任 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface InfraStudentTeacherMapper extends BaseMapperX<InfraStudentTeacherDO> {
+
+    default InfraStudentTeacherDO selectByStudentId(Long studentId) {
+        return selectOne(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+    default int deleteByStudentId(Long studentId) {
+        return delete(InfraStudentTeacherDO::getStudentId, studentId);
+    }
+
+}

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/sql/h2

@@ -0,0 +1,17 @@
+-- 将该建表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/create_tables.sql 文件里
+CREATE TABLE IF NOT EXISTS "infra_student" (
+    "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
+    "name" varchar NOT NULL,
+    "description" varchar NOT NULL,
+    "birthday" varchar NOT NULL,
+    "sex" int NOT NULL,
+    "enabled" bit NOT NULL,
+    "avatar" varchar NOT NULL,
+    "video" varchar NOT NULL,
+    "memo" varchar NOT NULL,
+    "create_time" datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    PRIMARY KEY ("id")
+) COMMENT '学生表';
+
+-- 将该删表 SQL 语句,添加到 yudao-module-infra-biz 模块的 test/resources/sql/clean.sql 文件里
+DELETE FROM "infra_student";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/sql/sql

@@ -0,0 +1,55 @@
+-- 菜单 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status, component_name
+)
+VALUES (
+    '学生管理', '', 2, 0, 888,
+    'student', '', 'infra/demo/index', 0, 'InfraStudent'
+);
+
+-- 按钮父菜单ID
+-- 暂时只支持 MySQL。如果你是 Oracle、PostgreSQL、SQLServer 的话,需要手动修改 @parentId 的部分的代码
+SELECT @parentId := LAST_INSERT_ID();
+
+-- 按钮 SQL
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生查询', 'infra:student:query', 3, 1, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生创建', 'infra:student:create', 3, 2, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生更新', 'infra:student:update', 3, 3, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生删除', 'infra:student:delete', 3, 4, @parentId,
+    '', '', '', 0
+);
+INSERT INTO system_menu(
+    name, permission, type, sort, parent_id,
+    path, icon, component, status
+)
+VALUES (
+    '学生导出', 'infra:student:export', 3, 5, @parentId,
+    '', '', '', 0
+);

+ 57 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/ts/index

@@ -0,0 +1,57 @@
+import request from '@/config/axios'
+
+export interface StudentVO {
+  id: number
+  name: string
+  description: string
+  birthday: Date
+  sex: number
+  enabled: boolean
+  avatar: string
+  video: string
+  memo: string
+}
+
+// 查询学生分页
+export const getStudentPage = async (params) => {
+  return await request.get({ url: `/infra/student/page`, params })
+}
+
+// 查询学生详情
+export const getStudent = async (id: number) => {
+  return await request.get({ url: `/infra/student/get?id=` + id })
+}
+
+// 新增学生
+export const createStudent = async (data: StudentVO) => {
+  return await request.post({ url: `/infra/student/create`, data })
+}
+
+// 修改学生
+export const updateStudent = async (data: StudentVO) => {
+  return await request.put({ url: `/infra/student/update`, data })
+}
+
+// 删除学生
+export const deleteStudent = async (id: number) => {
+  return await request.delete({ url: `/infra/student/delete?id=` + id })
+}
+
+// 导出学生 Excel
+export const exportStudent = async (params) => {
+  return await request.download({ url: `/infra/student/export-excel`, params })
+}
+
+// ==================== 子表(学生联系人) ====================
+
+// 获得学生联系人列表
+export const getStudentContactListByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/student/student-contact/list-by-student-id?studentId=` + studentId })
+}
+
+// ==================== 子表(学生班主任) ====================
+
+// 获得学生班主任
+export const getStudentTeacherByStudentId = async (studentId) => {
+  return await request.get({ url: `/infra/student/student-teacher/get-by-student-id?studentId=` + studentId })
+}

+ 174 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue3_master_normal/vue/StudentContactForm

@@ -0,0 +1,174 @@
+<template>
+  <el-form
+    ref="formRef"
+    :model="formData"
+    :rules="formRules"
+    v-loading="formLoading"
+    label-width="0px"
+    :inline-message="true"
+  >
+    <el-table :data="formData" class="-mt-10px">
+      <el-table-column label="序号" type="index" width="100" />
+       <el-table-column label="名字" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.name`" :rules="formRules.name" class="mb-0px!">
+            <el-input v-model="row.name" placeholder="请输入名字" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.description`" :rules="formRules.description" class="mb-0px!">
+            <el-input v-model="row.description" type="textarea" placeholder="请输入简介" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+            <el-date-picker
+              v-model="row.birthday"
+              type="date"
+              value-format="x"
+              placeholder="选择出生日期"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.sex`" :rules="formRules.sex" class="mb-0px!">
+            <el-select v-model="row.sex" placeholder="请选择性别">
+                <el-option
+                  v-for="dict in getIntDictOptions(DICT_TYPE.SYSTEM_USER_SEX)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+            </el-select>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否有效" min-width="150">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.enabled`" :rules="formRules.enabled" class="mb-0px!">
+            <el-radio-group v-model="row.enabled">
+                <el-radio
+                  v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
+                  :key="dict.value"
+                  :label="dict.value"
+                >
+                  {{ dict.label }}
+                </el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="头像" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+            <UploadImg v-model="row.avatar" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="附件" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+            <UploadFile v-model="row.video" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" min-width="400">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+            <Editor v-model="row.memo" height="150px" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" fixed="right" label="操作" width="60">
+        <template #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+  </el-row>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as StudentApi from '@/api/infra/demo'
+
+const props = defineProps<{
+  studentId: undefined // 学生编号(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+  studentId: [{ required: true, message: '学生编号不能为空', trigger: 'blur' }],
+  name: [{ required: true, message: '名字不能为空', trigger: 'blur' }],
+  description: [{ required: true, message: '简介不能为空', trigger: 'blur' }],
+  birthday: [{ required: true, message: '出生日期不能为空', trigger: 'blur' }],
+  sex: [{ required: true, message: '性别不能为空', trigger: 'change' }],
+  enabled: [{ required: true, message: '是否有效不能为空', trigger: 'blur' }],
+  avatar: [{ required: true, message: '头像不能为空', trigger: 'blur' }],
+  memo: [{ required: true, message: '备注不能为空', trigger: 'blur' }]
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.studentId,
+  async (val) => {
+    // 1. 重置表单
+    formData.value = []
+    // 2. val 非空,则加载数据
+    if (!val) {
+      return;
+    }
+    try {
+      formLoading.value = true
+      formData.value = await StudentApi.getStudentContactListByStudentId(val)
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+
+/** 新增按钮操作 */
+const handleAdd = () => {
+  const row = {
+    id: undefined,
+    studentId: undefined,
+    name: undefined,
+    description: undefined,
+    birthday: undefined,
+    sex: undefined,
+    enabled: undefined,
+    avatar: undefined,
+    video: undefined,
+    memo: undefined
+  }
+  row.studentId = props.studentId
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

Некоторые файлы не были показаны из-за большого количества измененных файлов