Bläddra i källkod

!765 Vue2 + Element UI 代码生成器
Merge pull request !765 from 芋道源码/feature/sub-table

芋道源码 1 år sedan
förälder
incheckning
7724d6e830
100 ändrade filer med 8565 tillägg och 295 borttagningar
  1. 17 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  2. 97 5
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm
  3. 205 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm
  4. 2 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm
  5. 347 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm
  6. 165 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm
  7. 4 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm
  8. 320 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm
  9. 171 200
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm
  10. 13 89
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineTest.java
  11. 95 0
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue2Test.java
  12. 95 0
      yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue3Test.java
  13. 73 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/assert.json
  14. 6 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/ErrorCodeConstants_手动操作
  15. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentContactDO
  16. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentContactMapper
  17. 183 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentController
  18. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentDO
  19. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentMapper
  20. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentPageReqVO
  21. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentRespVO
  22. 52 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentSaveReqVO
  23. 139 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentService
  24. 180 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentServiceImpl
  25. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentServiceImplTest
  26. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentTeacherDO
  27. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/java/InfraStudentTeacherMapper
  28. 141 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/js/student
  29. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/sql/h2
  30. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/sql/sql
  31. 151 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactForm
  32. 129 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactList
  33. 149 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentForm
  34. 151 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherForm
  35. 129 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherList
  36. 233 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/index
  37. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/xml/InfraStudentMapper
  38. 73 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/assert.json
  39. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/ErrorCodeConstants_手动操作
  40. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentContactDO
  41. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentContactMapper
  42. 117 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentController
  43. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentDO
  44. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentMapper
  45. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentPageReqVO
  46. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentRespVO
  47. 58 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentSaveReqVO
  48. 77 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentService
  49. 147 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentServiceImpl
  50. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentServiceImplTest
  51. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentTeacherDO
  52. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/java/InfraStudentTeacherMapper
  53. 74 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/js/student
  54. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/sql/h2
  55. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/sql/sql
  56. 177 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactForm
  57. 89 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactList
  58. 180 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentForm
  59. 127 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherForm
  60. 93 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherList
  61. 222 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/index
  62. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/xml/InfraStudentMapper
  63. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/assert.json
  64. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/ErrorCodeConstants_手动操作
  65. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentContactDO
  66. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentContactMapper
  67. 117 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentController
  68. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentDO
  69. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentMapper
  70. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentPageReqVO
  71. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentRespVO
  72. 58 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentSaveReqVO
  73. 77 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentService
  74. 147 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentServiceImpl
  75. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentServiceImplTest
  76. 71 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentTeacherDO
  77. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/java/InfraStudentTeacherMapper
  78. 74 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/js/student
  79. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/sql/h2
  80. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/sql/sql
  81. 177 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentContactForm
  82. 180 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentForm
  83. 127 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentTeacherForm
  84. 205 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/index
  85. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/xml/InfraStudentMapper
  86. 49 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/assert.json
  87. 3 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/ErrorCodeConstants_手动操作
  88. 95 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentController
  89. 67 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentDO
  90. 30 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentMapper
  91. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentPageReqVO
  92. 60 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentRespVO
  93. 50 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentSaveReqVO
  94. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentService
  95. 74 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImpl
  96. 146 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImplTest
  97. 53 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/js/student
  98. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/sql/h2
  99. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/sql/sql
  100. 0 0
      yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/vue/StudentForm

+ 17 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java

@@ -104,6 +104,18 @@ public class CodegenEngine {
                     vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
             .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"),
                     vueFilePath("api/${table.moduleName}/${classNameVar}.js"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/form.vue"),
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_normal.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
+            .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
             // Vue3 标准模版
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"),
                     vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
@@ -285,6 +297,10 @@ public class CodegenEngine {
         if (StrUtil.count(content, "dateFormatter") == 1) {
             content = StrUtils.removeLineContains(content, "dateFormatter");
         }
+        // Vue2 界面:修正 $refs
+        if (StrUtil.count(content, "this.refs") >= 1) {
+            content = content.replace("this.refs", "this.$refs");
+        }
         // Vue 界面:去除多的 dict 相关,只有一个的情况下,说明没使用到
         if (StrUtil.count(content, "getIntDictOptions") == 1) {
             content = content.replace("getIntDictOptions, ", "");
@@ -452,7 +468,7 @@ public class CodegenEngine {
     }
 
     private static String vueFilePath(String path) {
-        return "yudao-ui-${sceneEnum.basePackage}/" + // 顶级目录
+        return "yudao-ui-${sceneEnum.basePackage}-vue2/" + // 顶级目录
                 "src/" + path;
     }
 

+ 97 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/api/api.js.vm

@@ -35,21 +35,113 @@ export function get${simpleClassName}(id) {
   })
 }
 
+#if ( $table.templateType != 2 )
 // 获得${table.classComment}分页
-export function get${simpleClassName}Page(query) {
+export function get${simpleClassName}Page(params) {
   return request({
     url: '${baseURL}/page',
     method: 'get',
-    params: query
+    params
   })
 }
-
+#else
+// 获得${table.classComment}列表
+export function get${simpleClassName}List(params) {
+  return request({
+    url: '${baseURL}/list',
+    method: 'get',
+    params
+  })
+}
+#end
 // 导出${table.classComment} Excel
-export function export${simpleClassName}Excel(query) {
+export function export${simpleClassName}Excel(params) {
   return request({
     url: '${baseURL}/export-excel',
     method: 'get',
-    params: query,
+    params,
     responseType: 'blob'
   })
 }
+## 特殊:主子表专属逻辑 TODO @puhui999:下面方法的【空格】不太对
+#foreach ($subTable in $subTables)
+  #set ($index = $foreach.count - 1)
+  #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+  #set ($subPrimaryColumn = $subPrimaryColumns.get($index))##当前 primary 字段
+  #set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
+  #set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+  #set ($subSimpleClassName_strikeCase = $subSimpleClassName_strikeCases.get($index))
+  #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+  #set ($subClassNameVar = $subClassNameVars.get($index))
+
+// ==================== 子表($subTable.classComment) ====================
+  ## 情况一:MASTER_ERP 时,需要分查询页子表
+  #if ( $table.templateType == 11 )
+
+  // 获得${subTable.classComment}分页
+  export function get${subSimpleClassName}Page(params) {
+    return request({
+      url: '${baseURL}/${subSimpleClassName_strikeCase}/page',
+      method: 'get',
+      params
+    })
+  }
+    ## 情况二:非 MASTER_ERP 时,需要列表查询子表
+  #else
+    #if ( $subTable.subJoinMany )
+
+    // 获得${subTable.classComment}列表
+    export function get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}) {
+      return request({
+        url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField},
+        method: 'get'
+      })
+    }
+    #else
+
+    // 获得${subTable.classComment}
+    export function get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}) {
+      return request({
+        url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField},
+        method: 'get'
+      })
+    }
+    #end
+  #end
+  ## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
+  #if ( $table.templateType == 11 )
+  // 新增${subTable.classComment}
+  export function create${subSimpleClassName}(data) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/create`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 修改${subTable.classComment}
+  export function update${subSimpleClassName}(data) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/update`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 删除${subTable.classComment}
+  export function delete${subSimpleClassName}(id) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id,
+      method: 'delete'
+    })
+  }
+
+  // 获得${subTable.classComment}
+  export function get${subSimpleClassName}(id) {
+    return request({
+      url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id,
+      method: 'get'
+    })
+  }
+  #end
+#end

+ 205 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_erp.vue.vm

