refactor(generator): 重构代码生成功能,由指定路径生成模式调整为下载模式,更方便复杂场景

Closes #40
This commit is contained in:
Charles7c 2024-03-15 00:24:16 +08:00
parent ab97a08f48
commit df0c0dd7dc
9 changed files with 128 additions and 127 deletions

View File

@ -20,6 +20,7 @@ import cn.hutool.core.map.MapUtil;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import top.charles7c.continew.starter.data.core.enums.DatabaseType;
import java.util.List;
import java.util.Map;
@ -43,7 +44,7 @@ public class GeneratorProperties {
/**
* 类型映射
*/
private Map<String, Map<String, List<String>>> typeMappings = MapUtil.newHashMap();
private Map<DatabaseType, Map<String, List<String>>> typeMappings = MapUtil.newHashMap();
/**
* 模板配置

View File

@ -16,30 +16,24 @@
package top.charles7c.continew.admin.generator.model.entity;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import lombok.AccessLevel;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.Setter;
import io.swagger.v3.oas.annotations.media.Schema;
import org.hibernate.validator.constraints.Length;
import com.baomidou.mybatisplus.annotation.*;
import com.fasterxml.jackson.annotation.JsonIgnore;
import cn.hutool.core.util.StrUtil;
import top.charles7c.continew.admin.common.constant.RegexConstants;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 生成配置实体
*
@ -80,13 +74,6 @@ public class GenConfigDO implements Serializable {
@Length(max = 60, message = "包名称不能超过 {max} 个字符")
private String packageName;
/**
* 前端路径
*/
@Schema(description = "前端路径", example = "D:/continew-admin-ui/src/views/system/user")
@Length(max = 255, message = "前端路径不能超过 {max} 个字符")
private String frontendPath;
/**
* 业务名称
*/

View File

@ -16,9 +16,8 @@
package top.charles7c.continew.admin.generator.service;
import java.sql.SQLException;
import java.util.List;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import top.charles7c.continew.admin.generator.model.entity.FieldConfigDO;
import top.charles7c.continew.admin.generator.model.entity.GenConfigDO;
import top.charles7c.continew.admin.generator.model.query.TableQuery;
@ -28,6 +27,9 @@ import top.charles7c.continew.admin.generator.model.resp.TableResp;
import top.charles7c.continew.starter.extension.crud.model.query.PageQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.PageResp;
import java.sql.SQLException;
import java.util.List;
/**
* 代码生成业务接口
*
@ -84,6 +86,8 @@ public interface GeneratorService {
* 生成代码
*
* @param tableName 表名称
* @param request 请求对象
* @param response 响应对象
*/
void generate(String tableName);
void generate(String tableName, HttpServletRequest request, HttpServletResponse response);
}

View File

