Browse Source

Merge remote-tracking branch 'upstream/master'

dhb52 1 year ago
parent
commit
677a6d348f
100 changed files with 4343 additions and 1100 deletions
  1. 202 63
      sql/mysql/ruoyi-vue-pro.sql
  2. 7 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java
  3. 1 1
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java
  4. 30 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/json/JsonUtils.java
  5. 37 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java
  6. 16 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java
  7. 24 1
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java
  8. 19 3
      yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java
  9. 10 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java
  10. 17 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java
  11. 12 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java
  12. 93 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/Demo01ContactController.java
  13. 28 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java
  14. 47 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java
  15. 36 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java
  16. 90 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java
  17. 25 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java
  18. 31 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java
  19. 24 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java
  20. 197 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/Demo03StudentController.java
  21. 1 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/package-info.java
  22. 13 19
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/vo/Demo03StudentPageReqVO.java
  23. 41 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/vo/Demo03StudentRespVO.java
  24. 39 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/vo/Demo03StudentSaveReqVO.java
  25. 8 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/package-info.java
  26. 0 19
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.http
  27. 0 97
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.java
  28. 0 32
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoBaseVO.java
  29. 0 11
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java
  30. 0 38
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExcelVO.java
  31. 0 33
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java
  32. 0 19
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoRespVO.java
  33. 0 16
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java
  34. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java
  35. 0 36
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/test/TestDemoConvert.java
  36. 39 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java
  37. 54 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java
  38. 41 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java
  39. 43 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java
  40. 43 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java
  41. 50 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java
  42. 0 50
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/test/TestDemoDO.java
  43. 5 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java
  44. 26 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java
  45. 35 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java
  46. 34 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/Demo03CourseMapper.java
  47. 32 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/Demo03GradeMapper.java
  48. 27 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/Demo03StudentMapper.java
  49. 0 45
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/test/TestDemoMapper.java
  50. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java
  51. 30 1
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java
  52. 8 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java
  53. 39 6
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java
  54. 12 4
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java
  55. 223 36
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java
  56. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactService.java
  57. 72 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java
  58. 55 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryService.java
  59. 134 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java
  60. 158 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/Demo03StudentService.java
  61. 217 0
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/Demo03StudentServiceImpl.java
  62. 0 75
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoService.java
  63. 0 91
      yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImpl.java
  64. 155 33
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm
  65. 0 13
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm
  66. 0 39
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm
  67. 0 30
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm
  68. 0 45
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm
  69. 11 5
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/listReqVO.vm
  70. 9 3
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm
  71. 35 6
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm
  72. 65 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/saveReqVO.vm
  73. 0 30
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm
  74. 0 34
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm
  75. 6 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do.vm
  76. 49 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do_sub.vm
  77. 19 3
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm
  78. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm
  79. 51 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper_sub.vm
  80. 19 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm
  81. 89 12
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/service.vm
  82. 279 17
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm
  83. 15 12
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm
  84. 3 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/h2.vm
  85. 2 2
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm
  86. 66 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm
  87. 205 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_erp.vue.vm
  88. 2 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_inner.vue.vm
  89. 362 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/form_sub_normal.vue.vm
  90. 181 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_erp.vue.vm
  91. 4 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/components/list_sub_inner.vue.vm
  92. 113 49
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm
  93. 115 31
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm
  94. 0 5
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm
  95. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/form.vue.vm
  96. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/index.vue.vm
  97. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/form.vue.vm
  98. 1 1
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_vben/views/index.vue.vm
  99. 0 0
      yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/null/.gitkeep
  100. 0 23
      yudao-module-infra/yudao-module-infra-biz/src/test-integration/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/SchemaColumnMapperTest.java

+ 202 - 63
sql/mysql/ruoyi-vue-pro.sql

@@ -11,7 +11,7 @@
  Target Server Version : 80034
  File Encoding         : 65001
 
- Date: 04/11/2023 20:42:49
+ Date: 18/11/2023 17:48:18
 */
 
 SET NAMES utf8mb4;