@@ -0,0 +1,205 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+          #foreach($column in $subColumns)
+              #if ($column.createOperation || $column.updateOperation)
+                  #set ($dictType = $column.dictType)
+                  #set ($javaField = $column.javaField)
+                  #set ($javaType = $column.javaType)
+                  #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+                  #set ($comment = $column.columnComment)
+                  #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                  #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "imageUpload")## 图片上传
+                      #set ($hasImageUploadColumn = true)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <ImageUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "fileUpload")## 文件上传
+                      #set ($hasFileUploadColumn = true)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <FileUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "editor")## 文本编辑器
+                      #set ($hasEditorColumn = true)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <editor v-model="formData.${javaField}" :min-height="192"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "select")## 下拉框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                          #else##没数据字典
+                            <el-option label="请选择字典生成" value="" />
+                          #end
+                      </el-select>
+                    </el-form-item>
+                  #elseif($column.htmlType == "checkbox")## 多选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-checkbox-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                          #else##没数据字典
+                            <el-checkbox>请选择字典生成</el-checkbox>
+                          #end
+                      </el-checkbox-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "radio")## 单选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-radio-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                      #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                          #else##没数据字典
+                            <el-radio label="1">请选择字典生成</el-radio>
+                          #end
+                      </el-radio-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "datetime")## 时间框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-date-picker clearable v-model="formData.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "textarea")## 文本框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                  #end
+              #end
+          #end
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}';
+      #if ($hasImageUploadColumn)
+      import ImageUpload from '@/components/ImageUpload';
+      #end
+      #if ($hasFileUploadColumn)
+      import FileUpload from '@/components/FileUpload';
+      #end
+      #if ($hasEditorColumn)
+      import Editor from '@/components/Editor';
+      #end
+  export default {
+    name: "${subSimpleClassName}Form",
+    components: {
+        #if ($hasImageUploadColumn)
+          ImageUpload,
+        #end
+        #if ($hasFileUploadColumn)
+          FileUpload,
+        #end
+        #if ($hasEditorColumn)
+          Editor,
+        #end
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+            #foreach ($column in $subColumns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        },
+        // 表单校验
+        formRules: {
+            #foreach ($column in $subColumns)
+                #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
+                    #set($comment=$column.columnComment)
+                        $column.javaField: [{ required: true, message: "${comment}不能为空", trigger: #if($column.htmlType == "select")"change"#else"blur"#end }],
+                #end
+            #end
+        },
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+      async open(id, ${subJoinColumn.javaField}) {
+        this.dialogVisible = true;
+        this.reset();
+        this.formData.${subJoinColumn.javaField} = ${subJoinColumn.javaField};
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await ${simpleClassName}Api.get${subSimpleClassName}(id);
+            this.formData = res.data;
+            this.dialogTitle = "修改${subTable.classComment}";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.dialogTitle = "新增${subTable.classComment}";
+      },
+      /** 提交按钮 */
+      async submitForm() {
+        await this.#[[$]]#refs["formRef"].validate();
+        this.formLoading = true;
+        try {
+            const data = this.formData;
+            // 修改的提交
+            if (data.${primaryColumn.javaField}) {
+            await  ${simpleClassName}Api.update${subSimpleClassName}(data);
+            this.#[[$modal]]#.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.#[[$]]#emit('success');
+              return;
+            }
+            // 添加的提交
+              await ${simpleClassName}Api.create${subSimpleClassName}(data);
+              this.#[[$modal]]#.msgSuccess("新增成功");
+              this.dialogVisible = false;
+              this.#[[$]]#emit('success');
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+            #foreach ($column in $subColumns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 2 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_inner.vue.vm

@@ -0,0 +1,2 @@
+## 主表的 normal 和 inner 使用相同的 form 表单
+#parse("codegen/vue/views/components/form_sub_normal.vue.vm")

+ 347 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/form_sub_normal.vue.vm

@@ -0,0 +1,347 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+<template>
+  <div class="app-container">
+    #if ( $subTable.subJoinMany )## 情况一:一对多,table + form
+      <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" />
+            #foreach($column in $subColumns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #set ($dictType = $column.dictType)
+                    #set ($javaField = $column.javaField)
+                    #set ($javaType = $column.javaType)
+                    #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+                    #set ($comment = $column.columnComment)
+                    #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                    #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-input v-model="row.${javaField}" placeholder="请输入${comment}" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "imageUpload")## 图片上传
+                        #set ($hasImageUploadColumn = true)
+                      <el-table-column label="${comment}" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <ImageUpload v-model="row.${javaField}"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "fileUpload")## 文件上传
+                        #set ($hasFileUploadColumn = true)
+                      <el-table-column label="${comment}" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <FileUpload v-model="row.${javaField}"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "editor")## 文本编辑器
+                        #set ($hasEditorColumn = true)
+                      <el-table-column label="${comment}" min-width="400">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <Editor v-model="row.${javaField}" :min-height="192"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "select")## 下拉框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-select v-model="row.${javaField}" placeholder="请选择${comment}">
+                                #if ("" != $dictType)## 有数据字典
+                                  <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                             :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                                #else##没数据字典
+                                  <el-option label="请选择字典生成" value="" />
+                                #end
+                            </el-select>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "checkbox")## 多选框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-checkbox-group v-model="row.${javaField}">
+                                #if ("" != $dictType)## 有数据字典
+                                  <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                               :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                                #else##没数据字典
+                                  <el-checkbox>请选择字典生成</el-checkbox>
+                                #end
+                            </el-checkbox-group>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "radio")## 单选框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-radio-group v-model="row.${javaField}">
+                                #if ("" != $dictType)## 有数据字典
+                                  <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                            :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                            #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                                #else##没数据字典
+                                  <el-radio label="1">请选择字典生成</el-radio>
+                                #end
+                            </el-radio-group>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "datetime")## 时间框
+                      <el-table-column label="${comment}" min-width="150">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-date-picker clearable v-model="row.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #elseif($column.htmlType == "textarea")## 文本框
+                      <el-table-column label="${comment}" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+                            <el-input v-model="row.${javaField}" type="textarea" placeholder="请输入${comment}" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                    #end
+                #end
+            #end
+          <el-table-column align="center" fixed="right" label="操作" width="60">
+            <template v-slot="{ $index }">
+              <el-link @click="handleDelete($index)">—</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form>
+      <el-row justify="center" class="mt-3">
+        <el-button @click="handleAdd" round>+ 添加${subTable.classComment}</el-button>
+      </el-row>
+    #else## 情况二:一对一,form
+      <el-form
+        ref="formRef"
+        :model="formData"
+        :rules="formRules"
+        label-width="100px"
+        v-loading="formLoading"
+      >
+          #foreach($column in $subColumns)
+              #if ($column.createOperation || $column.updateOperation)
+                  #set ($dictType = $column.dictType)
+                  #set ($javaField = $column.javaField)
+                  #set ($javaType = $column.javaType)
+                  #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+                  #set ($comment = $column.columnComment)
+                  #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                  #elseif ($column.htmlType == "input" && !$column.primaryKey)
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "imageUpload")## 图片上传
+                      #set ($hasImageUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <ImageUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "fileUpload")## 文件上传
+                      #set ($hasFileUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <FileUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "editor")## 文本编辑器
+                      #set ($hasEditorColumn = true)
+                    <el-form-item label="${comment}">
+                      <Editor v-model="formData.${javaField}" :min-height="192"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "select")## 下拉框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                          #else##没数据字典
+                            <el-option label="请选择字典生成" value="" />
+                          #end
+                      </el-select>
+                    </el-form-item>
+                  #elseif($column.htmlType == "checkbox")## 多选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-checkbox-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                          #else##没数据字典
+                            <el-checkbox>请选择字典生成</el-checkbox>
+                          #end
+                      </el-checkbox-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "radio")## 单选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-radio-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                      #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                          #else##没数据字典
+                            <el-radio label="1">请选择字典生成</el-radio>
+                          #end
+                      </el-radio-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "datetime")## 时间框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-date-picker clearable v-model="formData.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "textarea")## 文本框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #end
+              #end
+          #end
+      </el-form>
+    #end
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}';
+      #if ($hasImageUploadColumn)
+      import ImageUpload from '@/components/ImageUpload';
+      #end
+      #if ($hasFileUploadColumn)
+      import FileUpload from '@/components/FileUpload';
+      #end
+      #if ($hasEditorColumn)
+      import Editor from '@/components/Editor';
+      #end
+  export default {
+    name: "${subSimpleClassName}Form",
+    components: {
+        #if ($hasImageUploadColumn)
+          ImageUpload,
+        #end
+        #if ($hasFileUploadColumn)
+          FileUpload,
+        #end
+        #if ($hasEditorColumn)
+          Editor,
+        #end
+    },
+    props:[
+      '${subJoinColumn.javaField}'
+    ],// ${subJoinColumn.columnComment}(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+            #foreach ($column in $subColumns)
+                #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
+                    #set($comment=$column.columnComment)
+                        $column.javaField: [{ required: true, message: "${comment}不能为空", trigger: #if($column.htmlType == "select")"change"#else"blur"#end }],
+                #end
+            #end
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      ${subJoinColumn.javaField}:{
+        handler(val) {
+          // 1. 重置表单
+            #if ( $subTable.subJoinMany )
+              this.formData = []
+            #else
+              this.formData = {
+                  #foreach ($column in $subColumns)
+                      #if ($column.createOperation || $column.updateOperation)
+                          #if ($column.htmlType == "checkbox")
+                                  $column.javaField: [],
+                          #else
+                                  $column.javaField: undefined,
+                          #end
+                      #end
+                  #end
+              }
+            #end
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            // 这里还是需要获取一下 this 的不然取不到 formData
+            const that = this;
+            #if ( $subTable.subJoinMany )
+            ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(val).then(function (res){
+              that.formData = res.data;
+            })
+            #else
+            ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(val).then(function (res){
+              const data = res.data;
+              if (!data) {
+                return
+              }
+              that.formData = data;
+            })
+            #end
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+        #if ( $subTable.subJoinMany )
+          /** 新增按钮操作 */
+          handleAdd() {
+            const row = {
+                #foreach ($column in $subColumns)
+                    #if ($column.createOperation || $column.updateOperation)
+                        #if ($column.htmlType == "checkbox")
+                                $column.javaField: [],
+                        #else
+                                $column.javaField: undefined,
+                        #end
+                    #end
+                #end
+            }
+            row.${subJoinColumn.javaField} = this.${subJoinColumn.javaField};
+            this.formData.push(row);
+          },
+          /** 删除按钮操作 */
+          handleDelete(index) {
+            this.formData.splice(index, 1);
+          },
+        #end
+      /** 表单校验 */
+      validate(){
+        return this.#[[$]]#refs["formRef"].validate();
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData;
+      }
+    }
+  };
+</script>

+ 165 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_erp.vue.vm

@@ -0,0 +1,165 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+<template>
+  <div class="app-container">
+#if ($table.templateType == 11)
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
+      </el-col>
+    </el-row>
+#end
+      ## 列表
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+          #foreach($column in $subColumns)
+              #if ($column.listOperationResult)
+                  #set ($dictType=$column.dictType)
+                  #set ($javaField = $column.javaField)
+                  #set ($AttrName=$column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+                  #set ($comment=$column.columnComment)
+                  #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+                  #elseif ($column.javaType == "LocalDateTime")## 时间类型
+                <el-table-column label="${comment}" align="center" prop="${javaField}" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.${javaField}) }}</span>
+                  </template>
+                </el-table-column>
+                  #elseif($column.dictType && "" != $column.dictType)## 数据字典
+                <el-table-column label="${comment}" align="center" prop="${javaField}">
+                  <template v-slot="scope">
+                    <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.${column.javaField}" />
+                  </template>
+                </el-table-column>
+              #else
+                <el-table-column label="${comment}" align="center" prop="${javaField}" />
+              #end
+          #end
+      #end
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.${primaryColumn.javaField})"
+                   v-hasPermi="['${permissionPrefix}:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['${permissionPrefix}:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+#if ($table.templateType == 11)
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+  <!-- 对话框(添加 / 修改) -->
+  <${subSimpleClassName}Form ref="formRef" @success="getList" />
+#end
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}';
+  #if ($table.templateType == 11)
+  import ${subSimpleClassName}Form from './${subSimpleClassName}Form.vue';
+  #end
+  export default {
+    name: "${subSimpleClassName}List",
+#if ($table.templateType == 11)
+    components: {
+       ${subSimpleClassName}Form
+    },
+#end
+    props:[
+      '${subJoinColumn.javaField}'
+    ],// ${subJoinColumn.columnComment}(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+#if ($table.templateType == 11)
+        // 列表的总页数
+        total: 0,
+        // 查询参数
+        queryParams: {
+          pageNo: 1,
+          pageSize: 10,
+          ${subJoinColumn.javaField}: undefined
+        }
+#end
+      };
+    },
+#if ($table.templateType != 11)
+    created() {
+      this.getList();
+    },
+#end
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        ${subJoinColumn.javaField}:{
+            handler(val) {
+              this.queryParams.${subJoinColumn.javaField} = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      async getList() {
+        try {
+          this.loading = true;
+          #if ($table.templateType == 11)
+            const res = await ${simpleClassName}Api.get${subSimpleClassName}Page(this.queryParams);
+            this.list = res.data.list;
+            this.total = res.data.total;
+          #else
+              #if ( $subTable.subJoinMany )
+                const res = await ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(this.${subJoinColumn.javaField});
+                this.list = res.data;
+              #else
+                const res = await  ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(this.${subJoinColumn.javaField});
+                const data = res.data;
+                if (!data) {
+                  return;
+                }
+                this.list.push(data);
+              #end
+          #end
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+#if ($table.templateType == 11)
+      /** 添加/修改操作 */
+      openForm(id) {
+        if (!this.${subJoinColumn.javaField}) {
+          this.#[[$modal]]#.msgError('请选择一个${table.classComment}');
+          return;
+        }
+        this.#[[$]]#refs["formRef"].open(id, this.${subJoinColumn.javaField});
+      },
+      /** 删除按钮操作 */
+      async handleDelete(row) {
+        const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
+        await this.#[[$modal]]#.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField} + '"的数据项?');
+        try {
+          await ${simpleClassName}Api.delete${subSimpleClassName}(${primaryColumn.javaField});
+          await this.getList();
+          this.#[[$modal]]#.msgSuccess("删除成功");
+        } catch {}
+      },
+#end
+    }
+  };
+</script>

+ 4 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/components/list_sub_inner.vue.vm

@@ -0,0 +1,4 @@
+## 子表的 erp 和 inner 使用相似的 list 列表,差异主要两点:
+## 1)inner 使用 list 不分页,erp 使用 page 分页
+## 2)erp 支持单个子表的新增、修改、删除,inner 不支持
+#parse("codegen/vue/views/components/list_sub_erp.vue.vm")

+ 320 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/form.vue.vm

@@ -0,0 +1,320 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+          #foreach($column in $columns)
+              #if ($column.createOperation || $column.updateOperation)
+                  #set ($dictType = $column.dictType)
+                  #set ($javaField = $column.javaField)
+                  #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
+                  #set ($comment = $column.columnComment)
+                  #if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <TreeSelect
+                        v-model="formData.${javaField}"
+                        :options="${classNameVar}Tree"
+                        :normalizer="normalizer"
+                        placeholder="请选择${comment}"
+                      />
+                    </el-form-item>
+                  #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" placeholder="请输入${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "imageUpload")## 图片上传
+                      #set ($hasImageUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <ImageUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "fileUpload")## 文件上传
+                      #set ($hasFileUploadColumn = true)
+                    <el-form-item label="${comment}">
+                      <FileUpload v-model="formData.${javaField}"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "editor")## 文本编辑器
+                      #set ($hasEditorColumn = true)
+                    <el-form-item label="${comment}">
+                      <Editor v-model="formData.${javaField}" :min-height="192"/>
+                    </el-form-item>
+                  #elseif($column.htmlType == "select")## 下拉框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
+                          #else##没数据字典
+                            <el-option label="请选择字典生成" value="" />
+                          #end
+                      </el-select>
+                    </el-form-item>
+                  #elseif($column.htmlType == "checkbox")## 多选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-checkbox-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
+                          #else##没数据字典
+                            <el-checkbox>请选择字典生成</el-checkbox>
+                          #end
+                      </el-checkbox-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "radio")## 单选框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-radio-group v-model="formData.${javaField}">
+                          #if ("" != $dictType)## 有数据字典
+                            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
+                                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"
+                                      #else:label="dict.value"#end>{{dict.label}}</el-radio>
+                          #else##没数据字典
+                            <el-radio label="1">请选择字典生成</el-radio>
+                          #end
+                      </el-radio-group>
+                    </el-form-item>
+                  #elseif($column.htmlType == "datetime")## 时间框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-date-picker clearable v-model="formData.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
+                    </el-form-item>
+                  #elseif($column.htmlType == "textarea")## 文本框
+                    <el-form-item label="${comment}" prop="${javaField}">
+                      <el-input v-model="formData.${javaField}" type="textarea" placeholder="请输入内容" />
+                    </el-form-item>
+                  #end
+              #end
+          #end
+      </el-form>
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+          <!-- 子表的表单 -->
+          <el-tabs v-model="subTabsName">
+              #foreach ($subTable in $subTables)
+                  #set ($index = $foreach.count - 1)
+                  #set ($subClassNameVar = $subClassNameVars.get($index))
+                  #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+                  #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+                <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                  <${subSimpleClassName}Form ref="${subClassNameVar}FormRef" :${subJoinColumn_strikeCase}="formData.id" />
+                </el-tab-pane>
+              #end
+          </el-tabs>
+        #end
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}';
+  #if ($hasImageUploadColumn)
+  import ImageUpload from '@/components/ImageUpload';
+  #end
+  #if ($hasFileUploadColumn)
+  import FileUpload from '@/components/FileUpload';
+  #end
+  #if ($hasEditorColumn)
+  import Editor from '@/components/Editor';
+  #end
+  ## 特殊:树表专属逻辑
+  #if ( $table.templateType == 2 )
+  import TreeSelect from "@riophae/vue-treeselect";
+  import "@riophae/vue-treeselect/dist/vue-treeselect.css";
+  #end
+  ## 特殊:主子表专属逻辑
+  #if ( $table.templateType == 10 || $table.templateType == 12 )
+      #foreach ($subSimpleClassName in $subSimpleClassNames)
+      import ${subSimpleClassName}Form from './components/${subSimpleClassName}Form.vue'
+      #end
+  #end
+  export default {
+    name: "${simpleClassName}Form",
+    components: {
+        #if ($hasImageUploadColumn)
+          ImageUpload,
+        #end
+        #if ($hasFileUploadColumn)
+          FileUpload,
+        #end
+        #if ($hasEditorColumn)
+          Editor,
+        #end
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          TreeSelect,
+        #end
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+            #foreach ($subSimpleClassName in $subSimpleClassNames)
+               ${subSimpleClassName}Form,
+            #end
+        #end
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+            #foreach ($column in $columns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        },
+        // 表单校验
+        formRules: {
+            #foreach ($column in $columns)
+                #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
+                    #set($comment=$column.columnComment)
+                        $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
+                #end
+            #end
+        },
+          ## 特殊:树表专属逻辑
+          #if ( $table.templateType == 2 )
+             ${classNameVar}Tree: [], // 树形结构
+          #end
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+        #if ( $subTables && $subTables.size() > 0 )
+            /** 子表的表单 */
+             subTabsName: '$subClassNameVars.get(0)'
+        #end
+        #end
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+     async open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await ${simpleClassName}Api.get${simpleClassName}(id);
+            this.formData = res.data;
+            this.title = "修改${table.classComment}";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增${table.classComment}";
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+        await this.get${simpleClassName}Tree();
+        #end
+      },
+      /** 提交按钮 */
+      async submitForm() {
+        // 校验主表
+        await this.$refs["formRef"].validate();
+          ## 特殊:主子表专属逻辑
+          #if ( $table.templateType == 10 || $table.templateType == 12 )
+              #if ( $subTables && $subTables.size() > 0 )
+                // 校验子表
+                  #foreach ($subTable in $subTables)
+                      #set ($index = $foreach.count - 1)
+                      #set ($subClassNameVar = $subClassNameVars.get($index))
+                    try {
+                      ## 代码生成后会替换为正确的 refs
+                      await this.refs['${subClassNameVar}FormRef'].validate();
+                    } catch (e) {
+                      this.subTabsName = '${subClassNameVar}';
+                      return;
+                    }
+                  #end
+              #end
+          #end
+        this.formLoading = true;
+        try {
+          const data = this.formData;
+        ## 特殊:主子表专属逻辑
+        #if ( $table.templateType == 10 || $table.templateType == 12 )
+        #if ( $subTables && $subTables.size() > 0 )
+            // 拼接子表的数据
+            #foreach ($subTable in $subTables)
+                #set ($index = $foreach.count - 1)
+                #set ($subClassNameVar = $subClassNameVars.get($index))
+              data.${subClassNameVar}#if ( $subTable.subJoinMany)s#end = this.refs['${subClassNameVar}FormRef'].getData();
+            #end
+        #end
+        #end
+          // 修改的提交
+          if (data.${primaryColumn.javaField}) {
+            await ${simpleClassName}Api.update${simpleClassName}(data);
+            this.#[[$modal]]#.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.#[[$]]#emit('success');
+            return;
+          }
+          // 添加的提交
+          await ${simpleClassName}Api.create${simpleClassName}(data);
+          this.#[[$modal]]#.msgSuccess("新增成功");
+          this.dialogVisible = false;
+          this.#[[$]]#emit('success');
+        } finally {
+          this.formLoading = false;
+        }
+      },
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          /** 获得${table.classComment}树 */
+         async get${simpleClassName}Tree() {
+            this.${classNameVar}Tree = [];
+            const res = await ${simpleClassName}Api.get${simpleClassName}List();
+            const root = { id: 0, name: '顶级${table.classComment}', children: [] };
+            root.children = this.handleTree(res.data, 'id', '${treeParentColumn.javaField}')
+            this.${classNameVar}Tree.push(root)
+          },
+        #end
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          /** 转换${table.classComment}数据结构 */
+          normalizer(node) {
+            if (node.children && !node.children.length) {
+              delete node.children;
+            }
+              #if ($treeNameColumn.javaField == "name")
+                return {
+                  id: node.id,
+                  label: node.name,
+                  children: node.children
+                };
+              #else
+                return {
+                  id: node.id,
+                  label: node['$treeNameColumn.javaField'],
+                  children: node.children
+                };
+              #end
+          },
+        #end
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+            #foreach ($column in $columns)
+                #if ($column.createOperation || $column.updateOperation)
+                    #if ($column.htmlType == "checkbox")
+                            $column.javaField: [],
+                    #else
+                            $column.javaField: undefined,
+                    #end
+                #end
+            #end
+        };
+        this.resetForm("formRef");
+      }
+    }
+  };
+</script>

