# scaffold 项目之代码生成 - 单表
# 简介
在大部分项目中,其实整体架构出来了,后面的要加新功能基本按照已有的模块来加一份 CURD 操作,除了 curd 逻辑不一样,其他基本一致,比如:controller、service、mapper 等等,如果这些都要自己手动去写的话非常枯燥,且浪费时间,效率也低。
所以这种重复的代码可以交给程序来生成,项目提供了 codegen 代码生成器,我们只要重点关注对于一个需求怎么设计好表结构,就可以一键生成 前端页面+后端代码+单元测试+Swgger接口文档+Vaildator 参数校验
# 使用
# 数据库表结构设计
设计用户组的数据库表名为 system_group,其建表语句如下:
create table `system_group`( | |
id bigint not null, | |
name varchar(255), | |
status tinyint not null | |
) |
注意事项:
- 表名的前缀要和模块对应,例如系统模块是
system那么表明就是:system_xxx
** 疑问:** 为什么前缀要保持一致呢? ** 答:** 代码生成器会自动解析前缀,获得其所属的模块,从而简化配置过程。
- 设置 ID 主键,一般推荐使用
bigint长整形,并设置自增长。 - 正确设置每个字段是否允许空,代码生成器会根据它生成参数是否允许空的校验规则。
- 正确设置注释,代码生成器会根据它生成字段名与提示等信息。
- 添加
creator、create_time,updater,update_time,deleted是必须设置的系统字段;如果开启多租户的功能,并且该表需要多租户的隔离,则需要添加tenant_id字段。
# 代码生成
# 代码的实现
/** | |
* <p> Project: scaffold - CodegenBuilder </p> | |
* | |
* 代码生成器的 Builder,负责: | |
* <p> | |
* <li> | |
* 1. 将数据库的表 {@link TableInfo} 定义,构建成 {@link CodegenTableDO} | |
* <li> | |
* 2. 将数据库的列 {@link TableField} 构定义,建成 {@link CodegenColumnDO} | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Component | |
public class CodegenBuilder { | |
/** | |
* 字段名与 {@link CodegenColumnListConditionEnum} 的默认映射 | |
* 注意,字段的匹配以后缀的方式 | |
*/ | |
private static final Map<String, CodegenColumnListConditionEnum> COLUMN_LIST_OPERATION_CONDITION_MAPPINGS = | |
MapUtil.<String, CodegenColumnListConditionEnum>builder() | |
.put("name", CodegenColumnListConditionEnum.LIKE) | |
.put("time", CodegenColumnListConditionEnum.BETWEEN) | |
.put("date", CodegenColumnListConditionEnum.BETWEEN) | |
.build(); | |
/** | |
* 字段名与 {@link CodegenColumnHtmlTypeEnum} 的默认映射 | |
* 注意,字段的匹配以后缀的方式 | |
*/ | |
private static final Map<String, CodegenColumnHtmlTypeEnum> COLUMN_HTML_TYPE_MAPPINGS = | |
MapUtil.<String, CodegenColumnHtmlTypeEnum>builder() | |
.put("status", CodegenColumnHtmlTypeEnum.RADIO) | |
.put("sex", CodegenColumnHtmlTypeEnum.RADIO) | |
.put("type", CodegenColumnHtmlTypeEnum.SELECT) | |
.put("image", CodegenColumnHtmlTypeEnum.IMAGE_UPLOAD) | |
.put("file", CodegenColumnHtmlTypeEnum.FILE_UPLOAD) | |
.put("content", CodegenColumnHtmlTypeEnum.EDITOR) | |
.put("description", CodegenColumnHtmlTypeEnum.EDITOR) | |
.put("demo", CodegenColumnHtmlTypeEnum.EDITOR) | |
.put("time", CodegenColumnHtmlTypeEnum.DATETIME) | |
.put("date", CodegenColumnHtmlTypeEnum.DATETIME) | |
.build(); | |
/** | |
* 多租户编号的字段名 | |
*/ | |
public static final String TENANT_ID_FIELD = "tenantId"; | |
/** | |
* {@link com.tz.scaffold.framework.mybatis.core.dataobject.BaseDO} 的字段 | |
*/ | |
public static final Set<String> BASE_DO_FIELDS = new HashSet<>(); | |
/** | |
* 新增操作,不需要传递的字段 | |
*/ | |
private static final Set<String> CREATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); | |
/** | |
* 修改操作,不需要传递的字段 | |
*/ | |
private static final Set<String> UPDATE_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet(); | |
/** | |
* 列表操作的条件,不需要传递的字段 | |
*/ | |
private static final Set<String> LIST_OPERATION_EXCLUDE_COLUMN = Sets.newHashSet("id"); | |
/** | |
* 列表操作的结果,不需要返回的字段 | |
*/ | |
private static final Set<String> LIST_OPERATION_RESULT_EXCLUDE_COLUMN = Sets.newHashSet(); | |
static { | |
Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName())); | |
BASE_DO_FIELDS.add(TENANT_ID_FIELD); | |
// 处理 OPERATION 相关的字段 | |
CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); | |
UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); | |
LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); | |
// 创建时间,还是可能需要传递的 | |
LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); | |
LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); | |
// 创建时间,还是需要返回的 | |
LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); | |
} | |
public CodegenTableDO buildTable(TableInfo tableInfo) { | |
CodegenTableDO table = CodegenConvert.INSTANCE.convert(tableInfo); | |
initTableDefault(table); | |
return table; | |
} | |
/** | |
* 初始化 Table 表的默认字段 | |
* | |
* @param table 表定义 | |
*/ | |
private void initTableDefault(CodegenTableDO table) { | |
// 以 system_dept 举例子。moduleName 为 system、businessName 为 dept、className 为 Dept | |
// 如果希望以 System 前缀,则可以手动在【代码生成 - 修改生成配置 - 基本信息】,将实体类名称改为 SystemDept 即可 | |
String tableName = table.getTableName().toLowerCase(); | |
// 第一步,_ 前缀的前面,作为 module 名字;第二步,moduleName 必须小写; | |
table.setModuleName(subBefore(tableName, '_', false).toLowerCase()); | |
// 第一步,第一个 _ 前缀的后面,作为 module 名字;第二步,可能存在多个 _ 的情况,转换成驼峰;第三步,businessName 必须小写; | |
table.setBusinessName(toCamelCase(subAfter(tableName, '_', false)).toLowerCase()); | |
// 驼峰 + 首字母大写;第一步,第一个 _ 前缀的后面,作为 class 名字;第二步,驼峰命名 | |
table.setClassName(upperFirst(toCamelCase(subAfter(tableName, '_', false)))); | |
// 去除结尾的表,作为类描述 | |
table.setClassComment(StrUtil.removeSuffixIgnoreCase(table.getTableComment(), "表")); | |
table.setTemplateType(CodegenTemplateTypeEnum.ONE.getType()); | |
} | |
public List<CodegenColumnDO> buildColumns(Long tableId, List<TableField> tableFields) { | |
List<CodegenColumnDO> columns = CodegenConvert.INSTANCE.convertList(tableFields); | |
int index = 1; | |
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 列的默认字段 | |
// 处理 CRUD 相关的字段的默认值 | |
processColumnOperation(column); | |
// 处理 UI 相关的字段的默认值 | |
processColumnUI(column); | |
// 处理字段的 swagger example 示例 | |
processColumnExample(column); | |
} | |
return columns; | |
} | |
private void processColumnOperation(CodegenColumnDO column) { | |
// 处理 createOperation 字段 | |
column.setCreateOperation(!CREATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) | |
// 对于主键,创建时无需传递 | |
&& !column.getPrimaryKey()); | |
// 处理 updateOperation 字段 | |
column.setUpdateOperation(!UPDATE_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) | |
// 对于主键,更新时需要传递 | |
|| column.getPrimaryKey()); | |
// 处理 listOperation 字段 | |
column.setListOperation(!LIST_OPERATION_EXCLUDE_COLUMN.contains(column.getJavaField()) | |
// 对于主键,列表过滤不需要传递 | |
&& !column.getPrimaryKey()); | |
// 处理 listOperationCondition 字段 | |
COLUMN_LIST_OPERATION_CONDITION_MAPPINGS.entrySet().stream() | |
.filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) | |
.findFirst().ifPresent(entry -> column.setListOperationCondition(entry.getValue().getCondition())); | |
if (column.getListOperationCondition() == null) { | |
column.setListOperationCondition(CodegenColumnListConditionEnum.EQ.getCondition()); | |
} | |
// 处理 listOperationResult 字段 | |
column.setListOperationResult(!LIST_OPERATION_RESULT_EXCLUDE_COLUMN.contains(column.getJavaField())); | |
} | |
private void processColumnUI(CodegenColumnDO column) { | |
// 基于后缀进行匹配 | |
COLUMN_HTML_TYPE_MAPPINGS.entrySet().stream() | |
.filter(entry -> StrUtil.endWithIgnoreCase(column.getJavaField(), entry.getKey())) | |
.findFirst().ifPresent(entry -> column.setHtmlType(entry.getValue().getType())); | |
// 如果是 Boolean 类型时,设置为 radio 类型. | |
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()); | |
} | |
} | |
/** | |
* 处理字段的 swagger example 示例 | |
* | |
* @param column 字段 | |
*/ | |
private void processColumnExample(CodegenColumnDO column) { | |
//id、price、count 等可能是整数的后缀 | |
if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "id", "price", "count")) { | |
column.setExample(String.valueOf(randomInt(1, Short.MAX_VALUE))); | |
return; | |
} | |
// name | |
if (StrUtil.endWithIgnoreCase(column.getJavaField(), "name")) { | |
column.setExample(randomEle(new String[]{"张三", "李四", "王五", "赵六", "芋艿"})); | |
return; | |
} | |
// status | |
if (StrUtil.endWithAnyIgnoreCase(column.getJavaField(), "status", "type")) { | |
column.setExample(randomEle(new String[]{"1", "2"})); | |
return; | |
} | |
// url | |
if (StrUtil.endWithIgnoreCase(column.getColumnName(), "url")) { | |
column.setExample("https://scaffold.tzzfj.cn"); | |
return; | |
} | |
// reason | |
if (StrUtil.endWithIgnoreCase(column.getColumnName(), "reason")) { | |
column.setExample(randomEle(new String[]{"不喜欢", "不对", "不好", "不香"})); | |
return; | |
} | |
// description、memo、remark | |
if (StrUtil.endWithAnyIgnoreCase(column.getColumnName(), "description", "memo", "remark")) { | |
column.setExample(randomEle(new String[]{"你猜", "随便", "你说的对"})); | |
return; | |
} | |
} | |
} |
大致流程:
- 查询对应数据源的对应表信息
- 用 MyBatis 通过数据源 id 和表名称获取对应的表信息和对应的字段信息
- 设置 java 需要的信息,例如:模块名称、包路径、类名、类注释、创建人
- 处理每个字段的类型转成对应的 java 类型
- 处理每个字段的 UI 显示的组件,如果是 boolean 类型设置成:radio 类型,如果是时间类型,就设置成时间组件
- 处理每个字段的 swagger 列子
# 导入表
点击 [基础设施 -> 代码生成] 菜单,点击 [基于 DB 导入] 按钮,选择 system_group 表,后点击 [确认] 按钮,需要数据库有这张表,才会显示
# 编辑配置
当我们导入一个表后,会生成对应的表的配置类,改配置类会记录实际表的表名、字段、配置、字段类型、长度等信息,我们可以进行编辑,编辑后点击同步可以直接修改数据库的表信息
# 预览或生成代码
生成对应的 后端java文件 , 前端vue文件 ,基于 Velocity 模板引擎,生成具体的代码。 具体代码如下:
package com.tz.scaffold.module.infra.service.codegen.inner; | |
/** | |
* <p> Project: scaffold - CodegenEngine </p> | |
* | |
* 代码生成的引擎,用于具体生成代码 | |
* <p> | |
* 目前基于 {@link org.apache.velocity.app.Velocity} 模板引擎实现 | |
* <p> | |
* 考虑到 Java 模板引擎的框架非常多,Freemarker、Velocity、Thymeleaf 等等,所以我们采用 hutool 封装的 {@link cn.hutool.extra.template.Template} 抽象 | |
* @author Tz | |
* @date 2024/01/09 23:45 | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
@Component | |
public class CodegenEngine { | |
/** | |
* 后端的模板配置 | |
* | |
* key:模板在 resources 的地址 | |
* value:生成的路径 | |
*/ | |
private static final Map<String, String> SERVER_TEMPLATES = MapUtil.<String, String>builder(new LinkedHashMap<>()) // 有序 | |
// Java module-biz Main | |
.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/saveReqVO"), javaModuleImplVOFilePath("SaveReqVO")) | |
.put(javaTemplatePath("controller/controller"), javaModuleImplControllerFilePath()) | |
.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")) | |
.put(javaTemplatePath("service/service"), | |
javaModuleImplMainFilePath("service/${table.businessName}/${table.className}Service")) | |
// Java module-biz Test | |
.put(javaTemplatePath("test/serviceTest"), | |
javaModuleImplTestFilePath("service/${table.businessName}/${table.className}ServiceImplTest")) | |
// Java module-api Main | |
.put(javaTemplatePath("enums/errorcode"), javaModuleApiMainFilePath("enums/ErrorCodeConstants_手动操作")) | |
// SQL | |
.put("codegen/sql/sql.vm", "sql/sql.sql") | |
.put("codegen/sql/h2.vm", "sql/h2.sql") | |
.build(); | |
/** | |
* 后端的配置模版 | |
* | |
* key1:UI 模版的类型 {@link CodegenFrontTypeEnum#getType ()} | |
* key2:模板在 resources 的地址 | |
* value:生成的路径 | |
*/ | |
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}/${table.businessName}/index.vue")) | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("api/api.js"), | |
vueFilePath("api/${table.moduleName}/${table.businessName}/index.js")) | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/form.vue"), | |
vueFilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) | |
// 特殊:主子表专属逻辑 | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_normal.vue"), | |
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) | |
// 特殊:主子表专属逻辑 | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_inner.vue"), | |
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) | |
// 特殊:主子表专属逻辑 | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/form_sub_erp.vue"), | |
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}Form.vue")) | |
// 特殊:主子表专属逻辑 | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_inner.vue"), | |
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) | |
// 特殊:主子表专属逻辑 | |
.put(CodegenFrontTypeEnum.VUE2.getType(), vueTemplatePath("views/components/list_sub_erp.vue"), | |
vueFilePath("views/${table.moduleName}/${table.businessName}/components/${subSimpleClassName}List.vue")) | |
// Vue3 标准模版 | |
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/index.vue"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) | |
.put(CodegenFrontTypeEnum.VUE3.getType(), vue3TemplatePath("views/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}/${table.businessName}/index.ts")) | |
// Vue3 Schema 模版 | |
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/data.ts"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) | |
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/index.vue"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) | |
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("views/form.vue"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Form.vue")) | |
.put(CodegenFrontTypeEnum.VUE3_SCHEMA.getType(), vue3SchemaTemplatePath("api/api.ts"), | |
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) | |
// Vue3 vben 模版 | |
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/data.ts"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/${classNameVar}.data.ts")) | |
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/index.vue"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/index.vue")) | |
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("views/form.vue"), | |
vue3FilePath("views/${table.moduleName}/${table.businessName}/${simpleClassName}Modal.vue")) | |
.put(CodegenFrontTypeEnum.VUE3_VBEN.getType(), vue3VbenTemplatePath("api/api.ts"), | |
vue3FilePath("api/${table.moduleName}/${table.businessName}/index.ts")) | |
.build(); | |
@Resource | |
private CodegenProperties codegenProperties; | |
/** | |
* 模板引擎,由 hutool 实现 | |
*/ | |
private final TemplateEngine templateEngine; | |
/** | |
* 全局通用变量映射 | |
*/ | |
private final Map<String, Object> globalBindingMap = new HashMap<>(); | |
public CodegenEngine() { | |
// 初始化 TemplateEngine 属性 | |
TemplateConfig config = new TemplateConfig(); | |
config.setResourceMode(TemplateConfig.ResourceMode.CLASSPATH); | |
this.templateEngine = new VelocityEngine(config); | |
} | |
@PostConstruct | |
@VisibleForTesting | |
void initGlobalBindingMap() { | |
// 全局配置 | |
globalBindingMap.put("basePackage", codegenProperties.getBasePackage()); | |
// 用于后续获取测试类的 package 地址 | |
globalBindingMap.put("baseFrameworkPackage", codegenProperties.getBasePackage() | |
+ '.' + "framework"); | |
// 全局 Java Bean | |
globalBindingMap.put("CommonResultClassName", CommonResult.class.getName()); | |
globalBindingMap.put("PageResultClassName", PageResult.class.getName()); | |
// VO 类,独有字段 | |
globalBindingMap.put("PageParamClassName", PageParam.class.getName()); | |
globalBindingMap.put("DictFormatClassName", DictFormat.class.getName()); | |
// DO 类,独有字段 | |
globalBindingMap.put("BaseDOClassName", BaseDO.class.getName()); | |
globalBindingMap.put("baseDOFields", CodegenBuilder.BASE_DO_FIELDS); | |
globalBindingMap.put("QueryWrapperClassName", LambdaQueryWrapperX.class.getName()); | |
globalBindingMap.put("BaseMapperClassName", BaseMapperX.class.getName()); | |
// Util 工具类 | |
globalBindingMap.put("ServiceExceptionUtilClassName", ServiceExceptionUtil.class.getName()); | |
globalBindingMap.put("DateUtilsClassName", DateUtils.class.getName()); | |
globalBindingMap.put("ExcelUtilsClassName", ExcelUtils.class.getName()); | |
globalBindingMap.put("LocalDateTimeUtilsClassName", LocalDateTimeUtils.class.getName()); | |
globalBindingMap.put("ObjectUtilsClassName", ObjectUtils.class.getName()); | |
globalBindingMap.put("DictConvertClassName", DictConvert.class.getName()); | |
globalBindingMap.put("OperateLogClassName", OperateLog.class.getName()); | |
globalBindingMap.put("OperateTypeEnumClassName", OperateTypeEnum.class.getName()); | |
globalBindingMap.put("BeanUtils", BeanUtils.class.getName()); | |
} | |
/** | |
* 生成代码 | |
* | |
* @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"); | |
} | |
// Vue2 界面:修正 $refs | |
if (StrUtil.count(content, "this.refs") >= 1) { | |
content = content.replace("this.refs", "this.$refs"); | |
} | |
// Vue 界面:去除多的 dict 相关,只有一个的情况下,说明没使用到 | |
if (StrUtil.count(content, "getIntDictOptions") == 1) { | |
content = content.replace("getIntDictOptions, ", ""); | |
} | |
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); | |
bindingMap.put("columns", columns); | |
// 主键字段 | |
bindingMap.put("primaryColumn", CollectionUtils.findFirst(columns, CodegenColumnDO::getPrimaryKey)); | |
bindingMap.put("sceneEnum", CodegenSceneEnum.valueOf(table.getScene())); | |
//className 相关 | |
// 去掉指定前缀,将 TestDictType 转换成 DictType. 因为在 create 等方法后,不需要带上 Test 前缀 | |
String simpleClassName = removePrefix(table.getClassName(), upperFirst(table.getModuleName())); | |
bindingMap.put("simpleClassName", simpleClassName); | |
// 将 DictType 转换成 dict_type | |
bindingMap.put("simpleClassName_underlineCase", toUnderlineCase(simpleClassName)); | |
// 将 DictType 转换成 dictType,用于变量 | |
bindingMap.put("classNameVar", lowerFirst(simpleClassName)); | |
// 将 DictType 转换成 dict-type | |
String simpleClassNameStrikeCase = toSymbolCase(simpleClassName, '-'); | |
bindingMap.put("simpleClassName_strikeCase", simpleClassNameStrikeCase); | |
//permission 前缀 | |
bindingMap.put("permissionPrefix", table.getModuleName() + ":" + simpleClassNameStrikeCase); | |
// 特殊:树表专属逻辑 | |
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); | |
// 将 DictType 转换成 dict-type | |
subJoinColumnStrikeCases.add(toSymbolCase(subColumn.getJavaField(), '-')); | |
//className 相关 | |
String subSimpleClassName = removePrefix(subTable.getClassName(), upperFirst(subTable.getModuleName())); | |
subSimpleClassNames.add(subSimpleClassName); | |
// 将 DictType 转换成 dict_type | |
simpleClassNameUnderlineCases.add(toUnderlineCase(subSimpleClassName)); | |
// 将 DictType 转换成 dictType,用于变量 | |
subClassNameVars.add(lowerFirst(subSimpleClassName)); | |
// 将 DictType 转换成 dict-type | |
subSimpleClassNameStrikeCases.add(toSymbolCase(subSimpleClassName, '-')); | |
} | |
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) { | |
Map<String, String> templates = new LinkedHashMap<>(); | |
templates.putAll(SERVER_TEMPLATES); | |
templates.putAll(FRONT_TEMPLATES.row(frontType)); | |
return templates; | |
} | |
@SuppressWarnings("unchecked") | |
private String formatFilePath(String filePath, Map<String, Object> bindingMap) { | |
filePath = StrUtil.replace(filePath, "${basePackage}", | |
getStr(bindingMap, "basePackage").replaceAll("\\.", "/")); | |
filePath = StrUtil.replace(filePath, "${classNameVar}", | |
getStr(bindingMap, "classNameVar")); | |
filePath = StrUtil.replace(filePath, "${simpleClassName}", | |
getStr(bindingMap, "simpleClassName")); | |
//sceneEnum 包含的字段 | |
CodegenSceneEnum sceneEnum = (CodegenSceneEnum) bindingMap.get("sceneEnum"); | |
filePath = StrUtil.replace(filePath, "${sceneEnum.prefixClass}", sceneEnum.getPrefixClass()); | |
filePath = StrUtil.replace(filePath, "${sceneEnum.basePackage}", sceneEnum.getBasePackage()); | |
//table 包含的字段 | |
CodegenTableDO table = (CodegenTableDO) bindingMap.get("table"); | |
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; | |
} | |
private static String javaTemplatePath(String path) { | |
return "codegen/java/" + path + ".vm"; | |
} | |
private static String javaModuleImplVOFilePath(String path) { | |
return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + | |
"vo/${sceneEnum.prefixClass}${table.className}" + path, "biz", "main"); | |
} | |
private static String javaModuleImplControllerFilePath() { | |
return javaModuleFilePath("controller/${sceneEnum.basePackage}/${table.businessName}/" + | |
"${sceneEnum.prefixClass}${table.className}Controller", "biz", "main"); | |
} | |
private static String javaModuleImplMainFilePath(String path) { | |
return javaModuleFilePath(path, "biz", "main"); | |
} | |
private static String javaModuleApiMainFilePath(String path) { | |
return javaModuleFilePath(path, "api", "main"); | |
} | |
private static String javaModuleImplTestFilePath(String path) { | |
return javaModuleFilePath(path, "biz", "test"); | |
} | |
private static String javaModuleFilePath(String path, String module, String src) { | |
// 顶级模块 | |
return "scaffold-module-${table.moduleName}/" + | |
// 子模块 | |
"scaffold-module-${table.moduleName}-" + module + "/" + | |
"src/" + src + "/java/${basePackage}/module/${table.moduleName}/" + path + ".java"; | |
} | |
private static String mapperXmlFilePath() { | |
// 顶级模块 | |
return "scaffold-module-${table.moduleName}/" + | |
// 子模块 | |
"scaffold-module-${table.moduleName}-biz/" + | |
"src/main/resources/mapper/${table.businessName}/${table.className}Mapper.xml"; | |
} | |
private static String vueTemplatePath(String path) { | |
return "codegen/vue/" + path + ".vm"; | |
} | |
private static String vueFilePath(String path) { | |
// 顶级目录 | |
return "scaffold-ui-${sceneEnum.basePackage}-vue2/" + | |
"src/" + path; | |
} | |
private static String vue3TemplatePath(String path) { | |
return "codegen/vue3/" + path + ".vm"; | |
} | |
private static String vue3FilePath(String path) { | |
// 顶级目录 | |
return "scaffold-ui-${sceneEnum.basePackage}-vue3/" + | |
"src/" + path; | |
} | |
private static String vue3SchemaTemplatePath(String path) { | |
return "codegen/vue3_schema/" + path + ".vm"; | |
} | |
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"); | |
} | |
} |
velocity 模板内容(PageReqVO 类)
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 ${PageParamClassName}; | |
#foreach ($column in $columns) | |
#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; | |
import static ${DateUtilsClassName}.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND; | |
#break | |
#end | |
#end | |
## 字段模板 | |
#macro(columnTpl $prefix $prefixStr) | |
@Schema(description = "${prefixStr}${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) | |
private ${column.javaType}#if ("$!prefix" != "") ${prefix}${JavaField}#else ${column.javaField}#end; | |
#end | |
@Schema(description = "${sceneEnum.name} - ${table.classComment}分页 Request VO") | |
@Data | |
@EqualsAndHashCode(callSuper = true) | |
@ToString(callSuper = true) | |
public class ${sceneEnum.prefixClass}${table.className}PageReqVO extends PageParam { | |
#foreach ($column in $columns) | |
#if (${column.listOperation})##查询操作 | |
#if (${column.listOperationCondition} == "BETWEEN")## 情况一,Between 的时候 | |
@Schema(description = "${column.columnComment}"#if ("$!column.example" != ""), example = "${column.example}"#end) | |
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) | |
private ${column.javaType}[] ${column.javaField}; | |
#else##情况二,非 Between 的时间 | |
#columnTpl('', '') | |
#end | |
#end | |
#end | |
} |
流程说明:
- 查询数据库中导入配置并处理过的表
- 创建好所有需要生成的
velocity模板- 根据查询出来的表信息和字段信息准备好模板需要的内容
- 通过 Hutool 包中的模板处理工具来处理模板
- 返回结果