@@ -385,7 +385,7 @@ CREATE TABLE `infra_api_error_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1781 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 1964 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统异常日志';
 
 -- ----------------------------
 -- Records of infra_api_error_log
@@ -423,7 +423,7 @@ CREATE TABLE `infra_codegen_column`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1804 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 1905 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表字段定义';
 
 -- ----------------------------
 -- Records of infra_codegen_column
@@ -450,13 +450,18 @@ CREATE TABLE `infra_codegen_table`  (
   `template_type` tinyint NOT NULL DEFAULT 1 COMMENT '模板类型',
   `front_type` tinyint NOT NULL COMMENT '前端类型',
   `parent_menu_id` bigint NULL DEFAULT NULL COMMENT '父菜单编号',
+  `master_table_id` bigint NULL DEFAULT NULL COMMENT '主表的编号',
+  `sub_join_column_id` bigint NULL DEFAULT NULL COMMENT '子表关联主表的字段编号',
+  `sub_join_many` bit(1) NULL DEFAULT NULL COMMENT '主表与子表是否一对多',
+  `tree_parent_column_id` bigint NULL DEFAULT NULL COMMENT '树表的父字段编号',
+  `tree_name_column_id` bigint NULL DEFAULT NULL COMMENT '树表的名字字段编号',
   `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
   `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 136 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
+) ENGINE = InnoDB AUTO_INCREMENT = 146 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '代码生成表定义';
 
 -- ----------------------------
 -- Records of infra_codegen_table
@@ -521,6 +526,150 @@ CREATE TABLE `infra_data_source_config`  (
 BEGIN;
 COMMIT;
 
+-- ----------------------------
+-- Table structure for infra_demo01_contact
+-- ----------------------------
+DROP TABLE IF EXISTS `infra_demo01_contact`;
+CREATE TABLE `infra_demo01_contact`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
+  `sex` tinyint(1) NOT NULL COMMENT '性别',
+  `birthday` datetime NOT NULL COMMENT '出生年',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
+  `avatar` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '示例联系人表';
+
+-- ----------------------------
+-- Records of infra_demo01_contact
+-- ----------------------------
+BEGIN;
+INSERT INTO `infra_demo01_contact` (`id`, `name`, `sex`, `birthday`, `description`, `avatar`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '土豆', 2, '2023-11-07 00:00:00', '<p>天蚕土豆!呀</p>', 'http://127.0.0.1:48080/admin-api/infra/file/4/get/46f8fa1a37db3f3960d8910ff2fe3962ab3b2db87cf2f8ccb4dc8145b8bdf237.jpeg', '1', '2023-11-15 23:34:30', '1', '2023-11-15 23:47:39', b'0', 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for infra_demo02_category
+-- ----------------------------
+DROP TABLE IF EXISTS `infra_demo02_category`;
+CREATE TABLE `infra_demo02_category`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
+  `parent_id` bigint NOT NULL COMMENT '父级编号',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '示例分类表';
+
+-- ----------------------------
+-- Records of infra_demo02_category
+-- ----------------------------
+BEGIN;
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '土豆', 0, '1', '2023-11-15 23:34:30', '1', '2023-11-16 20:24:23', b'0', 1);
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '番茄', 0, '1', '2023-11-16 20:24:00', '1', '2023-11-16 20:24:15', b'0', 1);
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, '怪怪', 0, '1', '2023-11-16 20:24:32', '1', '2023-11-16 20:24:32', b'0', 1);
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '小番茄', 2, '1', '2023-11-16 20:24:39', '1', '2023-11-16 20:24:39', b'0', 1);
+INSERT INTO `infra_demo02_category` (`id`, `name`, `parent_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, '大番茄', 2, '1', '2023-11-16 20:24:46', '1', '2023-11-16 20:24:46', b'0', 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for infra_demo03_course
+-- ----------------------------
+DROP TABLE IF EXISTS `infra_demo03_course`;
+CREATE TABLE `infra_demo03_course`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `student_id` bigint NOT NULL COMMENT '学生编号',
+  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
+  `score` tinyint NOT NULL COMMENT '分数',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 14 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '学生课程表';
+
+-- ----------------------------
+-- Records of infra_demo03_course
+-- ----------------------------
+BEGIN;
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 2, '语文', 66, '1', '2023-11-16 23:21:49', '1', '2023-11-16 23:21:49', b'0', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (3, 2, '数学', 22, '1', '2023-11-16 23:21:49', '1', '2023-11-16 23:21:49', b'0', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (6, 5, '体育', 23, '1', '2023-11-16 23:22:46', '1', '2023-11-16 15:44:40', b'1', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 5, '计算机', 11, '1', '2023-11-16 23:22:46', '1', '2023-11-16 15:44:40', b'1', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 5, '体育', 23, '1', '2023-11-16 23:22:46', '1', '2023-11-16 15:47:09', b'1', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (9, 5, '计算机', 11, '1', '2023-11-16 23:22:46', '1', '2023-11-16 15:47:09', b'1', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (10, 5, '体育', 23, '1', '2023-11-16 23:22:46', '1', '2023-11-16 23:47:10', b'0', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (11, 5, '计算机', 11, '1', '2023-11-16 23:22:46', '1', '2023-11-16 23:47:10', b'0', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (12, 2, '电脑', 33, '1', '2023-11-17 00:20:42', '1', '2023-11-16 16:20:45', b'1', 1);
+INSERT INTO `infra_demo03_course` (`id`, `student_id`, `name`, `score`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (13, 9, '滑雪', 12, '1', '2023-11-17 13:13:20', '1', '2023-11-17 13:13:20', b'0', 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for infra_demo03_grade
+-- ----------------------------
+DROP TABLE IF EXISTS `infra_demo03_grade`;
+CREATE TABLE `infra_demo03_grade`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `student_id` bigint NOT NULL COMMENT '学生编号',
+  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
+  `teacher` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '班主任',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '学生班级表';
+
+-- ----------------------------
+-- Records of infra_demo03_grade
+-- ----------------------------
+BEGIN;
+INSERT INTO `infra_demo03_grade` (`id`, `student_id`, `name`, `teacher`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (7, 2, '三年 2 班', '周杰伦', '1', '2023-11-16 23:21:49', '1', '2023-11-16 23:21:49', b'0', 1);
+INSERT INTO `infra_demo03_grade` (`id`, `student_id`, `name`, `teacher`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (8, 5, '华为', '遥遥领先', '1', '2023-11-16 23:22:46', '1', '2023-11-16 23:47:10', b'0', 1);
+INSERT INTO `infra_demo03_grade` (`id`, `student_id`, `name`, `teacher`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (9, 9, '小图', '小娃111', '1', '2023-11-17 13:10:23', '1', '2023-11-17 13:10:23', b'0', 1);
+COMMIT;
+
+-- ----------------------------
+-- Table structure for infra_demo03_student
+-- ----------------------------
+DROP TABLE IF EXISTS `infra_demo03_student`;
+CREATE TABLE `infra_demo03_student`  (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
+  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
+  `sex` tinyint NOT NULL COMMENT '性别',
+  `birthday` datetime NOT NULL COMMENT '出生日期',
+  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '简介',
+  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
+  `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
+  PRIMARY KEY (`id`) USING BTREE
+) ENGINE = InnoDB AUTO_INCREMENT = 10 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '学生表';
+
+-- ----------------------------
+-- Records of infra_demo03_student
+-- ----------------------------
+BEGIN;
+INSERT INTO `infra_demo03_student` (`id`, `name`, `sex`, `birthday`, `description`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '小白', 1, '2023-11-16 00:00:00', '<p>厉害</p>', '1', '2023-11-16 23:21:49', '1', '2023-11-17 16:49:06', b'0', 1);
+INSERT INTO `infra_demo03_student` (`id`, `name`, `sex`, `birthday`, `description`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (5, '大黑', 2, '2023-11-13 00:00:00', '<p>你在教我做事?</p>', '1', '2023-11-16 23:22:46', '1', '2023-11-17 16:49:07', b'0', 1);
+INSERT INTO `infra_demo03_student` (`id`, `name`, `sex`, `birthday`, `description`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (9, '小花', 1, '2023-11-07 00:00:00', '<p>哈哈哈</p>', '1', '2023-11-17 00:04:47', '1', '2023-11-17 16:49:08', b'0', 1);
+COMMIT;
+
 -- ----------------------------
 -- Table structure for infra_file
 -- ----------------------------
@@ -539,7 +688,7 @@ CREATE TABLE `infra_file`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1108 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1128 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file
@@ -588,7 +737,7 @@ CREATE TABLE `infra_file_content`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 202 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
+) ENGINE = InnoDB AUTO_INCREMENT = 221 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '文件表';
 
 -- ----------------------------
 -- Records of infra_file_content
@@ -665,31 +814,6 @@ CREATE TABLE `infra_job_log`  (
 BEGIN;
 COMMIT;
 
--- ----------------------------
--- Table structure for infra_test_demo
--- ----------------------------
-DROP TABLE IF EXISTS `infra_test_demo`;
-CREATE TABLE `infra_test_demo`  (
-  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
-  `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名字',
-  `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态',
-  `type` tinyint NOT NULL COMMENT '类型',
-  `category` tinyint NOT NULL COMMENT '分类',
-  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
-  `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
-  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
-  `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '更新者',
-  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
-  `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
-  PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
-
--- ----------------------------
--- Records of infra_test_demo
--- ----------------------------
-BEGIN;
-COMMIT;
-
 -- ----------------------------
 -- Table structure for member_address
 -- ----------------------------
@@ -1131,7 +1255,7 @@ CREATE TABLE `system_dept`  (
 -- Records of system_dept
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-06-19 00:29:10', b'0', 1);
+INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, '芋道源码', 0, 0, 1, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2023-11-14 23:30:36', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (101, '深圳总公司', 100, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '1', '2022-05-16 20:25:23', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (102, '长沙分公司', 100, 2, NULL, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '', '2021-12-15 05:01:40', b'0', 1);
 INSERT INTO `system_dept` (`id`, `name`, `parent_id`, `sort`, `leader_user_id`, `phone`, `email`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, '研发部门', 101, 1, 104, '15888888888', 'ry@qq.com', 0, 'admin', '2021-01-05 17:03:47', '103', '2022-01-14 01:04:14', b'0', 1);
@@ -1165,14 +1289,14 @@ CREATE TABLE `system_dict_data`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1435 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
+) ENGINE = InnoDB AUTO_INCREMENT = 1447 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典数据表';
 
 -- ----------------------------
 -- Records of system_dict_data
 -- ----------------------------
 BEGIN;
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, 1, '男', '1', 'system_user_sex', 0, 'default', 'A', '性别男', 'admin', '2021-01-05 17:03:48', '1', '2022-03-29 00:14:39', b'0');
-INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 2, '女', '2', 'system_user_sex', 1, 'success', '', '性别女', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 01:30:51', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2, 2, '女', '2', 'system_user_sex', 0, 'success', '', '性别女', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 23:30:37', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (8, 1, '正常', '1', 'infra_job_status', 0, 'success', '', '正常状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:38', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (9, 2, '暂停', '2', 'infra_job_status', 0, 'danger', '', '停用状态', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:33:45', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (12, 1, '系统内置', '1', 'infra_config_type', 0, 'danger', '', '参数类型 - 系统内置', 'admin', '2021-01-05 17:03:48', '1', '2022-02-16 19:06:02', b'0');
@@ -1474,6 +1598,12 @@ INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `st
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1438, 31, '微信公众平台', '31', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:18', '1', '2023-11-04 13:05:18', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1439, 32, '微信开放平台', '32', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:30', '1', '2023-11-04 13:05:30', b'0');
 INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1440, 34, '微信小程序', '34', 'system_social_type', 0, '', '', '', '1', '2023-11-04 13:05:38', '1', '2023-11-04 13:07:16', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1441, 1, '上架', '1', 'crm_product_status', 0, 'success', '', '', '1', '2023-10-30 21:49:34', '1', '2023-10-30 21:49:34', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1442, 0, '下架', '0', 'crm_product_status', 0, 'success', '', '', '1', '2023-10-30 21:49:13', '1', '2023-10-30 21:49:13', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1443, 15, '子表', '15', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-13 23:06:16', '1', '2023-11-13 23:06:16', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1444, 10, '主表(标准模式)', '10', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-14 12:32:49', '1', '2023-11-14 12:32:49', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1445, 11, '主表(ERP 模式)', '11', 'infra_codegen_template_type', 0, 'default', '', '', '1', '2023-11-14 12:33:05', '1', '2023-11-14 12:33:05', b'0');
+INSERT INTO `system_dict_data` (`id`, `sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1446, 12, '主表(内嵌模式)', '12', 'infra_codegen_template_type', 0, '', '', '', '1', '2023-11-14 12:33:31', '1', '2023-11-14 12:33:31', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -1494,7 +1624,7 @@ CREATE TABLE `system_dict_type`  (
   `deleted_time` datetime NULL DEFAULT NULL COMMENT '删除时间',
   PRIMARY KEY (`id`) USING BTREE,
   UNIQUE INDEX `dict_type`(`type` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 600 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
+) ENGINE = InnoDB AUTO_INCREMENT = 605 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '字典类型表';
 
 -- ----------------------------
 -- Records of system_dict_type
@@ -1573,6 +1703,7 @@ INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creat
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (188, '客户来源', 'crm_customer_source', 0, 'CRM 客户来源', '1', '2023-10-28 23:00:34', '1', '2023-10-28 15:11:16', b'0', NULL);
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (600, 'Banner 位置', 'promotion_banner_position', 0, '', '1', '2023-10-08 07:24:25', '1', '2023-11-04 13:04:02', b'0', '1970-01-01 00:00:00');
 INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (601, '社交类型', 'system_social_type', 0, '', '1', '2023-11-04 13:03:54', '1', '2023-11-04 13:03:54', b'0', '1970-01-01 00:00:00');
+INSERT INTO `system_dict_type` (`id`, `name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `deleted_time`) VALUES (604, '产品状态', 'crm_product_status', 0, '', '1', '2023-10-30 21:47:59', '1', '2023-10-30 21:48:45', b'0', '1970-01-01 00:00:00');
 COMMIT;
 
 -- ----------------------------
@@ -1621,7 +1752,7 @@ CREATE TABLE `system_login_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2631 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 2647 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '系统访问记录';
 
 -- ----------------------------
 -- Records of system_login_log
@@ -1751,7 +1882,7 @@ CREATE TABLE `system_menu`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 2449 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
+) ENGINE = InnoDB AUTO_INCREMENT = 2504 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表';
 
 -- ----------------------------
 -- Records of system_menu
@@ -1839,11 +1970,7 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1065, '设置用户角色', 'system:permission:assign-user-role', 3, 8, 101, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-07 10:23:28', '', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1066, '获得 Redis 监控信息', 'infra:redis:get-monitor-info', 3, 1, 113, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-26 01:02:31', '', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1067, '获得 Redis Key 列表', 'infra:redis:get-key-list', 3, 2, 113, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-01-26 01:02:52', '', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1070, '代码生成示例', 'infra:test-demo:query', 2, 1, 2, 'test-demo', 'validCode', 'infra/testDemo/index', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1071, '测试示例表创建', 'infra:test-demo:create', 3, 1, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1072, '测试示例表更新', 'infra:test-demo:update', 3, 2, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1073, '测试示例表删除', 'infra:test-demo:delete', 3, 3, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0');
-INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1074, '测试示例表导出', 'infra:test-demo:export', 3, 4, 1070, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2022-04-20 17:03:10', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1070, '代码生成案例', '', 1, 1, 2, 'demo', 'ep:aim', 'infra/testDemo/index', NULL, 0, b'1', b'1', b'1', '', '2021-02-06 12:42:49', '1', '2023-11-15 23:45:53', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1075, '任务触发', 'infra:job:trigger', 3, 8, 110, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2021-02-07 13:03:10', '', '2022-04-20 17:03:10', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1076, '数据库文档', '', 2, 4, 2, 'db-doc', 'table', 'infra/dbDoc/index', 'InfraDBDoc', 0, b'1', b'1', b'1', '', '2021-02-08 01:41:47', '1', '2023-04-08 09:13:38', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1077, '监控平台', '', 2, 13, 2, 'skywalking', 'eye-open', 'infra/skywalking/index', 'InfraSkyWalking', 0, b'1', b'1', b'1', '', '2021-02-08 20:41:31', '1', '2023-04-08 10:39:06', b'0');
@@ -2310,6 +2437,26 @@ INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_i
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2451, '三方应用更新', 'system:social-client:update', 3, 3, 2448, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-11-04 12:44:27', '1', '2023-11-04 12:44:27', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2452, '三方应用删除', 'system:social-client:delete', 3, 4, 2448, '', '', '', '', 0, b'1', b'1', b'1', '1', '2023-11-04 12:44:43', '1', '2023-11-04 12:44:43', b'0');
 INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2453, '三方用户', 'system:social-user:query', 2, 2, 2447, 'user', 'ep:avatar', 'system/social/user/index.vue', 'SocialUser', 0, b'1', b'1', b'1', '1', '2023-11-04 14:01:05', '1', '2023-11-04 14:01:05', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2472, '主子表(内嵌)', '', 2, 12, 1070, 'demo03-inner', 'fa:power-off', 'infra/demo/demo03/inner/index', 'Demo03StudentInner', 0, b'1', b'1', b'1', '', '2023-11-13 04:39:51', '1', '2023-11-16 23:53:46', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2478, '单表(增删改查)', '', 2, 1, 1070, 'demo01-contact', 'ep:bicycle', 'infra/demo/demo01/index', 'Demo01Contact', 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '1', '2023-11-16 20:34:40', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2479, '示例联系人查询', 'infra:demo01-contact:query', 3, 1, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2480, '示例联系人创建', 'infra:demo01-contact:create', 3, 2, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2481, '示例联系人更新', 'infra:demo01-contact:update', 3, 3, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2482, '示例联系人删除', 'infra:demo01-contact:delete', 3, 4, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2483, '示例联系人导出', 'infra:demo01-contact:export', 3, 5, 2478, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-15 14:42:30', '', '2023-11-15 14:42:30', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2484, '树表(增删改查)', '', 2, 2, 1070, 'demo02-category', 'fa:tree', 'infra/demo/demo02/index', 'Demo02Category', 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '1', '2023-11-16 20:35:01', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2485, '示例分类查询', 'infra:demo02-category:query', 3, 1, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2486, '示例分类创建', 'infra:demo02-category:create', 3, 2, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2487, '示例分类更新', 'infra:demo02-category:update', 3, 3, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2488, '示例分类删除', 'infra:demo02-category:delete', 3, 4, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2489, '示例分类导出', 'infra:demo02-category:export', 3, 5, 2484, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:18:27', '', '2023-11-16 12:18:27', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2490, '主子表(标准)', '', 2, 10, 1070, 'demo03-normal', 'fa:battery-3', 'infra/demo/demo03/normal/index', 'Demo03StudentNormal', 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '1', '2023-11-16 23:10:03', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2491, '学生查询', 'infra:demo03-student:query', 3, 1, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2492, '学生创建', 'infra:demo03-student:create', 3, 2, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2493, '学生更新', 'infra:demo03-student:update', 3, 3, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2494, '学生删除', 'infra:demo03-student:delete', 3, 4, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2495, '学生导出', 'infra:demo03-student:export', 3, 5, 2490, '', '', '', NULL, 0, b'1', b'1', b'1', '', '2023-11-16 12:53:37', '', '2023-11-16 12:53:37', b'0');
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (2497, '主子表(ERP)', '', 2, 11, 1070, 'demo03-erp', 'ep:calendar', 'infra/demo/demo03/erp/index', 'Demo03StudentERP', 0, b'1', b'1', b'1', '', '2023-11-16 15:50:59', '1', '2023-11-17 13:19:56', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -2336,7 +2483,7 @@ CREATE TABLE `system_notice`  (
 -- ----------------------------
 BEGIN;
 INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, '芋道的公众', '<p>新版本内容133</p>', 1, 0, 'admin', '2021-01-05 17:03:48', '1', '2022-05-04 21:00:20', b'0', 1);
-INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 若依系统凌晨维护', '<p><img src=\"http://test.yudao.iocoder.cn/b7cb3cf49b4b3258bf7309a09dd2f4e5.jpg\">维护内容</p>', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2022-05-11 12:34:24', b'0', 1);
+INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, '维护通知:2018-07-01 若依系统凌晨维护', '<p><img src=\"http://test.yudao.iocoder.cn/b7cb3cf49b4b3258bf7309a09dd2f4e5.jpg\" alt=\"\" data-href=\"\" style=\"\"/>1111</p>', 2, 1, 'admin', '2021-01-05 17:03:48', '1', '2023-11-11 12:51:11', b'0', 1);
 INSERT INTO `system_notice` (`id`, `title`, `content`, `type`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, '我是测试标题', '<p>哈哈哈哈123</p>', 1, 0, '110', '2022-02-22 01:01:25', '110', '2022-02-22 01:01:46', b'0', 121);
 COMMIT;
 
@@ -2430,7 +2577,7 @@ CREATE TABLE `system_oauth2_access_token`  (
   PRIMARY KEY (`id`) USING BTREE,
   INDEX `idx_access_token`(`access_token` ASC) USING BTREE,
   INDEX `idx_refresh_token`(`refresh_token` ASC) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 3152 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 3467 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 访问令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_access_token
@@ -2552,7 +2699,7 @@ CREATE TABLE `system_oauth2_refresh_token`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 1099 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
+) ENGINE = InnoDB AUTO_INCREMENT = 1115 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'OAuth2 刷新令牌';
 
 -- ----------------------------
 -- Records of system_oauth2_refresh_token
@@ -2592,7 +2739,7 @@ CREATE TABLE `system_operate_log`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 8845 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
+) ENGINE = InnoDB AUTO_INCREMENT = 9090 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录';
 
 -- ----------------------------
 -- Records of system_operate_log
@@ -2618,15 +2765,15 @@ CREATE TABLE `system_post`  (
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   `tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
+) ENGINE = InnoDB AUTO_INCREMENT = 6 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '岗位信息表';
 
 -- ----------------------------
 -- Records of system_post
 -- ----------------------------
 BEGIN;
 INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'ceo', '董事长', 1, 0, '', 'admin', '2021-01-06 17:03:48', '1', '2023-02-11 15:19:04', b'0', 1);
-INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2021-12-12 10:47:47', b'0', 1);
-INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 'user', '普通员工', 4, 0, '111', 'admin', '2021-01-05 17:03:48', '1', '2023-02-11 15:19:00', b'0', 1);
+INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2, 'se', '项目经理', 2, 0, '', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 09:18:20', b'0', 1);
+INSERT INTO `system_post` (`id`, `code`, `name`, `sort`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (4, 'user', '普通员工', 4, 0, '111', 'admin', '2021-01-05 17:03:48', '1', '2023-11-15 09:18:18', b'0', 1);
 COMMIT;
 
 -- ----------------------------
@@ -2819,10 +2966,6 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1657, 101, 1066, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1658, 101, 1067, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1659, 101, 1070, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1660, 101, 1071, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1661, 101, 1072, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1662, 101, 1073, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1663, 101, 1074, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1664, 101, 1075, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1665, 101, 1076, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1666, 101, 1077, '1', '2022-04-01 22:21:37', '1', '2022-04-01 22:21:37', b'0', 1);
@@ -3140,10 +3283,6 @@ INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_t
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2027, 2, 1066, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2028, 2, 1067, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2029, 2, 1070, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2030, 2, 1071, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2031, 2, 1072, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2032, 2, 1073, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
-INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2033, 2, 1074, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2034, 2, 1075, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2035, 2, 1076, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
 INSERT INTO `system_role_menu` (`id`, `role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (2036, 2, 1082, '1', '2023-01-25 08:42:52', '1', '2023-01-25 08:42:52', b'0', 1);
@@ -3643,7 +3782,7 @@ CREATE TABLE `system_sms_log`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 503 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
+) ENGINE = InnoDB AUTO_INCREMENT = 502 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信日志';
 
 -- ----------------------------
 -- Records of system_sms_log
@@ -3673,7 +3812,7 @@ CREATE TABLE `system_sms_template`  (
   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
   `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
   PRIMARY KEY (`id`) USING BTREE
-) ENGINE = InnoDB AUTO_INCREMENT = 17 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
+) ENGINE = InnoDB AUTO_INCREMENT = 16 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '短信模板';
 
 -- ----------------------------
 -- Records of system_sms_template
@@ -3803,9 +3942,9 @@ CREATE TABLE `system_tenant`  (
 -- Records of system_tenant
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'https://www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2022-02-23 12:15:11', b'0');
-INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'http://www.iocoder.cn', 111, '2024-03-11 00:00:00', 20, '1', '2022-02-22 00:56:14', '1', '2023-09-16 16:59:42', b'0');
-INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'https://www.iocoder.cn', 111, '2022-04-30 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2023-09-16 16:59:27', b'0');
+INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (1, '芋道源码', NULL, '芋艿', '17321315478', 0, 'www.iocoder.cn', 0, '2099-02-19 17:14:16', 9999, '1', '2021-01-05 17:03:47', '1', '2023-11-06 11:41:41', b'0');
+INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (121, '小租户', 110, '小王2', '15601691300', 0, 'zsxq.iocoder.cn', 111, '2024-03-11 00:00:00', 20, '1', '2022-02-22 00:56:14', '1', '2023-11-06 11:41:47', b'0');
+INSERT INTO `system_tenant` (`id`, `name`, `contact_user_id`, `contact_name`, `contact_mobile`, `status`, `website`, `package_id`, `expire_time`, `account_count`, `creator`, `create_time`, `updater`, `update_time`, `deleted`) VALUES (122, '测试租户', 113, '芋道', '15601691300', 0, 'test.iocoder.cn', 111, '2022-04-30 00:00:00', 50, '1', '2022-03-07 21:37:58', '1', '2023-11-06 11:41:53', b'0');
 COMMIT;
 
 -- ----------------------------
@@ -3944,7 +4083,7 @@ CREATE TABLE `system_users`  (
 -- Records of system_users
 -- ----------------------------
 BEGIN;
-INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '0:0:0:0:0:0:0:1', '2023-11-04 10:33:16', 'admin', '2021-01-05 17:03:47', NULL, '2023-11-04 10:33:16', b'0', 1);
+INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (1, 'admin', '$2a$10$mRMIYLDtRHlf6.9ipiqH1.Z.bh/R9dO9d5iHiGYPigi6r5KOoR2Wm', '芋道源码', '管理员', 103, '[1]', 'aoteman@126.com', '15612345678', 1, 'http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac4b608e151e62a74d9633f15d085aee026eedeb60.png', 0, '0:0:0:0:0:0:0:1', '2023-11-18 17:19:30', 'admin', '2021-01-05 17:03:47', NULL, '2023-11-18 17:19:30', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (100, 'yudao', '$2a$10$11U48RhyJ5pSBYWSn12AD./ld671.ycSzJHbyrtpeoMeYiw31eo8a', '芋道', '不要吓我', 104, '[1]', 'yudao@iocoder.cn', '15601691300', 1, '', 1, '127.0.0.1', '2022-07-09 23:03:33', '', '2021-01-07 09:07:17', NULL, '2022-07-09 23:03:33', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (103, 'yuanma', '$2a$10$YMpimV4T6BtDhIaA8jSW.u8UTGBeGhc/qwXP4oxoMr4mOw9.qttt6', '源码', NULL, 106, NULL, 'yuanma@iocoder.cn', '15601701300', 0, '', 0, '127.0.0.1', '2022-07-08 01:26:27', '', '2021-01-13 23:50:35', NULL, '2022-07-08 01:26:27', b'0', 1);
 INSERT INTO `system_users` (`id`, `username`, `password`, `nickname`, `remark`, `dept_id`, `post_ids`, `email`, `mobile`, `sex`, `avatar`, `status`, `login_ip`, `login_date`, `creator`, `create_time`, `updater`, `update_time`, `deleted`, `tenant_id`) VALUES (104, 'test', '$2a$10$GP8zvqHB//TekuzYZSBYAuBQJiNq1.fxQVDYJ.uBCOnWCtDVKE4H6', '测试号', NULL, 107, '[1,2]', '111@qq.com', '15601691200', 1, '', 0, '0:0:0:0:0:0:0:1', '2023-09-24 18:21:19', '', '2021-01-21 02:13:53', NULL, '2023-09-24 18:21:19', b'0', 1);

+ 7 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/pojo/PageParam.java

@@ -15,6 +15,13 @@ public class PageParam implements Serializable {
     private static final Integer PAGE_NO = 1;
     private static final Integer PAGE_SIZE = 10;
 
+    /**
+     * 每页条数 - 不分页
+     *
+     * 例如说,导出接口,可以设置 {@link #pageSize} 为 -1 不分页,查询所有数据。
+     */
+    public static final Integer PAGE_SIZE_NONE = -1;
+
     @Schema(description = "页码,从 1 开始", requiredMode = Schema.RequiredMode.REQUIRED,example = "1")
     @NotNull(message = "页码不能为空")
     @Min(value = 1, message = "页码最小值为 1")

+ 1 - 1
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/collection/CollectionUtils.java

@@ -238,7 +238,7 @@ public class CollectionUtils {
         if (CollUtil.isEmpty(from)) {
             return null;
         }
-        assert from.size() > 0; // 断言,避免告警
+        assert !from.isEmpty(); // 断言,避免告警
         T t = from.stream().max(Comparator.comparing(valueFunc)).get();
         return valueFunc.apply(t);
     }

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

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

+ 37 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/object/BeanUtils.java

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.framework.common.util.object;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+
+import java.util.List;
+
+/**
+ * Bean 工具类
+ *
+ * 1. 默认使用 {@link cn.hutool.core.bean.BeanUtil} 作为实现类,虽然不同 bean 工具的性能有差别,但是对绝大多数同学的项目,不用在意这点性能
+ * 2. 针对复杂的对象转换,可以搜参考 AuthConvert 实现,通过 mapstruct + default 配合实现
+ *
+ * @author 芋道源码
+ */
+public class BeanUtils {
+
+    public static <T> T toBean(Object source, Class<T> targetClass) {
+        return BeanUtil.toBean(source, targetClass);
+    }
+
+    public static <S, T> List<T> toBean(List<S> source, Class<T> targetType) {
+        if (source == null) {
+            return null;
+        }
+        return CollectionUtils.convertList(source, s -> toBean(s, targetType));
+    }
+
+    public static  <S, T> PageResult<T> toBean(PageResult<S> source, Class<T> targetType) {
+        if (source == null) {
+            return null;
+        }
+        return new PageResult<>(toBean(source.getList(), targetType), source.getTotal());
+    }
+
+}

+ 16 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/string/StrUtils.java

@@ -50,4 +50,20 @@ public class StrUtils {
         return Arrays.stream(integers).boxed().collect(Collectors.toList());
     }
 
+    /**
+     * 移除字符串中,包含指定字符串的行
+     *
+     * @param content 字符串
+     * @param sequence 包含的字符串
+     * @return 移除后的字符串
+     */
+    public static String removeLineContains(String content, String sequence) {
+        if (StrUtil.isEmpty(content) || StrUtil.isEmpty(sequence)) {
+            return content;
+        }
+        return Arrays.stream(content.split("\n"))
+                .filter(line -> !line.contains(sequence))
+                .collect(Collectors.joining("\n"));
+    }
+
 }

+ 24 - 1
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/mybatis/core/mapper/BaseMapperX.java

@@ -26,6 +26,12 @@ import java.util.List;
 public interface BaseMapperX<T> extends MPJBaseMapper<T> {
 
     default PageResult<T> selectPage(PageParam pageParam, @Param("ew") Wrapper<T> queryWrapper) {
+        // 特殊:不分页,直接查询全部
+        if (PageParam.PAGE_SIZE_NONE.equals(pageParam.getPageNo())) {
+            List<T> list = selectList(queryWrapper);
+            return new PageResult<>(list, (long) list.size());
+        }
+
         // MyBatis Plus 查询
         IPage<T> mpPage = MyBatisUtils.buildPage(pageParam);
         selectPage(mpPage, queryWrapper);
@@ -93,10 +99,15 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         return selectList(new LambdaQueryWrapper<T>().in(field, values));
     }
 
+    @Deprecated
     default List<T> selectList(SFunction<T, ?> leField, SFunction<T, ?> geField, Object value) {
         return selectList(new LambdaQueryWrapper<T>().le(leField, value).ge(geField, value));
     }
 
+    default List<T> selectList(SFunction<T, ?> field1, Object value1, SFunction<T, ?> field2, Object value2) {
+        return selectList(new LambdaQueryWrapper<T>().eq(field1, value1).eq(field2, value2));
+    }
+
     /**
      * 批量插入,适合大量数据插入
      *
@@ -128,8 +139,20 @@ public interface BaseMapperX<T> extends MPJBaseMapper<T> {
         Db.updateBatchById(entities, size);
     }
 
-    default void saveOrUpdateBatch(Collection<T> collection) {
+    default void insertOrUpdate(T entity) {
+        Db.saveOrUpdate(entity);
+    }
+
+    default void insertOrUpdateBatch(Collection<T> collection) {
         Db.saveOrUpdateBatch(collection);
     }
 
+    default int delete(String field, String value) {
+        return delete(new QueryWrapper<T>().eq(field, value));
+    }
+
+    default int delete(SFunction<T, ?> field, Object value) {
+        return delete(new LambdaQueryWrapper<T>().eq(field, value));
+    }
+
 }

+ 19 - 3
yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java

@@ -42,9 +42,10 @@ public interface ErrorCodeConstants {
     ErrorCode CODEGEN_SYNC_NONE_CHANGE = new ErrorCode(1_003_001_007, "同步失败,不存在改变");
     ErrorCode CODEGEN_TABLE_INFO_TABLE_COMMENT_IS_NULL = new ErrorCode(1_003_001_008, "数据库的表注释未填写");
     ErrorCode CODEGEN_TABLE_INFO_COLUMN_COMMENT_IS_NULL = new ErrorCode(1_003_001_009, "数据库的表字段({})注释未填写");
-
-    // ========== 字典类型(测试)1-001-005-000 ==========
-    ErrorCode TEST_DEMO_NOT_EXISTS = new ErrorCode(1_001_005_000, "测试示例不存在");
+    ErrorCode CODEGEN_MASTER_TABLE_NOT_EXISTS = new ErrorCode(1_003_001_010, "主表(id={})定义不存在,请检查");
+    ErrorCode CODEGEN_SUB_COLUMN_NOT_EXISTS = new ErrorCode(1_003_001_011, "子表的字段(id={})不存在,请检查");
+    ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE = new ErrorCode(1_003_001_012, "主表生成代码失败,原因:它没有子表");
+    ErrorCode CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_COLUMN = new ErrorCode(1_003_001_013, "主表生成代码失败,原因:它的子表({})没有字段");
 
     // ========== 文件配置 1-001-006-000 ==========
     ErrorCode FILE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_006_000, "文件配置不存在");
@@ -54,4 +55,19 @@ public interface ErrorCodeConstants {
     ErrorCode DATA_SOURCE_CONFIG_NOT_EXISTS = new ErrorCode(1_001_007_000, "数据源配置不存在");
     ErrorCode DATA_SOURCE_CONFIG_NOT_OK = new ErrorCode(1_001_007_001, "数据源配置不正确,无法进行连接");
 
+    // ========== 数据源配置 1-001-107-000 ==========
+    ErrorCode DEMO_STUDENT_NOT_EXISTS = new ErrorCode(1_001_107_000, "学生不存在");
+
+    // ========== 学生 1-001-201-000 ==========
+    ErrorCode DEMO01_CONTACT_NOT_EXISTS = new ErrorCode(1_001_201_000, "示例联系人不存在");
+    ErrorCode DEMO02_CATEGORY_NOT_EXISTS = new ErrorCode(1_001_201_001, "示例分类不存在");
+    ErrorCode DEMO02_CATEGORY_EXITS_CHILDREN = new ErrorCode(1_001_201_002, "存在存在子示例分类,无法删除");
+    ErrorCode DEMO02_CATEGORY_PARENT_NOT_EXITS = new ErrorCode(1_001_201_003,"父级示例分类不存在");
+    ErrorCode DEMO02_CATEGORY_PARENT_ERROR = new ErrorCode(1_001_201_004, "不能设置自己为父示例分类");
+    ErrorCode DEMO02_CATEGORY_NAME_DUPLICATE = new ErrorCode(1_001_201_005, "已经存在该名字的示例分类");
+    ErrorCode DEMO02_CATEGORY_PARENT_IS_CHILD = new ErrorCode(1_001_201_006, "不能设置自己的子示例分类为父示例分类");
+    ErrorCode DEMO03_STUDENT_NOT_EXISTS = new ErrorCode(1_001_201_007, "学生不存在");
+    ErrorCode DEMO03_GRADE_NOT_EXISTS = new ErrorCode(1_001_201_008, "学生班级不存在");
+    ErrorCode DEMO03_GRADE_EXISTS = new ErrorCode(1_001_201_009, "学生班级已存在");
+
 }

+ 10 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java

@@ -60,10 +60,19 @@ public class CodegenController {
         return success(codegenService.getDatabaseTableList(dataSourceConfigId, name, comment));
     }
 
+    @GetMapping("/table/list")
+    @Operation(summary = "获得表定义列表")
+    @Parameter(name = "dataSourceConfigId", description = "数据源配置的编号", required = true, example = "1")
+    @PreAuthorize("@ss.hasPermission('infra:codegen:query')")
+    public CommonResult<List<CodegenTableRespVO>> getCodegenTableList(@RequestParam(value = "dataSourceConfigId") Long dataSourceConfigId) {
+        List<CodegenTableDO> list = codegenService.getCodegenTableList(dataSourceConfigId);
+        return success(CodegenConvert.INSTANCE.convertList05(list));
+    }
+
     @GetMapping("/table/page")
     @Operation(summary = "获得表定义分页")
     @PreAuthorize("@ss.hasPermission('infra:codegen:query')")
-    public CommonResult<PageResult<CodegenTableRespVO>> getCodeGenTablePage(@Valid CodegenTablePageReqVO pageReqVO) {
+    public CommonResult<PageResult<CodegenTableRespVO>> getCodegenTablePage(@Valid CodegenTablePageReqVO pageReqVO) {
         PageResult<CodegenTableDO> pageResult = codegenService.getCodegenTablePage(pageReqVO);
         return success(CodegenConvert.INSTANCE.convertPage(pageResult));
     }

+ 17 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/CodegenUpdateReqVO.java

@@ -4,6 +4,8 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.column.CodegenColumnBaseVO;
 import cn.iocoder.yudao.module.infra.controller.admin.codegen.vo.table.CodegenTableBaseVO;
 import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
+import com.fasterxml.jackson.annotation.JsonIgnore;
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
 import lombok.EqualsAndHashCode;
@@ -37,12 +39,27 @@ public class CodegenUpdateReqVO {
         private Long id;
 
         @AssertTrue(message = "上级菜单不能为空,请前往 [修改生成配置 -> 生成信息] 界面,设置“上级菜单”字段")
+        @JsonIgnore
         public boolean isParentMenuIdValid() {
             // 生成场景为管理后台时,必须设置上级菜单,不然生成的菜单 SQL 是无父级菜单的
             return ObjectUtil.notEqual(getScene(), CodegenSceneEnum.ADMIN.getScene())
                     || getParentMenuId() != null;
         }
 
+        @AssertTrue(message = "关联的父表信息不全")
+        @JsonIgnore
+        public boolean isSubValid() {
+            return ObjectUtil.notEqual(getTemplateType(), CodegenTemplateTypeEnum.SUB)
+                    || (ObjectUtil.isAllNotEmpty(getMasterTableId(), getSubJoinColumnId(), getSubJoinMany()));
+        }
+
+        @AssertTrue(message = "关联的树表信息不全")
+        @JsonIgnore
+        public boolean isTreeValid() {
+            return ObjectUtil.notEqual(getTemplateType(), CodegenTemplateTypeEnum.TREE)
+                    || (ObjectUtil.isAllNotEmpty(getTreeParentColumnId(), getTreeNameColumnId()));
+        }
+
     }
 
     @Schema(description = "更新表定义")

+ 12 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/vo/table/CodegenTableBaseVO.java

@@ -58,4 +58,16 @@ public class CodegenTableBaseVO {
     @Schema(description = "父菜单编号", example = "1024")
     private Long parentMenuId;
 
+    @Schema(description = "主表的编号", example = "2048")
+    private Long masterTableId;
+    @Schema(description = "子表关联主表的字段编号", example = "4096")
+    private Long subJoinColumnId;
+    @Schema(description = "主表与子表是否一对多", example = "4096")
+    private Boolean subJoinMany;
+
+    @Schema(description = "树表的父字段编号", example = "8192")
+    private Long treeParentColumnId;
+    @Schema(description = "树表的名字字段编号", example = "16384")
+    private Long treeNameColumnId;
+
 }

+ 93 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/Demo01ContactController.java

@@ -0,0 +1,93 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactRespVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO;
+import cn.iocoder.yudao.module.infra.service.demo.demo01.Demo01ContactService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 示例联系人")
+@RestController
+@RequestMapping("/infra/demo01-contact")
+@Validated
+public class Demo01ContactController {
+
+    @Resource
+    private Demo01ContactService demo01ContactService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建示例联系人")
+    @PreAuthorize("@ss.hasPermission('infra:demo01-contact:create')")
+    public CommonResult<Long> createDemo01Contact(@Valid @RequestBody Demo01ContactSaveReqVO createReqVO) {
+        return success(demo01ContactService.createDemo01Contact(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新示例联系人")
+    @PreAuthorize("@ss.hasPermission('infra:demo01-contact:update')")
+    public CommonResult<Boolean> updateDemo01Contact(@Valid @RequestBody Demo01ContactSaveReqVO updateReqVO) {
+        demo01ContactService.updateDemo01Contact(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除示例联系人")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:demo01-contact:delete')")
+    public CommonResult<Boolean> deleteDemo01Contact(@RequestParam("id") Long id) {
+        demo01ContactService.deleteDemo01Contact(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得示例联系人")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:demo01-contact:query')")
+    public CommonResult<Demo01ContactRespVO> getDemo01Contact(@RequestParam("id") Long id) {
+        Demo01ContactDO demo01Contact = demo01ContactService.getDemo01Contact(id);
+        return success(BeanUtils.toBean(demo01Contact, Demo01ContactRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得示例联系人分页")
+    @PreAuthorize("@ss.hasPermission('infra:demo01-contact:query')")
+    public CommonResult<PageResult<Demo01ContactRespVO>> getDemo01ContactPage(@Valid Demo01ContactPageReqVO pageReqVO) {
+        PageResult<Demo01ContactDO> pageResult = demo01ContactService.getDemo01ContactPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, Demo01ContactRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出示例联系人 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:demo01-contact:export')")
+    @OperateLog(type = EXPORT)
+    public void exportDemo01ContactExcel(@Valid Demo01ContactPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<Demo01ContactDO> list = demo01ContactService.getDemo01ContactPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "示例联系人.xls", "数据", Demo01ContactRespVO.class,
+                        BeanUtils.toBean(list, Demo01ContactRespVO.class));
+    }
+
+}

+ 28 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactPageReqVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.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 Demo01ContactPageReqVO extends PageParam {
+
+    @Schema(description = "名字", example = "张三")
+    private String name;
+
+    @Schema(description = "性别", example = "1")
+    private Integer sex;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 47 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactRespVO.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.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 Demo01ContactRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21555")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @ExcelProperty("名字")
+    private String name;
+
+    @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)
+    @ExcelProperty("出生年")
+    private LocalDateTime birthday;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "头像")
+    @ExcelProperty("头像")
+    private String avatar;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 36 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo01/vo/Demo01ContactSaveReqVO.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 示例联系人新增/修改 Request VO")
+@Data
+public class Demo01ContactSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "21555")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "出生年", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生年不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "你说的对")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+    @Schema(description = "头像")
+    private String avatar;
+
+}

+ 90 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/Demo02CategoryController.java

@@ -0,0 +1,90 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryRespVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO;
+import cn.iocoder.yudao.module.infra.service.demo.demo02.Demo02CategoryService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 示例分类")
+@RestController
+@RequestMapping("/infra/demo02-category")
+@Validated
+public class Demo02CategoryController {
+
+    @Resource
+    private Demo02CategoryService demo02CategoryService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建示例分类")
+    @PreAuthorize("@ss.hasPermission('infra:demo02-category:create')")
+    public CommonResult<Long> createDemo02Category(@Valid @RequestBody Demo02CategorySaveReqVO createReqVO) {
+        return success(demo02CategoryService.createDemo02Category(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新示例分类")
+    @PreAuthorize("@ss.hasPermission('infra:demo02-category:update')")
+    public CommonResult<Boolean> updateDemo02Category(@Valid @RequestBody Demo02CategorySaveReqVO updateReqVO) {
+        demo02CategoryService.updateDemo02Category(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除示例分类")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:demo02-category:delete')")
+    public CommonResult<Boolean> deleteDemo02Category(@RequestParam("id") Long id) {
+        demo02CategoryService.deleteDemo02Category(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得示例分类")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:demo02-category:query')")
+    public CommonResult<Demo02CategoryRespVO> getDemo02Category(@RequestParam("id") Long id) {
+        Demo02CategoryDO demo02Category = demo02CategoryService.getDemo02Category(id);
+        return success(BeanUtils.toBean(demo02Category, Demo02CategoryRespVO.class));
+    }
+
+    @GetMapping("/list")
+    @Operation(summary = "获得示例分类列表")
+    @PreAuthorize("@ss.hasPermission('infra:demo02-category:query')")
+    public CommonResult<List<Demo02CategoryRespVO>> getDemo02CategoryList(@Valid Demo02CategoryListReqVO listReqVO) {
+        List<Demo02CategoryDO> list = demo02CategoryService.getDemo02CategoryList(listReqVO);
+        return success(BeanUtils.toBean(list, Demo02CategoryRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出示例分类 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:demo02-category:export')")
+    @OperateLog(type = EXPORT)
+    public void exportDemo02CategoryExcel(@Valid Demo02CategoryListReqVO listReqVO,
+              HttpServletResponse response) throws IOException {
+        List<Demo02CategoryDO> list = demo02CategoryService.getDemo02CategoryList(listReqVO);
+        // 导出 Excel
+        ExcelUtils.write(response, "示例分类.xls", "数据", Demo02CategoryRespVO.class,
+                        BeanUtils.toBean(list, Demo02CategoryRespVO.class));
+    }
+
+}

+ 25 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryListReqVO.java

@@ -0,0 +1,25 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+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
+public class Demo02CategoryListReqVO {
+
+    @Schema(description = "名字", example = "芋艿")
+    private String name;
+
+    @Schema(description = "父级编号", example = "6080")
+    private Long parentId;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

+ 31 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategoryRespVO.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDateTime;
+
+@Schema(description = "管理后台 - 示例分类 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class Demo02CategoryRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10304")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6080")
+    @ExcelProperty("父级编号")
+    private Long parentId;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 24 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo02/vo/Demo02CategorySaveReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+@Schema(description = "管理后台 - 示例分类新增/修改 Request VO")
+@Data
+public class Demo02CategorySaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "10304")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "父级编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "6080")
+    @NotNull(message = "父级编号不能为空")
+    private Long parentId;
+
+}

+ 197 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/Demo03StudentController.java

@@ -0,0 +1,197 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentRespVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO;
+import cn.iocoder.yudao.module.infra.service.demo.demo03.Demo03StudentService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import javax.validation.Valid;
+import java.io.IOException;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
+
+@Tag(name = "管理后台 - 学生")
+@RestController
+@RequestMapping("/infra/demo03-student")
+@Validated
+public class Demo03StudentController {
+
+    @Resource
+    private Demo03StudentService demo03StudentService;
+
+    @PostMapping("/create")
+    @Operation(summary = "创建学生")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')")
+    public CommonResult<Long> createDemo03Student(@Valid @RequestBody Demo03StudentSaveReqVO createReqVO) {
+        return success(demo03StudentService.createDemo03Student(createReqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新学生")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')")
+    public CommonResult<Boolean> updateDemo03Student(@Valid @RequestBody Demo03StudentSaveReqVO updateReqVO) {
+        demo03StudentService.updateDemo03Student(updateReqVO);
+        return success(true);
+    }
+
+    @DeleteMapping("/delete")
+    @Operation(summary = "删除学生")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')")
+    public CommonResult<Boolean> deleteDemo03Student(@RequestParam("id") Long id) {
+        demo03StudentService.deleteDemo03Student(id);
+        return success(true);
+    }
+
+    @GetMapping("/get")
+    @Operation(summary = "获得学生")
+    @Parameter(name = "id", description = "编号", required = true, example = "1024")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<Demo03StudentRespVO> getDemo03Student(@RequestParam("id") Long id) {
+        Demo03StudentDO demo03Student = demo03StudentService.getDemo03Student(id);
+        return success(BeanUtils.toBean(demo03Student, Demo03StudentRespVO.class));
+    }
+
+    @GetMapping("/page")
+    @Operation(summary = "获得学生分页")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<PageResult<Demo03StudentRespVO>> getDemo03StudentPage(@Valid Demo03StudentPageReqVO pageReqVO) {
+        PageResult<Demo03StudentDO> pageResult = demo03StudentService.getDemo03StudentPage(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, Demo03StudentRespVO.class));
+    }
+
+    @GetMapping("/export-excel")
+    @Operation(summary = "导出学生 Excel")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:export')")
+    @OperateLog(type = EXPORT)
+    public void exportDemo03StudentExcel(@Valid Demo03StudentPageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<Demo03StudentDO> list = demo03StudentService.getDemo03StudentPage(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "学生.xls", "数据", Demo03StudentRespVO.class,
+                        BeanUtils.toBean(list, Demo03StudentRespVO.class));
+    }
+
+    // ==================== 子表(学生课程) ====================
+
+    @GetMapping("/demo03-course/page")
+    @Operation(summary = "获得学生课程分页")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<PageResult<Demo03CourseDO>> getDemo03CoursePage(PageParam pageReqVO,
+                                                                        @RequestParam("studentId") Long studentId) {
+        return success(demo03StudentService.getDemo03CoursePage(pageReqVO, studentId));
+    }
+
+    @PostMapping("/demo03-course/create")
+    @Operation(summary = "创建学生课程")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')")
+    public CommonResult<Long> createDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) {
+        return success(demo03StudentService.createDemo03Course(demo03Course));
+    }
+
+    @PutMapping("/demo03-course/update")
+    @Operation(summary = "更新学生课程")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')")
+    public CommonResult<Boolean> updateDemo03Course(@Valid @RequestBody Demo03CourseDO demo03Course) {
+        demo03StudentService.updateDemo03Course(demo03Course);
+        return success(true);
+    }
+
+    @DeleteMapping("/demo03-course/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除学生课程")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')")
+    public CommonResult<Boolean> deleteDemo03Course(@RequestParam("id") Long id) {
+        demo03StudentService.deleteDemo03Course(id);
+        return success(true);
+    }
+
+    @GetMapping("/demo03-course/get")
+    @Operation(summary = "获得学生课程")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<Demo03CourseDO> getDemo03Course(@RequestParam("id") Long id) {
+        return success(demo03StudentService.getDemo03Course(id));
+    }
+
+    @GetMapping("/demo03-course/list-by-student-id")
+    @Operation(summary = "获得学生课程列表")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<List<Demo03CourseDO>> getDemo03CourseListByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(demo03StudentService.getDemo03CourseListByStudentId(studentId));
+    }
+
+    // ==================== 子表(学生班级) ====================
+
+    @GetMapping("/demo03-grade/page")
+    @Operation(summary = "获得学生班级分页")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<PageResult<Demo03GradeDO>> getDemo03GradePage(PageParam pageReqVO,
+                                                                      @RequestParam("studentId") Long studentId) {
+        return success(demo03StudentService.getDemo03GradePage(pageReqVO, studentId));
+    }
+
+    @PostMapping("/demo03-grade/create")
+    @Operation(summary = "创建学生班级")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:create')")
+    public CommonResult<Long> createDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) {
+        return success(demo03StudentService.createDemo03Grade(demo03Grade));
+    }
+
+    @PutMapping("/demo03-grade/update")
+    @Operation(summary = "更新学生班级")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:update')")
+    public CommonResult<Boolean> updateDemo03Grade(@Valid @RequestBody Demo03GradeDO demo03Grade) {
+        demo03StudentService.updateDemo03Grade(demo03Grade);
+        return success(true);
+    }
+
+    @DeleteMapping("/demo03-grade/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除学生班级")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:delete')")
+    public CommonResult<Boolean> deleteDemo03Grade(@RequestParam("id") Long id) {
+        demo03StudentService.deleteDemo03Grade(id);
+        return success(true);
+    }
+
+    @GetMapping("/demo03-grade/get")
+    @Operation(summary = "获得学生班级")
+    @Parameter(name = "id", description = "编号", required = true)
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<Demo03GradeDO> getDemo03Grade(@RequestParam("id") Long id) {
+        return success(demo03StudentService.getDemo03Grade(id));
+    }
+
+    @GetMapping("/demo03-grade/get-by-student-id")
+    @Operation(summary = "获得学生班级")
+    @Parameter(name = "studentId", description = "学生编号")
+    @PreAuthorize("@ss.hasPermission('infra:demo03-student:query')")
+    public CommonResult<Demo03GradeDO> getDemo03GradeByStudentId(@RequestParam("studentId") Long studentId) {
+        return success(demo03StudentService.getDemo03GradeByStudentId(studentId));
+    }
+
+}

+ 1 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03;

+ 13 - 19
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoPageReqVO.java → yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/vo/Demo03StudentPageReqVO.java

@@ -1,36 +1,30 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo;
 
-import java.time.LocalDateTime;
+import lombok.*;
+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")
+@Schema(description = "管理后台 - 学生分页 Request VO")
 @Data
 @EqualsAndHashCode(callSuper = true)
 @ToString(callSuper = true)
-public class TestDemoPageReqVO extends PageParam {
+public class Demo03StudentPageReqVO extends PageParam {
 
-    @Schema(description = "名字")
+    @Schema(description = "名字", example = "芋艿")
     private String name;
 
-    @Schema(description = "状态")
-    private Integer status;
-
-    @Schema(description = "类型")
-    private Integer type;
-
-    @Schema(description = "分类")
-    private Integer category;
+    @Schema(description = "性别")
+    private Integer sex;
 
-    @Schema(description = "备注")
-    private String remark;
+    @Schema(description = "简介", example = "随便")
+    private String description;
 
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
     private LocalDateTime[] createTime;
 
-}
+}

+ 41 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/vo/Demo03StudentRespVO.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+
+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 Demo03StudentRespVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525")
+    @ExcelProperty("编号")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @ExcelProperty("名字")
+    private String name;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty(value = "性别", converter = DictConvert.class)
+    @DictFormat("system_user_sex") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+    private Integer sex;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("出生日期")
+    private LocalDateTime birthday;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便")
+    @ExcelProperty("简介")
+    private String description;
+
+    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
+    @ExcelProperty("创建时间")
+    private LocalDateTime createTime;
+
+}

+ 39 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/demo03/vo/Demo03StudentSaveReqVO.java

@@ -0,0 +1,39 @@
+package cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+import java.time.LocalDateTime;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO;
+
+@Schema(description = "管理后台 - 学生新增/修改 Request VO")
+@Data
+public class Demo03StudentSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "8525")
+    private Long id;
+
+    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
+    @NotEmpty(message = "名字不能为空")
+    private String name;
+
+    @Schema(description = "性别", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "性别不能为空")
+    private Integer sex;
+
+    @Schema(description = "出生日期", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotNull(message = "出生日期不能为空")
+    private LocalDateTime birthday;
+
+    @Schema(description = "简介", requiredMode = Schema.RequiredMode.REQUIRED, example = "随便")
+    @NotEmpty(message = "简介不能为空")
+    private String description;
+
+
+    private List<Demo03CourseDO> demo03Courses;
+
+    private Demo03GradeDO demo03Grade;
+
+}

+ 8 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/demo/package-info.java

@@ -0,0 +1,8 @@
+/**
+ * 代码生成示例
+ *
+ * 1. demo01:单表(增删改查)
+ * 2. demo02:单表(树形结构)
+ * 3. demo03:主子表(标准模式)+ 主子表(ERP 模式)+ 主子表(内嵌模式)
+ */
+package cn.iocoder.yudao.module.infra.controller.admin.demo;

+ 0 - 19
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.http

@@ -1,19 +0,0 @@
-### 请求 /infra/test-demo/get 接口 => 成功
-GET {{baseUrl}}/infra/test-demo/get?id=106
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
-
-### 请求 /infra/test-demo/update 接口 => 成功
-PUT {{baseUrl}}/infra/test-demo/update
-Authorization: Bearer {{token}}
-tenant-id: {{adminTenentId}}
-Content-Type: application/json
-
-
-{
-  "id": 106,
-  "name": "测试",
-  "status": "0",
-  "type": 1,
-  "category": 1
-}

+ 0 - 97
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/TestDemoController.java

@@ -1,97 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test;
-
-import cn.iocoder.yudao.framework.common.pojo.CommonResult;
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
-import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.*;
-import cn.iocoder.yudao.module.infra.convert.test.TestDemoConvert;
-import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO;
-import cn.iocoder.yudao.module.infra.service.test.TestDemoService;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.Operation;
-import org.springframework.security.access.prepost.PreAuthorize;
-import org.springframework.validation.annotation.Validated;
-import org.springframework.web.bind.annotation.*;
-
-import javax.annotation.Resource;
-import javax.servlet.http.HttpServletResponse;
-import javax.validation.Valid;
-import java.io.IOException;
-import java.util.Collection;
-import java.util.List;
-
-import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
-import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.EXPORT;
-
-@Tag(name = "管理后台 - 字典类型")
-@RestController
-@RequestMapping("/infra/test-demo")
-@Validated
-public class TestDemoController {
-
-    @Resource
-    private TestDemoService testDemoService;
-
-    @PostMapping("/create")
-    @Operation(summary = "创建字典类型")
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:create')")
-    public CommonResult<Long> createTestDemo(@Valid @RequestBody TestDemoCreateReqVO createReqVO) {
-        return success(testDemoService.createTestDemo(createReqVO));
-    }
-
-    @PutMapping("/update")
-    @Operation(summary = "更新字典类型")
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:update')")
-    public CommonResult<Boolean> updateTestDemo(@Valid @RequestBody TestDemoUpdateReqVO updateReqVO) {
-        testDemoService.updateTestDemo(updateReqVO);
-        return success(true);
-    }
-
-    @DeleteMapping("/delete")
-    @Operation(summary = "删除字典类型")
-    @Parameter(name = "id", description = "编号", required = true)
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:delete')")
-    public CommonResult<Boolean> deleteTestDemo(@RequestParam("id") Long id) {
-        testDemoService.deleteTestDemo(id);
-        return success(true);
-    }
-
-    @GetMapping("/get")
-    @Operation(summary = "获得字典类型")
-    @Parameter(name = "id", description = "编号", required = true, example = "1024")
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:query')")
-    public CommonResult<TestDemoRespVO> getTestDemo(@RequestParam("id") Long id) {
-        TestDemoDO testDemo = testDemoService.getTestDemo(id);
-        return success(TestDemoConvert.INSTANCE.convert(testDemo));
-    }
-
-    @GetMapping("/list")
-    @Operation(summary = "获得字典类型列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:query')")
-    public CommonResult<List<TestDemoRespVO>> getTestDemoList(@RequestParam("ids") Collection<Long> ids) {
-        List<TestDemoDO> list = testDemoService.getTestDemoList(ids);
-        return success(TestDemoConvert.INSTANCE.convertList(list));
-    }
-
-    @GetMapping("/page")
-    @Operation(summary = "获得字典类型分页")
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:query')")    public CommonResult<PageResult<TestDemoRespVO>> getTestDemoPage(@Valid TestDemoPageReqVO pageVO) {
-        PageResult<TestDemoDO> pageResult = testDemoService.getTestDemoPage(pageVO);
-        return success(TestDemoConvert.INSTANCE.convertPage(pageResult));
-    }
-
-    @GetMapping("/export-excel")
-    @Operation(summary = "导出字典类型 Excel")
-    @PreAuthorize("@ss.hasPermission('infra:test-demo:export')")    @OperateLog(type = EXPORT)
-    public void exportTestDemoExcel(@Valid TestDemoExportReqVO exportReqVO,
-              HttpServletResponse response) throws IOException {
-        List<TestDemoDO> list = testDemoService.getTestDemoList(exportReqVO);
-        // 导出 Excel
-        List<TestDemoExcelVO> datas = TestDemoConvert.INSTANCE.convertList02(list);
-        ExcelUtils.write(response, "字典类型.xls", "数据", TestDemoExcelVO.class, datas);
-    }
-
-}

+ 0 - 32
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoBaseVO.java

@@ -1,32 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import javax.validation.constraints.*;
-
-/**
-* 字典类型 Base VO,提供给添加、修改、详细的子 VO 使用
-* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
-*/
-@Data
-public class TestDemoBaseVO {
-
-    @Schema(description = "名字", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "名字不能为空")
-    private String name;
-
-    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "状态不能为空")
-    private Integer status;
-
-    @Schema(description = "类型", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "类型不能为空")
-    private Integer type;
-
-    @Schema(description = "分类", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "分类不能为空")
-    private Integer category;
-
-    @Schema(description = "备注")
-    private String remark;
-
-}

+ 0 - 11
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoCreateReqVO.java

@@ -1,11 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-@Schema(description = "管理后台 - 字典类型创建 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class TestDemoCreateReqVO extends TestDemoBaseVO {
-
-}

+ 0 - 38
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExcelVO.java

@@ -1,38 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-
-import lombok.*;
-
-import java.time.LocalDateTime;
-
-import com.alibaba.excel.annotation.ExcelProperty;
-
-/**
- * 字典类型 Excel VO
- *
- * @author 芋道源码
- */
-@Data
-public class TestDemoExcelVO {
-
-    @ExcelProperty("编号")
-    private Long id;
-
-    @ExcelProperty("名字")
-    private String name;
-
-    @ExcelProperty("状态")
-    private Integer status;
-
-    @ExcelProperty("类型")
-    private Integer type;
-
-    @ExcelProperty("分类")
-    private Integer category;
-
-    @ExcelProperty("备注")
-    private String remark;
-
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
-
-}

+ 0 - 33
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoExportReqVO.java

@@ -1,33 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-import java.time.LocalDateTime;
-import org.springframework.format.annotation.DateTimeFormat;
-
-import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-
-@Schema(description = "管理后台 - 字典类型 Excel 导出 Request VO,参数和 TestDemoPageReqVO 是一致的")
-@Data
-public class TestDemoExportReqVO {
-
-    @Schema(description = "名字")
-    private String name;
-
-    @Schema(description = "状态")
-    private Integer status;
-
-    @Schema(description = "类型")
-    private Integer type;
-
-    @Schema(description = "分类")
-    private Integer category;
-
-    @Schema(description = "备注")
-    private String remark;
-
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-    @Schema(description = "创建时间")
-    private LocalDateTime[] createTime;
-
-}

+ 0 - 19
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoRespVO.java

@@ -1,19 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-
-import java.time.LocalDateTime;
-
-@Schema(description = "管理后台 - 字典类型 Response VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class TestDemoRespVO extends TestDemoBaseVO {
-
-    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED)
-    private Long id;
-
-    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
-    private LocalDateTime createTime;
-
-}

+ 0 - 16
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/test/vo/TestDemoUpdateReqVO.java

@@ -1,16 +0,0 @@
-package cn.iocoder.yudao.module.infra.controller.admin.test.vo;
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import javax.validation.constraints.*;
-
-@Schema(description = "管理后台 - 字典类型更新 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class TestDemoUpdateReqVO extends TestDemoBaseVO {
-
-    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED)
-    @NotNull(message = "编号不能为空")
-    private Long id;
-
-}

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/codegen/CodegenConvert.java

@@ -56,7 +56,7 @@ public interface CodegenConvert {
 
     // ========== CodegenTableDO 相关 ==========
 
-//    List<CodegenTableRespVO> convertList02(List<CodegenTableDO> list);
+    List<CodegenTableRespVO> convertList05(List<CodegenTableDO> list);
 
     CodegenTableRespVO convert(CodegenTableDO bean);
 

+ 0 - 36
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/convert/test/TestDemoConvert.java

@@ -1,36 +0,0 @@
-package cn.iocoder.yudao.module.infra.convert.test;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExcelVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoRespVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO;
-import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO;
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-
-import java.util.List;
-
-/**
- * 字典类型 Convert
- *
- * @author 芋道源码
- */
-@Mapper
-public interface TestDemoConvert {
-
-    TestDemoConvert INSTANCE = Mappers.getMapper(TestDemoConvert.class);
-
-    TestDemoDO convert(TestDemoCreateReqVO bean);
-
-    TestDemoDO convert(TestDemoUpdateReqVO bean);
-
-    TestDemoRespVO convert(TestDemoDO bean);
-
-    List<TestDemoRespVO> convertList(List<TestDemoDO> list);
-
-    PageResult<TestDemoRespVO> convertPage(PageResult<TestDemoDO> page);
-
-    List<TestDemoExcelVO> convertList02(List<TestDemoDO> list);
-
-}

+ 39 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/codegen/CodegenTableDO.java

@@ -116,4 +116,43 @@ public class CodegenTableDO extends BaseDO {
      */
     private Long parentMenuId;
 
+    // ========== 主子表相关字段 ==========
+
+    /**
+     * 主表的编号
+     *
+     * 关联 {@link CodegenTableDO#getId()}
+     */
+    private Long masterTableId;
+    /**
+     * 【自己】子表关联主表的字段编号
+     *
+     * 关联 {@link CodegenColumnDO#getId()}
+     */
+    private Long subJoinColumnId;
+    /**
+     * 主表与子表是否一对多
+     *
+     * true:一对多
+     * false:一对一
+     */
+    private Boolean subJoinMany;
+
+    // ========== 树表相关字段 ==========
+
+    /**
+     * 树表的父字段编号
+     *
+     * 关联 {@link CodegenColumnDO#getId()}
+     */
+    private Long treeParentColumnId;
+    /**
+     * 树表的名字字段编号
+     *
+     * 名字的用途:新增或修改时,select 框展示的字段
+     *
+     * 关联 {@link CodegenColumnDO#getId()}
+     */
+    private Long treeNameColumnId;
+
 }

+ 54 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo01/Demo01ContactDO.java

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+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_demo01_contact")
+@KeySequence("infra_demo01_contact_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Demo01ContactDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 出生年
+     */
+    private LocalDateTime birthday;
+    /**
+     * 简介
+     */
+    private String description;
+    /**
+     * 头像
+     */
+    private String avatar;
+
+}

+ 41 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo02/Demo02CategoryDO.java

@@ -0,0 +1,41 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02;
+
+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_demo02_category")
+@KeySequence("infra_demo02_category_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Demo02CategoryDO extends BaseDO {
+
+    public static final Long PARENT_ID_ROOT = 0L;
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 父级编号
+     */
+    private Long parentId;
+
+}

+ 43 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03CourseDO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03;
+
+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_demo03_course")
+@KeySequence("infra_demo03_course_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Demo03CourseDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 分数
+     */
+    private Integer score;
+
+}

+ 43 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03GradeDO.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03;
+
+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_demo03_grade")
+@KeySequence("infra_demo03_grade_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Demo03GradeDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 学生编号
+     */
+    private Long studentId;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 班主任
+     */
+    private String teacher;
+
+}

+ 50 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/demo/demo03/Demo03StudentDO.java

@@ -0,0 +1,50 @@
+package cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03;
+
+import lombok.*;
+import java.util.*;
+import java.time.LocalDateTime;
+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_demo03_student")
+@KeySequence("infra_demo03_student_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class Demo03StudentDO extends BaseDO {
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+    /**
+     * 名字
+     */
+    private String name;
+    /**
+     * 性别
+     *
+     * 枚举 {@link TODO system_user_sex 对应的类}
+     */
+    private Integer sex;
+    /**
+     * 出生日期
+     */
+    private LocalDateTime birthday;
+    /**
+     * 简介
+     */
+    private String description;
+
+}

+ 0 - 50
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/test/TestDemoDO.java

@@ -1,50 +0,0 @@
-package cn.iocoder.yudao.module.infra.dal.dataobject.test;
-
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-import com.baomidou.mybatisplus.annotation.KeySequence;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
-import lombok.*;
-
-/**
- * 字典类型 DO
- *
- * @author 芋道源码
- */
-@TableName("infra_test_demo")
-@KeySequence("infra_test_demo_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class TestDemoDO extends BaseDO {
-
-    /**
-     * 编号
-     */
-    @TableId
-    private Long id;
-    /**
-     * 名字
-     */
-    private String name;
-    /**
-     * 状态
-     */
-    private Integer status;
-    /**
-     * 类型
-     */
-    private Integer type;
-    /**
-     * 分类
-     */
-    private Integer category;
-    /**
-     * 备注
-     */
-    private String remark;
-
-}

+ 5 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/codegen/CodegenTableMapper.java

@@ -29,4 +29,9 @@ public interface CodegenTableMapper extends BaseMapperX<CodegenTableDO> {
         return selectList(CodegenTableDO::getDataSourceConfigId, dataSourceConfigId);
     }
 
+    default List<CodegenTableDO> selectListByTemplateTypeAndMasterTableId(Integer templateType, Long masterTableId) {
+        return selectList(CodegenTableDO::getTemplateType, templateType,
+                CodegenTableDO::getMasterTableId, masterTableId);
+    }
+
 }

+ 26 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo01/Demo01ContactMapper.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo01;
+
+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.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 示例联系人 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface Demo01ContactMapper extends BaseMapperX<Demo01ContactDO> {
+
+    default PageResult<Demo01ContactDO> selectPage(Demo01ContactPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<Demo01ContactDO>()
+                .likeIfPresent(Demo01ContactDO::getName, reqVO.getName())
+                .eqIfPresent(Demo01ContactDO::getSex, reqVO.getSex())
+                .betweenIfPresent(Demo01ContactDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(Demo01ContactDO::getId));
+    }
+
+}

+ 35 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo02/Demo02CategoryMapper.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo02;
+
+import java.util.*;
+
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 示例分类 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface Demo02CategoryMapper extends BaseMapperX<Demo02CategoryDO> {
+
+    default List<Demo02CategoryDO> selectList(Demo02CategoryListReqVO reqVO) {
+        return selectList(new LambdaQueryWrapperX<Demo02CategoryDO>()
+                .likeIfPresent(Demo02CategoryDO::getName, reqVO.getName())
+                .eqIfPresent(Demo02CategoryDO::getParentId, reqVO.getParentId())
+                .betweenIfPresent(Demo02CategoryDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(Demo02CategoryDO::getId));
+    }
+
+	default Demo02CategoryDO selectByParentIdAndName(Long parentId, String name) {
+	    return selectOne(Demo02CategoryDO::getParentId, parentId, Demo02CategoryDO::getName, name);
+	}
+
+    default Long selectCountByParentId(Long parentId) {
+        return selectCount(Demo02CategoryDO::getParentId, parentId);
+    }
+
+}

+ 34 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/Demo03CourseMapper.java

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

+ 32 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/Demo03GradeMapper.java

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

+ 27 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/demo/demo03/Demo03StudentMapper.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03;
+
+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.demo03.Demo03StudentDO;
+import org.apache.ibatis.annotations.Mapper;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.*;
+
+/**
+ * 学生 Mapper
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface Demo03StudentMapper extends BaseMapperX<Demo03StudentDO> {
+
+    default PageResult<Demo03StudentDO> selectPage(Demo03StudentPageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<Demo03StudentDO>()
+                .likeIfPresent(Demo03StudentDO::getName, reqVO.getName())
+                .eqIfPresent(Demo03StudentDO::getSex, reqVO.getSex())
+                .eqIfPresent(Demo03StudentDO::getDescription, reqVO.getDescription())
+                .betweenIfPresent(Demo03StudentDO::getCreateTime, reqVO.getCreateTime())
+                .orderByDesc(Demo03StudentDO::getId));
+    }
+
+}

+ 0 - 45
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/test/TestDemoMapper.java

@@ -1,45 +0,0 @@
-package cn.iocoder.yudao.module.infra.dal.mysql.test;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO;
-import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO;
-import org.apache.ibatis.annotations.Mapper;
-
-import java.util.List;
-
-/**
- * 字典类型 Mapper
- *
- * @author 芋道源码
- */
-@Mapper
-public interface TestDemoMapper extends BaseMapperX<TestDemoDO> {
-
-    default PageResult<TestDemoDO> selectPage(TestDemoPageReqVO reqVO) {
-        return selectPage(reqVO, new LambdaQueryWrapperX<TestDemoDO>()
-                .likeIfPresent(TestDemoDO::getName, reqVO.getName())
-                .eqIfPresent(TestDemoDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(TestDemoDO::getType, reqVO.getType())
-                .eqIfPresent(TestDemoDO::getCategory, reqVO.getCategory())
-                .eqIfPresent(TestDemoDO::getRemark, reqVO.getRemark())
-                .betweenIfPresent(TestDemoDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(TestDemoDO::getId));
-    }
-
-    default List<TestDemoDO> selectList(TestDemoExportReqVO reqVO) {
-        return selectList(new LambdaQueryWrapperX<TestDemoDO>()
-                .likeIfPresent(TestDemoDO::getName, reqVO.getName())
-                .eqIfPresent(TestDemoDO::getStatus, reqVO.getStatus())
-                .eqIfPresent(TestDemoDO::getType, reqVO.getType())
-                .eqIfPresent(TestDemoDO::getCategory, reqVO.getCategory())
-                .eqIfPresent(TestDemoDO::getRemark, reqVO.getRemark())
-                .betweenIfPresent(TestDemoDO::getCreateTime, reqVO.getCreateTime())
-                .orderByDesc(TestDemoDO::getId));
-    }
-
-    List<TestDemoDO> selectList2();
-
-}

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenColumnHtmlTypeEnum.java

@@ -16,8 +16,8 @@ public enum CodegenColumnHtmlTypeEnum {
     RADIO("radio"), // 单选框
     CHECKBOX("checkbox"), // 复选框
     DATETIME("datetime"), // 日期控件
-    UPLOAD_IMAGE("upload_image"), // 上传图片
-    UPLOAD_FILE("upload_file"), // 上传文件
+    IMAGE_UPLOAD("imageUpload"), // 上传图片
+    FILE_UPLOAD("fileUpload"), // 上传文件
     EDITOR("editor"), // 富文本控件
     ;
 

+ 30 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/enums/codegen/CodegenTemplateTypeEnum.java

@@ -1,8 +1,11 @@
 package cn.iocoder.yudao.module.infra.enums.codegen;
 
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
 import lombok.AllArgsConstructor;
 import lombok.Getter;
 
+import java.util.Objects;
+
 /**
  * 代码生成模板类型
  *
@@ -12,8 +15,13 @@ import lombok.Getter;
 @Getter
 public enum CodegenTemplateTypeEnum {
 
-    CRUD(1), // 单表(增删改查)
+    ONE(1), // 单表(增删改查)
     TREE(2), // 树表(增删改查)
+
+    MASTER_NORMAL(10), // 主子表 - 主表 - 普通模式
+    MASTER_ERP(11), // 主子表 - 主表 - ERP 模式
+    MASTER_INNER(12), // 主子表 - 主表 - 内嵌模式
+    SUB(15), // 主子表 - 子表
     ;
 
     /**
@@ -21,4 +29,25 @@ public enum CodegenTemplateTypeEnum {
      */
     private final Integer type;
 
+    /**
+     * 是否为主表
+     *
+     * @param type 类型
+     * @return 是否主表
+     */
+    public static boolean isMaster(Integer type) {
+        return ObjectUtils.equalsAny(type,
+                MASTER_NORMAL.type, MASTER_ERP.type, MASTER_INNER.type);
+    }
+
+    /**
+     * 是否为树表
+     *
+     * @param type 类型
+     * @return 是否树表
+     */
+    public static boolean isTree(Integer type) {
+        return Objects.equals(type, TREE.type);
+    }
+
 }

+ 8 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java

@@ -48,6 +48,14 @@ public interface CodegenService {
      */
     void deleteCodegen(Long tableId);
 
+    /**
+     * 获得表定义列表
+     *
+     * @param dataSourceConfigId 数据源配置的编号
+     * @return 表定义列表
+     */
+    List<CodegenTableDO> getCodegenTableList(Long dataSourceConfigId);
+
     /**
      * 获得表定义分页
      *

+ 39 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java

@@ -14,6 +14,7 @@ import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO;
 import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenColumnMapper;
 import cn.iocoder.yudao.module.infra.dal.mysql.codegen.CodegenTableMapper;
 import cn.iocoder.yudao.module.infra.enums.codegen.CodegenSceneEnum;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
 import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
 import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenBuilder;
 import cn.iocoder.yudao.module.infra.service.codegen.inner.CodegenEngine;
@@ -25,10 +26,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import javax.annotation.Resource;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+import java.util.*;
 import java.util.function.BiPredicate;
 import java.util.stream.Collectors;
 
@@ -129,6 +127,16 @@ public class CodegenServiceImpl implements CodegenService {
         if (codegenTableMapper.selectById(updateReqVO.getTable().getId()) == null) {
             throw exception(CODEGEN_TABLE_NOT_EXISTS);
         }
+        // 校验主表字段存在
+        if (Objects.equals(updateReqVO.getTable().getTemplateType(), CodegenTemplateTypeEnum.SUB.getType())) {
+            if (codegenTableMapper.selectById(updateReqVO.getTable().getMasterTableId()) == null) {
+                throw exception(CODEGEN_MASTER_TABLE_NOT_EXISTS, updateReqVO.getTable().getMasterTableId());
+            }
+            if (CollUtil.findOne(updateReqVO.getColumns(),  // 关联主表的字段不存在
+                    column -> column.getId().equals(updateReqVO.getTable().getSubJoinColumnId())) == null) {
+                throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, updateReqVO.getTable().getSubJoinColumnId());
+            }
+        }
 
         // 更新 table 表定义
         CodegenTableDO updateTableObj = CodegenConvert.INSTANCE.convert(updateReqVO.getTable());
@@ -208,6 +216,11 @@ public class CodegenServiceImpl implements CodegenService {
         codegenColumnMapper.deleteListByTableId(tableId);
     }
 
+    @Override
+    public List<CodegenTableDO> getCodegenTableList(Long dataSourceConfigId) {
+        return codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId);
+    }
+
     @Override
     public PageResult<CodegenTableDO> getCodegenTablePage(CodegenTablePageReqVO pageReqVO) {
         return codegenTableMapper.selectPage(pageReqVO);
@@ -235,14 +248,34 @@ public class CodegenServiceImpl implements CodegenService {
             throw exception(CODEGEN_COLUMN_NOT_EXISTS);
         }
 
+        // 如果是主子表,则加载对应的子表信息
+        List<CodegenTableDO> subTables = null;
+        List<List<CodegenColumnDO>> subColumnsList = null;
+        if (CodegenTemplateTypeEnum.isMaster(table.getTemplateType())) {
+            // 校验子表存在
+            subTables = codegenTableMapper.selectListByTemplateTypeAndMasterTableId(
+                    CodegenTemplateTypeEnum.SUB.getType(), tableId);
+            if (CollUtil.isEmpty(subTables)) {
+                throw exception(CODEGEN_MASTER_GENERATION_FAIL_NO_SUB_TABLE);
+            }
+            // 校验子表的关联字段存在
+            subColumnsList = new ArrayList<>();
+            for (CodegenTableDO subTable : subTables) {
+                List<CodegenColumnDO> subColumns = codegenColumnMapper.selectListByTableId(subTable.getId());
+                if (CollUtil.findOne(subColumns, column -> column.getId().equals(subTable.getSubJoinColumnId())) == null) {
+                    throw exception(CODEGEN_SUB_COLUMN_NOT_EXISTS, subTable.getId());
+                }
+                subColumnsList.add(subColumns);
+            }
+        }
+
         // 执行生成
-        return codegenEngine.execute(table, columns);
+        return codegenEngine.execute(table, columns, subTables, subColumnsList);
     }
 
     @Override
     public List<DatabaseTableRespVO> getDatabaseTableList(Long dataSourceConfigId, String name, String comment) {
         List<TableInfo> tables = databaseTableService.getTableList(dataSourceConfigId, name, comment);
-        // 移除已经生成的表
         // 移除在 Codegen 中,已经存在的
         Set<String> existsTables = CollectionUtils.convertSet(
                 codegenTableMapper.selectListByDataSourceConfigId(dataSourceConfigId), CodegenTableDO::getTableName);

+ 12 - 4
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java

@@ -15,6 +15,7 @@ import com.baomidou.mybatisplus.generator.config.po.TableInfo;
 import com.google.common.collect.Sets;
 import org.springframework.stereotype.Component;
 
+import java.time.LocalDateTime;
 import java.util.*;
 
 import static cn.hutool.core.text.CharSequenceUtil.*;
@@ -49,8 +50,8 @@ public class CodegenBuilder {
                     .put("status", CodegenColumnHtmlTypeEnum.RADIO)
                     .put("sex", CodegenColumnHtmlTypeEnum.RADIO)
                     .put("type", CodegenColumnHtmlTypeEnum.SELECT)
-                    .put("image", CodegenColumnHtmlTypeEnum.UPLOAD_IMAGE)
-                    .put("file", CodegenColumnHtmlTypeEnum.UPLOAD_FILE)
+                    .put("image", CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD)
+                    .put("file", CodegenColumnHtmlTypeEnum.FILE_UPLOAD)
                     .put("content", CodegenColumnHtmlTypeEnum.EDITOR)
                     .put("description", CodegenColumnHtmlTypeEnum.EDITOR)
                     .put("demo", CodegenColumnHtmlTypeEnum.EDITOR)
@@ -118,7 +119,7 @@ public class CodegenBuilder {
         table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false))));
         // 去除结尾的表,作为类描述
         table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表"));
-        table.setTemplateType(CodegenTemplateTypeEnum.CRUD.getType());
+        table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType());
     }
 
     public List<CodegenColumnDO> buildColumns(Long tableId, List<TableField> tableFields) {
@@ -127,6 +128,10 @@ public class CodegenBuilder {
         for (CodegenColumnDO column : columns) {
             column.setTableId(tableId);
             column.setOrdinalPosition(index++);
+            // 特殊处理:Byte => Integer
+            if (Byte.class.getSimpleName().equals(column.getJavaType())) {
+                column.setJavaType(Integer.class.getSimpleName());
+            }
             // 初始化 Column 列的默认字段
             processColumnOperation(column); // 处理 CRUD 相关的字段的默认值
             processColumnUI(column); // 处理 UI 相关的字段的默认值
@@ -162,10 +167,13 @@ public class CodegenBuilder {
                 .filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey()))
                 .findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType()));
         // 如果是 Boolean 类型时,设置为 radio 类型.
-        // 其它类型,因为字段名可以相对保障,所以不进行处理。例如说 date 对应 datetime 类型.
         if (Boolean.class.getSimpleName().equals(column.getJavaType())) {
             column.setHtmlType(CodegenColumnHtmlTypeEnum.RADIO.getType());
         }
+        // 如果是 LocalDateTime 类型,则设置为 datetime 类型
+        if (LocalDateTime.class.getSimpleName().equals(column.getJavaType())) {
+            column.setHtmlType(CodegenColumnHtmlTypeEnum.DATETIME.getType());
+        }
         // 兜底,设置默认为 input 类型
         if (column.getHtmlType() == null) {
             column.setHtmlType(CodegenColumnHtmlTypeEnum.INPUT.getType());

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

@@ -1,6 +1,8 @@
 package cn.iocoder.yudao.module.infra.service.codegen.inner;
 
+import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.hutool.extra.template.TemplateConfig;
 import cn.hutool.extra.template.TemplateEngine;
@@ -12,7 +14,9 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.date.DateUtils;
 import cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.common.util.string.StrUtils;
 import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
 import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
 import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
@@ -25,7 +29,9 @@ 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.CodegenSceneEnum;
+import cn.iocoder.yudao.module.infra.enums.codegen.CodegenTemplateTypeEnum;
 import cn.iocoder.yudao.module.infra.framework.codegen.config.CodegenProperties;
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.collect.ImmutableTable;
 import com.google.common.collect.Maps;
 import com.google.common.collect.Table;
@@ -33,10 +39,7 @@ import org.springframework.stereotype.Component;
 
 import javax.annotation.PostConstruct;
 import javax.annotation.Resource;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 
 import static cn.hutool.core.map.MapUtil.getStr;
 import static cn.hutool.core.text.CharSequenceUtil.*;
@@ -60,20 +63,19 @@ public class CodegenEngine {
      */
     private static final Map<String, String> SERVER_TEMPLATES = MapUtil.<String, String>builder(new LinkedHashMap<>()) // 有序
             // Java module-biz Main
-            .put(javaTemplatePath("controller/vo/baseVO"), javaModuleImplVOFilePath("BaseVO"))
-            .put(javaTemplatePath("controller/vo/createReqVO"), javaModuleImplVOFilePath("CreateReqVO"))
             .put(javaTemplatePath("controller/vo/pageReqVO"), javaModuleImplVOFilePath("PageReqVO"))
+            .put(javaTemplatePath("controller/vo/listReqVO"), javaModuleImplVOFilePath("ListReqVO"))
             .put(javaTemplatePath("controller/vo/respVO"), javaModuleImplVOFilePath("RespVO"))
-            .put(javaTemplatePath("controller/vo/updateReqVO"), javaModuleImplVOFilePath("UpdateReqVO"))
-            .put(javaTemplatePath("controller/vo/exportReqVO"), javaModuleImplVOFilePath("ExportReqVO"))
-            .put(javaTemplatePath("controller/vo/excelVO"), javaModuleImplVOFilePath("ExcelVO"))
+            .put(javaTemplatePath("controller/vo/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO"))
             .put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath())
-            .put(javaTemplatePath("convert/convert"),
-                    javaModuleImplMainFilePath("convert/${table.businessName}/${table.className}Convert"))
             .put(javaTemplatePath("dal/do"),
                     javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${table.className}DO"))
+            .put(javaTemplatePath("dal/do_sub"), // 特殊:主子表专属逻辑
+                    javaModuleImplMainFilePath("dal/dataobject/${table.businessName}/${subTable.className}DO"))
             .put(javaTemplatePath("dal/mapper"),
                     javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${table.className}Mapper"))
+            .put(javaTemplatePath("dal/mapper_sub"), // 特殊:主子表专属逻辑
+                    javaModuleImplMainFilePath("dal/mysql/${table.businessName}/${subTable.className}Mapper"))
             .put(javaTemplatePath("dal/mapper.xml"), mapperXmlFilePath())
             .put(javaTemplatePath("service/serviceImpl"),
                     javaModuleImplMainFilePath("service/${table.businessName}/${table.className}ServiceImpl"))
@@ -99,34 +101,44 @@ public class CodegenEngine {
     private static final Table<Integer, String, String> FRONT_TEMPLATES = ImmutableTable.<Integer, String, String>builder()
             // Vue2 标准模版
             .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/index.vue"),
-                    vueFilePath("views/${table.moduleName}/${classNameVar}/index.vue"))
+                    vueFilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
             .put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"),
                     vueFilePath("api/${table.moduleName}/${classNameVar}.js"))
             // Vue3 标准模版
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/form.vue"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Form.vue"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_normal.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/form_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue"))
+            .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_inner.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
+            .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/components/list_sub_erp.vue"),  // 特殊:主子表专属逻辑
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue"))
             .put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("api/api.ts"),
-                    vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts"))
+                    vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
             // Vue3 Schema 模版
             .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/${classNameVar}.data.ts"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
             .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
             .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Form.vue"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue"))
             .put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"),
-                    vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts"))
+                    vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
             // Vue3 vben 模版
             .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/${classNameVar}.data.ts"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts"))
             .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/index.vue"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/index.vue"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue"))
             .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/form.vue"),
-                    vue3FilePath("views/${table.moduleName}/${classNameVar}/${simpleClassName}Modal.vue"))
+                    vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue"))
             .put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("api/api.ts"),
-                    vue3FilePath("api/${table.moduleName}/${classNameVar}/index.ts"))
+                    vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts"))
             .build();
 
     @Resource
@@ -149,7 +161,8 @@ public class CodegenEngine {
     }
 
     @PostConstruct
-    private void initGlobalBindingMap() {
+    @VisibleForTesting
+    void initGlobalBindingMap() {
         // 全局配置
         globalBindingMap.put("basePackage", codegenProperties.getBasePackage());
         globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage()
@@ -174,9 +187,122 @@ public class CodegenEngine {
         globalBindingMap.put("DictConvertClassName", DictConvert.class.getName());
         globalBindingMap.put("OperateLogClassName", OperateLog.class.getName());
         globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName());
+        globalBindingMap.put("BeanUtils", BeanUtils.class.getName());
     }
 
-    public Map<String, String> execute(CodegenTableDO table, List<CodegenColumnDO> columns) {
+    /**
+     * 生成代码
+     *
+     * @param table 表定义
+     * @param columns table 的字段定义数组
+     * @param subTables 子表数组,当且仅当主子表时使用
+     * @param subColumnsList subTables 的字段定义数组
+     * @return 生成的代码,key 是路径,value 是对应代码
+     */
+    public Map<String, String> execute(CodegenTableDO table, List<CodegenColumnDO> columns,
+                                       List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
+        // 1.1 初始化 bindMap 上下文
+        Map<String, Object> bindingMap = initBindingMap(table, columns, subTables, subColumnsList);
+        // 1.2 获得模版
+        Map<String, String> templates = getTemplates(table.getFrontType());
+
+        // 2. 执行生成
+        Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
+        templates.forEach((vmPath, filePath) -> {
+            // 2.1 特殊:主子表专属逻辑
+            if (isSubTemplate(vmPath)) {
+                generateSubCode(table, subTables, result, vmPath, filePath, bindingMap);
+                return;
+                // 2.2 特殊:树表专属逻辑
+            } else if (isPageReqVOTemplate(vmPath)) {
+                // 减少多余的类生成,例如说 PageVO.java 类
+                if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
+                    return;
+                }
+            } else if (isListReqVOTemplate(vmPath)) {
+                // 减少多余的类生成,例如说 ListVO.java 类
+                if (!CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
+                    return;
+                }
+            }
+            // 2.3 默认生成
+            generateCode(result, vmPath, filePath, bindingMap);
+        });
+        return result;
+    }
+
+    private void generateCode(Map<String, String> result, String vmPath,
+                              String filePath, Map<String, Object> bindingMap) {
+        filePath = formatFilePath(filePath, bindingMap);
+        String content = templateEngine.getTemplate(vmPath).render(bindingMap);
+        // 格式化代码
+        content = prettyCode(content);
+        result.put(filePath, content);
+    }
+
+    private void generateSubCode(CodegenTableDO table, List<CodegenTableDO> subTables,
+                                 Map<String, String> result, String vmPath,
+                                 String filePath, Map<String, Object> bindingMap) {
+        // 没有子表,所以不生成
+        if (CollUtil.isEmpty(subTables)) {
+            return;
+        }
+        // 主子表的模式匹配。目的:过滤掉个性化的模版
+        if (vmPath.contains("_normal")
+                && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_NORMAL.getType())) {
+            return;
+        }
+        if (vmPath.contains("_erp")
+                && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_ERP.getType())) {
+            return;
+        }
+        if (vmPath.contains("_inner")
+                && ObjectUtil.notEqual(table.getTemplateType(), CodegenTemplateTypeEnum.MASTER_INNER.getType())) {
+            return;
+        }
+
+        // 逐个生成
+        for (int i = 0; i < subTables.size(); i++) {
+            bindingMap.put("subIndex", i);
+            generateCode(result, vmPath, filePath, bindingMap);
+        }
+        bindingMap.remove("subIndex");
+    }
+
+    /**
+     * 格式化生成后的代码
+     *
+     * 因为尽量让 vm 模版简单,所以统一的处理都在这个方法。
+     * 如果不处理,Vue 的 Pretty 格式校验可能会报错
+     *
+     * @param content 格式化前的代码
+     * @return 格式化后的代码
+     */
+    private String prettyCode(String content) {
+        // Vue 界面:去除字段后面多余的 , 逗号,解决前端的 Pretty 代码格式检查的报错
+        content = content.replaceAll(",\n}", "\n}").replaceAll(",\n  }", "\n  }");
+        // Vue 界面:去除多的 dateFormatter,只有一个的情况下,说明没使用到
+        if (StrUtil.count(content, "dateFormatter") == 1) {
+            content = StrUtils.removeLineContains(content, "dateFormatter");
+        }
+        // Vue 界面:去除多的 dict 相关,只有一个的情况下,说明没使用到
+        if (StrUtil.count(content, "getIntDictOptions") == 1) {
+            content = content.replace("getIntDictOptions, ", "");
+        }
+        if (StrUtil.count(content, "getStrDictOptions") == 1) {
+            content = content.replace("getStrDictOptions, ", "");
+        }
+        if (StrUtil.count(content, "getBoolDictOptions") == 1) {
+            content = content.replace("getBoolDictOptions, ", "");
+        }
+        if (StrUtil.count(content, "DICT_TYPE.") == 0) {
+            content = StrUtils.removeLineContains(content, "DICT_TYPE");
+        }
+        return content;
+    }
+
+    private Map<String, Object> initBindingMap(CodegenTableDO table, List<CodegenColumnDO> columns,
+                                               List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
         // 创建 bindingMap
         Map<String, Object> bindingMap = new HashMap<>(globalBindingMap);
         bindingMap.put("table", table);
@@ -196,17 +322,54 @@ public class CodegenEngine {
         // permission 前缀
         bindingMap.put("permissionPrefix", table.getModuleName() + ":" + simpleClassNameStrikeCase);
 
-        // 执行生成
-        Map<String, String> templates = getTemplates(table.getFrontType());
-        Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
-        templates.forEach((vmPath, filePath) -> {
-            filePath = formatFilePath(filePath, bindingMap);
-            String content = templateEngine.getTemplate(vmPath).render(bindingMap);
-            // 去除字段后面多余的 , 逗号
-            content = content.replaceAll(",\n}", "\n}").replaceAll(",\n  }", "\n  }");
-            result.put(filePath, content);
-        });
-        return result;
+        // 特殊:树表专属逻辑
+        if (CodegenTemplateTypeEnum.isTree(table.getTemplateType())) {
+            CodegenColumnDO treeParentColumn = CollUtil.findOne(columns,
+                    column -> Objects.equals(column.getId(), table.getTreeParentColumnId()));
+            bindingMap.put("treeParentColumn", treeParentColumn);
+            bindingMap.put("treeParentColumn_javaField_underlineCase", toUnderlineCase(treeParentColumn.getJavaField()));
+            CodegenColumnDO treeNameColumn = CollUtil.findOne(columns,
+                    column -> Objects.equals(column.getId(), table.getTreeNameColumnId()));
+            bindingMap.put("treeNameColumn", treeNameColumn);
+            bindingMap.put("treeNameColumn_javaField_underlineCase", toUnderlineCase(treeNameColumn.getJavaField()));
+        }
+
+        // 特殊:主子表专属逻辑
+        if (CollUtil.isNotEmpty(subTables)) {
+            // 创建 bindingMap
+            bindingMap.put("subTables", subTables);
+            bindingMap.put("subColumnsList", subColumnsList);
+            List<CodegenColumnDO> subPrimaryColumns = new ArrayList<>();
+            List<CodegenColumnDO> subJoinColumns = new ArrayList<>();
+            List<String> subJoinColumnStrikeCases = new ArrayList<>();
+            List<String> subSimpleClassNames = new ArrayList<>();
+            List<String> subClassNameVars = new ArrayList<>();
+            List<String> simpleClassNameUnderlineCases = new ArrayList<>();
+            List<String> subSimpleClassNameStrikeCases = new ArrayList<>();
+            for (int i = 0; i < subTables.size(); i++) {
+                CodegenTableDO subTable = subTables.get(i);
+                List<CodegenColumnDO> subColumns = subColumnsList.get(i);
+                subPrimaryColumns.add(CollectionUtils.findFirst(subColumns, CodegenColumnDO::getPrimaryKey)); //
+                CodegenColumnDO subColumn = CollectionUtils.findFirst(subColumns, // 关联的字段
+                        column -> Objects.equals(column.getId(), subTable.getSubJoinColumnId()));
+                subJoinColumns.add(subColumn);
+                subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); // 将 DictType 转换成 dict-type
+                // className 相关
+                String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName()));
+                subSimpleClassNames.add(subSimpleClassName);
+                simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); // 将 DictType 转换成 dict_type
+                subClassNameVars.add(lowerFirst(subSimpleClassName)); // 将 DictType 转换成 dictType,用于变量
+                subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); // 将 DictType 转换成 dict-type
+            }
+            bindingMap.put("subPrimaryColumns", subPrimaryColumns);
+            bindingMap.put("subJoinColumns", subJoinColumns);
+            bindingMap.put("subJoinColumn_strikeCases", subJoinColumnStrikeCases);
+            bindingMap.put("subSimpleClassNames", subSimpleClassNames);
+            bindingMap.put("simpleClassNameUnderlineCases", simpleClassNameUnderlineCases);
+            bindingMap.put("subClassNameVars", subClassNameVars);
+            bindingMap.put("subSimpleClassName_strikeCases", subSimpleClassNameStrikeCases);
+        }
+        return bindingMap;
     }
 
     private Map<String, String> getTemplates(Integer frontType) {
@@ -216,6 +379,7 @@ public class CodegenEngine {
         return templates;
     }
 
+    @SuppressWarnings("unchecked")
     private String formatFilePath(String filePath, Map<String, Object> bindingMap) {
         filePath = StrUtil.replace(filePath, "${basePackage}",
                 getStr(bindingMap, "basePackage").replaceAll("\\.", "/"));
@@ -232,6 +396,16 @@ public class CodegenEngine {
         filePath = StrUtil.replace(filePath, "${table.moduleName}", table.getModuleName());
         filePath = StrUtil.replace(filePath, "${table.businessName}", table.getBusinessName());
         filePath = StrUtil.replace(filePath, "${table.className}", table.getClassName());
+        // 特殊:主子表专属逻辑
+        Integer subIndex = (Integer) bindingMap.get("subIndex");
+        if (subIndex != null) {
+            CodegenTableDO subTable = ((List<CodegenTableDO>) bindingMap.get("subTables")).get(subIndex);
+            filePath = StrUtil.replace(filePath, "${subTable.moduleName}", subTable.getModuleName());
+            filePath = StrUtil.replace(filePath, "${subTable.businessName}", subTable.getBusinessName());
+            filePath = StrUtil.replace(filePath, "${subTable.className}", subTable.getClassName());
+            filePath = StrUtil.replace(filePath, "${subSimpleClassName}",
+                    ((List<String>) bindingMap.get("subSimpleClassNames")).get(subIndex));
+        }
         return filePath;
     }
 
@@ -298,4 +472,17 @@ public class CodegenEngine {
     private static String vue3VbenTemplatePath(String path) {
         return "codegen/vue3_vben/" + path + ".vm";
     }
+
+    private static boolean isSubTemplate(String path) {
+        return path.contains("_sub");
+    }
+
+    private static boolean isPageReqVOTemplate(String path) {
+        return path.contains("pageReqVO");
+    }
+
+    private static boolean isListReqVOTemplate(String path) {
+        return path.contains("listReqVO");
+    }
+
 }

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactService.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.infra.service.demo.demo01;
+
+import javax.validation.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+
+/**
+ * 示例联系人 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface Demo01ContactService {
+
+    /**
+     * 创建示例联系人
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createDemo01Contact(@Valid Demo01ContactSaveReqVO createReqVO);
+
+    /**
+     * 更新示例联系人
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDemo01Contact(@Valid Demo01ContactSaveReqVO updateReqVO);
+
+    /**
+     * 删除示例联系人
+     *
+     * @param id 编号
+     */
+    void deleteDemo01Contact(Long id);
+
+    /**
+     * 获得示例联系人
+     *
+     * @param id 编号
+     * @return 示例联系人
+     */
+    Demo01ContactDO getDemo01Contact(Long id);
+
+    /**
+     * 获得示例联系人分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 示例联系人分页
+     */
+    PageResult<Demo01ContactDO> getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO);
+
+}

+ 72 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo01/Demo01ContactServiceImpl.java

@@ -0,0 +1,72 @@
+package cn.iocoder.yudao.module.infra.service.demo.demo01;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactPageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo01.vo.Demo01ContactSaveReqVO;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo01.Demo01ContactDO;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo01.Demo01ContactMapper;
+
+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 Demo01ContactServiceImpl implements Demo01ContactService {
+
+    @Resource
+    private Demo01ContactMapper demo01ContactMapper;
+
+    @Override
+    public Long createDemo01Contact(Demo01ContactSaveReqVO createReqVO) {
+        // 插入
+        Demo01ContactDO demo01Contact = BeanUtils.toBean(createReqVO, Demo01ContactDO.class);
+        demo01ContactMapper.insert(demo01Contact);
+        // 返回
+        return demo01Contact.getId();
+    }
+
+    @Override
+    public void updateDemo01Contact(Demo01ContactSaveReqVO updateReqVO) {
+        // 校验存在
+        validateDemo01ContactExists(updateReqVO.getId());
+        // 更新
+        Demo01ContactDO updateObj = BeanUtils.toBean(updateReqVO, Demo01ContactDO.class);
+        demo01ContactMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteDemo01Contact(Long id) {
+        // 校验存在
+        validateDemo01ContactExists(id);
+        // 删除
+        demo01ContactMapper.deleteById(id);
+    }
+
+    private void validateDemo01ContactExists(Long id) {
+        if (demo01ContactMapper.selectById(id) == null) {
+            throw exception(DEMO01_CONTACT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public Demo01ContactDO getDemo01Contact(Long id) {
+        return demo01ContactMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<Demo01ContactDO> getDemo01ContactPage(Demo01ContactPageReqVO pageReqVO) {
+        return demo01ContactMapper.selectPage(pageReqVO);
+    }
+
+}

+ 55 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryService.java

@@ -0,0 +1,55 @@
+package cn.iocoder.yudao.module.infra.service.demo.demo02;
+
+import java.util.*;
+import javax.validation.*;
+
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO;
+
+/**
+ * 示例分类 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface Demo02CategoryService {
+
+    /**
+     * 创建示例分类
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createDemo02Category(@Valid Demo02CategorySaveReqVO createReqVO);
+
+    /**
+     * 更新示例分类
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDemo02Category(@Valid Demo02CategorySaveReqVO updateReqVO);
+
+    /**
+     * 删除示例分类
+     *
+     * @param id 编号
+     */
+    void deleteDemo02Category(Long id);
+
+    /**
+     * 获得示例分类
+     *
+     * @param id 编号
+     * @return 示例分类
+     */
+    Demo02CategoryDO getDemo02Category(Long id);
+
+    /**
+     * 获得示例分类列表
+     *
+     * @param listReqVO 查询条件
+     * @return 示例分类列表
+     */
+    List<Demo02CategoryDO> getDemo02CategoryList(Demo02CategoryListReqVO listReqVO);
+
+}

+ 134 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo02/Demo02CategoryServiceImpl.java

@@ -0,0 +1,134 @@
+package cn.iocoder.yudao.module.infra.service.demo.demo02;
+
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategoryListReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo02.vo.Demo02CategorySaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo02.Demo02CategoryDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo02.Demo02CategoryMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.List;
+import java.util.Objects;
+
+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 Demo02CategoryServiceImpl implements Demo02CategoryService {
+
+    @Resource
+    private Demo02CategoryMapper demo02CategoryMapper;
+
+    @Override
+    public Long createDemo02Category(Demo02CategorySaveReqVO createReqVO) {
+        // 校验父级编号的有效性
+        validateParentDemo02Category(null, createReqVO.getParentId());
+        // 校验名字的唯一性
+        validateDemo02CategoryNameUnique(null, createReqVO.getParentId(), createReqVO.getName());
+
+        // 插入
+        Demo02CategoryDO demo02Category = BeanUtils.toBean(createReqVO, Demo02CategoryDO.class);
+        demo02CategoryMapper.insert(demo02Category);
+        // 返回
+        return demo02Category.getId();
+    }
+
+    @Override
+    public void updateDemo02Category(Demo02CategorySaveReqVO updateReqVO) {
+        // 校验存在
+        validateDemo02CategoryExists(updateReqVO.getId());
+        // 校验父级编号的有效性
+        validateParentDemo02Category(updateReqVO.getId(), updateReqVO.getParentId());
+        // 校验名字的唯一性
+        validateDemo02CategoryNameUnique(updateReqVO.getId(), updateReqVO.getParentId(), updateReqVO.getName());
+
+        // 更新
+        Demo02CategoryDO updateObj = BeanUtils.toBean(updateReqVO, Demo02CategoryDO.class);
+        demo02CategoryMapper.updateById(updateObj);
+    }
+
+    @Override
+    public void deleteDemo02Category(Long id) {
+        // 校验存在
+        validateDemo02CategoryExists(id);
+        // 校验是否有子示例分类
+        if (demo02CategoryMapper.selectCountByParentId(id) > 0) {
+            throw exception(DEMO02_CATEGORY_EXITS_CHILDREN);
+        }
+        // 删除
+        demo02CategoryMapper.deleteById(id);
+    }
+
+    private void validateDemo02CategoryExists(Long id) {
+        if (demo02CategoryMapper.selectById(id) == null) {
+            throw exception(DEMO02_CATEGORY_NOT_EXISTS);
+        }
+    }
+
+    private void validateParentDemo02Category(Long id, Long parentId) {
+        if (parentId == null || Demo02CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
+            return;
+        }
+        // 1. 不能设置自己为父示例分类
+        if (Objects.equals(id, parentId)) {
+            throw exception(DEMO02_CATEGORY_PARENT_ERROR);
+        }
+        // 2. 父示例分类不存在
+        Demo02CategoryDO parentDemo02Category = demo02CategoryMapper.selectById(parentId);
+        if (parentDemo02Category == null) {
+            throw exception(DEMO02_CATEGORY_PARENT_NOT_EXITS);
+        }
+        // 3. 递归校验父示例分类,如果父示例分类是自己的子示例分类,则报错,避免形成环路
+        if (id == null) { // id 为空,说明新增,不需要考虑环路
+            return;
+        }
+        for (int i = 0; i < Short.MAX_VALUE; i++) {
+            // 3.1 校验环路
+            parentId = parentDemo02Category.getParentId();
+            if (Objects.equals(id, parentId)) {
+                throw exception(DEMO02_CATEGORY_PARENT_IS_CHILD);
+            }
+            // 3.2 继续递归下一级父示例分类
+            if (parentId == null || Demo02CategoryDO.PARENT_ID_ROOT.equals(parentId)) {
+                break;
+            }
+            parentDemo02Category = demo02CategoryMapper.selectById(parentId);
+            if (parentDemo02Category == null) {
+                break;
+            }
+        }
+    }
+
+    private void validateDemo02CategoryNameUnique(Long id, Long parentId, String name) {
+        Demo02CategoryDO demo02Category = demo02CategoryMapper.selectByParentIdAndName(parentId, name);
+        if (demo02Category == null) {
+            return;
+        }
+        // 如果 id 为空,说明不用比较是否为相同 id 的示例分类
+        if (id == null) {
+            throw exception(DEMO02_CATEGORY_NAME_DUPLICATE);
+        }
+        if (!Objects.equals(demo02Category.getId(), id)) {
+            throw exception(DEMO02_CATEGORY_NAME_DUPLICATE);
+        }
+    }
+
+    @Override
+    public Demo02CategoryDO getDemo02Category(Long id) {
+        return demo02CategoryMapper.selectById(id);
+    }
+
+    @Override
+    public List<Demo02CategoryDO> getDemo02CategoryList(Demo02CategoryListReqVO listReqVO) {
+        return demo02CategoryMapper.selectList(listReqVO);
+    }
+
+}

+ 158 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/Demo03StudentService.java

@@ -0,0 +1,158 @@
+package cn.iocoder.yudao.module.infra.service.demo.demo03;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO;
+
+import javax.validation.Valid;
+import java.util.List;
+
+/**
+ * 学生 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface Demo03StudentService {
+
+    /**
+     * 创建学生
+     *
+     * @param createReqVO 创建信息
+     * @return 编号
+     */
+    Long createDemo03Student(@Valid Demo03StudentSaveReqVO createReqVO);
+
+    /**
+     * 更新学生
+     *
+     * @param updateReqVO 更新信息
+     */
+    void updateDemo03Student(@Valid Demo03StudentSaveReqVO updateReqVO);
+
+    /**
+     * 删除学生
+     *
+     * @param id 编号
+     */
+    void deleteDemo03Student(Long id);
+
+    /**
+     * 获得学生
+     *
+     * @param id 编号
+     * @return 学生
+     */
+    Demo03StudentDO getDemo03Student(Long id);
+
+    /**
+     * 获得学生分页
+     *
+     * @param pageReqVO 分页查询
+     * @return 学生分页
+     */
+    PageResult<Demo03StudentDO> getDemo03StudentPage(Demo03StudentPageReqVO pageReqVO);
+
+
+    // ==================== 子表(学生课程) ====================
+
+    /**
+     * 获得学生课程列表
+     *
+     * @param studentId 学生编号
+     * @return 学生课程列表
+     */
+    List<Demo03CourseDO> getDemo03CourseListByStudentId(Long studentId);
+
+    /**
+     * 获得学生课程分页
+     *
+     * @param pageReqVO 分页查询
+     * @param studentId 学生编号
+     * @return 学生课程分页
+     */
+    PageResult<Demo03CourseDO> getDemo03CoursePage(PageParam pageReqVO, Long studentId);
+
+    /**
+     * 创建学生课程
+     *
+     * @param demo03Course 创建信息
+     * @return 编号
+     */
+    Long createDemo03Course(@Valid Demo03CourseDO demo03Course);
+
+    /**
+     * 更新学生课程
+     *
+     * @param demo03Course 更新信息
+     */
+    void updateDemo03Course(@Valid Demo03CourseDO demo03Course);
+
+    /**
+     * 删除学生课程
+     *
+     * @param id 编号
+     */
+    void deleteDemo03Course(Long id);
+
+    /**
+     * 获得学生课程
+     *
+     * @param id 编号
+     * @return 学生课程
+     */
+    Demo03CourseDO getDemo03Course(Long id);
+
+    // ==================== 子表(学生班级) ====================
+
+    /**
+     * 获得学生班级
+     *
+     * @param studentId 学生编号
+     * @return 学生班级
+     */
+    Demo03GradeDO getDemo03GradeByStudentId(Long studentId);
+
+    /**
+     * 获得学生班级分页
+     *
+     * @param pageReqVO 分页查询
+     * @param studentId 学生编号
+     * @return 学生班级分页
+     */
+    PageResult<Demo03GradeDO> getDemo03GradePage(PageParam pageReqVO, Long studentId);
+
+    /**
+     * 创建学生班级
+     *
+     * @param demo03Grade 创建信息
+     * @return 编号
+     */
+    Long createDemo03Grade(@Valid Demo03GradeDO demo03Grade);
+
+    /**
+     * 更新学生班级
+     *
+     * @param demo03Grade 更新信息
+     */
+    void updateDemo03Grade(@Valid Demo03GradeDO demo03Grade);
+
+    /**
+     * 删除学生班级
+     *
+     * @param id 编号
+     */
+    void deleteDemo03Grade(Long id);
+
+    /**
+     * 获得学生班级
+     *
+     * @param id 编号
+     * @return 学生班级
+     */
+    Demo03GradeDO getDemo03Grade(Long id);
+
+}

+ 217 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/demo/demo03/Demo03StudentServiceImpl.java

@@ -0,0 +1,217 @@
+package cn.iocoder.yudao.module.infra.service.demo.demo03;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentPageReqVO;
+import cn.iocoder.yudao.module.infra.controller.admin.demo.demo03.vo.Demo03StudentSaveReqVO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03CourseDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03GradeDO;
+import cn.iocoder.yudao.module.infra.dal.dataobject.demo.demo03.Demo03StudentDO;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.Demo03CourseMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.Demo03GradeMapper;
+import cn.iocoder.yudao.module.infra.dal.mysql.demo.demo03.Demo03StudentMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import javax.annotation.Resource;
+import java.util.List;
+
+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 Demo03StudentServiceImpl implements Demo03StudentService {
+
+    @Resource
+    private Demo03StudentMapper demo03StudentMapper;
+    @Resource
+    private Demo03CourseMapper demo03CourseMapper;
+    @Resource
+    private Demo03GradeMapper demo03GradeMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Long createDemo03Student(Demo03StudentSaveReqVO createReqVO) {
+        // 插入
+        Demo03StudentDO demo03Student = BeanUtils.toBean(createReqVO, Demo03StudentDO.class);
+        demo03StudentMapper.insert(demo03Student);
+
+        // 插入子表
+        createDemo03CourseList(demo03Student.getId(), createReqVO.getDemo03Courses());
+        createDemo03Grade(demo03Student.getId(), createReqVO.getDemo03Grade());
+        // 返回
+        return demo03Student.getId();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateDemo03Student(Demo03StudentSaveReqVO updateReqVO) {
+        // 校验存在
+        validateDemo03StudentExists(updateReqVO.getId());
+        // 更新
+        Demo03StudentDO updateObj = BeanUtils.toBean(updateReqVO, Demo03StudentDO.class);
+        demo03StudentMapper.updateById(updateObj);
+
+        // 更新子表
+        updateDemo03CourseList(updateReqVO.getId(), updateReqVO.getDemo03Courses());
+        updateDemo03Grade(updateReqVO.getId(), updateReqVO.getDemo03Grade());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void deleteDemo03Student(Long id) {
+        // 校验存在
+        validateDemo03StudentExists(id);
+        // 删除
+        demo03StudentMapper.deleteById(id);
+
+        // 删除子表
+        deleteDemo03CourseByStudentId(id);
+        deleteDemo03GradeByStudentId(id);
+    }
+
+    private void validateDemo03StudentExists(Long id) {
+        if (demo03StudentMapper.selectById(id) == null) {
+            throw exception(DEMO03_STUDENT_NOT_EXISTS);
+        }
+    }
+
+    @Override
+    public Demo03StudentDO getDemo03Student(Long id) {
+        return demo03StudentMapper.selectById(id);
+    }
+
+    @Override
+    public PageResult<Demo03StudentDO> getDemo03StudentPage(Demo03StudentPageReqVO pageReqVO) {
+        return demo03StudentMapper.selectPage(pageReqVO);
+    }
+
+    // ==================== 子表(学生课程) ====================
+
+    @Override
+    public List<Demo03CourseDO> getDemo03CourseListByStudentId(Long studentId) {
+        return demo03CourseMapper.selectListByStudentId(studentId);
+    }
+
+    private void createDemo03CourseList(Long studentId, List<Demo03CourseDO> list) {
+        if (list != null) {
+            list.forEach(o -> o.setStudentId(studentId));
+        }
+        demo03CourseMapper.insertBatch(list);
+    }
+
+    private void updateDemo03CourseList(Long studentId, List<Demo03CourseDO> list) {
+        deleteDemo03CourseByStudentId(studentId);
+		list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
+        createDemo03CourseList(studentId, list);
+    }
+
+    private void deleteDemo03CourseByStudentId(Long studentId) {
+        demo03CourseMapper.deleteByStudentId(studentId);
+    }
+
+    @Override
+    public PageResult<Demo03CourseDO> getDemo03CoursePage(PageParam pageReqVO, Long studentId) {
+        return demo03CourseMapper.selectPage(pageReqVO, studentId);
+    }
+
+    @Override
+    public Long createDemo03Course(Demo03CourseDO demo03Course) {
+        demo03CourseMapper.insert(demo03Course);
+        return demo03Course.getId();
+    }
+
+    @Override
+    public void updateDemo03Course(Demo03CourseDO demo03Course) {
+        demo03CourseMapper.updateById(demo03Course);
+    }
+
+    @Override
+    public void deleteDemo03Course(Long id) {
+        demo03CourseMapper.deleteById(id);
+    }
+
+    @Override
+    public Demo03CourseDO getDemo03Course(Long id) {
+        return demo03CourseMapper.selectById(id);
+    }
+
+    // ==================== 子表(学生班级) ====================
+
+    @Override
+    public Demo03GradeDO getDemo03GradeByStudentId(Long studentId) {
+        return demo03GradeMapper.selectByStudentId(studentId);
+    }
+
+    private void createDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) {
+        if (demo03Grade == null) {
+            return;
+        }
+        demo03Grade.setStudentId(studentId);
+        demo03GradeMapper.insert(demo03Grade);
+    }
+
+    private void updateDemo03Grade(Long studentId, Demo03GradeDO demo03Grade) {
+        if (demo03Grade == null) {
+			return;
+        }
+        demo03Grade.setStudentId(studentId);
+        demo03Grade.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
+        demo03GradeMapper.insertOrUpdate(demo03Grade);
+    }
+
+    private void deleteDemo03GradeByStudentId(Long studentId) {
+        demo03GradeMapper.deleteByStudentId(studentId);
+    }
+
+    @Override
+    public PageResult<Demo03GradeDO> getDemo03GradePage(PageParam pageReqVO, Long studentId) {
+        return demo03GradeMapper.selectPage(pageReqVO, studentId);
+    }
+
+    @Override
+    public Long createDemo03Grade(Demo03GradeDO demo03Grade) {
+        // 校验是否已经存在
+        if (demo03GradeMapper.selectByStudentId(demo03Grade.getStudentId()) != null) {
+            throw exception(DEMO03_GRADE_EXISTS);
+        }
+        demo03GradeMapper.insert(demo03Grade);
+        return demo03Grade.getId();
+    }
+
+    @Override
+    public void updateDemo03Grade(Demo03GradeDO demo03Grade) {
+        // 校验存在
+        validateDemo03GradeExists(demo03Grade.getId());
+        // 更新
+        demo03GradeMapper.updateById(demo03Grade);
+    }
+
+    @Override
+    public void deleteDemo03Grade(Long id) {
+        // 校验存在
+        validateDemo03GradeExists(id);
+        // 删除
+        demo03GradeMapper.deleteById(id);
+    }
+
+    @Override
+    public Demo03GradeDO getDemo03Grade(Long id) {
+        return demo03GradeMapper.selectById(id);
+    }
+
+    private void validateDemo03GradeExists(Long id) {
+        if (demo03GradeMapper.selectById(id) == null) {
+            throw exception(DEMO03_GRADE_NOT_EXISTS);
+        }
+    }
+
+}

+ 0 - 75
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoService.java

@@ -1,75 +0,0 @@
-package cn.iocoder.yudao.module.infra.service.test;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO;
-import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO;
-
-import javax.validation.Valid;
-import java.util.Collection;
-import java.util.List;
-
-/**
- * 字典类型 Service 接口
- *
- * @author 芋道源码
- */
-public interface TestDemoService {
-
-    /**
-     * 创建字典类型
-     *
-     * @param createReqVO 创建信息
-     * @return 编号
-     */
-    Long createTestDemo(@Valid TestDemoCreateReqVO createReqVO);
-
-    /**
-     * 更新字典类型
-     *
-     * @param updateReqVO 更新信息
-     */
-    void updateTestDemo(@Valid TestDemoUpdateReqVO updateReqVO);
-
-    /**
-     * 删除字典类型
-     *
-     * @param id 编号
-     */
-    void deleteTestDemo(Long id);
-
-    /**
-     * 获得字典类型
-     *
-     * @param id 编号
-     * @return 字典类型
-     */
-    TestDemoDO getTestDemo(Long id);
-
-    /**
-     * 获得字典类型列表
-     *
-     * @param ids 编号
-     * @return 字典类型列表
-     */
-    List<TestDemoDO> getTestDemoList(Collection<Long> ids);
-
-    /**
-     * 获得字典类型分页
-     *
-     * @param pageReqVO 分页查询
-     * @return 字典类型分页
-     */
-    PageResult<TestDemoDO> getTestDemoPage(TestDemoPageReqVO pageReqVO);
-
-    /**
-     * 获得字典类型列表, 用于 Excel 导出
-     *
-     * @param exportReqVO 查询条件
-     * @return 字典类型列表
-     */
-    List<TestDemoDO> getTestDemoList(TestDemoExportReqVO exportReqVO);
-
-}

+ 0 - 91
yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/test/TestDemoServiceImpl.java

@@ -1,91 +0,0 @@
-package cn.iocoder.yudao.module.infra.service.test;
-
-import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoCreateReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoExportReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoPageReqVO;
-import cn.iocoder.yudao.module.infra.controller.admin.test.vo.TestDemoUpdateReqVO;
-import cn.iocoder.yudao.module.infra.convert.test.TestDemoConvert;
-import cn.iocoder.yudao.module.infra.dal.dataobject.test.TestDemoDO;
-import cn.iocoder.yudao.module.infra.dal.mysql.test.TestDemoMapper;
-import org.springframework.cache.annotation.CacheEvict;
-import org.springframework.cache.annotation.Cacheable;
-import org.springframework.stereotype.Service;
-import org.springframework.validation.annotation.Validated;
-
-import javax.annotation.Resource;
-import java.util.Collection;
-import java.util.List;
-
-import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
-import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.TEST_DEMO_NOT_EXISTS;
-
-/**
- * 字典类型 Service 实现类
- *
- * @author 芋道源码
- */
-@Service
-@Validated
-public class TestDemoServiceImpl implements TestDemoService {
-
-    @Resource
-    private TestDemoMapper testDemoMapper;
-
-    @Override
-    public Long createTestDemo(TestDemoCreateReqVO createReqVO) {
-        // 插入
-        TestDemoDO testDemo = TestDemoConvert.INSTANCE.convert(createReqVO);
-        testDemoMapper.insert(testDemo);
-        // 返回
-        return testDemo.getId();
-    }
-
-    @Override
-    @CacheEvict(value = "test", key = "#updateReqVO.id")
-    public void updateTestDemo(TestDemoUpdateReqVO updateReqVO) {
-        // 校验存在
-        validateTestDemoExists(updateReqVO.getId());
-        // 更新
-        TestDemoDO updateObj = TestDemoConvert.INSTANCE.convert(updateReqVO);
-        testDemoMapper.updateById(updateObj);
-    }
-
-    @Override
-    @CacheEvict(value = "test", key = "#id")
-    public void deleteTestDemo(Long id) {
-        // 校验存在
-        validateTestDemoExists(id);
-        // 删除
-        testDemoMapper.deleteById(id);
-    }
-
-    private void validateTestDemoExists(Long id) {
-        if (testDemoMapper.selectById(id) == null) {
-            throw exception(TEST_DEMO_NOT_EXISTS);
-        }
-    }
-
-    @Override
-    @Cacheable(cacheNames = "test", key = "#id")
-    public TestDemoDO getTestDemo(Long id) {
-        return testDemoMapper.selectById(id);
-    }
-
-    @Override
-    public List<TestDemoDO> getTestDemoList(Collection<Long> ids) {
-        return testDemoMapper.selectBatchIds(ids);
-    }
-
-    @Override
-    public PageResult<TestDemoDO> getTestDemoPage(TestDemoPageReqVO pageReqVO) {
-//        testDemoMapper.selectList2();
-        return testDemoMapper.selectPage(pageReqVO);
-    }
-
-    @Override
-    public List<TestDemoDO> getTestDemoList(TestDemoExportReqVO exportReqVO) {
-        return testDemoMapper.selectList(exportReqVO);
-    }
-
-}

+ 155 - 33
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/controller.vm

@@ -15,8 +15,10 @@ import javax.servlet.http.*;
 import java.util.*;
 import java.io.IOException;
 
+import ${PageParamClassName};
 import ${PageResultClassName};
 import ${CommonResultClassName};
+import ${BeanUtils};
 import static ${CommonResultClassName}.success;
 
 import ${ExcelUtilsClassName};
@@ -26,7 +28,10 @@ import static ${OperateTypeEnumClassName}.*;
 
 import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
 import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
-import ${basePackage}.module.${table.moduleName}.convert.${table.businessName}.${table.className}Convert;
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO;
+#end
 import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.${table.className}Service;
 
 @Tag(name = "${sceneEnum.name} - ${table.classComment}")
@@ -41,17 +46,19 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
 
     @PostMapping("/create")
     @Operation(summary = "创建${table.classComment}")
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')")#end
-
-    public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO) {
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')")
+#end
+    public CommonResult<${primaryColumn.javaType}> create${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) {
         return success(${classNameVar}Service.create${simpleClassName}(createReqVO));
     }
 
     @PutMapping("/update")
     @Operation(summary = "更新${table.classComment}")
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')")#end
-
-    public CommonResult<Boolean> update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO) {
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')")
+#end
+    public CommonResult<Boolean> update${simpleClassName}(@Valid @RequestBody ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) {
         ${classNameVar}Service.update${simpleClassName}(updateReqVO);
         return success(true);
     }
@@ -59,8 +66,9 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
     @DeleteMapping("/delete")
     @Operation(summary = "删除${table.classComment}")
     @Parameter(name = "id", description = "编号", required = true)
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")#end
-
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")
+#end
     public CommonResult<Boolean> delete${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) {
         ${classNameVar}Service.delete${simpleClassName}(id);
         return success(true);
@@ -69,43 +77,157 @@ public class ${sceneEnum.prefixClass}${table.className}Controller {
     @GetMapping("/get")
     @Operation(summary = "获得${table.classComment}")
     @Parameter(name = "id", description = "编号", required = true, example = "1024")
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end
-
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
     public CommonResult<${sceneEnum.prefixClass}${table.className}RespVO> get${simpleClassName}(@RequestParam("id") ${primaryColumn.javaType} id) {
         ${table.className}DO ${classNameVar} = ${classNameVar}Service.get${simpleClassName}(id);
-        return success(${table.className}Convert.INSTANCE.convert(${classNameVar}));
-    }
-
-    @GetMapping("/list")
-    @Operation(summary = "获得${table.classComment}列表")
-    @Parameter(name = "ids", description = "编号列表", required = true, example = "1024,2048")
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end
-
-    public CommonResult<List<${sceneEnum.prefixClass}${table.className}RespVO>> get${simpleClassName}List(@RequestParam("ids") Collection<${primaryColumn.javaType}> ids) {
-        List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(ids);
-        return success(${table.className}Convert.INSTANCE.convertList(list));
+        return success(BeanUtils.toBean(${classNameVar}, ${sceneEnum.prefixClass}${table.className}RespVO.class));
     }
 
+#if ( $table.templateType != 2 )
     @GetMapping("/page")
     @Operation(summary = "获得${table.classComment}分页")
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")#end
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
+    public CommonResult<PageResult<${sceneEnum.prefixClass}${table.className}RespVO>> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) {
+        PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO);
+        return success(BeanUtils.toBean(pageResult, ${sceneEnum.prefixClass}${table.className}RespVO.class));
+    }
 
-    public CommonResult<PageResult<${sceneEnum.prefixClass}${table.className}RespVO>> get${simpleClassName}Page(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageVO) {
-        PageResult<${table.className}DO> pageResult = ${classNameVar}Service.get${simpleClassName}Page(pageVO);
-        return success(${table.className}Convert.INSTANCE.convertPage(pageResult));
+## 特殊:树表专属逻辑(树不需要分页接口)
+#else
+    @GetMapping("/list")
+    @Operation(summary = "获得${table.classComment}列表")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
+    public CommonResult<List<${sceneEnum.prefixClass}${table.className}RespVO>> get${simpleClassName}List(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) {
+        List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO);
+        return success(BeanUtils.toBean(list, ${sceneEnum.prefixClass}${table.className}RespVO.class));
     }
 
+#end
     @GetMapping("/export-excel")
     @Operation(summary = "导出${table.classComment} Excel")
-#if ($sceneEnum.scene == 1)    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')")#end
-
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:export')")
+#end
     @OperateLog(type = EXPORT)
-    public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO,
+#if ( $table.templateType != 2 )
+    public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO,
+              HttpServletResponse response) throws IOException {
+        pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+        List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}Page(pageReqVO).getList();
+        // 导出 Excel
+        ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class,
+                        BeanUtils.toBean(list, ${table.className}RespVO.class));
+    }
+## 特殊:树表专属逻辑(树不需要分页接口)
+#else
+    public void export${simpleClassName}Excel(@Valid ${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO,
               HttpServletResponse response) throws IOException {
-        List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(exportReqVO);
+        List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(listReqVO);
         // 导出 Excel
-        List<${sceneEnum.prefixClass}${table.className}ExcelVO> datas = ${table.className}Convert.INSTANCE.convertList02(list);
-        ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${sceneEnum.prefixClass}${table.className}ExcelVO.class, datas);
+        ExcelUtils.write(response, "${table.classComment}.xls", "数据", ${table.className}RespVO.class,
+                        BeanUtils.toBean(list, ${table.className}RespVO.class));
+    }
+#end
+
+## 特殊:主子表专属逻辑
+#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 )
+    @GetMapping("/${subSimpleClassName_strikeCase}/page")
+    @Operation(summary = "获得${subTable.classComment}分页")
+    @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
+    public CommonResult<PageResult<${subTable.className}DO>> get${subSimpleClassName}Page(PageParam pageReqVO,
+                                                                                        @RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return success(${classNameVar}Service.get${subSimpleClassName}Page(pageReqVO, ${subJoinColumn.javaField}));
+    }
+
+## 情况二:非 MASTER_ERP 时,需要列表查询子表
+#else
+    #if ( $subTable.subJoinMany )
+    @GetMapping("/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}")
+    @Operation(summary = "获得${subTable.classComment}列表")
+    @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
+    public CommonResult<List<${subTable.className}DO>> get${subSimpleClassName}ListBy${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return success(${classNameVar}Service.get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaField}));
+    }
+
+    #else
+    @GetMapping("/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}")
+    @Operation(summary = "获得${subTable.classComment}")
+    @Parameter(name = "${subJoinColumn.javaField}", description = "${subJoinColumn.columnComment}")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
+    public CommonResult<${subTable.className}DO> get${subSimpleClassName}By${SubJoinColumnName}(@RequestParam("${subJoinColumn.javaField}") ${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return success(${classNameVar}Service.get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField}));
+    }
+
+    #end
+#end
+## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
+#if ( $table.templateType == 11 )
+    @PostMapping("/${subSimpleClassName_strikeCase}/create")
+    @Operation(summary = "创建${subTable.classComment}")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:create')")
+#end
+    public CommonResult<${subPrimaryColumn.javaType}> create${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) {
+        return success(${classNameVar}Service.create${subSimpleClassName}(${subClassNameVar}));
+    }
+
+    @PutMapping("/${subSimpleClassName_strikeCase}/update")
+    @Operation(summary = "更新${subTable.classComment}")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:update')")
+#end
+    public CommonResult<Boolean> update${subSimpleClassName}(@Valid @RequestBody ${subTable.className}DO ${subClassNameVar}) {
+        ${classNameVar}Service.update${subSimpleClassName}(${subClassNameVar});
+        return success(true);
+    }
+
+    @DeleteMapping("/${subSimpleClassName_strikeCase}/delete")
+    @Parameter(name = "id", description = "编号", required = true)
+    @Operation(summary = "删除${subTable.classComment}")
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:delete')")
+#end
+    public CommonResult<Boolean> delete${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) {
+        ${classNameVar}Service.delete${subSimpleClassName}(id);
+        return success(true);
     }
 
-}
+	@GetMapping("/${subSimpleClassName_strikeCase}/get")
+	@Operation(summary = "获得${subTable.classComment}")
+	@Parameter(name = "id", description = "编号", required = true)
+#if ($sceneEnum.scene == 1)
+    @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')")
+#end
+	public CommonResult<${subTable.className}DO> get${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) {
+	    return success(${classNameVar}Service.get${subSimpleClassName}(id));
+	}
+
+#end
+#end
+}

+ 0 - 13
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/_column.vm

@@ -1,13 +0,0 @@
-## 提供给 baseVO、createVO、updateVO 生成字段
-    @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end)
-#if (!${column.nullable})## 判断 @NotEmpty 和 @NotNull 注解
-#if (${field.fieldType} == 'String')
-    @NotEmpty(message = "${column.columnComment}不能为空")
-#else
-    @NotNull(message = "${column.columnComment}不能为空")
-#end
-#end
-#if (${column.javaType} == "LocalDateTime")## 时间类型
-    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
-#end
-    private ${column.javaType} ${column.javaField};

+ 0 - 39
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/baseVO.vm

@@ -1,39 +0,0 @@
-package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-#foreach ($column in $columns)
-#if (${column.javaType} == "BigDecimal")
-import java.math.BigDecimal;
-#end
-#if (${column.javaType} == "LocalDateTime")
-import java.time.LocalDateTime;
-#end
-#end
-import javax.validation.constraints.*;
-## 处理 Date 字段的引入
-#foreach ($column in $columns)
-#if (${column.createOperation} && ${column.updateOperation} && ${column.listOperationResult}
-    && ${column.javaType} == "LocalDateTime")## 时间类型
-import org.springframework.format.annotation.DateTimeFormat;
-
-import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-#break
-#end
-#end
-
-/**
- * ${table.classComment} Base VO,提供给添加、修改、详细的子 VO 使用
- * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
- */
-@Data
-public class ${sceneEnum.prefixClass}${table.className}BaseVO {
-
-#foreach ($column in $columns)
-#if (${column.createOperation} && ${column.updateOperation} && ${column.listOperationResult})##通用操作
-    #parse("codegen/java/controller/vo/_column.vm")
-
-#end
-#end
-}

+ 0 - 30
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/createReqVO.vm

@@ -1,30 +0,0 @@
-package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
-
-import lombok.*;
-import java.util.*;
-import io.swagger.v3.oas.annotations.media.Schema;
-import javax.validation.constraints.*;
-## 处理 Date 字段的引入
-#foreach ($column in $columns)
-#if (${column.createOperation} && (!${column.updateOperation} || !${column.listOperationResult})
-    && ${column.javaType} == "LocalDateTime")## 时间类型
-import org.springframework.format.annotation.DateTimeFormat;
-import java.time.LocalDateTime;
-import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-#break
-#end
-#end
-
-@Schema(description = "${sceneEnum.name} - ${table.classComment}创建 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class ${sceneEnum.prefixClass}${table.className}CreateReqVO extends ${sceneEnum.prefixClass}${table.className}BaseVO {
-
-#foreach ($column in $columns)
-#if (${column.createOperation} && (!${column.updateOperation} || !${column.listOperationResult}))##不是通用字段
-    #parse("codegen/java/controller/vo/_column.vm")
-
-#end
-#end
-}

+ 0 - 45
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/excelVO.vm

@@ -1,45 +0,0 @@
-package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-#foreach ($column in $columns)
-#if (${column.javaType} == "BigDecimal")
-import java.math.BigDecimal;
-#end
-#if (${column.javaType} == "LocalDateTime")
-import java.time.LocalDateTime;
-#end
-#end
-
-import com.alibaba.excel.annotation.ExcelProperty;
-#foreach ($column in $columns)
-#if ("$!column.dictType" != "")## 有设置数据字典
-import ${DictFormatClassName};
-import ${DictConvertClassName};
-
-#break
-#end
-#end
-
-/**
- * ${table.classComment} Excel VO
- *
- * @author ${table.author}
- */
-@Data
-public class ${sceneEnum.prefixClass}${table.className}ExcelVO {
-
-#foreach ($column in $columns)
-    #if (${column.listOperationResult})##返回字段
-    #if ("$!column.dictType" != "")##处理枚举值
-    @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
-    @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 XXXDictTypeConstants 枚举类中
-    #else
-    @ExcelProperty("${column.columnComment}")
-    #end
-    private ${column.javaType} ${column.javaField};
-
-    #end
-#end
-}

+ 11 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/exportReqVO.vm → yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/listReqVO.vm

@@ -4,9 +4,15 @@ import lombok.*;
 import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import ${PageParamClassName};
-## 处理 Date 字段的引入
 #foreach ($column in $columns)
-#if (${column.listOperation} && ${column.javaType} == "LocalDateTime")## 时间类型
+#if (${column.javaType} == "BigDecimal")
+import java.math.BigDecimal;
+#break
+#end
+#end
+## 处理 LocalDateTime 字段的引入
+#foreach ($column in $columns)
+#if (${column.listOperation} && ${column.javaType} == "LocalDateTime")
 import java.time.LocalDateTime;
 import org.springframework.format.annotation.DateTimeFormat;
 
@@ -20,9 +26,9 @@ import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
     private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end;
 #end
 
-@Schema(description = "${sceneEnum.name} - ${table.classComment} Excel 导出 Request VO,参数和 ${table.className}PageReqVO 是一致的")
+@Schema(description = "${sceneEnum.name} - ${table.classComment}列表 Request VO")
 @Data
-public class ${sceneEnum.prefixClass}${table.className}ExportReqVO {
+public class ${sceneEnum.prefixClass}${table.className}ListReqVO {
 
 #foreach ($column in $columns)
 #if (${column.listOperation})##查询操作
@@ -36,4 +42,4 @@ public class ${sceneEnum.prefixClass}${table.className}ExportReqVO {
 
 #end
 #end
-}
+}

+ 9 - 3
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/pageReqVO.vm

@@ -4,9 +4,15 @@ import lombok.*;
 import java.util.*;
 import io.swagger.v3.oas.annotations.media.Schema;
 import ${PageParamClassName};
-## 处理 Date 字段的引入
 #foreach ($column in $columns)
-#if (${column.listOperation} && ${column.javaType} == "LocalDateTime")## 时间类型
+#if (${column.javaType} == "BigDecimal")
+import java.math.BigDecimal;
+#break
+#end
+#end
+## 处理 LocalDateTime 字段的引入
+#foreach ($column in $columns)
+#if (${column.listOperationCondition} && ${column.javaType} == "LocalDateTime")
 import org.springframework.format.annotation.DateTimeFormat;
 import java.time.LocalDateTime;
 
@@ -38,4 +44,4 @@ public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PagePar
 
 #end
 #end
-}
+}

+ 35 - 6
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/respVO.vm

@@ -2,24 +2,53 @@ package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePac
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.*;
+import java.util.*;
+## 处理 BigDecimal 字段的引入
+import java.util.*;
 #foreach ($column in $columns)
-#if (${column.javaType} == "LocalDateTime")
+#if (${column.javaType} == "BigDecimal")
+import java.math.BigDecimal;
+#break
+#end
+#end
+## 处理 LocalDateTime 字段的引入
+#foreach ($column in $columns)
+#if (${column.listOperationResult} && ${column.javaType} == "LocalDateTime")
+import org.springframework.format.annotation.DateTimeFormat;
 import java.time.LocalDateTime;
 #break
 #end
 #end
+## 处理 Excel 导出
+import com.alibaba.excel.annotation.*;
+#foreach ($column in $columns)
+#if ("$!column.dictType" != "")## 有设置数据字典
+import ${DictFormatClassName};
+import ${DictConvertClassName};
+#break
+#end
+#end
 
 @Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO")
 @Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class ${sceneEnum.prefixClass}${table.className}RespVO extends ${sceneEnum.prefixClass}${table.className}BaseVO {
+@ExcelIgnoreUnannotated
+public class ${sceneEnum.prefixClass}${table.className}RespVO {
 
+## 逐个处理字段
 #foreach ($column in $columns)
-#if (${column.listOperationResult} && (!${column.createOperation} || !${column.updateOperation}))##不是通用字段
+#if (${column.listOperationResult})
+## 1. 处理 Swagger 注解
     @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end)
+## 2. 处理 Excel 导出
+#if ("$!column.dictType" != "")##处理枚举值
+    @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class)
+    @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中
+#else
+    @ExcelProperty("${column.columnComment}")
+#end
+## 3. 处理字段定义
     private ${column.javaType} ${column.javaField};
 
 #end
 #end
-}
+}

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

@@ -0,0 +1,65 @@
+package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+## 处理 BigDecimal 字段的引入
+import java.util.*;
+#foreach ($column in $columns)
+#if (${column.javaType} == "BigDecimal")
+import java.math.BigDecimal;
+#break
+#end
+#end
+## 处理 LocalDateTime 字段的引入
+#foreach ($column in $columns)
+#if ((${column.createOperation} || ${column.updateOperation}) && ${column.javaType} == "LocalDateTime")
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+#break
+#end
+#end
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO;
+#end
+
+@Schema(description = "${sceneEnum.name} - ${table.classComment}新增/修改 Request VO")
+@Data
+public class ${sceneEnum.prefixClass}${table.className}SaveReqVO {
+
+## 逐个处理字段
+#foreach ($column in $columns)
+#if (${column.createOperation} || ${column.updateOperation})
+## 1. 处理 Swagger 注解
+    @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end)
+## 2. 处理 Validator 参数校验
+#if (!${column.nullable} && !${column.primaryKey})
+#if (${column.javaType} == 'String')
+    @NotEmpty(message = "${column.columnComment}不能为空")
+#else
+    @NotNull(message = "${column.columnComment}不能为空")
+#end
+#end
+## 3. 处理字段定义
+    private ${column.javaType} ${column.javaField};
+
+#end
+#end
+## 特殊:主子表专属逻辑(非 ERP 模式)
+#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+    #if ( $subTable.subJoinMany)
+    @Schema(description = "${subTable.classComment}列表")
+    private List<${subTable.className}DO> ${subClassNameVars.get($index)}s;
+
+    #else
+    @Schema(description = "${subTable.classComment}")
+    private ${subTable.className}DO ${subClassNameVars.get($index)};
+
+    #end
+#end
+#end
+}

+ 0 - 30
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/controller/vo/updateReqVO.vm

@@ -1,30 +0,0 @@
-package ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import javax.validation.constraints.*;
-## 处理 Date 字段的引入
-#foreach ($column in $columns)
-#if (${column.updateOperation} && (!${column.createOperation} || !${column.listOperationResult})
-    && ${column.javaType} == "LocalDateTime")## 时间类型
-import org.springframework.format.annotation.DateTimeFormat;
-import java.time.LocalDateTime;
-import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
-#break
-#end
-#end
-
-@Schema(description = "${sceneEnum.name} - ${table.classComment}更新 Request VO")
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-public class ${sceneEnum.prefixClass}${table.className}UpdateReqVO extends ${sceneEnum.prefixClass}${table.className}BaseVO {
-
-#foreach ($column in $columns)
-#if (${column.updateOperation} && (!${column.createOperation} || !${column.listOperationResult}))##不是通用字段
-    #parse("codegen/java/controller/vo/_column.vm")
-
-#end
-#end
-}

+ 0 - 34
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/convert/convert.vm

@@ -1,34 +0,0 @@
-package ${basePackage}.module.${table.moduleName}.convert.${table.businessName};
-
-import java.util.*;
-
-import ${PageResultClassName};
-
-import org.mapstruct.Mapper;
-import org.mapstruct.factory.Mappers;
-import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
-import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
-
-/**
- * ${table.classComment} Convert
- *
- * @author ${table.author}
- */
-@Mapper
-public interface ${table.className}Convert {
-
-    ${table.className}Convert INSTANCE = Mappers.getMapper(${table.className}Convert.class);
-
-    ${table.className}DO convert(${sceneEnum.prefixClass}${table.className}CreateReqVO bean);
-
-    ${table.className}DO convert(${sceneEnum.prefixClass}${table.className}UpdateReqVO bean);
-
-    ${sceneEnum.prefixClass}${table.className}RespVO convert(${table.className}DO bean);
-
-    List<${sceneEnum.prefixClass}${table.className}RespVO> convertList(List<${table.className}DO> list);
-
-    PageResult<${sceneEnum.prefixClass}${table.className}RespVO> convertPage(PageResult<${table.className}DO> page);
-
-    List<${sceneEnum.prefixClass}${table.className}ExcelVO> convertList02(List<${table.className}DO> list);
-
-}

+ 6 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do.vm

@@ -28,6 +28,11 @@ import ${BaseDOClassName};
 @AllArgsConstructor
 public class ${table.className}DO extends BaseDO {
 
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+    public static final Long ${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT = 0L;
+
+#end
 #foreach ($column in $columns)
 #if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段
     /**
@@ -44,4 +49,4 @@ public class ${table.className}DO extends BaseDO {
 #end
 #end
 
-}
+}

+ 49 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/do_sub.vm

@@ -0,0 +1,49 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+package ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName};
+
+import lombok.*;
+import java.util.*;
+#foreach ($column in $subColumns)
+#if (${column.javaType} == "BigDecimal")
+import java.math.BigDecimal;
+#end
+#if (${column.javaType} == "LocalDateTime")
+import java.time.LocalDateTime;
+#end
+#end
+import com.baomidou.mybatisplus.annotation.*;
+import ${BaseDOClassName};
+
+/**
+ * ${subTable.classComment} DO
+ *
+ * @author ${subTable.author}
+ */
+@TableName("${subTable.tableName.toLowerCase()}")
+@KeySequence("${subTable.tableName.toLowerCase()}_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ${subTable.className}DO extends BaseDO {
+
+#foreach ($column in $subColumns)
+#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段
+    /**
+     * ${column.columnComment}
+    #if ("$!column.dictType" != "")##处理枚举值
+     *
+     * 枚举 {@link TODO ${column.dictType} 对应的类}
+    #end
+     */
+    #if (${column.primaryKey})##处理主键
+    @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#end
+    #end
+    private ${column.javaType} ${column.javaField};
+#end
+#end
+
+}

+ 19 - 3
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.vm

@@ -49,18 +49,34 @@ import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePack
 @Mapper
 public interface ${table.className}Mapper extends BaseMapperX<${table.className}DO> {
 
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
     default PageResult<${table.className}DO> selectPage(${sceneEnum.prefixClass}${table.className}PageReqVO reqVO) {
         return selectPage(reqVO, new LambdaQueryWrapperX<${table.className}DO>()
 			#listCondition()
                 .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序
 
     }
-
-    default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ExportReqVO reqVO) {
+#else
+    default List<${table.className}DO> selectList(${sceneEnum.prefixClass}${table.className}ListReqVO reqVO) {
         return selectList(new LambdaQueryWrapperX<${table.className}DO>()
 			#listCondition()
                 .orderByDesc(${table.className}DO::getId));## 大多数情况下,id 倒序
 
     }
+#end
+
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写
+#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写
+	default ${table.className}DO selectBy${TreeParentJavaField}And${TreeNameJavaField}(Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) {
+	    return selectOne(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField}, ${table.className}DO::get${TreeNameJavaField}, ${treeNameColumn.javaField});
+	}
+
+    default Long selectCountBy${TreeParentJavaField}(${treeParentColumn.javaType} ${treeParentColumn.javaField}) {
+        return selectCount(${table.className}DO::get${TreeParentJavaField}, ${treeParentColumn.javaField});
+    }
 
-}
+#end
+}

+ 1 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper.xml.vm

@@ -9,4 +9,4 @@
         文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
      -->
 
-</mapper>
+</mapper>

+ 51 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/dal/mapper_sub.vm

@@ -0,0 +1,51 @@
+#set ($subTable = $subTables.get($subIndex))##当前表
+#set ($subColumns = $subJoinColumnsList.get($subIndex))##当前字段数组
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+package ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName};
+
+import java.util.*;
+
+import ${PageResultClassName};
+import ${PageParamClassName};
+import ${QueryWrapperClassName};
+import ${BaseMapperClassName};
+import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ${subTable.classComment} Mapper
+ *
+ * @author ${subTable.author}
+ */
+@Mapper
+public interface ${subTable.className}Mapper extends BaseMapperX<${subTable.className}DO> {
+
+## 情况一:MASTER_ERP 时,需要分查询页子表
+#if ( $table.templateType == 11 )
+    default PageResult<${subTable.className}DO> selectPage(PageParam reqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<${subTable.className}DO>()
+            .eq(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField})
+            .orderByDesc(${subTable.className}DO::getId));## 大多数情况下,id 倒序
+
+    }
+
+## 情况二:非 MASTER_ERP 时,需要列表查询子表
+#else
+    #if ( $subTable.subJoinMany)
+    default List<${subTable.className}DO> selectListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return selectList(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField});
+    }
+
+    #else
+    default ${subTable.className}DO selectBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return selectOne(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField});
+    }
+
+    #end
+    #end
+    default int deleteBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return delete(${subTable.className}DO::get${SubJoinColumnName}, ${subJoinColumn.javaField});
+    }
+
+}

+ 19 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/enums/errorcode.vm

@@ -1,3 +1,22 @@
 // TODO 待办:请将下面的错误码复制到 yudao-module-${table.moduleName}-api 模块的 ErrorCodeConstants 类中。注意,请给“TODO 补充编号”设置一个错误码编号!!!
 // ========== ${table.classComment} TODO 补充编号 ==========
 ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${table.classComment}不存在");
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN = new ErrorCode(TODO 补充编号, "存在存在子${table.classComment},无法删除");
+ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS = new ErrorCode(TODO 补充编号,"父级${table.classComment}不存在");
+ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR = new ErrorCode(TODO 补充编号, "不能设置自己为父${table.classComment}");
+ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE = new ErrorCode(TODO 补充编号, "已经存在该${treeNameColumn.columnComment}的${table.classComment}");
+ErrorCode ${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD = new ErrorCode(TODO 补充编号, "不能设置自己的子${table.className}为父${table.className}");
+#end
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 )## 特殊:ERP 情况
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+#set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.get($index))
+ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}不存在");
+#if ( !$subTable.subJoinMany )
+ErrorCode ${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS = new ErrorCode(TODO 补充编号, "${subTable.classComment}已存在");
+#end
+#end
+#end

+ 89 - 12
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/service.vm

@@ -4,7 +4,12 @@ import java.util.*;
 import javax.validation.*;
 import ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
 import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO;
+#end
 import ${PageResultClassName};
+import ${PageParamClassName};
 
 /**
  * ${table.classComment} Service 接口
@@ -19,14 +24,14 @@ public interface ${table.className}Service {
      * @param createReqVO 创建信息
      * @return 编号
      */
-    ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO);
+    ${primaryColumn.javaType} create${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO);
 
     /**
      * 更新${table.classComment}
      *
      * @param updateReqVO 更新信息
      */
-    void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO);
+    void update${simpleClassName}(@Valid ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO);
 
     /**
      * 删除${table.classComment}
@@ -43,28 +48,100 @@ public interface ${table.className}Service {
      */
     ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id);
 
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
+    /**
+     * 获得${table.classComment}分页
+     *
+     * @param pageReqVO 分页查询
+     * @return ${table.classComment}分页
+     */
+    PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO);
+#else
     /**
      * 获得${table.classComment}列表
      *
-     * @param ids 编号
+     * @param listReqVO 查询条件
      * @return ${table.classComment}列表
      */
-    List<${table.className}DO> get${simpleClassName}List(Collection<${primaryColumn.javaType}> ids);
+    List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO);
+#end
+
+## 特殊:主子表专属逻辑
+#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 ($subClassNameVar = $subClassNameVars.get($index))
+    // ==================== 子表($subTable.classComment) ====================
 
+## 情况一:MASTER_ERP 时,需要分查询页子表
+#if ( $table.templateType == 11 )
     /**
-     * 获得${table.classComment}分页
+     * 获得${subTable.classComment}分页
      *
      * @param pageReqVO 分页查询
-     * @return ${table.classComment}分页
+     * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment}
+     * @return ${subTable.classComment}分页
      */
-    PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO);
+    PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField});
 
+## 情况二:非 MASTER_ERP 时,需要列表查询子表
+#else
+    #if ( $subTable.subJoinMany )
     /**
-     * 获得${table.classComment}列表, 用于 Excel 导出
+     * 获得${subTable.classComment}列表
      *
-     * @param exportReqVO 查询条件
-     * @return ${table.classComment}列表
+     * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment}
+     * @return ${subTable.classComment}列表
+     */
+    List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField});
+
+    #else
+    /**
+     * 获得${subTable.classComment}
+     *
+     * @param ${subJoinColumn.javaField} ${subJoinColumn.columnComment}
+     * @return ${subTable.classComment}
      */
-    List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO);
+    ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField});
+
+    #end
+#end
+## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
+#if ( $table.templateType == 11 )
+    /**
+     * 创建${subTable.classComment}
+     *
+     * @param ${subClassNameVar} 创建信息
+     * @return 编号
+     */
+    ${subPrimaryColumn.javaType} create${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar});
+
+    /**
+     * 更新${subTable.classComment}
+     *
+     * @param ${subClassNameVar} 更新信息
+     */
+    void update${subSimpleClassName}(@Valid ${subTable.className}DO ${subClassNameVar});
+
+    /**
+     * 删除${subTable.classComment}
+     *
+     * @param id 编号
+     */
+    void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id);
+
+	/**
+	 * 获得${subTable.classComment}
+	 *
+	 * @param id 编号
+     * @return ${subTable.classComment}
+	 */
+    ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id);
 
-}
+#end
+#end
+}

+ 279 - 17
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/service/serviceImpl.vm

@@ -3,21 +3,29 @@ package ${basePackage}.module.${table.moduleName}.service.${table.businessName};
 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 ${basePackage}.module.${table.moduleName}.controller.${sceneEnum.basePackage}.${table.businessName}.vo.*;
 import ${basePackage}.module.${table.moduleName}.dal.dataobject.${table.businessName}.${table.className}DO;
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+import ${basePackage}.module.${subTable.moduleName}.dal.dataobject.${subTable.businessName}.${subTable.className}DO;
+#end
 import ${PageResultClassName};
+import ${PageParamClassName};
+import ${BeanUtils};
 
-import ${basePackage}.module.${table.moduleName}.convert.${table.businessName}.${table.className}Convert;
 import ${basePackage}.module.${table.moduleName}.dal.mysql.${table.businessName}.${table.className}Mapper;
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+import ${basePackage}.module.${subTable.moduleName}.dal.mysql.${subTable.businessName}.${subTable.className}Mapper;
+#end
 
 import static ${ServiceExceptionUtilClassName}.exception;
 import static ${basePackage}.module.${table.moduleName}.enums.ErrorCodeConstants.*;
 
-import cn.hutool.core.collection.CollUtil;
-import cn.hutool.core.collection.ListUtil;
-
 /**
  * ${table.classComment} Service 实现类
  *
@@ -29,31 +37,121 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
 
     @Resource
     private ${table.className}Mapper ${classNameVar}Mapper;
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+    @Resource
+    private ${subTable.className}Mapper ${subClassNameVars.get($index)}Mapper;
+#end
 
     @Override
-    public ${primaryColumn.javaType} create${simpleClassName}(${sceneEnum.prefixClass}${table.className}CreateReqVO createReqVO) {
+## 特殊:主子表专属逻辑(非 ERP 模式)
+#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
+    @Transactional(rollbackFor = Exception.class)
+#end
+    public ${primaryColumn.javaType} create${simpleClassName}(${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO) {
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写
+#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写
+        // 校验${treeParentColumn.columnComment}的有效性
+        validateParent${simpleClassName}(null, createReqVO.get${TreeParentJavaField}());
+        // 校验${treeNameColumn.columnComment}的唯一性
+        validate${simpleClassName}${TreeNameJavaField}Unique(null, createReqVO.get${TreeParentJavaField}(), createReqVO.get${TreeNameJavaField}());
+
+#end
         // 插入
-        ${table.className}DO ${classNameVar} = ${table.className}Convert.INSTANCE.convert(createReqVO);
+        ${table.className}DO ${classNameVar} = BeanUtils.toBean(createReqVO, ${table.className}DO.class);
         ${classNameVar}Mapper.insert(${classNameVar});
+## 特殊:主子表专属逻辑(非 ERP 模式)
+#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
+
+        // 插入子表
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+#set ($subSimpleClassName = $subSimpleClassNames.get($index))
+#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+    #if ( $subTable.subJoinMany)
+        create${subSimpleClassName}List(${classNameVar}.getId(), createReqVO.get${subSimpleClassNames.get($index)}s());
+    #else
+        create${subSimpleClassName}(${classNameVar}.getId(), createReqVO.get${subSimpleClassNames.get($index)}());
+    #end
+#end
+#end
         // 返回
         return ${classNameVar}.getId();
     }
 
     @Override
-    public void update${simpleClassName}(${sceneEnum.prefixClass}${table.className}UpdateReqVO updateReqVO) {
+## 特殊:主子表专属逻辑(非 ERP 模式)
+#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11 )
+    @Transactional(rollbackFor = Exception.class)
+#end
+    public void update${simpleClassName}(${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO) {
         // 校验存在
         validate${simpleClassName}Exists(updateReqVO.getId());
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写
+#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写
+        // 校验${treeParentColumn.columnComment}的有效性
+        validateParent${simpleClassName}(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}());
+        // 校验${treeNameColumn.columnComment}的唯一性
+        validate${simpleClassName}${TreeNameJavaField}Unique(updateReqVO.getId(), updateReqVO.get${TreeParentJavaField}(), updateReqVO.get${TreeNameJavaField}());
+
+#end
         // 更新
-        ${table.className}DO updateObj = ${table.className}Convert.INSTANCE.convert(updateReqVO);
+        ${table.className}DO updateObj = BeanUtils.toBean(updateReqVO, ${table.className}DO.class);
         ${classNameVar}Mapper.updateById(updateObj);
+## 特殊:主子表专属逻辑(非 ERP 模式)
+#if ( $subTables && $subTables.size() > 0 && $table.templateType != 11)
+
+        // 更新子表
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+#set ($subSimpleClassName = $subSimpleClassNames.get($index))
+#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+    #if ( $subTable.subJoinMany)
+        update${subSimpleClassName}List(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}s());
+    #else
+        update${subSimpleClassName}(updateReqVO.getId(), updateReqVO.get${subSimpleClassNames.get($index)}());
+    #end
+#end
+#end
     }
 
     @Override
+## 特殊:主子表专属逻辑
+#if ( $subTables && $subTables.size() > 0)
+    @Transactional(rollbackFor = Exception.class)
+#end
     public void delete${simpleClassName}(${primaryColumn.javaType} id) {
         // 校验存在
         validate${simpleClassName}Exists(id);
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+#set ($ParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写
+        // 校验是否有子${table.classComment}
+        if (${classNameVar}Mapper.selectCountBy${ParentJavaField}(id) > 0) {
+            throw exception(${simpleClassName_underlineCase.toUpperCase()}_EXITS_CHILDREN);
+        }
+#end
         // 删除
         ${classNameVar}Mapper.deleteById(id);
+## 特殊:主子表专属逻辑
+#if ( $subTables && $subTables.size() > 0)
+
+        // 删除子表
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+#set ($subSimpleClassName = $subSimpleClassNames.get($index))
+#set ($subJoinColumn = $subJoinColumns.get($index))##当前 join 字段
+#set ($SubJoinColumnName = $subJoinColumn.javaField.substring(0,1).toUpperCase() + ${subJoinColumn.javaField.substring(1)})##首字母大写
+        delete${subSimpleClassName}By${SubJoinColumnName}(id);
+#end
+#end
     }
 
     private void validate${simpleClassName}Exists(${primaryColumn.javaType} id) {
@@ -62,27 +160,191 @@ public class ${table.className}ServiceImpl implements ${table.className}Service
         }
     }
 
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+#set ($TreeParentJavaField = $treeParentColumn.javaField.substring(0,1).toUpperCase() + ${treeParentColumn.javaField.substring(1)})##首字母大写
+#set ($TreeNameJavaField = $treeNameColumn.javaField.substring(0,1).toUpperCase() + ${treeNameColumn.javaField.substring(1)})##首字母大写
+    private void validateParent${simpleClassName}(Long id, Long ${treeParentColumn.javaField}) {
+        if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) {
+            return;
+        }
+        // 1. 不能设置自己为父${table.classComment}
+        if (Objects.equals(id, ${treeParentColumn.javaField})) {
+            throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_ERROR);
+        }
+        // 2. 父${table.classComment}不存在
+        ${simpleClassName}DO parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField});
+        if (parent${simpleClassName} == null) {
+            throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_NOT_EXITS);
+        }
+        // 3. 递归校验父${table.classComment},如果父${table.classComment}是自己的子${table.classComment},则报错,避免形成环路
+        if (id == null) { // id 为空,说明新增,不需要考虑环路
+            return;
+        }
+        for (int i = 0; i < Short.MAX_VALUE; i++) {
+            // 3.1 校验环路
+            ${treeParentColumn.javaField} = parent${simpleClassName}.get${TreeParentJavaField}();
+            if (Objects.equals(id, ${treeParentColumn.javaField})) {
+                throw exception(${simpleClassName_underlineCase.toUpperCase()}_PARENT_IS_CHILD);
+            }
+            // 3.2 继续递归下一级父${table.classComment}
+            if (${treeParentColumn.javaField} == null || ${simpleClassName}DO.${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT.equals(${treeParentColumn.javaField})) {
+                break;
+            }
+            parent${simpleClassName} = ${classNameVar}Mapper.selectById(${treeParentColumn.javaField});
+            if (parent${simpleClassName} == null) {
+                break;
+            }
+        }
+    }
+
+    private void validate${simpleClassName}${TreeNameJavaField}Unique(Long id, Long ${treeParentColumn.javaField}, String ${treeNameColumn.javaField}) {
+        ${simpleClassName}DO ${classNameVar} = ${classNameVar}Mapper.selectBy${TreeParentJavaField}And${TreeNameJavaField}(${treeParentColumn.javaField}, ${treeNameColumn.javaField});
+        if (${classNameVar} == null) {
+            return;
+        }
+        // 如果 id 为空,说明不用比较是否为相同 id 的${table.classComment}
+        if (id == null) {
+            throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE);
+        }
+        if (!Objects.equals(${classNameVar}.getId(), id)) {
+            throw exception(${simpleClassName_underlineCase.toUpperCase()}_${treeNameColumn_javaField_underlineCase.toUpperCase()}_DUPLICATE);
+        }
+    }
+
+#end
     @Override
     public ${table.className}DO get${simpleClassName}(${primaryColumn.javaType} id) {
         return ${classNameVar}Mapper.selectById(id);
     }
 
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
+    @Override
+    public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) {
+        return ${classNameVar}Mapper.selectPage(pageReqVO);
+    }
+#else
+    @Override
+    public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ListReqVO listReqVO) {
+        return ${classNameVar}Mapper.selectList(listReqVO);
+    }
+#end
+
+## 特殊:主子表专属逻辑
+#foreach ($subTable in $subTables)
+#set ($index = $foreach.count - 1)
+#set ($subSimpleClassName = $subSimpleClassNames.get($index))
+#set ($simpleClassNameUnderlineCase = $simpleClassNameUnderlineCases.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 ($subClassNameVar = $subClassNameVars.get($index))
+    // ==================== 子表($subTable.classComment) ====================
+
+## 情况一:MASTER_ERP 时,需要分查询页子表
+#if ( $table.templateType == 11 )
+    @Override
+    public PageResult<${subTable.className}DO> get${subSimpleClassName}Page(PageParam pageReqVO, ${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return ${subClassNameVars.get($index)}Mapper.selectPage(pageReqVO, ${subJoinColumn.javaField});
+    }
+
+## 情况二:非 MASTER_ERP 时,需要列表查询子表
+#else
+    #if ( $subTable.subJoinMany )
     @Override
-    public List<${table.className}DO> get${simpleClassName}List(Collection<${primaryColumn.javaType}> ids) {
-        if (CollUtil.isEmpty(ids)) {
-            return ListUtil.empty();
+    public List<${subTable.className}DO> get${subSimpleClassName}ListBy${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return ${subClassNameVars.get($index)}Mapper.selectListBy${SubJoinColumnName}(${subJoinColumn.javaField});
+    }
+
+    #else
+    @Override
+    public ${subTable.className}DO get${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaType} ${subJoinColumn.javaField}) {
+        return ${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subJoinColumn.javaField});
+    }
+
+    #end
+#end
+## 情况一:MASTER_ERP 时,支持单个的新增、修改、删除操作
+#if ( $table.templateType == 11 )
+    @Override
+    public ${subPrimaryColumn.javaType} create${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) {
+## 特殊:一对一时,需要保证只有一条,不能重复插入
+#if ( !$subTable.subJoinMany)
+        // 校验是否已经存在
+        if (${subClassNameVars.get($index)}Mapper.selectBy${SubJoinColumnName}(${subClassNameVar}.get${SubJoinColumnName}()) != null) {
+            throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_EXISTS);
         }
-        return ${classNameVar}Mapper.selectBatchIds(ids);
+        // 插入
+#end
+        ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar});
+        return ${subClassNameVar}.getId();
     }
 
     @Override
-    public PageResult<${table.className}DO> get${simpleClassName}Page(${sceneEnum.prefixClass}${table.className}PageReqVO pageReqVO) {
-        return ${classNameVar}Mapper.selectPage(pageReqVO);
+    public void update${subSimpleClassName}(${subTable.className}DO ${subClassNameVar}) {
+        // 校验存在
+        validate${subSimpleClassName}Exists(${subClassNameVar}.getId());
+        // 更新
+        ${subClassNameVars.get($index)}Mapper.updateById(${subClassNameVar});
+    }
+
+    @Override
+    public void delete${subSimpleClassName}(${subPrimaryColumn.javaType} id) {
+        // 校验存在
+        validate${subSimpleClassName}Exists(id);
+        // 删除
+        ${subClassNameVars.get($index)}Mapper.deleteById(id);
     }
 
     @Override
-    public List<${table.className}DO> get${simpleClassName}List(${sceneEnum.prefixClass}${table.className}ExportReqVO exportReqVO) {
-        return ${classNameVar}Mapper.selectList(exportReqVO);
+    public ${subTable.className}DO get${subSimpleClassName}(${subPrimaryColumn.javaType} id) {
+        return ${subClassNameVars.get($index)}Mapper.selectById(id);
+    }
+
+    private void validate${subSimpleClassName}Exists(${subPrimaryColumn.javaType} id) {
+        if (${subClassNameVar}Mapper.selectById(id) == null) {
+            throw exception(${simpleClassNameUnderlineCase.toUpperCase()}_NOT_EXISTS);
+        }
+    }
+
+## 情况二:非 MASTER_ERP 时,支持批量的新增、修改操作
+#else
+    #if ( $subTable.subJoinMany)
+    private void create${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
+        list.forEach(o -> o.set$SubJoinColumnName(${subJoinColumn.javaField}));
+        ${subClassNameVars.get($index)}Mapper.insertBatch(list);
+    }
+
+    private void update${subSimpleClassName}List(${primaryColumn.javaType} ${subJoinColumn.javaField}, List<${subTable.className}DO> list) {
+        delete${subSimpleClassName}By${SubJoinColumnName}(${subJoinColumn.javaField});
+		list.forEach(o -> o.setId(null).setUpdater(null).setUpdateTime(null)); // 解决更新情况下:1)id 冲突;2)updateTime 不更新
+        create${subSimpleClassName}List(${subJoinColumn.javaField}, list);
+    }
+
+    #else
+    private void create${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) {
+        if (${subClassNameVar} == null) {
+            return;
+        }
+        ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField});
+        ${subClassNameVars.get($index)}Mapper.insert(${subClassNameVar});
+    }
+
+    private void update${subSimpleClassName}(${primaryColumn.javaType} ${subJoinColumn.javaField}, ${subTable.className}DO ${subClassNameVar}) {
+        if (${subClassNameVar} == null) {
+			return;
+        }
+        ${subClassNameVar}.set$SubJoinColumnName(${subJoinColumn.javaField});
+        ${subClassNameVar}.setUpdater(null).setUpdateTime(null); // 解决更新情况下:updateTime 不更新
+        ${subClassNameVars.get($index)}Mapper.insertOrUpdate(${subClassNameVar});
+    }
+
+    #end
+#end
+    private void delete${subSimpleClassName}By${SubJoinColumnName}(${primaryColumn.javaType} ${subJoinColumn.javaField}) {
+        ${subClassNameVars.get($index)}Mapper.deleteBy${SubJoinColumnName}(${subJoinColumn.javaField});
     }
 
-}
+#end
+}

+ 15 - 12
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/java/test/serviceTest.vm

@@ -77,15 +77,15 @@ public class ${table.className}ServiceImplTest extends BaseDbUnitTest {
     @Test
     public void testCreate${simpleClassName}_success() {
         // 准备参数
-        ${sceneEnum.prefixClass}${table.className}CreateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}CreateReqVO.class);
+        ${sceneEnum.prefixClass}${table.className}SaveReqVO createReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class).setId(null);
 
         // 调用
-        ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(reqVO);
+        ${primaryColumn.javaType} ${classNameVar}Id = ${classNameVar}Service.create${simpleClassName}(createReqVO);
         // 断言
         assertNotNull(${classNameVar}Id);
         // 校验记录的属性是否正确
         ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(${classNameVar}Id);
-        assertPojoEquals(reqVO, ${classNameVar});
+        assertPojoEquals(createReqVO, ${classNameVar}, "id");
     }
 
     @Test
@@ -94,24 +94,24 @@ public class ${table.className}ServiceImplTest extends BaseDbUnitTest {
         ${table.className}DO db${simpleClassName} = randomPojo(${table.className}DO.class);
         ${classNameVar}Mapper.insert(db${simpleClassName});// @Sql: 先插入出一条存在的数据
         // 准备参数
-        ${sceneEnum.prefixClass}${table.className}UpdateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}UpdateReqVO.class, o -> {
+        ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class, o -> {
             o.setId(db${simpleClassName}.getId()); // 设置更新的 ID
         });
 
         // 调用
-        ${classNameVar}Service.update${simpleClassName}(reqVO);
+        ${classNameVar}Service.update${simpleClassName}(updateReqVO);
         // 校验是否更新正确
-        ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(reqVO.getId()); // 获取最新的
-        assertPojoEquals(reqVO, ${classNameVar});
+        ${table.className}DO ${classNameVar} = ${classNameVar}Mapper.selectById(updateReqVO.getId()); // 获取最新的
+        assertPojoEquals(updateReqVO, ${classNameVar});
     }
 
     @Test
     public void testUpdate${simpleClassName}_notExists() {
         // 准备参数
-        ${sceneEnum.prefixClass}${table.className}UpdateReqVO reqVO = randomPojo(${sceneEnum.prefixClass}${table.className}UpdateReqVO.class);
+        ${sceneEnum.prefixClass}${table.className}SaveReqVO updateReqVO = randomPojo(${sceneEnum.prefixClass}${table.className}SaveReqVO.class);
 
         // 调用, 并断言异常
-        assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(reqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS);
+        assertServiceException(() -> ${classNameVar}Service.update${simpleClassName}(updateReqVO), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS);
     }
 
     @Test
@@ -137,6 +137,8 @@ public class ${table.className}ServiceImplTest extends BaseDbUnitTest {
         assertServiceException(() -> ${classNameVar}Service.delete${simpleClassName}(id), ${simpleClassName_underlineCase.toUpperCase()}_NOT_EXISTS);
     }
 
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
     @Test
     @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
     public void testGet${simpleClassName}Page() {
@@ -149,11 +151,11 @@ public class ${table.className}ServiceImplTest extends BaseDbUnitTest {
        assertEquals(1, pageResult.getList().size());
        assertPojoEquals(db${simpleClassName}, pageResult.getList().get(0));
     }
-
+#else
     @Test
     @Disabled  // TODO 请修改 null 为需要的值,然后删除 @Disabled 注解
     public void testGet${simpleClassName}List() {
-       #getPageCondition("ExportReqVO")
+       #getPageCondition("ListReqVO")
 
        // 调用
        List<${table.className}DO> list = ${classNameVar}Service.get${simpleClassName}List(reqVO);
@@ -161,5 +163,6 @@ public class ${table.className}ServiceImplTest extends BaseDbUnitTest {
        assertEquals(1, list.size());
        assertPojoEquals(db${simpleClassName}, list.get(0));
     }
+#end
 
-}
+}

+ 3 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/h2.vm

@@ -23,6 +23,8 @@ CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" (
     "${column.columnName}" ${dataType} DEFAULT '',
     #elseif (${column.columnName} == 'deleted')
     "deleted" bit NOT NULL DEFAULT FALSE,
+    #elseif (${column.columnName} == 'tenantId')
+    "tenant_id" bigint NOT NULL DEFAULT 0,
     #else
     "${column.columnName.toLowerCase()}" ${dataType}#if (${column.nullable} == false) NOT NULL#end,
     #end
@@ -32,4 +34,4 @@ CREATE TABLE IF NOT EXISTS "${table.tableName.toLowerCase()}" (
 ) COMMENT '${table.tableComment}';
 
 -- 将该删表 SQL 语句,添加到 yudao-module-${table.moduleName}-biz 模块的 test/resources/sql/clean.sql 文件里
-DELETE FROM "${table.tableName}";
+DELETE FROM "${table.tableName}";

+ 2 - 2
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/sql/sql.vm

@@ -5,7 +5,7 @@ INSERT INTO system_menu(
 )
 VALUES (
     '${table.classComment}管理', '', 2, 0, ${table.parentMenuId},
-    '${simpleClassName_strikeCase}', '', '${table.moduleName}/${classNameVar}/index', 0, '${table.className}'
+    '${simpleClassName_strikeCase}', '', '${table.moduleName}/${table.businessName}/index', 0, '${table.className}'
 );
 
 -- 按钮父菜单ID
@@ -25,4 +25,4 @@ VALUES (
     '${table.classComment}${functionName}', '${permissionPrefix}:${functionOps.get($index)}', 3, $foreach.count, @parentId,
     '', '', '', 0
 );
-#end
+#end

+ 66 - 1
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/api/api.ts.vm

@@ -15,10 +15,17 @@ export interface ${simpleClassName}VO {
 #end
 }
 
-// 查询${table.classComment}列表
+#if ( $table.templateType != 2 )
+// 查询${table.classComment}分页
 export const get${simpleClassName}Page = async (params) => {
   return await request.get({ url: `${baseURL}/page`, params })
 }
+#else
+// 查询${table.classComment}列表
+export const get${simpleClassName}List = async (params) => {
+  return await request.get({ url: `${baseURL}/list`, params })
+}
+#end
 
 // 查询${table.classComment}详情
 export const get${simpleClassName} = async (id: number) => {
@@ -44,3 +51,61 @@ export const delete${simpleClassName} = async (id: number) => {
 export const export${simpleClassName} = async (params) => {
   return await request.download({ url: `${baseURL}/export-excel`, params })
 }
+## 特殊:主子表专属逻辑
+#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 const get${subSimpleClassName}Page = async (params) => {
+  return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/page`, params })
+}
+## 情况二:非 MASTER_ERP 时,需要列表查询子表
+#else
+  #if ( $subTable.subJoinMany )
+
+// 获得${subTable.classComment}列表
+export const get${subSimpleClassName}ListBy${SubJoinColumnName} = async (${subJoinColumn.javaField}) => {
+  return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/list-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
+}
+  #else
+
+// 获得${subTable.classComment}
+export const get${subSimpleClassName}By${SubJoinColumnName} = async (${subJoinColumn.javaField}) => {
+  return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get-by-${subJoinColumn_strikeCase}?${subJoinColumn.javaField}=` + ${subJoinColumn.javaField} })
+}
+  #end
+#end
+## 特殊:MASTER_ERP 时,支持单个的新增、修改、删除操作
+#if ( $table.templateType == 11 )
+// 新增${subTable.classComment}
+export const create${subSimpleClassName} = async (data) => {
+  return await request.post({ url: `${baseURL}/${subSimpleClassName_strikeCase}/create`, data })
+}
+
+// 修改${subTable.classComment}
+export const update${subSimpleClassName} = async (data) => {
+  return await request.put({ url: `${baseURL}/${subSimpleClassName_strikeCase}/update`, data })
+}
+
+// 删除${subTable.classComment}
+export const delete${subSimpleClassName} = async (id: number) => {
+  return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id })
+}
+
+// 获得${subTable.classComment}
+export const get${subSimpleClassName} = async (id: number) => {
+  return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id })
+}
+#end
+#end

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

@@ -0,0 +1,205 @@
+#set ($subColumns = $subColumnsList.get($subIndex))##当前字段数组
+#set ($subSimpleClassName = $subSimpleClassNames.get($subIndex))
+#set ($subJoinColumn = $subJoinColumns.get($subIndex))##当前 join 字段
+<template>
+  <Dialog :title="dialogTitle" v-model="dialogVisible">
+    <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)
+        #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+        #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+            #set ($dictMethod = "getIntDictOptions")
+        #elseif ($javaType == "String")
+            #set ($dictMethod = "getStrDictOptions")
+        #elseif ($javaType == "Boolean")
+            #set ($dictMethod = "getBoolDictOptions")
+        #end
+        #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")## 图片上传
+      <el-form-item label="${comment}" prop="${javaField}">
+        <UploadImg v-model="formData.${javaField}" />
+      </el-form-item>
+        #elseif($column.htmlType == "fileUpload")## 文件上传
+      <el-form-item label="${comment}" prop="${javaField}">
+        <UploadFile v-model="formData.${javaField}" />
+      </el-form-item>
+        #elseif($column.htmlType == "editor")## 文本编辑器
+      <el-form-item label="${comment}" prop="${javaField}">
+        <Editor v-model="formData.${javaField}" height="150px" />
+      </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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
+                #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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ 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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+            :key="dict.value"
+            :label="dict.value"
+          >
+            {{ 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
+          v-model="formData.${javaField}"
+          type="date"
+          value-format="x"
+          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>
+    <template #footer>
+      <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
+      <el-button @click="dialogVisible = false">取 消</el-button>
+    </template>
+  </Dialog>
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const dialogVisible = ref(false) // 弹窗的是否展示
+const dialogTitle = ref('') // 弹窗的标题
+const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+const formType = ref('') // 表单的类型:create - 新增;update - 修改
+const formData = ref({
+#foreach ($column in $subColumns)
+    #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+  $column.javaField: [],
+      #else
+  $column.javaField: undefined,
+      #end
+    #end
+#end
+})
+const formRules = reactive({
+#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
+})
+const formRef = ref() // 表单 Ref
+
+/** 打开弹窗 */
+const open = async (type: string, id?: number, ${subJoinColumn.javaField}: number) => {
+  dialogVisible.value = true
+  dialogTitle.value = t('action.' + type)
+  formType.value = type
+  resetForm()
+  formData.value.${subJoinColumn.javaField} = ${subJoinColumn.javaField}
+  // 修改时,设置数据
+  if (id) {
+    formLoading.value = true
+    try {
+      formData.value = await ${simpleClassName}Api.get${subSimpleClassName}(id)
+    } finally {
+      formLoading.value = false
+    }
+  }
+}
+defineExpose({ open }) // 提供 open 方法,用于打开弹窗
+
+/** 提交表单 */
+const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
+const submitForm = async () => {
+  // 校验表单
+  await formRef.value.validate()
+  // 提交请求
+  formLoading.value = true
+  try {
+    const data = formData.value
+    if (formType.value === 'create') {
+      await ${simpleClassName}Api.create${subSimpleClassName}(data)
+      message.success(t('common.createSuccess'))
+    } else {
+      await ${simpleClassName}Api.update${subSimpleClassName}(data)
+      message.success(t('common.updateSuccess'))
+    }
+    dialogVisible.value = false
+    // 发送操作成功的事件
+    emit('success')
+  } finally {
+    formLoading.value = false
+  }
+}
+
+/** 重置表单 */
+const resetForm = () => {
+  formData.value = {
+#foreach ($column in $subColumns)
+  #if ($column.createOperation || $column.updateOperation)
+      #if ($column.htmlType == "checkbox")
+    $column.javaField: [],
+      #else
+    $column.javaField: undefined,
+      #end
+  #end
+#end
+  }
+  formRef.value?.resetFields()
+}
+</script>

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

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

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

@@ -0,0 +1,362 @@
+#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>
+#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)
+        #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+        #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+            #set ($dictMethod = "getIntDictOptions")
+        #elseif ($javaType == "String")
+            #set ($dictMethod = "getStrDictOptions")
+        #elseif ($javaType == "Boolean")
+            #set ($dictMethod = "getBoolDictOptions")
+        #end
+        #if ( $column.id == $subJoinColumn.id) ## 特殊:忽略主子表的 join 字段,不用填写
+        #elseif ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+      <el-table-column label="${comment}" min-width="150">
+        <template #default="{ 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")## 图片上传
+      <el-table-column label="${comment}" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+            <UploadImg v-model="row.${javaField}" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+        #elseif($column.htmlType == "fileUpload")## 文件上传
+      <el-table-column label="${comment}" min-width="200">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+            <UploadFile v-model="row.${javaField}" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+        #elseif($column.htmlType == "editor")## 文本编辑器
+      <el-table-column label="${comment}" min-width="400">
+        <template #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+            <Editor v-model="row.${javaField}" height="150px" />
+          </el-form-item>
+        </template>
+      </el-table-column>
+        #elseif($column.htmlType == "select")## 下拉框
+      <el-table-column label="${comment}" min-width="150">
+        <template #default="{ 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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              #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 #default="{ 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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                  :key="dict.value"
+                  :label="dict.value"
+                >
+                  {{ 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 #default="{ 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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+                  :key="dict.value"
+                  :label="dict.value"
+                >
+                  {{ 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 #default="{ row, $index }">
+          <el-form-item :prop="`${$index}.${javaField}`" :rules="formRules.${javaField}" class="mb-0px!">
+            <el-date-picker
+              v-model="row.${javaField}"
+              type="date"
+              value-format="x"
+              placeholder="选择${comment}"
+            />
+          </el-form-item>
+        </template>
+      </el-table-column>
+        #elseif($column.htmlType == "textarea")## 文本框
+      <el-table-column label="${comment}" min-width="200">
+        <template #default="{ 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 #default="{ $index }">
+          <el-button @click="handleDelete($index)" link>—</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-form>
+  <el-row justify="center" class="mt-3">
+    <el-button @click="handleAdd" round>+ 添加${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)
+      #set ($dictMethod = "getDictOptions")## 计算使用哪个 dict 字典方法
+      #if ($javaType == "Integer" || $javaType == "Long" || $javaType == "Byte" || $javaType == "Short")
+        #set ($dictMethod = "getIntDictOptions")
+      #elseif ($javaType == "String")
+          #set ($dictMethod = "getStrDictOptions")
+      #elseif ($javaType == "Boolean")
+          #set ($dictMethod = "getBoolDictOptions")
+      #end
+      #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")## 图片上传
+    <el-form-item label="${comment}" prop="${javaField}">
+      <UploadImg v-model="formData.${javaField}" />
+    </el-form-item>
+      #elseif($column.htmlType == "fileUpload")## 文件上传
+    <el-form-item label="${comment}" prop="${javaField}">
+      <UploadFile v-model="formData.${javaField}" />
+    </el-form-item>
+      #elseif($column.htmlType == "editor")## 文本编辑器
+    <el-form-item label="${comment}" prop="${javaField}">
+      <Editor v-model="formData.${javaField}" height="150px" />
+    </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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+          :key="dict.value"
+          :label="dict.label"
+          :value="dict.value"
+        />
+              #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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+          :key="dict.value"
+          :label="dict.value"
+        >
+          {{ 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 $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
+          :key="dict.value"
+          :label="dict.value"
+          >
+          {{ 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
+        v-model="formData.${javaField}"
+        type="date"
+        value-format="x"
+        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
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+
+const props = defineProps<{
+  ${subJoinColumn.javaField}: undefined // ${subJoinColumn.columnComment}(主表的关联字段)
+}>()
+const formLoading = ref(false) // 表单的加载中
+const formData = ref([])
+const formRules = reactive({
+#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
+})
+const formRef = ref() // 表单 Ref
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.${subJoinColumn.javaField},
+  async (val) => {
+    // 1. 重置表单
+#if ( $subTable.subJoinMany )
+    formData.value = []
+#else
+    formData.value = {
+    #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 {
+      formLoading.value = true
+#if ( $subTable.subJoinMany )
+      formData.value = await ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(val)
+#else
+      const data = await ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(val)
+      if (!data) {
+        return
+      }
+      formData.value = data
+#end
+    } finally {
+      formLoading.value = false
+    }
+  },
+  { immediate: true }
+)
+#if ( $subTable.subJoinMany )
+
+/** 新增按钮操作 */
+const 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} = props.${subJoinColumn.javaField}
+  formData.value.push(row)
+}
+
+/** 删除按钮操作 */
+const handleDelete = (index) => {
+  formData.value.splice(index, 1)
+}
+#end
+
+/** 表单校验 */
+const validate = () => {
+  return formRef.value.validate()
+}
+
+/** 表单值 */
+const getData = () => {
+  return formData.value
+}
+
+defineExpose({ validate, getData })
+</script>

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

@@ -0,0 +1,181 @@
+#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>
+  <!-- 列表 -->
+  <ContentWrap>
+#if ($table.templateType == 11)
+    <el-button
+      type="primary"
+      plain
+      @click="openForm('create')"
+      v-hasPermi="['${permissionPrefix}:create']"
+    >
+      <Icon icon="ep:plus" class="mr-5px" /> 新增
+    </el-button>
+#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}"
+        :formatter="dateFormatter"
+        width="180px"
+      />
+        #elseif($column.dictType && "" != $column.dictType)## 数据字典
+      <el-table-column label="${comment}" align="center" prop="${javaField}">
+        <template #default="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
+    #if ($table.templateType == 11)
+      <el-table-column label="操作" align="center">
+        <template #default="scope">
+          <el-button
+            link
+            type="primary"
+            @click="openForm('update', scope.row.id)"
+            v-hasPermi="['${permissionPrefix}:update']"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            @click="handleDelete(scope.row.id)"
+            v-hasPermi="['${permissionPrefix}:delete']"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    #end
+    </el-table>
+    #if ($table.templateType == 11)
+    <!-- 分页 -->
+    <Pagination
+      :total="total"
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      @pagination="getList"
+    />
+    #end
+  </ContentWrap>
+#if ($table.templateType == 11)
+    <!-- 表单弹窗:添加/修改 -->
+    <${subSimpleClassName}Form ref="formRef" @success="getList" />
+#end
+</template>
+<script setup lang="ts">
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import { dateFormatter } from '@/utils/formatTime'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+#if ($table.templateType == 11)
+import ${subSimpleClassName}Form from './${subSimpleClassName}Form.vue'
+#end
+
+const { t } = useI18n() // 国际化
+const message = useMessage() // 消息弹窗
+
+const props = defineProps<{
+  ${subJoinColumn.javaField}: undefined // ${subJoinColumn.columnComment}(主表的关联字段)
+}>()
+const loading = ref(false) // 列表的加载中
+const list = ref([]) // 列表的数据
+#if ($table.templateType == 11)
+const total = ref(0) // 列表的总页数
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  ${subJoinColumn.javaField}: undefined
+})
+
+/** 监听主表的关联字段的变化,加载对应的子表数据 */
+watch(
+  () => props.${subJoinColumn.javaField},
+  (val) => {
+    queryParams.${subJoinColumn.javaField} = val
+    handleQuery()
+  },
+  { immediate: false }
+)
+#end
+
+/** 查询列表 */
+const getList = async () => {
+  loading.value = true
+  try {
+#if ($table.templateType == 11)
+    const data = await ${simpleClassName}Api.get${subSimpleClassName}Page(queryParams)
+    list.value = data.list
+    total.value = data.total
+#else
+  #if ( $subTable.subJoinMany )
+    list.value = await ${simpleClassName}Api.get${subSimpleClassName}ListBy${SubJoinColumnName}(props.${subJoinColumn.javaField})
+  #else
+    const data = await ${simpleClassName}Api.get${subSimpleClassName}By${SubJoinColumnName}(props.${subJoinColumn.javaField})
+    if (!data) {
+      return
+    }
+    list.value.push(data)
+  #end
+#end
+  } finally {
+    loading.value = false
+  }
+}
+
+/** 搜索按钮操作 */
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+#if ($table.templateType == 11)
+
+/** 添加/修改操作 */
+const formRef = ref()
+const openForm = (type: string, id?: number) => {
+  if (!props.${subJoinColumn.javaField}) {
+    message.error('请选择一个${table.classComment}')
+    return
+  }
+  formRef.value.open(type, id, props.${subJoinColumn.javaField})
+}
+
+/** 删除按钮操作 */
+const handleDelete = async (id: number) => {
+  try {
+    // 删除的二次确认
+    await message.delConfirm()
+    // 发起删除
+    await ${simpleClassName}Api.delete${subSimpleClassName}(id)
+    message.success(t('common.delSuccess'))
+    // 刷新列表
+    await getList()
+  } catch {}
+}
+#end
+#if ($table.templateType != 11)
+
+/** 初始化 **/
+onMounted(() => {
+  getList()
+})
+#end
+</script>

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

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

+ 113 - 49
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/form.vue.vm

@@ -7,7 +7,6 @@
       label-width="100px"
       v-loading="formLoading"
     >
-#set ($dictMethods = [])## 使用到的 dict 字典方法
 #foreach($column in $columns)
     #if ($column.createOperation || $column.updateOperation)
         #set ($dictType = $column.dictType)
@@ -23,31 +22,41 @@
         #elseif ($javaType == "Boolean")
             #set ($dictMethod = "getBoolDictOptions")
         #end
-        #if ($column.htmlType == "input" && !$column.primaryKey)## 忽略主键,不用在表单里
+        #if ( $table.templateType == 2 && $column.id == $treeParentColumn.id )
+      <el-form-item label="${comment}" prop="${javaField}">
+        <el-tree-select
+          v-model="formData.${javaField}"
+          :data="${classNameVar}Tree"
+          #if ($treeNameColumn.javaField == "name")
+          :props="defaultProps"
+          #else
+          :props="{...defaultProps, label: '$treeNameColumn.javaField'}"
+          #end
+          check-strictly
+          default-expand-all
+          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}">
+      <el-form-item label="${comment}" prop="${javaField}">
         <UploadImg v-model="formData.${javaField}" />
       </el-form-item>
         #elseif($column.htmlType == "fileUpload")## 文件上传
-            #set ($hasFileUploadColumn = true)
-      <el-form-item label="${comment}">
+      <el-form-item label="${comment}" prop="${javaField}">
         <UploadFile v-model="formData.${javaField}" />
       </el-form-item>
         #elseif($column.htmlType == "editor")## 文本编辑器
-      <el-form-item label="${comment}">
+      <el-form-item label="${comment}" prop="${javaField}">
         <Editor v-model="formData.${javaField}" height="150px" />
       </el-form-item>
         #elseif($column.htmlType == "select")## 下拉框
       <el-form-item label="${comment}" prop="${javaField}">
         <el-select v-model="formData.${javaField}" placeholder="请选择${comment}">
                 #if ("" != $dictType)## 有数据字典
-                    #if (!$dictMethods.contains($dictMethod))## 如果不存在,则添加到 dictMethods 数组中,后续好 import
-                      #set($ignore = $dictMethods.add($dictMethod) )
-                    #end
           <el-option
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
@@ -63,9 +72,6 @@
       <el-form-item label="${comment}" prop="${javaField}">
         <el-checkbox-group v-model="formData.${javaField}">
                 #if ("" != $dictType)## 有数据字典
-                    #if (!$dictMethods.contains($dictMethod))## 如果不存在,则添加到 dictMethods 数组中,后续好 import
-                      #set($ignore = $dictMethods.add($dictMethod) )
-                    #end
           <el-checkbox
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
@@ -82,9 +88,6 @@
       <el-form-item label="${comment}" prop="${javaField}">
         <el-radio-group v-model="formData.${javaField}">
                 #if ("" != $dictType)## 有数据字典
-                    #if (!$dictMethods.contains($dictMethod))## 如果不存在,则添加到 dictMethods 数组中,后续好 import
-                      #set($ignore = $dictMethods.add($dictMethod) )
-                    #end
           <el-radio
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
@@ -114,6 +117,21 @@
     #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
     <template #footer>
       <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
       <el-button @click="dialogVisible = false">取 消</el-button>
@@ -121,10 +139,18 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-#if ($dictMethods.size() > 0)
-import { DICT_TYPE#foreach ($dictMethod in $dictMethods), ${dictMethod}#end } from '@/utils/dict'
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+import { defaultProps, handleTree } from '@/utils/tree'
+#end
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+#foreach ($subSimpleClassName in $subSimpleClassNames)
+import ${subSimpleClassName}Form from './components/${subSimpleClassName}Form.vue'
+#end
 #end
-import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${classNameVar}'
 
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗
@@ -134,37 +160,40 @@ const dialogTitle = ref('') // 弹窗的标题
 const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
 const formType = ref('') // 表单的类型:create - 新增;update - 修改
 const formData = ref({
-#set ($listOperationLastIndex = -1)## 求最后一个需要 , 的地方
 #foreach ($column in $columns)
     #if ($column.createOperation || $column.updateOperation)
-        #set ($listOperationLastIndex = $foreach.index)
-    #end
-#end
-#foreach ($column in $columns)
-    #if ($column.createOperation || $column.updateOperation)
-        #if ($column.htmlType == "checkbox")
-  $column.javaField: []#if($foreach.index < $listOperationLastIndex),#end
-        #else
-  $column.javaField: undefined#if($foreach.index < $listOperationLastIndex),#end
-        #end
+      #if ($column.htmlType == "checkbox")
+  $column.javaField: [],
+      #else
+  $column.javaField: undefined,
+      #end
     #end
 #end
 })
 const formRules = reactive({
-#set ($listOperationLastIndex = -1)## 求最后一个需要 , 的地方
-#foreach ($column in $columns)
-    #if (($column.createOperation || $column.updateOperation) && !$column.nullable && !${column.primaryKey})## 创建或者更新操作 && 要求非空 && 非主键
-        #set ($listOperationLastIndex = $foreach.index)
-    #end
-#end
 #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 }]#if($foreach.index < $listOperationLastIndex),#end
+  $column.javaField: [{ required: true, message: '${comment}不能为空', trigger: #if($column.htmlType == 'select')'change'#else'blur'#end }],
     #end
 #end
 })
 const formRef = ref() // 表单 Ref
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+const ${classNameVar}Tree = ref() // 树形结构
+#end
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 10 || $table.templateType == 12 )
+#if ( $subTables && $subTables.size() > 0 )
+
+/** 子表的表单 */
+const subTabsName = ref('$subClassNameVars.get(0)')
+#foreach ($subClassNameVar in $subClassNameVars)
+const ${subClassNameVar}FormRef = ref()
+#end
+#end
+#end
 
 /** 打开弹窗 */
 const open = async (type: string, id?: number) => {
@@ -181,6 +210,10 @@ const open = async (type: string, id?: number) => {
       formLoading.value = false
     }
   }
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+  await get${simpleClassName}Tree()
+#end
 }
 defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 
@@ -188,13 +221,38 @@ defineExpose({ open }) // 提供 open 方法,用于打开弹窗
 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
 const submitForm = async () => {
   // 校验表单
-  if (!formRef) return
-  const valid = await formRef.value.validate()
-  if (!valid) return
+  await formRef.value.validate()
+## 特殊:主子表专属逻辑
+#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 {
+    await ${subClassNameVar}FormRef.value.validate()
+  } catch (e) {
+    subTabsName.value = '${subClassNameVar}'
+    return
+  }
+  #end
+#end
+#end
   // 提交请求
   formLoading.value = true
   try {
     const data = formData.value as unknown as ${simpleClassName}Api.${simpleClassName}VO
+## 特殊:主子表专属逻辑
+#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 = ${subClassNameVar}FormRef.value.getData()
+  #end
+#end
+#end
     if (formType.value === 'create') {
       await ${simpleClassName}Api.create${simpleClassName}(data)
       message.success(t('common.createSuccess'))
@@ -213,22 +271,28 @@ const submitForm = async () => {
 /** 重置表单 */
 const resetForm = () => {
   formData.value = {
-#set ($listOperationLastIndex = -1)## 求最后一个需要 , 的地方
-#foreach ($column in $columns)
-  #if ($column.createOperation || $column.updateOperation)
-      #set ($listOperationLastIndex = $foreach.index)
-  #end
-#end
 #foreach ($column in $columns)
   #if ($column.createOperation || $column.updateOperation)
       #if ($column.htmlType == "checkbox")
-    $column.javaField: []#if($foreach.index < $listOperationLastIndex),#end
+    $column.javaField: [],
       #else
-    $column.javaField: undefined#if($foreach.index < $listOperationLastIndex),#end
+    $column.javaField: undefined,
       #end
   #end
 #end
   }
   formRef.value?.resetFields()
 }
-</script>
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+
+/** 获得${table.classComment}树 */
+const get${simpleClassName}Tree = async () => {
+  ${classNameVar}Tree.value = []
+  const data = await ${simpleClassName}Api.get${simpleClassName}List()
+  const root: Tree = { id: 0, name: '顶级${table.classComment}', children: [] }
+  root.children = handleTree(data, 'id', '${treeParentColumn.javaField}')
+  ${classNameVar}Tree.value.push(root)
+}
+#end
+</script>

+ 115 - 31
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3/views/index.vue.vm

@@ -8,7 +8,6 @@
       :inline="true"
       label-width="68px"
     >
-    #set ($dictMethods = [])## 使用到的 dict 字典方法
     #foreach($column in $columns)
         #if ($column.listOperation)
             #set ($dictType = $column.dictType)
@@ -36,20 +35,13 @@
       </el-form-item>
             #elseif ($column.htmlType == "select" || $column.htmlType == "radio")
       <el-form-item label="${comment}" prop="${javaField}">
-                #if ($javaField.length() + $comment.length() > 8)
         <el-select
           v-model="queryParams.${javaField}"
           placeholder="请选择${comment}"
           clearable
           class="!w-240px"
         >
-                #else
-        <el-select v-model="queryParams.${javaField}" placeholder="请选择${comment}" clearable class="!w-240px">
-                #end
                 #if ("" != $dictType)## 设置了 dictType 数据字典的情况
-                    #if (!$dictMethods.contains($dictMethod))## 如果不存在,则添加到 dictMethods 数组中,后续好 import
-                        #set($ignore = $dictMethods.add($dictMethod) )
-                    #end
           <el-option
             v-for="dict in $dictMethod(DICT_TYPE.$dictType.toUpperCase())"
             :key="dict.value"
@@ -92,16 +84,12 @@
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
-        #if ($permissionPrefix.length() <= 12)
-        <el-button type="primary" @click="openForm('create')" v-hasPermi="['${permissionPrefix}:create']">
-        #else
         <el-button
           type="primary"
           plain
           @click="openForm('create')"
           v-hasPermi="['${permissionPrefix}:create']"
         >
-        #end
           <Icon icon="ep:plus" class="mr-5px" /> 新增
         </el-button>
         <el-button
@@ -113,13 +101,61 @@
         >
           <Icon icon="ep:download" class="mr-5px" /> 导出
         </el-button>
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+        <el-button type="danger" plain @click="toggleExpandAll">
+          <Icon icon="ep:sort" class="mr-5px" /> 展开/折叠
+        </el-button>
+#end
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <!-- 列表 -->
   <ContentWrap>
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      highlight-current-row
+      @current-change="handleCurrentChange"
+    >
+## 特殊:树表专属逻辑
+#elseif ( $table.templateType == 2 )
+    <el-table
+      v-loading="loading"
+      :data="list"
+      :stripe="true"
+      :show-overflow-tooltip="true"
+      row-key="id"
+      :default-expand-all="isExpandAll"
+      v-if="refreshTable"
+    >
+#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 model-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)
@@ -134,7 +170,7 @@
         :formatter="dateFormatter"
         width="180px"
       />
-        #elseif("" != $column.dictType)## 数据字典
+        #elseif($column.dictType && "" != $column.dictType)## 数据字典
       <el-table-column label="${comment}" align="center" prop="${javaField}">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.$dictType.toUpperCase()" :value="scope.row.${column.javaField}" />
@@ -177,21 +213,41 @@
 
   <!-- 表单弹窗:添加/修改 -->
   <${simpleClassName}Form ref="formRef" @success="getList" />
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 && $subTables && $subTables.size() > 0 )
+  <!-- 子表的列表 -->
+  <ContentWrap>
+    <el-tabs model-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}="currentRow.id" />
+      </el-tab-pane>
+      #end
+    </el-tabs>
+  </ContentWrap>
+#end
 </template>
 
 <script setup lang="ts">
-#if ($dictMethods.size() > 0)
-import { DICT_TYPE#foreach ($dictMethod in $dictMethods), ${dictMethod}#end } from '@/utils/dict'
-#end
-#foreach ($column in $columns)
-    #if ($column.listOperationResult && $column.htmlType == "datetime")
+import { getIntDictOptions, getStrDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
 import { dateFormatter } from '@/utils/formatTime'
-    #break
-    #end
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+import { handleTree } from '@/utils/tree'
 #end
 import download from '@/utils/download'
-import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${classNameVar}'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
 import ${simpleClassName}Form from './${simpleClassName}Form.vue'
+## 特殊:主子表专属逻辑
+#if ( $table.templateType != 10 )
+#foreach ($subSimpleClassName in $subSimpleClassNames)
+import ${subSimpleClassName}List from './components/${subSimpleClassName}List.vue'
+#end
+#end
 
 defineOptions({ name: '${table.className}' })
 
@@ -199,24 +255,24 @@ const message = useMessage() // 消息弹窗
 const { t } = useI18n() // 国际化
 
 const loading = ref(true) // 列表的加载中
-const total = ref(0) // 列表的总页数
 const list = ref([]) // 列表的数据
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
+const total = ref(0) // 列表的总页数
+#end
 const queryParams = reactive({
+## 特殊:树表专属逻辑(树不需要分页接口)
+#if ( $table.templateType != 2 )
   pageNo: 1,
   pageSize: 10,
-  #set ($listOperationLastIndex = -1)## 求最后一个需要 , 的地方
-  #foreach ($column in $columns)
-    #if ($column.listOperation)
-      #set ($listOperationLastIndex = $foreach.index)
-    #end
-  #end
+#end
   #foreach ($column in $columns)
     #if ($column.listOperation)
       #if ($column.listOperationCondition != 'BETWEEN')
-  $column.javaField: null#if($foreach.index < $listOperationLastIndex),#end
+  $column.javaField: null,
   #end
       #if ($column.htmlType == "datetime" || $column.listOperationCondition == "BETWEEN")
-  $column.javaField: []#if($foreach.index < $listOperationLastIndex),#end
+  $column.javaField: [],
       #end
     #end
   #end
@@ -228,9 +284,15 @@ const exportLoading = ref(false) // 导出的加载中
 const getList = async () => {
   loading.value = true
   try {
+## 特殊:树表专属逻辑(树不需要分页接口)
+  #if ( $table.templateType == 2 )
+    const data = await ${simpleClassName}Api.get${simpleClassName}List(queryParams)
+    list.value = handleTree(data, 'id', '${treeParentColumn.javaField}')
+  #else
     const data = await ${simpleClassName}Api.get${simpleClassName}Page(queryParams)
     list.value = data.list
     total.value = data.total
+  #end
   } finally {
     loading.value = false
   }
@@ -281,9 +343,31 @@ const handleExport = async () => {
     exportLoading.value = false
   }
 }
+## 特殊:主子表专属逻辑
+#if ( $table.templateType == 11 )
+
+/** 选中行操作 */
+const currentRow = ref({}) // 选中行
+const handleCurrentChange = (row) => {
+  currentRow.value = row
+}
+#end
+## 特殊:树表专属逻辑
+#if ( $table.templateType == 2 )
+
+/** 展开/折叠操作 */
+const isExpandAll = ref(true) // 是否展开,默认全部展开
+const refreshTable = ref(true) // 重新渲染表格状态
+const toggleExpandAll = async () => {
+  refreshTable.value = false
+  isExpandAll.value = !isExpandAll.value
+  await nextTick()
+  refreshTable.value = true
+}
+#end
 
 /** 初始化 **/
 onMounted(() => {
   getList()
 })
-</script>
+</script>

+ 0 - 5
yudao-module-infra/yudao-module-infra-biz/src/main/resources/codegen/vue3_schema/views/data.ts.vm

@@ -1,10 +1,5 @@
 import type { CrudSchema } from '@/hooks/web/useCrudSchemas'
-#foreach ($column in $columns)
-    #if ($column.listOperationResult && $column.htmlType == "datetime")
 import { dateFormatter } from '@/utils/formatTime'
-    #break
-    #end
-#end
 
 // 表单校验
 export const rules = reactive({

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

@@ -8,7 +8,7 @@
   </Dialog>
 </template>
 <script setup lang="ts">
-  import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${classNameVar}'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
 import { rules, allSchemas } from './${classNameVar}.data'
 const { t } = useI18n() // 国际化
 const message = useMessage() // 消息弹窗

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

@@ -54,7 +54,7 @@
 </template>
 <script setup lang="ts" name="${table.className}">
 import { allSchemas } from './${classNameVar}.data'
-import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${classNameVar}'
+import * as ${simpleClassName}Api from '@/api/${table.moduleName}/${table.businessName}'
 import ${simpleClassName}Form from './${simpleClassName}Form.vue'
 
 // tableObject:表格的属性对象,可获得分页大小、条数等属性

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

@@ -5,7 +5,7 @@ import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
 import { BasicForm, useForm } from '@/components/Form'
 import { BasicModal, useModalInner } from '@/components/Modal'
-import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${classNameVar}'
+import { create${simpleClassName}, get${simpleClassName}, update${simpleClassName} } from '@/api/${table.moduleName}/${table.businessName}'
 
 defineOptions({ name: '${table.className}Modal' })
 

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

@@ -5,7 +5,7 @@ import { useI18n } from '@/hooks/web/useI18n'
 import { useMessage } from '@/hooks/web/useMessage'
 import { useModal } from '@/components/Modal'
 import { useTable } from '@/components/Table'
-import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${classNameVar}'
+import { delete${simpleClassName}, export${simpleClassName}, get${simpleClassName}Page } from '@/api/${table.moduleName}/${table.businessName}'
 
 defineOptions({ name: '${table.className}' })
 

+ 0 - 0
yudao-module-infra/yudao-module-infra-biz/src/main/resources/mapper/null/.gitkeep


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

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

Some files were not shown because too many files changed in this diff