+ 171 - 200
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue/views/index.vue.vm

@@ -1,6 +1,5 @@
 <template>
   <div class="app-container">
-
     <!-- 搜索工作栏 -->
     <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
 #foreach($column in $columns)
@@ -47,18 +46,68 @@
     <!-- 操作工具栏 -->
     <el-row :gutter="10" class="mb8">
       <el-col :span="1.5">
-        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="handleAdd"
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
                    v-hasPermi="['${permissionPrefix}:create']">新增</el-button>
       </el-col>
       <el-col :span="1.5">
         <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
                    v-hasPermi="['${permissionPrefix}:export']">导出</el-button>
       </el-col>
+        ## 特殊:树表专属逻辑
+        #if ( $table.templateType == 2 )
+          <el-col :span="1.5">
+            <el-button type="danger" plain icon="el-icon-sort" size="mini" @click="toggleExpandAll">
+              展开/折叠
+            </el-button>
+          </el-col>
+        #end
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
-    <!-- 列表 -->
-    <el-table v-loading="loading" :data="list">
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+      <el-table
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :highlight-current-row="true"
+        :show-overflow-tooltip="true"
+        @current-change="handleCurrentChange"
+      >
+          ## 特殊:树表专属逻辑
+      #elseif ( $table.templateType == 2 )
+      <el-table
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :show-overflow-tooltip="true"
+        v-if="refreshTable"
+        row-key="id"
+        :default-expand-all="isExpandAll"
+        :tree-props="{children: 'children', hasChildren: 'hasChildren'}"
+      >
+      #else
+      <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+      #end
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 12 && $subTables && $subTables.size() > 0 )
+        <!-- 子表的列表 -->
+        <el-table-column type="expand">
+          <template #default="scope">
+            <el-tabs value="$subClassNameVars.get(0)">
+                #foreach ($subTable in $subTables)
+                    #set ($index = $foreach.count - 1)
+                    #set ($subClassNameVar = $subClassNameVars.get($index))
+                    #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+                    #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+                  <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+                    <${subSimpleClassName}List :${subJoinColumn_strikeCase}="scope.row.id" />
+                  </el-tab-pane>
+                #end
+            </el-tabs>
+          </template>
+        </el-table-column>
+      #end
 #foreach($column in $columns)
 #if ($column.listOperationResult)
     #set ($dictType=$column.dictType)