@ -24,9 +24,12 @@ import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.ZipUtil;
import cn.hutool.db.meta.Column;
import cn.hutool.system.SystemUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -43,15 +46,17 @@ import top.charles7c.continew.admin.generator.model.req.GenConfigReq;
import top.charles7c.continew.admin.generator.model.resp.GeneratePreviewResp;
import top.charles7c.continew.admin.generator.model.resp.TableResp;
import top.charles7c.continew.admin.generator.service.GeneratorService;
import top.charles7c.continew.starter.core.autoconfigure.project.ProjectProperties;
import top.charles7c.continew.starter.core.constant.StringConstants;
import top.charles7c.continew.starter.core.exception.BusinessException;
import top.charles7c.continew.starter.core.util.TemplateUtils;
import top.charles7c.continew.starter.core.util.validate.CheckUtils;
import top.charles7c.continew.starter.data.core.enums.DatabaseType;
import top.charles7c.continew.starter.data.core.util.MetaUtils;
import top.charles7c.continew.starter.data.core.util.Table;
import top.charles7c.continew.starter.core.util.validate.CheckUtils;
import top.charles7c.continew.starter.extension.crud.model.query.PageQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.PageResp;
import top.charles7c.continew.starter.web.util.FileUploadUtils;
import javax.sql.DataSource;
import java.io.File;
@ -73,6 +78,7 @@ public class GeneratorServiceImpl implements GeneratorService {
private final DataSource dataSource;
private final GeneratorProperties generatorProperties;
private final ProjectProperties projectProperties;
private final FieldConfigMapper fieldConfigMapper;
private final GenConfigMapper genConfigMapper;
@ -139,8 +145,7 @@ public class GeneratorServiceImpl implements GeneratorService {
Collection<Column> columnList = MetaUtils.getColumns(dataSource, tableName);
// 获取数据库对应的类型映射配置
DatabaseType databaseType = MetaUtils.getDatabaseType(dataSource);
Map<String, List<String>> typeMappingMap = generatorProperties.getTypeMappings()
.get(databaseType.getDatabase());
Map<String, List<String>> typeMappingMap = generatorProperties.getTypeMappings().get(databaseType);
CheckUtils.throwIfEmpty(typeMappingMap, "请先配置对应数据库的类型映射");
Set<Map.Entry<String, List<String>>> typeMappingEntrySet = typeMappingMap.entrySet();
// 新增或更新字段配置
@ -198,11 +203,6 @@ public class GeneratorServiceImpl implements GeneratorService {
fieldConfigMapper.insertBatch(fieldConfigList);
// 保存或更新生成配置信息
GenConfigDO newGenConfig = req.getGenConfig();
String frontendPath = newGenConfig.getFrontendPath();
if (StrUtil.isNotBlank(frontendPath)) {
CheckUtils.throwIf(!FileUtil.exist(frontendPath), "前端路径不存在");
CheckUtils.throwIf(!StrUtil.containsAll(frontendPath, "src", "views"), "前端路径配置错误");
}
GenConfigDO oldGenConfig = genConfigMapper.selectById(tableName);
if (null != oldGenConfig) {
BeanUtil.copyProperties(newGenConfig, oldGenConfig);
@ -236,106 +236,118 @@ public class GeneratorServiceImpl implements GeneratorService {
genConfigMap.put("className", className);
TemplateConfig templateConfig = templateConfigEntry.getValue();
String content = TemplateUtils.render(templateConfig.getTemplatePath(), genConfigMap);
GeneratePreviewResp codePreview = new GeneratePreviewResp();
codePreview.setFileName(className + FileNameUtil.EXT_JAVA);
codePreview.setContent(content);
codePreview.setBackend(true);
generatePreviewList.add(codePreview);
GeneratePreviewResp generatePreview = new GeneratePreviewResp();
generatePreview.setFileName(className + FileNameUtil.EXT_JAVA);
generatePreview.setContent(content);
generatePreview.setBackend(true);
generatePreviewList.add(generatePreview);
}
// 渲染前端代码
// api 代码
genConfigMap.put("fieldConfigs", fieldConfigList);
String apiContent = TemplateUtils.render("api.ftl", genConfigMap);
GeneratePreviewResp apiCodePreview = new GeneratePreviewResp();
apiCodePreview.setFileName(classNamePrefix.toLowerCase() + ".ts");
apiCodePreview.setContent(apiContent);
generatePreviewList.add(apiCodePreview);
GeneratePreviewResp apiGeneratePreview = new GeneratePreviewResp();
apiGeneratePreview.setFileName(classNamePrefix.toLowerCase() + ".ts");
apiGeneratePreview.setContent(apiContent);
generatePreviewList.add(apiGeneratePreview);
// view 代码
String viewContent = TemplateUtils.render("index.ftl", genConfigMap);
GeneratePreviewResp viewCodePreview = new GeneratePreviewResp();
viewCodePreview.setFileName("index.vue");
viewCodePreview.setContent(viewContent);
generatePreviewList.add(viewCodePreview);
GeneratePreviewResp viewGeneratePreview = new GeneratePreviewResp();
viewGeneratePreview.setFileName("index.vue");
viewGeneratePreview.setContent(viewContent);
generatePreviewList.add(viewGeneratePreview);
return generatePreviewList;
}
@Override
public void generate(String tableName) {
public void generate(String tableName, HttpServletRequest request, HttpServletResponse response) {
try {
// 初始化配置及数据
List<GeneratePreviewResp> generatePreviewList = this.preview(tableName);
GenConfigDO genConfig = genConfigMapper.selectById(tableName);
Boolean isOverride = genConfig.getIsOverride();
// 生成代码
String packageName = genConfig.getPackageName();
try {
// 生成后端代码
String classNamePrefix = genConfig.getClassNamePrefix();
// 1.确定后端代码基础路径
// 例如D:/continew-admin
String projectPath = SystemUtil.getUserInfo().getCurrentDir();
// 例如D:/continew-admin/continew-admin-system
File backendModuleFile = new File(projectPath, genConfig.getModuleName());
// 例如D:/continew-admin/continew-admin-system/src/main/java/top/charles7c/continew/admin/system
List<String> backendModuleChildPathList = CollUtil.newArrayList("src", "main", "java");
backendModuleChildPathList.addAll(StrUtil.split(packageName, StringConstants.DOT));
File backendParentFile = FileUtil.file(backendModuleFile, backendModuleChildPathList
.toArray(new String[0]));
// 2.生成代码
List<GeneratePreviewResp> backendCodePreviewList = generatePreviewList.stream()
.filter(GeneratePreviewResp::isBackend)
.toList();
Map<String, TemplateConfig> templateConfigMap = generatorProperties.getTemplateConfigs();
for (GeneratePreviewResp codePreview : backendCodePreviewList) {
// 例如D:/continew-admin/continew-admin-system/src/main/java/top/charles7c/continew/admin/system/service/impl/XxxServiceImpl.java
TemplateConfig templateConfig = templateConfigMap.get(codePreview.getFileName()
.replace(classNamePrefix, StringConstants.EMPTY)
.replace(FileNameUtil.EXT_JAVA, StringConstants.EMPTY));
File classParentFile = FileUtil.file(backendParentFile, StrUtil.splitToArray(templateConfig
.getPackageName(), StringConstants.DOT));
File classFile = new File(classParentFile, codePreview.getFileName());
// 如果已经存在且不允许覆盖则跳过
if (classFile.exists() && Boolean.FALSE.equals(isOverride)) {
continue;
}
FileUtil.writeUtf8String(codePreview.getContent(), classFile);
}
Map<Boolean, List<GeneratePreviewResp>> generatePreviewListMap = generatePreviewList.stream()
.collect(Collectors.groupingBy(GeneratePreviewResp::isBackend));
this.generateBackendCode(generatePreviewListMap.get(true), genConfig);
// 生成前端代码
String frontendPath = genConfig.getFrontendPath();
if (StrUtil.isBlank(frontendPath)) {
return;
}
List<GeneratePreviewResp> frontendCodePreviewList = generatePreviewList.stream()
.filter(p -> !p.isBackend())
.toList();
// 1.生成 api 代码
String apiModuleName = StrUtil.subSuf(packageName, StrUtil
List<GeneratePreviewResp> frontendGeneratePreviewList = generatePreviewListMap.get(false);
String packageName = genConfig.getPackageName();
String moduleName = StrUtil.subSuf(packageName, StrUtil
.lastIndexOfIgnoreCase(packageName, StringConstants.DOT) + 1);
GeneratePreviewResp apiCodePreview = frontendCodePreviewList.get(0);
// 例如D:/continew-admin-ui
List<String> frontendSubPathList = StrUtil.split(frontendPath, "src");
String frontendModulePath = frontendSubPathList.get(0);
// 例如D:/continew-admin-ui/src/api/tool/xxx.ts
File apiParentFile = FileUtil.file(frontendModulePath, "src", "api", apiModuleName);
File apiFile = new File(apiParentFile, apiCodePreview.getFileName());
if (apiFile.exists() && Boolean.FALSE.equals(isOverride)) {
return;
String tempDir = SystemUtil.getUserInfo().getTempDir();
// 例如continew-admin-ui/src
String frontendBasicPackagePath = tempDir + String.join(File.separator, projectProperties
.getAppName(), projectProperties.getAppName() + "-ui", "src");
// 1生成 api 代码
GeneratePreviewResp apiGeneratePreview = frontendGeneratePreviewList.get(0);
// 例如continew-admin-ui/src/src/api/system
String apiPath = String.join(File.separator, frontendBasicPackagePath, "api", moduleName);
// 例如continew-admin-ui/src/api/system/user.ts
File apiFile = new File(apiPath, apiGeneratePreview.getFileName());
if (!apiFile.exists() || Boolean.TRUE.equals(genConfig.getIsOverride())) {
FileUtil.writeUtf8String(apiGeneratePreview.getContent(), apiFile);
}
FileUtil.writeUtf8String(apiCodePreview.getContent(), apiFile);
// 2.生成 view 代码
GeneratePreviewResp viewCodePreview = frontendCodePreviewList.get(1);
// 例如D:/continew-admin-ui/src/views/system/xxx/index.vue
File indexFile = FileUtil.file(frontendPath, apiModuleName, StrUtil
.lowerFirst(classNamePrefix), "index.vue");
if (indexFile.exists() && Boolean.FALSE.equals(isOverride)) {
return;
// 2生成 view 代码
GeneratePreviewResp viewGeneratePreview = frontendGeneratePreviewList.get(1);
// 例如continew-admin-ui/src/views/system
String vuePath = String.join(File.separator, frontendBasicPackagePath, "views", moduleName, StrUtil
.lowerFirst(genConfig.getClassNamePrefix()));
// 例如continew-admin-ui/src/views/system/user/index.vue
File vueFile = new File(vuePath, viewGeneratePreview.getFileName());
if (!vueFile.exists() || Boolean.TRUE.equals(genConfig.getIsOverride())) {
FileUtil.writeUtf8String(viewGeneratePreview.getContent(), vueFile);
}
FileUtil.writeUtf8String(viewCodePreview.getContent(), indexFile);
// 打包下载
File tempDirFile = new File(tempDir, projectProperties.getAppName());
String zipFilePath = tempDirFile.getPath() + ".zip";
ZipUtil.zip(tempDirFile.getPath(), zipFilePath);
FileUploadUtils.download(request, response, new File(zipFilePath), true);
} catch (Exception e) {
log.error("Generate code occurred an error: {}. tableName: {}.", e.getMessage(), tableName, e);
log.error("Generate code of table '{}' occurred an error. {}", tableName, e.getMessage(), e);
throw new BusinessException("代码生成失败,请手动清理生成文件");
}
}
/**
* 生成后端代码
*
* @param generatePreviewList 生成预览列表
* @param genConfig 生成配置
*/
private void generateBackendCode(List<GeneratePreviewResp> generatePreviewList, GenConfigDO genConfig) {
String backendBasicPackagePath = this.buildBackendBasicPackagePath(genConfig);
Map<String, TemplateConfig> templateConfigMap = generatorProperties.getTemplateConfigs();
for (GeneratePreviewResp generatePreview : generatePreviewList) {
// 获取对应模板配置
TemplateConfig templateConfig = templateConfigMap.get(generatePreview.getFileName()
.replace(genConfig.getClassNamePrefix(), StringConstants.EMPTY)
.replace(FileNameUtil.EXT_JAVA, StringConstants.EMPTY));
// 例如continew-admin/continew-admin-system/src/main/java/top/charles7c/continew/admin/system/service/impl
String packagePath = String.join(File.separator, backendBasicPackagePath, templateConfig.getPackageName()
.replace(StringConstants.DOT, File.separator));
// 例如continew-admin/continew-admin-system/src/main/java/top/charles7c/continew/admin/system/service/impl/XxxServiceImpl.java
File classFile = new File(packagePath, generatePreview.getFileName());
// 如果已经存在且不允许覆盖则跳过
if (!classFile.exists() || Boolean.TRUE.equals(genConfig.getIsOverride())) {
FileUtil.writeUtf8String(generatePreview.getContent(), classFile);
}
}
}
/**
* 构建后端包路径
*
* @param genConfig 生成配置
* @return 后端包路径
*/
private String buildBackendBasicPackagePath(GenConfigDO genConfig) {
// 例如continew-admin/continew-admin-system/src/main/java/top/charles7c/continew/admin/system
return SystemUtil.getUserInfo().getTempDir() + String.join(File.separator, projectProperties
.getAppName(), projectProperties.getAppName(), genConfig.getModuleName(), "src", "main", "java", genConfig
.getPackageName()
.replace(StringConstants.DOT, File.separator));
}
/**
* 预处理生成配置
*

View File

@ -13,6 +13,8 @@
const { proxy } = getCurrentInstance() as any;
// const { dis_enable_status_enum } = proxy.useDict('dis_enable_status_enum');
const queryFormRef = ref();
const formRef = ref();
const dataList = ref<DataRecord[]>([]);
const dataDetail = ref<DataRecord>({
// TODO 待补充详情字段默认值
@ -102,7 +104,7 @@
form.value = {
// TODO 待补充需要重置的字段默认值,详情请参考其他模块 index.vue
};
proxy.$refs.formRef?.resetFields();
formRef.value.resetFields();
};
/**
@ -110,14 +112,14 @@
*/
const handleCancel = () => {
visible.value = false;
proxy.$refs.formRef.resetFields();
formRef.value.resetFields();
};
/**
* 确定
*/
const handleOk = () => {
proxy.$refs.formRef.validate((valid: any) => {
formRef.value.validate((valid: any) => {
if (!valid) {
if (form.value.id !== undefined) {
update(form.value, form.value.id).then((res) => {
@ -227,7 +229,7 @@
* 重置
*/
const resetQuery = () => {
proxy.$refs.queryRef.resetFields();
queryFormRef.value.resetFields();
handleQuery();
};
@ -266,7 +268,7 @@
<div class="header">
<!-- 搜索栏 -->
<div v-if="showQuery" class="header-query">
<a-form ref="queryRef" :model="queryParams" layout="inline">
<a-form ref="queryFormRef" :model="queryParams" layout="inline">
<#list fieldConfigs as fieldConfig>
<#if fieldConfig.showInQuery>
<a-form-item field="${fieldConfig.fieldName}" hide-label>
@ -347,7 +349,6 @@
<!-- 列表区域 -->
<a-table
ref="tableRef"
row-key="id"
:data="dataList"
:loading="loading"

View File

@ -21,6 +21,8 @@ import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
@ -32,7 +34,6 @@ import top.charles7c.continew.admin.generator.model.resp.GeneratePreviewResp;
import top.charles7c.continew.admin.generator.model.resp.TableResp;
import top.charles7c.continew.admin.generator.service.GeneratorService;
import top.charles7c.continew.starter.core.autoconfigure.project.ProjectProperties;
import top.charles7c.continew.starter.core.util.validate.ValidationUtils;
import top.charles7c.continew.starter.extension.crud.model.query.PageQuery;
import top.charles7c.continew.starter.extension.crud.model.resp.PageResp;
import top.charles7c.continew.starter.web.model.R;
@ -102,9 +103,7 @@ public class GeneratorController {
@Parameter(name = "tableName", description = "表名称", required = true, example = "sys_user", in = ParameterIn.PATH)
@SaCheckPermission("tool:generator:list")
@PostMapping("/{tableName}")
public R<Void> generate(@PathVariable String tableName) {
ValidationUtils.throwIf(projectProperties.isProduction(), "仅支持在开发环境生成代码");
generatorService.generate(tableName);
return R.ok("生成成功,请查看生成代码是否正确");
public void generate(@PathVariable String tableName, HttpServletRequest request, HttpServletResponse response) {
generatorService.generate(tableName, request, response);
}
}

View File

@ -8,7 +8,7 @@ generator:
- gen_field_config
## 类型映射
typeMappings:
MySQL:
MYSQL:
Integer:
- int
- tinyint

View File

@ -277,7 +277,6 @@ CREATE TABLE IF NOT EXISTS `gen_config` (
`table_name` varchar(64) NOT NULL COMMENT '表名称',
`module_name` varchar(60) NOT NULL COMMENT '模块名称',
`package_name` varchar(60) NOT NULL COMMENT '包名称',
`frontend_path` varchar(255) DEFAULT NULL COMMENT '前端路径',
`business_name` varchar(50) NOT NULL COMMENT '业务名称',
`author` varchar(100) NOT NULL COMMENT '作者',
`table_prefix` varchar(20) DEFAULT NULL COMMENT '表前缀',

View File

@ -462,7 +462,6 @@ CREATE TABLE IF NOT EXISTS "gen_config" (
"table_name" varchar(64) NOT NULL,
"module_name" varchar(60) NOT NULL,
"package_name" varchar(60) NOT NULL,
"frontend_path" varchar(255) DEFAULT NULL,
"business_name" varchar(50) NOT NULL,
"author" varchar(100) NOT NULL,
"table_prefix" varchar(20) DEFAULT NULL,
@ -474,7 +473,6 @@ CREATE TABLE IF NOT EXISTS "gen_config" (
COMMENT ON COLUMN "gen_config"."table_name" IS '表名称';
COMMENT ON COLUMN "gen_config"."module_name" IS '模块名称';
COMMENT ON COLUMN "gen_config"."package_name" IS '包名称';
COMMENT ON COLUMN "gen_config"."frontend_path" IS '前端路径';
COMMENT ON COLUMN "gen_config"."business_name" IS '业务名称';
COMMENT ON COLUMN "gen_config"."author" IS '作者';
COMMENT ON COLUMN "gen_config"."table_prefix" IS '表前缀';