@@ -84,102 +133,42 @@
 #end
       <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
         <template v-slot="scope">
-          <el-button size="mini" type="text" icon="el-icon-edit" @click="handleUpdate(scope.row)"
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.${primaryColumn.javaField})"
                      v-hasPermi="['${permissionPrefix}:update']">修改</el-button>
           <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
                      v-hasPermi="['${permissionPrefix}:delete']">删除</el-button>
         </template>
       </el-table-column>
     </el-table>
+## 特殊:树表专属逻辑(树不需要分页)
+#if ( $table.templateType != 2 )
     <!-- 分页组件 -->
     <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
                 @pagination="getList"/>
-
+#end
     <!-- 对话框(添加 / 修改) -->
-    <el-dialog :title="title" :visible.sync="open" width="500px" v-dialogDrag append-to-body>
-      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
-#foreach($column in $columns)
-#if ($column.createOperation || $column.updateOperation)
-    #set ($dictType = $column.dictType)
-    #set ($javaField = $column.javaField)
-    #set ($AttrName = $column.javaField.substring(0,1).toUpperCase() + ${column.javaField.substring(1)})
-    #set ($comment = $column.columnComment)
-#if ($column.htmlType == "input")
-  #if (!$column.primaryKey)## 忽略主键,不用在表单里
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-input v-model="form.${javaField}" placeholder="请输入${comment}" />
-        </el-form-item>
+    <${simpleClassName}Form ref="formRef" @success="getList" />
+  ## 特殊:主子表专属逻辑
+  #if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+    <!-- 子表的列表 -->
+      <el-tabs v-model="subTabsName">
+          #foreach ($subTable in $subTables)
+              #set ($index = $foreach.count - 1)
+              #set ($subClassNameVar = $subClassNameVars.get($index))
+              #set ($subSimpleClassName = $subSimpleClassNames.get($index))
+              #set ($subJoinColumn_strikeCase = $subJoinColumn_strikeCases.get($index))
+            <el-tab-pane label="${subTable.classComment}" name="$subClassNameVar">
+              <${subSimpleClassName}List v-if="currentRow.id" :${subJoinColumn_strikeCase}="currentRow.id" />
+            </el-tab-pane>
+          #end
+      </el-tabs>
   #end
-#elseif($column.htmlType == "imageUpload")## 图片上传
-        #set ($hasImageUploadColumn = true)
-        <el-form-item label="${comment}">
-          <imageUpload v-model="form.${javaField}"/>
-        </el-form-item>
-#elseif($column.htmlType == "fileUpload")## 文件上传
-        #set ($hasFileUploadColumn = true)
-        <el-form-item label="${comment}">
-          <fileUpload v-model="form.${javaField}"/>
-        </el-form-item>
-#elseif($column.htmlType == "editor")## 文本编辑器
-        #set ($hasEditorColumn = true)
-        <el-form-item label="${comment}">
-          <editor v-model="form.${javaField}" :min-height="192"/>
-        </el-form-item>
-#elseif($column.htmlType == "select")## 下拉框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-select v-model="form.${javaField}" placeholder="请选择${comment}">
-    #if ("" != $dictType)## 有数据字典
-            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
-                       :key="dict.value" :label="dict.label" #if ($column.javaType == "Integer" || $column.javaType == "Long"):value="parseInt(dict.value)"#else:value="dict.value"#end />
-    #else##没数据字典
-            <el-option label="请选择字典生成" value="" />
-    #end
-          </el-select>
-        </el-form-item>
-#elseif($column.htmlType == "checkbox")## 多选框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-checkbox-group v-model="form.${javaField}">
-    #if ("" != $dictType)## 有数据字典
-            <el-checkbox v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
-                         :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-checkbox>
-    #else##没数据字典
-            <el-checkbox>请选择字典生成</el-checkbox>
-    #end
-          </el-checkbox-group>
-        </el-form-item>
-#elseif($column.htmlType == "radio")## 单选框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-radio-group v-model="form.${javaField}">
-    #if ("" != $dictType)## 有数据字典
-            <el-radio v-for="dict in this.getDictDatas(DICT_TYPE.$dictType.toUpperCase())"
-                      :key="dict.value" #if($column.javaType == "Integer" || $column.javaType == "Long"):label="parseInt(dict.value)"#else:label="dict.value"#end>{{dict.label}}</el-radio>
-    #else##没数据字典
-            <el-radio label="1">请选择字典生成</el-radio>
-    #end
-          </el-radio-group>
-        </el-form-item>
-#elseif($column.htmlType == "datetime")## 时间框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-date-picker clearable v-model="form.${javaField}" type="date" value-format="timestamp" placeholder="选择${comment}" />
-        </el-form-item>
-#elseif($column.htmlType == "textarea")## 文本框
-        <el-form-item label="${comment}" prop="${javaField}">
-          <el-input v-model="form.${javaField}" type="textarea" placeholder="请输入内容" />
-        </el-form-item>
-#end
-#end
-#end
-      </el-form>
-      <div slot="footer" class="dialog-footer">
-        <el-button type="primary" @click="submitForm">确 定</el-button>
-        <el-button @click="cancel">取 消</el-button>
-      </div>
-    </el-dialog>
   </div>
 </template>
 
 <script>
-import { create${simpleClassName}, update${simpleClassName}, delete${simpleClassName}, get${simpleClassName}, get${simpleClassName}Page, export${simpleClassName}Excel } from "@/api/${table.moduleName}/${classNameVar}";
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}';
+import ${simpleClassName}Form from './${simpleClassName}Form.vue';
 #if ($hasImageUploadColumn)
 import ImageUpload from '@/components/ImageUpload';
 #end
@@ -189,10 +178,26 @@ import FileUpload from '@/components/FileUpload';
 #if ($hasEditorColumn)
 import Editor from '@/components/Editor';
 #end
-
+## 特殊:主子表专属逻辑
+#if ( $table.templateType != 10 )
+#if ( $subTables && $subTables.size() > 0 )
+    #foreach ($subSimpleClassName in $subSimpleClassNames)
+    import ${subSimpleClassName}List from './components/${subSimpleClassName}List.vue';
+    #end
+#end
+#end
 export default {
   name: "${simpleClassName}",
   components: {
+          ${simpleClassName}Form,
+## 特殊:主子表专属逻辑
+#if ( $table.templateType != 10 )
+#if ( $subTables && $subTables.size() > 0 )
+      #foreach ($subSimpleClassName in $subSimpleClassNames)
+          ${subSimpleClassName}List,
+      #end
+#end
+#end
 #if ($hasImageUploadColumn)
     ImageUpload,
 #end
@@ -211,40 +216,44 @@ export default {
       exportLoading: false,
       // 显示搜索条件
       showSearch: true,
-      // 总条数
-      total: 0,
+      ## 特殊:树表专属逻辑(树不需要分页接口)
+      #if ( $table.templateType != 2 )
+        // 总条数
+        total: 0,
+      #end
       // ${table.classComment}列表
       list: [],
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
       // 查询参数
       queryParams: {
-        pageNo: 1,
-        pageSize: 10,
+        ## 特殊:树表专属逻辑(树不需要分页接口)
+        #if ( $table.templateType != 2 )
+            pageNo: 1,
+            pageSize: 10,
+        #end
         #foreach ($column in $columns)
         #if ($column.listOperation)
         #if ($column.listOperationCondition != 'BETWEEN')
         $column.javaField: null,
         #end
-        #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
+        #if ($column.htmlType == "datetime" && $column.listOperationCondition == "BETWEEN")
         $column.javaField: [],
         #end
         #end
         #end
       },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-      #foreach ($column in $columns)
-      #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
-        #set($comment=$column.columnComment)
-        $column.javaField: [{ required: true, message: "${comment}不能为空", trigger: #if($column.htmlType == "select")"change"#else"blur"#end }],
-      #end
-      #end
-      }
+        ## 特殊:主子表专属逻辑-erp
+        #if ( $table.templateType == 11)
+            #if ( $subTables && $subTables.size() > 0 )
+              /** 子表的列表 */
+              subTabsName: '$subClassNameVars.get(0)'
+            #end
+        #end
     };
   },
   created() {
@@ -252,34 +261,21 @@ export default {
   },
   methods: {
     /** 查询列表 */
-    getList() {
+    async getList() {
+      try {
       this.loading = true;
-      // 执行查询
-      get${simpleClassName}Page(this.queryParams).then(response => {
-        this.list = response.data.list;
-        this.total = response.data.total;
+      ## 特殊:树表专属逻辑(树不需要分页接口)
+      #if ( $table.templateType == 2 )
+       const res = await ${simpleClassName}Api.get${simpleClassName}List(this.queryParams);
+       this.list = this.handleTree(res.data, 'id', '${treeParentColumn.javaField}');
+      #else
+        const res = await ${simpleClassName}Api.get${simpleClassName}Page(this.queryParams);
+        this.list = res.data.list;
+        this.total = res.data.total;
+      #end
+      } finally {
         this.loading = false;
-      });
-    },
-    /** 取消按钮 */
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    /** 表单重置 */
-    reset() {
-      this.form = {
-        #foreach ($column in $columns)
-        #if ($column.createOperation || $column.updateOperation)
-        #if ($column.htmlType == "checkbox")
-        $column.javaField: [],
-        #else
-        $column.javaField: undefined,
-        #end
-        #end
-        #end
-      };
-      this.resetForm("form");
+      }
     },
     /** 搜索按钮操作 */
     handleQuery() {
@@ -291,79 +287,54 @@ export default {
       this.resetForm("queryForm");
       this.handleQuery();
     },
-    /** 新增按钮操作 */
-    handleAdd() {
-      this.reset();
-      this.open = true;
-      this.title = "添加${table.classComment}";
-    },
-    /** 修改按钮操作 */
-    handleUpdate(row) {
-      this.reset();
-      const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
-      get${simpleClassName}(${primaryColumn.javaField}).then(response => {
-        this.form = response.data;
-        #foreach ($column in $columns)
-        #if($column.htmlType == "checkbox")## checkbox 特殊处理
-        this.form.$column.javaField = this.form.${column.javaField}.split(",");
-        #end
-        #end
-        this.open = true;
-        this.title = "修改${table.classComment}";
-      });
-    },
-    /** 提交按钮 */
-    submitForm() {
-      this.#[[$]]#refs["form"].validate(valid => {
-        if (!valid) {
-          return;
-        }
-        #foreach ($column in $columns)
-        #if($column.htmlType == "checkbox")
-        this.form.$column.javaField = this.form.${column.javaField}.join(",");
-        #end
-        #end
-        // 修改的提交
-        if (this.form.${primaryColumn.javaField} != null) {
-          update${simpleClassName}(this.form).then(response => {
-            this.#[[$modal]]#.msgSuccess("修改成功");
-            this.open = false;
-            this.getList();
-          });
-          return;
-        }
-        // 添加的提交
-        create${simpleClassName}(this.form).then(response => {
-          this.#[[$modal]]#.msgSuccess("新增成功");
-          this.open = false;
-          this.getList();
-        });
-      });
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.#[[$]]#refs["formRef"].open(id);
     },
     /** 删除按钮操作 */
-    handleDelete(row) {
+    async handleDelete(row) {
       const ${primaryColumn.javaField} = row.${primaryColumn.javaField};
-      this.#[[$modal]]#.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField} + '"的数据项?').then(function() {
-          return delete${simpleClassName}(${primaryColumn.javaField});
-        }).then(() => {
-          this.getList();
-          this.#[[$modal]]#.msgSuccess("删除成功");
-        }).catch(() => {});
+      await this.#[[$modal]]#.confirm('是否确认删除${table.classComment}编号为"' + ${primaryColumn.javaField} + '"的数据项?')
+      try {
+       await ${simpleClassName}Api.delete${simpleClassName}(${primaryColumn.javaField});
+       await this.getList();
+       this.#[[$modal]]#.msgSuccess("删除成功");
+      } catch {}
     },
     /** 导出按钮操作 */
-    handleExport() {
-      // 处理查询参数
-      let params = {...this.queryParams};
-      params.pageNo = undefined;
-      params.pageSize = undefined;
-      this.#[[$modal]]#.confirm('是否确认导出所有${table.classComment}数据项?').then(() => {
-          this.exportLoading = true;
-          return export${simpleClassName}Excel(params);
-        }).then(response => {
-          this.#[[$]]#download.excel(response, '${table.classComment}.xls');
-          this.exportLoading = false;
-        }).catch(() => {});
-    }
+    async handleExport() {
+      await this.#[[$modal]]#.confirm('是否确认导出所有${table.classComment}数据项?');
+      try {
+        this.exportLoading = true;
+        const res = await ${simpleClassName}Api.export${simpleClassName}Excel(this.queryParams);
+        this.#[[$]]#download.excel(res.data, '${table.classComment}.xls');
+      } catch {
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+      ## 特殊:主子表专属逻辑
+      #if ( $table.templateType == 11 )
+        /** 选中行操作 */
+        handleCurrentChange(row) {
+         this.currentRow = row;
+        #if ( $subTables && $subTables.size() > 0 )
+          /** 子表的列表 */
+          this.subTabsName = '$subClassNameVars.get(0)';
+        #end
+        },
+      #end
+      ## 特殊:树表专属逻辑
+      #if ( $table.templateType == 2 )
+        /** 展开/折叠操作 */
+        toggleExpandAll() {
+          this.refreshTable = false
+          this.isExpandAll = !this.isExpandAll
+          this.$nextTick(function () {
+            this.refreshTable = true
+          })
+        }
+      #end
   }
 };
 </script>

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

@@ -10,33 +10,32 @@ 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;
-import cn.iocoder.yudao.module.infra.enums.codegen.*;
 import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
-import org.apache.ibatis.type.JdbcType;
 import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
 import org.mockito.InjectMocks;
 import org.mockito.Spy;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
-import java.util.*;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
 
 /**
- * {@link CodegenEngine} 的单元测试
+ * {@link CodegenEngine} 的单元测试抽象基类
  *
  * @author 芋道源码
  */
-public class CodegenEngineTest extends BaseMockitoUnitTest {
+public abstract class CodegenEngineAbstractTest extends BaseMockitoUnitTest {
 
     @InjectMocks
-    private CodegenEngine codegenEngine;
+    protected CodegenEngine codegenEngine;
 
     @Spy
-    private CodegenProperties codegenProperties = new CodegenProperties()
+    protected CodegenProperties codegenProperties = new CodegenProperties()
             .setBasePackage("cn.iocoder.yudao");
 
     @BeforeEach
@@ -44,87 +43,12 @@ public class CodegenEngineTest extends BaseMockitoUnitTest {
         codegenEngine.initGlobalBindingMap();
     }
 
-    @Test
-    public void testExecute_vue3_one() {
-        // 准备参数
-        CodegenTableDO table = getTable("student")
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
-                .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
-        List<CodegenColumnDO> columns = getColumnList("student");
-
-        // 调用
-        Map<String, String> result = codegenEngine.execute(table, columns, null, null);
-        // 断言
-        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_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);
-        // 断言
-        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_master_normal() {
-        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "codegen/vue3_master_normal");
-    }
-
-    @Test
-    public void testExecute_vue3_master_erp() {
-        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_ERP, "codegen/vue3_master_erp");
-    }
-
-    @Test
-    public void testExecute_vue3_master_inner() {
-        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_INNER, "codegen/vue3_master_inner");
-    }
-
-    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);
-        List<CodegenColumnDO> contactColumns = getColumnList("contact");
-        // 准备参数(班主任)
-        CodegenTableDO teacherTable = getTable("teacher")
-                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
-                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
-                .setSubJoinColumnId(200L).setSubJoinMany(false);
-        List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
-
-        // 调用
-        Map<String, String> result = codegenEngine.execute(table, columns,
-                Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
-        // 断言
-        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");
-    }
-
-    private static CodegenTableDO getTable(String name) {
+    protected static CodegenTableDO getTable(String name) {
         String content = ResourceUtil.readUtf8Str("codegen/table/" + name + ".json");
         return JsonUtils.parseObject(content, "table", CodegenTableDO.class);
     }
 
-    private static List<CodegenColumnDO> getColumnList(String name) {
+    protected 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 -> {
@@ -148,7 +72,7 @@ public class CodegenEngineTest extends BaseMockitoUnitTest {
     }
 
     @SuppressWarnings("rawtypes")
-    private static void assertResult(Map<String, String> result, String path) {
+    protected 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());
@@ -169,7 +93,7 @@ public class CodegenEngineTest extends BaseMockitoUnitTest {
      * @param result 生成的代码
      * @param path 写入文件的路径
      */
-    private void writeFile(Map<String, String> result, String path) {
+    protected void writeFile(Map<String, String> result, String path) {
         // 生成压缩包
         String[] paths = result.keySet().toArray(new String[0]);
         ByteArrayInputStream[] ins = result.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new);
@@ -185,7 +109,7 @@ public class CodegenEngineTest extends BaseMockitoUnitTest {
      * @param result 生成的代码
      * @param basePath 写入文件的路径(绝对路径)
      */
-    private void writeResult(Map<String, String> result, String basePath) {
+    protected void writeResult(Map<String, String> result, String basePath) {
         // 写入文件内容
         List<Map<String, String>> asserts = new ArrayList<>();
         result.forEach((filePath, fileContent) -> {

+ 95 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue2Test.java

@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.infra.service.codegen.inner;
+
+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.enums.codegen.CodegenFrontTypeEnum;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link CodegenEngine} 的 Vue2 + Element UI 单元测试
+ *
+ * @author 芋道源码
+ */
+public class CodegenEngineVue2Test extends CodegenEngineAbstractTest {
+
+    @Test
+    public void testExecute_vue2_one() {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns, null, null);
+        // 断言
+        assertResult(result, "codegen/vue2_one");
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one");
+    }
+
+    @Test
+    public void testExecute_vue2_tree() {
+        // 准备参数
+        CodegenTableDO table = getTable("category")
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setTemplateType(CodegenTemplateTypeEnum.TREE.getType());
+        List<CodegenColumnDO> columns = getColumnList("category");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns, null, null);
+        // 断言
+        assertResult(result, "codegen/vue2_tree");
+//        writeResult(result, "/root/ruoyi-vue-pro/yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_tree");
+//        writeFile(result, "/Users/yunai/test/demo66.zip");
+    }
+
+    @Test
+    public void testExecute_vue2_master_normal() {
+        testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "codegen/vue2_master_normal");
+    }
+
+    @Test
+    public void testExecute_vue2_master_erp() {
+        testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_ERP, "codegen/vue2_master_erp");
+    }
+
+    @Test
+    public void testExecute_vue2_master_inner() {
+        testExecute_vue2_master(CodegenTemplateTypeEnum.MASTER_INNER, "codegen/vue2_master_inner");
+    }
+
+    private void testExecute_vue2_master(CodegenTemplateTypeEnum templateType,
+                                         String path) {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setTemplateType(templateType.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
+        // 准备参数(子表)
+        CodegenTableDO contactTable = getTable("contact")
+                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setSubJoinColumnId(100L).setSubJoinMany(true);
+        List<CodegenColumnDO> contactColumns = getColumnList("contact");
+        // 准备参数(班主任)
+        CodegenTableDO teacherTable = getTable("teacher")
+                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
+                .setFrontType(CodegenFrontTypeEnum.VUE2.getType())
+                .setSubJoinColumnId(200L).setSubJoinMany(false);
+        List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns,
+                Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
+        // 断言
+        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");
+    }
+
+}

+ 95 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngineVue3Test.java

@@ -0,0 +1,95 @@
+package cn.iocoder.yudao.module.infra.service.codegen.inner;
+
+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.enums.codegen.CodegenFrontTypeEnum;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * {@link CodegenEngine} 的 Vue2 + Element Plus 单元测试
+ *
+ * @author 芋道源码
+ */
+public class CodegenEngineVue3Test extends CodegenEngineAbstractTest {
+
+    @Test
+    public void testExecute_vue3_one() {
+        // 准备参数
+        CodegenTableDO table = getTable("student")
+                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
+                .setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
+        List<CodegenColumnDO> columns = getColumnList("student");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns, null, null);
+        // 断言
+        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_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);
+        // 断言
+        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_master_normal() {
+        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_NORMAL, "codegen/vue3_master_normal");
+    }
+
+    @Test
+    public void testExecute_vue3_master_erp() {
+        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_ERP, "codegen/vue3_master_erp");
+    }
+
+    @Test
+    public void testExecute_vue3_master_inner() {
+        testExecute_vue3_master(CodegenTemplateTypeEnum.MASTER_INNER, "codegen/vue3_master_inner");
+    }
+
+    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);
+        List<CodegenColumnDO> contactColumns = getColumnList("contact");
+        // 准备参数(班主任)
+        CodegenTableDO teacherTable = getTable("teacher")
+                .setTemplateType(CodegenTemplateTypeEnum.SUB.getType())
+                .setFrontType(CodegenFrontTypeEnum.VUE3.getType())
+                .setSubJoinColumnId(200L).setSubJoinMany(false);
+        List<CodegenColumnDO> teacherColumns = getColumnList("teacher");
+
+        // 调用
+        Map<String, String> result = codegenEngine.execute(table, columns,
+                Arrays.asList(contactTable, teacherTable), Arrays.asList(contactColumns, teacherColumns));
+        // 断言
+        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");
+    }
+
+}

+ 73 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactList.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherList.vue"
+} ]

+ 6 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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 补充编号, "学生班主任已存在");

+ 71 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/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;
+
+}

+ 30 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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/vue2_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/vue2_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/vue2_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));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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);
+    }
+
+}

+ 141 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/js/student

@@ -0,0 +1,141 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 子表(学生联系人) ====================
+  
+  // 获得学生联系人分页
+  export function getStudentContactPage(params) {
+    return request({
+      url: '/infra/student/student-contact/page',
+      method: 'get',
+      params
+    })
+  }
+        // 新增学生联系人
+  export function createStudentContact(data) {
+    return request({
+      url: `/infra/student/student-contact/create`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 修改学生联系人
+  export function updateStudentContact(data) {
+    return request({
+      url: `/infra/student/student-contact/update`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 删除学生联系人
+  export function deleteStudentContact(id) {
+    return request({
+      url: `/infra/student/student-contact/delete?id=` + id,
+      method: 'delete'
+    })
+  }
+
+  // 获得学生联系人
+  export function getStudentContact(id) {
+    return request({
+      url: `/infra/student/student-contact/get?id=` + id,
+      method: 'get'
+    })
+  }
+
+// ==================== 子表(学生班主任) ====================
+  
+  // 获得学生班主任分页
+  export function getStudentTeacherPage(params) {
+    return request({
+      url: '/infra/student/student-teacher/page',
+      method: 'get',
+      params
+    })
+  }
+        // 新增学生班主任
+  export function createStudentTeacher(data) {
+    return request({
+      url: `/infra/student/student-teacher/create`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 修改学生班主任
+  export function updateStudentTeacher(data) {
+    return request({
+      url: `/infra/student/student-teacher/update`,
+      method: 'post',
+      data
+    })
+  }
+
+  // 删除学生班主任
+  export function deleteStudentTeacher(id) {
+    return request({
+      url: `/infra/student/student-teacher/delete?id=` + id,
+      method: 'delete'
+    })
+  }
+
+  // 获得学生班主任
+  export function getStudentTeacher(id) {
+    return request({
+      url: `/infra/student/student-teacher/get?id=` + id,
+      method: 'get'
+    })
+  }

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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";

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/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
+);

+ 151 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactForm

@@ -0,0 +1,151 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                     <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件" prop="video">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注" prop="memo">
+                      <editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentContactForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        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" }],
+        },
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+      async open(id, studentId) {
+        this.dialogVisible = true;
+        this.reset();
+        this.formData.studentId = studentId;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await StudentApi.getStudentContact(id);
+            this.formData = res.data;
+            this.dialogTitle = "修改学生联系人";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.dialogTitle = "新增学生联系人";
+      },
+      /** 提交按钮 */
+      async submitForm() {
+        await this.$refs["formRef"].validate();
+        this.formLoading = true;
+        try {
+            const data = this.formData;
+            // 修改的提交
+            if (data.id) {
+            await  StudentApi.updateStudentContact(data);
+            this.$modal.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.$emit('success');
+              return;
+            }
+            // 添加的提交
+              await StudentApi.createStudentContact(data);
+              this.$modal.msgSuccess("新增成功");
+              this.dialogVisible = false;
+              this.$emit('success');
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 129 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentContactList

@@ -0,0 +1,129 @@
+<template>
+  <div class="app-container">
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+    </el-row>
+            <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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="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 v-slot="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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+  <!-- 对话框(添加 / 修改) -->
+  <StudentContactForm ref="formRef" @success="getList" />
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  import StudentContactForm from './StudentContactForm.vue';
+  export default {
+    name: "StudentContactList",
+    components: {
+       StudentContactForm
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+        // 列表的总页数
+        total: 0,
+        // 查询参数
+        queryParams: {
+          pageNo: 1,
+          pageSize: 10,
+          studentId: undefined
+        }
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      async getList() {
+        try {
+          this.loading = true;
+            const res = await StudentApi.getStudentContactPage(this.queryParams);
+            this.list = res.data.list;
+            this.total = res.data.total;
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+      /** 添加/修改操作 */
+      openForm(id) {
+        if (!this.studentId) {
+          this.$modal.msgError('请选择一个学生');
+          return;
+        }
+        this.$refs["formRef"].open(id, this.studentId);
+      },
+      /** 删除按钮操作 */
+      async handleDelete(row) {
+        const id = row.id;
+        await this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?');
+        try {
+          await StudentApi.deleteStudentContact(id);
+          await this.getList();
+          this.$modal.msgSuccess("删除成功");
+        } catch {}
+      },
+    }
+  };
+</script>

+ 149 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentForm

@@ -0,0 +1,149 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+              <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+      export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        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' }],
+        },
+                        };
+    },
+    methods: {
+      /** 打开弹窗 */
+     async open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await StudentApi.getStudent(id);
+            this.formData = res.data;
+            this.title = "修改学生";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      async submitForm() {
+        // 校验主表
+        await this.$refs["formRef"].validate();
+                  this.formLoading = true;
+        try {
+          const data = this.formData;
+                  // 修改的提交
+          if (data.id) {
+            await StudentApi.updateStudent(data);
+            this.$modal.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.$emit('success');
+            return;
+          }
+          // 添加的提交
+          await StudentApi.createStudent(data);
+          this.$modal.msgSuccess("新增成功");
+          this.dialogVisible = false;
+          this.$emit('success');
+        } finally {
+          this.formLoading = false;
+        }
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      }
+    }
+  };
+</script>

+ 151 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherForm

@@ -0,0 +1,151 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                     <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件" prop="video">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注" prop="memo">
+                      <editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentTeacherForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        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" }],
+        },
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+      async open(id, studentId) {
+        this.dialogVisible = true;
+        this.reset();
+        this.formData.studentId = studentId;
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await StudentApi.getStudentTeacher(id);
+            this.formData = res.data;
+            this.dialogTitle = "修改学生班主任";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.dialogTitle = "新增学生班主任";
+      },
+      /** 提交按钮 */
+      async submitForm() {
+        await this.$refs["formRef"].validate();
+        this.formLoading = true;
+        try {
+            const data = this.formData;
+            // 修改的提交
+            if (data.id) {
+            await  StudentApi.updateStudentTeacher(data);
+            this.$modal.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.$emit('success');
+              return;
+            }
+            // 添加的提交
+              await StudentApi.createStudentTeacher(data);
+              this.$modal.msgSuccess("新增成功");
+              this.dialogVisible = false;
+              this.$emit('success');
+        }finally {
+          this.formLoading = false;
+        }
+      },
+      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            studentId: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      },
+    }
+  };
+</script>

+ 129 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/StudentTeacherList

@@ -0,0 +1,129 @@
+<template>
+  <div class="app-container">
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+    </el-row>
+            <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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="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 v-slot="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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+  <!-- 对话框(添加 / 修改) -->
+  <StudentTeacherForm ref="formRef" @success="getList" />
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  import StudentTeacherForm from './StudentTeacherForm.vue';
+  export default {
+    name: "StudentTeacherList",
+    components: {
+       StudentTeacherForm
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+        // 列表的总页数
+        total: 0,
+        // 查询参数
+        queryParams: {
+          pageNo: 1,
+          pageSize: 10,
+          studentId: undefined
+        }
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      async getList() {
+        try {
+          this.loading = true;
+            const res = await StudentApi.getStudentTeacherPage(this.queryParams);
+            this.list = res.data.list;
+            this.total = res.data.total;
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+      /** 添加/修改操作 */
+      openForm(id) {
+        if (!this.studentId) {
+          this.$modal.msgError('请选择一个学生');
+          return;
+        }
+        this.$refs["formRef"].open(id, this.studentId);
+      },
+      /** 删除按钮操作 */
+      async handleDelete(row) {
+        const id = row.id;
+        await this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?');
+        try {
+          await StudentApi.deleteStudentTeacher(id);
+          await this.getList();
+          this.$modal.msgSuccess("删除成功");
+        } catch {}
+      },
+    }
+  };
+</script>

+ 233 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/vue/index

@@ -0,0 +1,233 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名字" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker clearable v-model="queryParams.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(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 size="small">
+          <el-option v-for="dict in this.getDictDatas(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" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['infra:student:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table
+        v-loading="loading"
+        :data="list"
+        :stripe="true"
+        :highlight-current-row="true"
+        :show-overflow-tooltip="true"
+        @current-change="handleCurrentChange"
+      >
+                      <el-table-column label="编号" align="center" prop="id">
+        <template v-slot="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 v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" align="center" prop="description">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.birthday) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="sex">
+        <template v-slot="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 v-slot="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">
+        <template v-slot="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 v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="memo">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['infra:student:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:student:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <StudentForm ref="formRef" @success="getList" />
+      <!-- 子表的列表 -->
+      <el-tabs v-model="subTabsName">
+            <el-tab-pane label="学生联系人" name="studentContact">
+              <StudentContactList v-if="currentRow.id" :student-id="currentRow.id" />
+            </el-tab-pane>
+            <el-tab-pane label="学生班主任" name="studentTeacher">
+              <StudentTeacherList v-if="currentRow.id" :student-id="currentRow.id" />
+            </el-tab-pane>
+      </el-tabs>
+  </div>
+</template>
+
+<script>
+import * as StudentApi from '@/api/infra/demo';
+import StudentForm from './StudentForm.vue';
+    import StudentContactList from './components/StudentContactList.vue';
+    import StudentTeacherList from './components/StudentTeacherList.vue';
+export default {
+  name: "Student",
+  components: {
+          StudentForm,
+          StudentContactList,
+          StudentTeacherList,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // 学生列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        name: null,
+        birthday: null,
+        sex: null,
+        enabled: null,
+        createTime: [],
+      },
+                      /** 子表的列表 */
+              subTabsName: 'studentContact'
+    };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    async getList() {
+      try {
+      this.loading = true;
+              const res = await StudentApi.getStudentPage(this.queryParams);
+        this.list = res.data.list;
+        this.total = res.data.total;
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    async handleDelete(row) {
+      const id = row.id;
+      await this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?')
+      try {
+       await StudentApi.deleteStudent(id);
+       await this.getList();
+       this.$modal.msgSuccess("删除成功");
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    async handleExport() {
+      await this.$modal.confirm('是否确认导出所有学生数据项?');
+      try {
+        this.exportLoading = true;
+        const res = await StudentApi.exportStudentExcel(this.queryParams);
+        this.$download.excel(res.data, '学生.xls');
+      } catch {
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+              /** 选中行操作 */
+        handleCurrentChange(row) {
+         this.currentRow = row;
+          /** 子表的列表 */
+          this.subTabsName = 'studentContact';
+        },
+        }
+};
+</script>

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_erp/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>

+ 73 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactList.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherList",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherList.vue"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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));
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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);
+    }
+
+}

+ 74 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/js/student

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 子表(学生联系人) ====================
+  
+    // 获得学生联系人列表
+    export function getStudentContactListByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-contact/list-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  
+// ==================== 子表(学生班主任) ====================
+  
+    // 获得学生班主任
+    export function getStudentTeacherByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-teacher/get-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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/vue2_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
+);

+ 177 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactForm

@@ -0,0 +1,177 @@
+<template>
+  <div class="app-container">
+      <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 v-slot="{ 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 v-slot="{ 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 v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+                            <el-date-picker clearable v-model="row.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="性别" min-width="150">
+                        <template v-slot="{ 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 this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                             :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                            </el-select>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="是否有效" min-width="150">
+                        <template v-slot="{ 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 this.getDictDatas(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 v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+                            <ImageUpload v-model="row.avatar"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="附件" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+                            <FileUpload v-model="row.video"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="备注" min-width="400">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+                            <Editor v-model="row.memo" :min-height="192"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+          <el-table-column align="center" fixed="right" label="操作" width="60">
+            <template v-slot="{ $index }">
+              <el-link @click="handleDelete($index)">—</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form>
+      <el-row justify="center" class="mt-3">
+        <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+      </el-row>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentContactForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        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" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = []
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            // 这里还是需要获取一下 this 的不然取不到 formData
+            const that = this;
+            StudentApi.getStudentContactListByStudentId(val).then(function (res){
+              that.formData = res.data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+          /** 新增按钮操作 */
+          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 = this.studentId;
+            this.formData.push(row);
+          },
+          /** 删除按钮操作 */
+          handleDelete(index) {
+            this.formData.splice(index, 1);
+          },
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate();
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData;
+      }
+    }
+  };
+</script>

+ 89 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentContactList

@@ -0,0 +1,89 @@
+<template>
+  <div class="app-container">
+            <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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="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 v-slot="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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  export default {
+    name: "StudentContactList",
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+      };
+    },
+    created() {
+      this.getList();
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      async getList() {
+        try {
+          this.loading = true;
+                const res = await StudentApi.getStudentContactListByStudentId(this.studentId);
+                this.list = res.data;
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+    }
+  };
+</script>

+ 180 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentForm

@@ -0,0 +1,180 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </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>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+          import StudentContactForm from './components/StudentContactForm.vue'
+      import StudentTeacherForm from './components/StudentTeacherForm.vue'
+  export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                               StudentContactForm,
+               StudentTeacherForm,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        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' }],
+        },
+                              /** 子表的表单 */
+             subTabsName: 'studentContact'
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+     async open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await StudentApi.getStudent(id);
+            this.formData = res.data;
+            this.title = "修改学生";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      async submitForm() {
+        // 校验主表
+        await this.$refs["formRef"].validate();
+                          // 校验子表
+                    try {
+                                            await this.$refs['studentContactFormRef'].validate();
+                    } catch (e) {
+                      this.subTabsName = 'studentContact';
+                      return;
+                    }
+                    try {
+                                            await this.$refs['studentTeacherFormRef'].validate();
+                    } catch (e) {
+                      this.subTabsName = 'studentTeacher';
+                      return;
+                    }
+        this.formLoading = true;
+        try {
+          const data = this.formData;
+                    // 拼接子表的数据
+              data.studentContacts = this.$refs['studentContactFormRef'].getData();
+              data.studentTeacher = this.$refs['studentTeacherFormRef'].getData();
+          // 修改的提交
+          if (data.id) {
+            await StudentApi.updateStudent(data);
+            this.$modal.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.$emit('success');
+            return;
+          }
+          // 添加的提交
+          await StudentApi.createStudent(data);
+          this.$modal.msgSuccess("新增成功");
+          this.dialogVisible = false;
+          this.$emit('success');
+        } finally {
+          this.formLoading = false;
+        }
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      }
+    }
+  };
+</script>

+ 127 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherForm

@@ -0,0 +1,127 @@
+<template>
+  <div class="app-container">
+      <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentTeacherForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        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" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = {
+                                  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 {
+            this.formLoading = true;
+            // 这里还是需要获取一下 this 的不然取不到 formData
+            const that = this;
+            StudentApi.getStudentTeacherByStudentId(val).then(function (res){
+              const data = res.data;
+              if (!data) {
+                return
+              }
+              that.formData = data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate();
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData;
+      }
+    }
+  };
+</script>

+ 93 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/StudentTeacherList

@@ -0,0 +1,93 @@
+<template>
+  <div class="app-container">
+            <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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.birthday) }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="性别" align="center" prop="sex">
+                  <template v-slot="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 v-slot="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" width="180">
+                  <template v-slot="scope">
+                    <span>{{ parseTime(scope.row.createTime) }}</span>
+                  </template>
+                </el-table-column>
+    <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <template v-slot="scope">
+        <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                   v-hasPermi="['infra:student:update']">修改</el-button>
+        <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                   v-hasPermi="['infra:student:delete']">删除</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  export default {
+    name: "StudentTeacherList",
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 遮罩层
+        loading: true,
+        // 列表的数据
+        list: [],
+      };
+    },
+    created() {
+      this.getList();
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+        studentId:{
+            handler(val) {
+              this.queryParams.studentId = val;
+              if (val){
+                this.handleQuery();
+              }
+            },
+            immediate: true
+      }
+    },
+    methods: {
+      /** 查询列表 */
+      async getList() {
+        try {
+          this.loading = true;
+                const res = await  StudentApi.getStudentTeacherByStudentId(this.studentId);
+                const data = res.data;
+                if (!data) {
+                  return;
+                }
+                this.list.push(data);
+        } finally {
+          this.loading = false;
+        }
+      },
+      /** 搜索按钮操作 */
+      handleQuery() {
+        this.queryParams.pageNo = 1;
+        this.getList();
+      },
+    }
+  };
+</script>

+ 222 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_inner/vue/index

@@ -0,0 +1,222 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名字" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker clearable v-model="queryParams.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(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 size="small">
+          <el-option v-for="dict in this.getDictDatas(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" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['infra:student:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+              <!-- 子表的列表 -->
+        <el-table-column type="expand">
+          <template #default="scope">
+            <el-tabs 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">
+        <template v-slot="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 v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" align="center" prop="description">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.birthday) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="sex">
+        <template v-slot="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 v-slot="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">
+        <template v-slot="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 v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="memo">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['infra:student:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:student:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <StudentForm ref="formRef" @success="getList" />
+    </div>
+</template>
+
+<script>
+import * as StudentApi from '@/api/infra/demo';
+import StudentForm from './StudentForm.vue';
+    import StudentContactList from './components/StudentContactList.vue';
+    import StudentTeacherList from './components/StudentTeacherList.vue';
+export default {
+  name: "Student",
+  components: {
+          StudentForm,
+          StudentContactList,
+          StudentTeacherList,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // 学生列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        name: null,
+        birthday: null,
+        sex: null,
+        enabled: null,
+        createTime: [],
+      },
+            };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    async getList() {
+      try {
+      this.loading = true;
+              const res = await StudentApi.getStudentPage(this.queryParams);
+        this.list = res.data.list;
+        this.total = res.data.total;
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    async handleDelete(row) {
+      const id = row.id;
+      await this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?')
+      try {
+       await StudentApi.deleteStudent(id);
+       await this.getList();
+       this.$modal.msgSuccess("删除成功");
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    async handleExport() {
+      await this.$modal.confirm('是否确认导出所有学生数据项?');
+      try {
+        this.exportLoading = true;
+        const res = await StudentApi.exportStudentExcel(this.queryParams);
+        this.$download.excel(res.data, '学生.xls');
+      } catch {
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+              }
+};
+</script>

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/assert.json

@@ -0,0 +1,67 @@
+[ {
+  "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-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+}, {
+  "contentPath" : "vue/StudentContactForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentContactForm.vue"
+}, {
+  "contentPath" : "vue/StudentTeacherForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/components/StudentTeacherForm.vue"
+} ]

+ 3 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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/vue2_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);
+    }
+
+}

+ 74 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/js/student

@@ -0,0 +1,74 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}
+
+// ==================== 子表(学生联系人) ====================
+  
+    // 获得学生联系人列表
+    export function getStudentContactListByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-contact/list-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  
+// ==================== 子表(学生班主任) ====================
+  
+    // 获得学生班主任
+    export function getStudentTeacherByStudentId(studentId) {
+      return request({
+        url: `/infra/student/student-teacher/get-by-student-id?studentId=` + studentId,
+        method: 'get'
+      })
+    }
+  

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_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/vue2_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
+);

+ 177 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentContactForm

@@ -0,0 +1,177 @@
+<template>
+  <div class="app-container">
+      <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 v-slot="{ 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 v-slot="{ 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 v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.birthday`" :rules="formRules.birthday" class="mb-0px!">
+                            <el-date-picker clearable v-model="row.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="性别" min-width="150">
+                        <template v-slot="{ 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 this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                             :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                            </el-select>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="是否有效" min-width="150">
+                        <template v-slot="{ 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 this.getDictDatas(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 v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.avatar`" :rules="formRules.avatar" class="mb-0px!">
+                            <ImageUpload v-model="row.avatar"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="附件" min-width="200">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.video`" :rules="formRules.video" class="mb-0px!">
+                            <FileUpload v-model="row.video"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+                      <el-table-column label="备注" min-width="400">
+                        <template v-slot="{ row, $index }">
+                          <el-form-item :prop="`${$index}.memo`" :rules="formRules.memo" class="mb-0px!">
+                            <Editor v-model="row.memo" :min-height="192"/>
+                          </el-form-item>
+                        </template>
+                      </el-table-column>
+          <el-table-column align="center" fixed="right" label="操作" width="60">
+            <template v-slot="{ $index }">
+              <el-link @click="handleDelete($index)">—</el-link>
+            </template>
+          </el-table-column>
+        </el-table>
+      </el-form>
+      <el-row justify="center" class="mt-3">
+        <el-button @click="handleAdd" round>+ 添加学生联系人</el-button>
+      </el-row>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentContactForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        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" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = []
+          // 2. val 非空,则加载数据
+          if (!val) {
+            return;
+          }
+          try {
+            this.formLoading = true;
+            // 这里还是需要获取一下 this 的不然取不到 formData
+            const that = this;
+            StudentApi.getStudentContactListByStudentId(val).then(function (res){
+              that.formData = res.data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+          /** 新增按钮操作 */
+          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 = this.studentId;
+            this.formData.push(row);
+          },
+          /** 删除按钮操作 */
+          handleDelete(index) {
+            this.formData.splice(index, 1);
+          },
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate();
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData;
+      }
+    }
+  };
+</script>

+ 180 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentForm

@@ -0,0 +1,180 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </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>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+  import ImageUpload from '@/components/ImageUpload';
+  import FileUpload from '@/components/FileUpload';
+  import Editor from '@/components/Editor';
+          import StudentContactForm from './components/StudentContactForm.vue'
+      import StudentTeacherForm from './components/StudentTeacherForm.vue'
+  export default {
+    name: "StudentForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+                               StudentContactForm,
+               StudentTeacherForm,
+    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        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' }],
+        },
+                              /** 子表的表单 */
+             subTabsName: 'studentContact'
+      };
+    },
+    methods: {
+      /** 打开弹窗 */
+     async open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await StudentApi.getStudent(id);
+            this.formData = res.data;
+            this.title = "修改学生";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增学生";
+              },
+      /** 提交按钮 */
+      async submitForm() {
+        // 校验主表
+        await this.$refs["formRef"].validate();
+                          // 校验子表
+                    try {
+                                            await this.$refs['studentContactFormRef'].validate();
+                    } catch (e) {
+                      this.subTabsName = 'studentContact';
+                      return;
+                    }
+                    try {
+                                            await this.$refs['studentTeacherFormRef'].validate();
+                    } catch (e) {
+                      this.subTabsName = 'studentTeacher';
+                      return;
+                    }
+        this.formLoading = true;
+        try {
+          const data = this.formData;
+                    // 拼接子表的数据
+              data.studentContacts = this.$refs['studentContactFormRef'].getData();
+              data.studentTeacher = this.$refs['studentTeacherFormRef'].getData();
+          // 修改的提交
+          if (data.id) {
+            await StudentApi.updateStudent(data);
+            this.$modal.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.$emit('success');
+            return;
+          }
+          // 添加的提交
+          await StudentApi.createStudent(data);
+          this.$modal.msgSuccess("新增成功");
+          this.dialogVisible = false;
+          this.$emit('success');
+        } finally {
+          this.formLoading = false;
+        }
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            name: undefined,
+                            description: undefined,
+                            birthday: undefined,
+                            sex: undefined,
+                            enabled: undefined,
+                            avatar: undefined,
+                            video: undefined,
+                            memo: undefined,
+        };
+        this.resetForm("formRef");
+      }
+    }
+  };
+</script>

+ 127 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/StudentTeacherForm

@@ -0,0 +1,127 @@
+<template>
+  <div class="app-container">
+      <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 clearable v-model="formData.birthday" type="date" value-format="timestamp" placeholder="选择出生日期" />
+                    </el-form-item>
+                    <el-form-item label="性别" prop="sex">
+                      <el-select v-model="formData.sex" placeholder="请选择性别">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.SYSTEM_USER_SEX)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(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 this.getDictDatas(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="头像">
+                      <ImageUpload v-model="formData.avatar"/>
+                    </el-form-item>
+                    <el-form-item label="附件">
+                      <FileUpload v-model="formData.video"/>
+                    </el-form-item>
+                    <el-form-item label="备注">
+                      <Editor v-model="formData.memo" :min-height="192"/>
+                    </el-form-item>
+      </el-form>
+  </div>
+</template>
+
+<script>
+  import * as StudentApi from '@/api/infra/demo';
+      import ImageUpload from '@/components/ImageUpload';
+      import FileUpload from '@/components/FileUpload';
+      import Editor from '@/components/Editor';
+  export default {
+    name: "StudentTeacherForm",
+    components: {
+          ImageUpload,
+          FileUpload,
+          Editor,
+    },
+    props:[
+      'studentId'
+    ],// 学生编号(主表的关联字段)
+    data() {
+      return {
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: [],
+        // 表单校验
+        formRules: {
+                        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" }],
+        },
+      };
+    },
+    watch:{/** 监听主表的关联字段的变化,加载对应的子表数据 */
+      studentId:{
+        handler(val) {
+          // 1. 重置表单
+              this.formData = {
+                                  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 {
+            this.formLoading = true;
+            // 这里还是需要获取一下 this 的不然取不到 formData
+            const that = this;
+            StudentApi.getStudentTeacherByStudentId(val).then(function (res){
+              const data = res.data;
+              if (!data) {
+                return
+              }
+              that.formData = data;
+            })
+          } finally {
+            this.formLoading = false;
+          }
+        },
+        immediate: true
+      }
+    },
+    methods: {
+      /** 表单校验 */
+      validate(){
+        return this.$refs["formRef"].validate();
+      },
+      /** 表单值 */
+      getData(){
+        return this.formData;
+      }
+    }
+  };
+</script>

+ 205 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/vue/index

@@ -0,0 +1,205 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="名字" prop="name">
+        <el-input v-model="queryParams.name" placeholder="请输入名字" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="出生日期" prop="birthday">
+        <el-date-picker clearable v-model="queryParams.birthday" type="date" value-format="yyyy-MM-dd" placeholder="选择出生日期" />
+      </el-form-item>
+      <el-form-item label="性别" prop="sex">
+        <el-select v-model="queryParams.sex" placeholder="请选择性别" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(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 size="small">
+          <el-option v-for="dict in this.getDictDatas(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" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['infra:student:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['infra:student:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+            <el-table-column label="编号" align="center" prop="id">
+        <template v-slot="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 v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.name" />
+        </template>
+      </el-table-column>
+      <el-table-column label="简介" align="center" prop="description">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.description" />
+        </template>
+      </el-table-column>
+      <el-table-column label="出生日期" align="center" prop="birthday" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.birthday) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="sex">
+        <template v-slot="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 v-slot="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">
+        <template v-slot="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 v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.video" />
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="memo">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.memo" />
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['infra:student:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['infra:student:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <StudentForm ref="formRef" @success="getList" />
+    </div>
+</template>
+
+<script>
+import * as StudentApi from '@/api/infra/demo';
+import StudentForm from './StudentForm.vue';
+export default {
+  name: "Student",
+  components: {
+          StudentForm,
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // 学生列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        name: null,
+        birthday: null,
+        sex: null,
+        enabled: null,
+        createTime: [],
+      },
+            };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    async getList() {
+      try {
+      this.loading = true;
+              const res = await StudentApi.getStudentPage(this.queryParams);
+        this.list = res.data.list;
+        this.total = res.data.total;
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    async handleDelete(row) {
+      const id = row.id;
+      await this.$modal.confirm('是否确认删除学生编号为"' + id + '"的数据项?')
+      try {
+       await StudentApi.deleteStudent(id);
+       await this.getList();
+       this.$modal.msgSuccess("删除成功");
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    async handleExport() {
+      await this.$modal.confirm('是否确认导出所有学生数据项?');
+      try {
+        this.exportLoading = true;
+        const res = await StudentApi.exportStudentExcel(this.queryParams);
+        this.$download.excel(res.data, '学生.xls');
+      } catch {
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+              }
+};
+</script>

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_master_normal/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>

+ 49 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/assert.json

@@ -0,0 +1,49 @@
+[ {
+  "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/InfraStudentMapper",
+  "filePath" : "yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/InfraStudentMapper.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-vue2/src/views/infra/demo/index.vue"
+}, {
+  "contentPath" : "js/student",
+  "filePath" : "yudao-ui-admin-vue2/src/api/infra/student.js"
+}, {
+  "contentPath" : "vue/StudentForm",
+  "filePath" : "yudao-ui-admin-vue2/src/views/infra/demo/StudentForm.vue"
+} ]

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

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

+ 95 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentController

@@ -0,0 +1,95 @@
+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.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));
+    }
+
+}

+ 67 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/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/vue2_one/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/vue2_one/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/vue2_one/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;
+
+}

+ 50 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentSaveReqVO

@@ -0,0 +1,50 @@
+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;
+
+@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;
+
+}

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentService

@@ -0,0 +1,55 @@
+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.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);
+
+}

+ 74 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/java/InfraStudentServiceImpl

@@ -0,0 +1,74 @@
+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.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 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;
+
+    @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
+    public void deleteStudent(Long id) {
+        // 校验存在
+        validateStudentExists(id);
+        // 删除
+        studentMapper.deleteById(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);
+    }
+
+}

+ 146 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/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));
+    }
+
+}

+ 53 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/js/student

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 创建学生
+export function createStudent(data) {
+  return request({
+    url: '/infra/student/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新学生
+export function updateStudent(data) {
+  return request({
+    url: '/infra/student/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除学生
+export function deleteStudent(id) {
+  return request({
+    url: '/infra/student/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得学生
+export function getStudent(id) {
+  return request({
+    url: '/infra/student/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得学生分页
+export function getStudentPage(params) {
+  return request({
+    url: '/infra/student/page',
+    method: 'get',
+    params
+  })
+}
+// 导出学生 Excel
+export function exportStudentExcel(params) {
+  return request({
+    url: '/infra/student/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/test/resources/codegen/vue2_one/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/vue2_one/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
+);

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


Vissa filer visades inte eftersom för många filer har ändrats