代码生成器研发与低代码平台引擎
代码生成器架构
元数据驱动
代码生成器的核心设计思想是元数据驱动:通过解析数据源中的数据库结构信息,提取表、列、索引、外键等元数据,生成对应的代码文件。元数据的流转链路如下:
数据源 → 表结构 → 列信息 → 关系 → 代码上下文 → 模板渲染 → 文件输出元数据模型定义:
// 数据源配置
public class DataSourceConfig {
private String url;
private String username;
private String password;
private String driverClassName; // com.mysql.cj.jdbc.Driver
private String dbType; // mysql, oracle, postgresql
}
// 表元数据
public class TableMetaData {
private String tableName;
private String tableComment;
private String schema;
private String catalog;
private String primaryKey; // 主键列名
private List<ColumnMetaData> columns;
private List<TableIndex> indexes;
private List<ForeignKey> foreignKeys;
}
// 列元数据
public class ColumnMetaData {
private String columnName;
private String columnComment;
private String jdbcType; // VARCHAR, INTEGER, DATE
private String javaType; // String, Integer, LocalDate
private int length;
private int precision;
private int scale;
private boolean nullable;
private boolean primaryKey;
private boolean autoIncrement;
private String defaultValue;
}
// 外键关系
public class ForeignKey {
private String fkName;
private String columnName;
private String referencedTable;
private String referencedColumn;
private String deleteRule; // CASCADE, SET NULL, RESTRICT
}元数据读取示例:
public class MetaDataReader {
public List<TableMetaData> readTables(DataSource dataSource) throws SQLException {
List<TableMetaData> tables = new ArrayList<>();
DatabaseMetaData metaData = dataSource.getConnection().getMetaData();
try (ResultSet rs = metaData.getTables(null, null, "%", new String[]{"TABLE"})) {
while (rs.next()) {
TableMetaData table = new TableMetaData();
table.setTableName(rs.getString("TABLE_NAME"));
table.setTableComment(getTableComment(metaData, table.getTableName()));
table.setColumns(readColumns(metaData, table.getTableName()));
table.setForeignKeys(readForeignKeys(metaData, table.getTableName()));
tables.add(table);
}
}
return tables;
}
private List<ColumnMetaData> readColumns(DatabaseMetaData metaData, String tableName) throws SQLException {
List<ColumnMetaData> columns = new ArrayList<>();
try (ResultSet rs = metaData.getColumns(null, null, tableName, "%")) {
while (rs.next()) {
ColumnMetaData col = new ColumnMetaData();
col.setColumnName(rs.getString("COLUMN_NAME"));
col.setJdbcType(rs.getString("TYPE_NAME"));
col.setLength(rs.getInt("COLUMN_SIZE"));
col.setNullable(rs.getInt("NULLABLE") == DatabaseMetaData.columnNullable);
col.setDefaultValue(rs.getString("COLUMN_DEF"));
col.setColumnComment(rs.getString("REMARKS"));
col.setJavaType(jdbcTypeToJavaType(col.getJdbcType()));
columns.add(col);
}
}
return columns;
}
private String jdbcTypeToJavaType(String jdbcType) {
Map<String, String> mapping = new HashMap<>();
mapping.put("VARCHAR", "String");
mapping.put("CHAR", "String");
mapping.put("INTEGER", "Integer");
mapping.put("BIGINT", "Long");
mapping.put("DECIMAL", "BigDecimal");
mapping.put("DATE", "LocalDate");
mapping.put("DATETIME", "LocalDateTime");
mapping.put("TIMESTAMP", "LocalDateTime");
mapping.put("TINYINT", "Boolean");
mapping.put("TEXT", "String");
mapping.put("BLOB", "byte[]");
return mapping.getOrDefault(jdbcType, "String");
}
}模板引擎选型
代码生成器的核心是将元数据与模板文件结合,生成目标代码。常见的模板引擎对比如下:
| 特性 | Freemarker | Velocity | Thymeleaf | Mustache |
|---|---|---|---|---|
| 语法简洁度 | 中等 | 简单 | 较复杂(XML风格) | 极简 |
| 类型安全 | 弱类型 | 弱类型 | 较强 | 无类型 |
| 自定义指令 | 支持(TemplateDirectiveModel) | 支持(EventHandler) | 支持(Dialect) | 有限 |
| 空值处理 | 内置 ?. 语法 | 需配置 | 内置 | 默认不输出 |
| 宏/函数 | 支持 macro | 支持 macro | 支持 fragment | 支持 partial |
| 性能 | 高(编译缓存) | 高 | 中等 | 高 |
| Spring 整合 | 完善 | 完善 | 原生 Spring Boot | 完善 |
| 社区活跃度 | 高 | 低(已退役) | 高 | 中 |
| 推荐场景 | Java 后端代码生成 | 兼容旧项目 | 前端模板/邮件 | 简单替换场景 |
推荐选型:Freemarker 是代码生成器场景的首选,原因如下:
- 成熟的 Java 模板引擎,社区生态完善
- 支持空值安全处理(
${fieldName!}) - 强大的自定义指令和宏机制,便于复用模板片段
- 编译缓存机制,批量生成时性能优异
- 对纯文本输出无额外标签污染(Thymeleaf 的 XML 标签在代码生成中引入额外复杂度)
生成流程
代码生成的标准流程分为四个阶段:
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 读取元数据 │ → │ 加载模板 │ → │ 上下文填充 │ → │ 文件输出 │
│ │ │ │ │ │ │ │
│ DB→Table │ │ .ftl解析 │ │ 注入变量 │ │ write+格式化│
└──────────┘ └──────────┘ └──────────┘ └──────────┘生成引擎核心实现:
public class CodeGeneratorEngine {
private Configuration freemarkerConfig;
private MetaDataReader metaDataReader;
private GeneratorConfig config;
public void generate(DataSource dataSource) {
// 1. 读取元数据
List<TableMetaData> tables = metaDataReader.readTables(dataSource);
for (TableMetaData table : tables) {
// 2. 构建上下文
TemplateContext context = buildContext(table);
// 3. 按生成策略渲染模板
List<GeneratorTask> tasks = config.getStrategy().resolveTasks(table);
for (GeneratorTask task : tasks) {
// 4. 模板渲染
String output = renderTemplate(task.getTemplateName(), context);
// 5. 文件输出(含覆盖策略处理)
writeFile(task.getOutputPath(), output, context);
}
}
}
private TemplateContext buildContext(TableMetaData table) {
TemplateContext ctx = new TemplateContext();
ctx.setTableName(table.getTableName());
ctx.setClassName(toPascalCase(table.getTableName()));
ctx.setPackageName(config.getPackageName());
ctx.setFields(table.getColumns().stream()
.map(this::toFieldMeta)
.collect(Collectors.toList()));
ctx.setImports(resolveImports(table.getColumns()));
ctx.setPrimaryKey(findPrimaryKey(table));
return ctx;
}
private String renderTemplate(String templateName, TemplateContext context) {
try {
Template template = freemarkerConfig.getTemplate(templateName);
StringWriter writer = new StringWriter();
template.process(context, writer);
return writer.toString();
} catch (Exception e) {
throw new GenerationException("模板渲染失败: " + templateName, e);
}
}
}代码生成器配置
YAML 配置示例:
generator:
# 数据源配置
datasource:
url: jdbc:mysql://localhost:3306/your_db?useUnicode=true&characterEncoding=utf-8
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
schema: public
# 包名结构
package:
parent: com.example.project
module: system
entity: entity
mapper: mapper
service: service
serviceImpl: service.impl
controller: controller
dto: dto
vo: vo
converter: converter
# 生成策略
strategy:
# 包含的表(支持通配符)
include-tables:
- sys_user
- sys_role
- sys_menu
# 排除的表
exclude-tables:
- flyway_schema_history
- quartz_*
# 要生成的模块
modules:
- entity
- mapper
- service
- controller
- vue
# 文件覆盖规则
file-override:
entity: skip # 已有实体不覆盖
mapper: override # Mapper 直接覆盖
service: backup # 备份后覆盖
controller: merge # 差异合并
vue: skip
# 表前缀移除(生成类名时自动去除)
table-prefix:
- sys_
- t_
# 模板路径
template:
base-path: templates/codegen
mappings:
entity: Entity.java.ftl
mapper: Mapper.java.ftl
mapperXml: Mapper.xml.ftl
service: Service.java.ftl
serviceImpl: ServiceImpl.java.ftl
controller: Controller.java.ftl
dto: DTO.java.ftl
vo: VO.java.ftl
converter: Converter.java.ftl
vueIndex: index.vue.ftl
vueApi: api.js.ftl模板设计
分层模板体系
代码生成器按分层架构设计模板文件,覆盖后端完整分层和前端页面:
templates/codegen/
├── java/ # Java 后端模板
│ ├── Entity.java.ftl
│ ├── Mapper.java.ftl
│ ├── Mapper.xml.ftl
│ ├── Service.java.ftl
│ ├── ServiceImpl.java.ftl
│ ├── Controller.java.ftl
│ ├── DTO.java.ftl # 数据传输对象(新增/修改请求)
│ ├── VO.java.ftl # 视图对象(列表/详情响应)
│ └── Converter.java.ftl # 实体与 VO/DTO 转换器
├── vue/ # Vue 前端模板
│ ├── index.vue.ftl # 列表页面
│ ├── search-form.vue.ftl # 搜索表单组件
│ ├── dialog-form.vue.ftl # 新增/编辑对话框
│ └── api.js.ftl # API 接口封装
└── common/ # 公共片段
├── import-macro.ftl # 导入语句宏
├── field-macro.ftl # 字段遍历宏
└── annotation-macro.ftl # 注解生成宏模板变量设计
上下文变量注入规范:
public class TemplateContext {
// -------- 包名相关 --------
private String parentPackage; // com.example.project
private String modulePackage; // system
private String entityPackage; // com.example.project.system.entity
private String mapperPackage;
private String servicePackage;
private String controllerPackage;
private String dtoPackage;
private String voPackage;
private String converterPackage;
// -------- 类名相关 --------
private String className; // SysUser
private String classNameLower; // sysUser
private String classNameSnake; // sys_user
private String classNamePlural; // SysUserList (集合类名)
// -------- 表相关 --------
private String tableName; // sys_user
private String tableComment; // 系统用户表
// -------- 字段相关 --------
private List<FieldMeta> fields; // 所有字段
private FieldMeta primaryKey; // 主键字段
private List<String> imports; // 需要导入的 Java 类型
// -------- JPA/MyBatis-Plus 相关 --------
private String primaryKeyType; // Long
private String primaryKeyName; // id
private boolean hasLocalDate; // 是否包含日期类型
private boolean hasBigDecimal; // 是否包含 BigDecimal
}
public class FieldMeta {
private String fieldName; // userName
private String fieldNameUpper; // UserName
private String columnName; // user_name
private String fieldType; // String
private String jdbcType; // VARCHAR
private String fieldComment; // 用户名
private int length; // 50
private boolean nullable; // 是否可为空
private boolean primaryKey; // 是否主键
private boolean autoIncrement; // 是否自增
private String defaultValue; // 默认值
private boolean queryable; // 是否参与查询
private boolean listVisible; // 列表是否显示
private boolean formVisible; // 表单是否显示
}CRUD 模板示例
Entity.java.ftl:
package ${entityPackage};
<#list imports as import>
import ${import};
</#list>
/**
* ${tableComment}
*/
<#if tableName??>
@TableName("${tableName}")
</#if>
public class ${className} implements Serializable {
private static final long serialVersionUID = 1L;
<#list fields as field>
<#if field.fieldComment??>
/** ${field.fieldComment} */
</#if>
<#if field.primaryKey>
@TableId(value = "${field.columnName}", type = IdType.${field.autoIncrement?then('AUTO', 'INPUT')})
<#else>
@TableField("${field.columnName}")
</#if>
private ${field.fieldType} ${field.fieldName};
</#list>
<#list fields as field>
public ${field.fieldType} get${field.fieldNameUpper}() {
return ${field.fieldName};
}
public void set${field.fieldNameUpper}(${field.fieldType} ${field.fieldName}) {
this.${field.fieldName} = ${field.fieldName};
}
</#list>
}Mapper.java.ftl:
package ${mapperPackage};
import ${entityPackage}.${className};
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
/**
* ${tableComment} Mapper 接口
*/
@Mapper
public interface ${className}Mapper extends BaseMapper<${className}> {
}Service.java.ftl:
package ${servicePackage};
import ${entityPackage}.${className};
import com.baomidou.mybatisplus.extension.service.IService;
/**
* ${tableComment} 服务接口
*/
public interface ${className}Service extends IService<${className}> {
}ServiceImpl.java.ftl:
package ${serviceImplPackage};
import ${entityPackage}.${className};
import ${mapperPackage}.${className}Mapper;
import ${servicePackage}.${className}Service;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
/**
* ${tableComment} 服务实现
*/
@Service
public class ${className}ServiceImpl extends ServiceImpl<${className}Mapper, ${className}>
implements ${className}Service {
}Controller.java.ftl:
package ${controllerPackage};
import ${entityPackage}.${className};
import ${dtoPackage}.${className}DTO;
import ${voPackage}.${className}VO;
import ${converterPackage}.${className}Converter;
import ${servicePackage}.${className}Service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
import java.util.List;
/**
* ${tableComment} 控制器
*/
@RestController
@RequestMapping("/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}")
@RequiredArgsConstructor
public class ${className}Controller {
private final ${className}Service ${classNameLower}Service;
@GetMapping("/page")
public Result<IPage<${className}VO>> page(${className}DTO dto, PageParam pageParam) {
Page<${className}> page = ${classNameLower}Service.page(pageParam.toPage());
return Result.success(page.convert(${className}Converter.INSTANCE::toVO));
}
@GetMapping("/{id}")
public Result<${className}VO> get(@PathVariable ${primaryKeyType} id) {
${className} entity = ${classNameLower}Service.getById(id);
return Result.success(${className}Converter.INSTANCE.toVO(entity));
}
@PostMapping
public Result<Void> add(@Valid @RequestBody ${className}DTO dto) {
${className} entity = ${className}Converter.INSTANCE.toEntity(dto);
${classNameLower}Service.save(entity);
return Result.success();
}
@PutMapping("/{id}")
public Result<Void> update(@PathVariable ${primaryKeyType} id, @Valid @RequestBody ${className}DTO dto) {
${className} entity = ${className}Converter.INSTANCE.toEntity(dto);
entity.set${primaryKey.fieldNameUpper}(id);
${classNameLower}Service.updateById(entity);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(@PathVariable ${primaryKeyType} id) {
${classNameLower}Service.removeById(id);
return Result.success();
}
@GetMapping("/list")
public Result<List<${className}VO>> list(${className}DTO dto) {
LambdaQueryWrapper<${className}> wrapper = buildQueryWrapper(dto);
List<${className}> list = ${classNameLower}Service.list(wrapper);
return Result.success(${className}Converter.INSTANCE.toVOList(list));
}
private LambdaQueryWrapper<${className}> buildQueryWrapper(${className}DTO dto) {
LambdaQueryWrapper<${className}> wrapper = new LambdaQueryWrapper<>();
<#list fields as field>
<#if field.queryable>
<#if field.fieldType == 'String'>
wrapper.like(dto.get${field.fieldNameUpper}() != null, ${className}::get${field.fieldNameUpper}, dto.get${field.fieldNameUpper}());
<#else>
wrapper.eq(dto.get${field.fieldNameUpper}() != null, ${className}::get${field.fieldNameUpper}, dto.get${field.fieldNameUpper}());
</#if>
</#if>
</#list>
return wrapper;
}
}DTO.java.ftl:
package ${dtoPackage};
<#list imports as import>
import ${import};
</#list>
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* ${tableComment} 数据传输对象
*/
@Data
@Schema(description = "${tableComment}数据传输对象")
public class ${className}DTO {
<#list fields as field>
<#if field.fieldComment??>
@Schema(description = "${field.fieldComment}")
</#if>
<#if !field.nullable && !field.primaryKey>
@NotBlank<#if field.fieldType != 'String'>(message = "${field.fieldComment}不能为空")</#if>
</#if>
private ${field.fieldType} ${field.fieldName};
</#list>
}VO.java.ftl:
package ${voPackage};
<#list imports as import>
import ${import};
</#list>
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
* ${tableComment} 视图对象
*/
@Data
@Schema(description = "${tableComment}视图对象")
public class ${className}VO {
<#list fields as field>
<#if field.fieldComment??>
@Schema(description = "${field.fieldComment}")
</#if>
private ${field.fieldType} ${field.fieldName};
</#list>
}Converter.java.ftl:
package ${converterPackage};
import ${entityPackage}.${className};
import ${dtoPackage}.${className}DTO;
import ${voPackage}.${className}VO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* ${tableComment} 对象转换器
*/
@Mapper
public interface ${className}Converter {
${className}Converter INSTANCE = Mappers.getMapper(${className}Converter.class);
${className} toEntity(${className}DTO dto);
${className}VO toVO(${className} entity);
List<${className}VO> toVOList(List<${className}> entityList);
}前端模板示例
index.vue.ftl:
<template>
<div class="${classNameLower}-container">
<SearchForm :model="queryParams" @search="handleSearch" @reset="handleReset" />
<div class="table-wrapper">
<div class="table-header">
<div class="title">${tableComment}</div>
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<el-table :data="tableData" v-loading="loading" border stripe>
<el-table-column type="index" label="序号" width="60" align="center" />
<#list fields as field>
<#if field.listVisible>
<el-table-column prop="${field.fieldName}" label="${field.fieldComment}" <#if field.fieldType == 'LocalDateTime'>width="180"</#if> />
</#if>
</#list>
<el-table-column label="操作" width="220" fixed="right">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
v-model:current-page="queryParams.page"
v-model:page-size="queryParams.size"
:total="total"
layout="total, sizes, prev, pager, next, jumper"
@size-change="getList"
@current-change="getList"
/>
</div>
<DialogForm
v-model="dialogVisible"
:title="dialogTitle"
:form-data="formData"
@success="getList"
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { get${className}Page, delete${className} } from '@/api/${classNameLower}'
import SearchForm from './components/search-form.vue'
import DialogForm from './components/dialog-form.vue'
import type { ${className}DTO, ${className}VO } from './types'
const loading = ref(false)
const tableData = ref<${className}VO[]>([])
const total = ref(0)
const dialogVisible = ref(false)
const dialogTitle = ref('')
const formData = ref<${className}DTO>({})
const queryParams = ref({
page: 1,
size: 10,
<#list fields as field>
<#if field.queryable>
${field.fieldName}: undefined,
</#if>
</#list>
})
const getList = async () => {
loading.value = true
try {
const res = await get${className}Page(queryParams.value)
tableData.value = res.data.records
total.value = res.data.total
} finally {
loading.value = false
}
}
const handleSearch = () => {
queryParams.value.page = 1
getList()
}
const handleReset = () => {
queryParams.value = {
page: 1,
size: 10,
<#list fields as field>
<#if field.queryable>
${field.fieldName}: undefined,
</#if>
</#list>
}
getList()
}
const handleAdd = () => {
dialogTitle.value = '新增${tableComment}'
formData.value = {}
dialogVisible.value = true
}
const handleEdit = (row: ${className}VO) => {
dialogTitle.value = '编辑${tableComment}'
formData.value = { ...row }
dialogVisible.value = true
}
const handleDelete = async (row: ${className}VO) => {
await delete${className}(row.id)
getList()
}
onMounted(() => {
getList()
})
</script>
<style scoped>
.${classNameLower}-container {
padding: 16px;
}
.table-wrapper {
background: #fff;
border-radius: 8px;
padding: 16px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.title {
font-size: 16px;
font-weight: 600;
}
</style>search-form.vue.ftl:
<template>
<el-form :model="model" layout="inline" class="search-form">
<#list fields as field>
<#if field.queryable>
<el-form-item label="${field.fieldComment}">
<#if field.fieldType == 'String'>
<el-input v-model="model.${field.fieldName}" placeholder="请输入${field.fieldComment}" clearable />
<#elseif field.fieldType == 'LocalDate' || field.fieldType == 'LocalDateTime'>
<el-date-picker v-model="model.${field.fieldName}" type="date" placeholder="选择${field.fieldComment}" clearable />
<#else>
<el-input-number v-model="model.${field.fieldName}" placeholder="请输入${field.fieldComment}" />
</#if>
</el-form-item>
</#if>
</#list>
<el-form-item>
<el-button type="primary" @click="$emit('search')">查询</el-button>
<el-button @click="$emit('reset')">重置</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
defineProps<{
model: Record<string, any>
}>()
defineEmits<{
search: []
reset: []
}>()
</script>
<style scoped>
.search-form {
background: #fff;
border-radius: 8px;
padding: 16px;
margin-bottom: 16px;
}
</style>dialog-form.vue.ftl:
<template>
<el-dialog v-model="visible" :title="title" width="600px">
<el-form ref="formRef" :model="formData" :rules="rules" label-width="100px">
<#list fields as field>
<#if field.formVisible>
<el-form-item label="${field.fieldComment}" prop="${field.fieldName}">
<#if field.fieldType == 'String'>
<el-input v-model="formData.${field.fieldName}" placeholder="请输入${field.fieldComment}" />
<#elseif field.fieldType == 'LocalDate' || field.fieldType == 'LocalDateTime'>
<el-date-picker v-model="formData.${field.fieldName}" type="date" placeholder="选择${field.fieldComment}" style="width: 100%" />
<#elseif field.fieldType == 'Integer' || field.fieldType == 'Long'>
<el-input-number v-model="formData.${field.fieldName}" placeholder="请输入${field.fieldComment}" style="width: 100%" />
<#elseif field.fieldType == 'BigDecimal'>
<el-input-number v-model="formData.${field.fieldName}" :precision="2" placeholder="请输入${field.fieldComment}" style="width: 100%" />
<#else>
<el-input v-model="formData.${field.fieldName}" placeholder="请输入${field.fieldComment}" />
</#if>
</el-form-item>
</#if>
</#list>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit">确认</el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import type { FormInstance } from 'element-plus'
import { add${className}, update${className} } from '@/api/${classNameLower}'
import type { ${className}DTO } from '../types'
const props = defineProps<{
modelValue: boolean
title: string
formData: ${className}DTO
}>()
const emit = defineEmits<{
'update:modelValue': [value: boolean]
success: []
}>()
const visible = ref(false)
const submitting = ref(false)
const formRef = ref<FormInstance>()
watch(() => props.modelValue, (val) => {
visible.value = val
})
watch(visible, (val) => {
emit('update:modelValue', val)
})
const rules: Record<string, any[]> = {
<#list fields as field>
<#if !field.nullable && !field.primaryKey>
${field.fieldName}: [
{ required: true, message: '请输入${field.fieldComment}', trigger: 'blur' }
],
</#if>
</#list>
}
const handleSubmit = async () => {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
if (props.formData.id) {
await update${className}(props.formData.id, props.formData)
} else {
await add${className}(props.formData)
}
visible.value = false
emit('success')
} finally {
submitting.value = false
}
}
</script> break
case 'openDialog':
// 打开对话框
break
case 'callApi':
// 调用 API
break
case 'showMessage':
// 显示消息
break
case 'confirm':
// 确认弹窗
break
}
}自定义生成策略
配置化模板选择
按不同粒度配置模板映射关系,支持三级覆盖:
public class TemplateSelectionStrategy {
private Map<String, String> globalTemplates; // 全局默认模板
private Map<String, Map<String, String>> moduleTemplates; // 按模块
private Map<String, Map<String, String>> tableTemplates; // 按表
/**
* 解析指定表的模板映射
* 优先级:按表 > 按模块 > 全局
*/
public Map<String, String> resolveTemplates(String tableName, String moduleName) {
Map<String, String> result = new HashMap<>(globalTemplates);
// 模块级覆盖
if (moduleTemplates.containsKey(moduleName)) {
result.putAll(moduleTemplates.get(moduleName));
}
// 表级覆盖
if (tableTemplates.containsKey(tableName)) {
result.putAll(tableTemplates.get(tableName));
}
return result;
}
}配置示例:
generator:
template:
# 全局默认模板
global:
entity: templates/default/Entity.java.ftl
mapper: templates/default/Mapper.java.ftl
service: templates/default/Service.java.ftl
controller: templates/default/Controller.java.ftl
vue: templates/default/index.vue.ftl
# 按模块覆盖
modules:
system:
entity: templates/system/Entity.java.ftl # 系统模块使用含审计字段的实体模板
report:
controller: templates/report/Controller.java.ftl # 报表模块使用只读控制器
# 按表覆盖
tables:
sys_config:
controller: templates/config/Controller.java.ftl # 配置表的自定义控制器
sys_log:
service: templates/log/Service.java.ftl # 日志表只读服务文件命名与路径策略
路径解析器实现:
public class FilePathResolver {
public String resolveOutputPath(String module, String templateType, TemplateContext context) {
String packagePath = context.getPackageName().replace('.', '/');
switch (templateType) {
case "entity":
return String.format(
"%s/%s/%s.java",
packagePath, context.getEntityPackage(), context.getClassName()
);
case "mapper":
return String.format(
"%s/%s/%sMapper.java",
packagePath, context.getMapperPackage(), context.getClassName()
);
case "service":
return String.format(
"%s/%s/%sService.java",
packagePath, context.getServicePackage(), context.getClassName()
);
case "controller":
return String.format(
"%s/%s/%sController.java",
packagePath, context.getControllerPackage(), context.getClassName()
);
case "vue":
return String.format(
"src/views/%s/%s/index.vue",
module, context.getClassNameLower()
);
case "vueApi":
return String.format(
"src/api/%s/%s.ts",
module, context.getClassNameLower()
);
default:
throw new IllegalArgumentException("未知模板类型: " + templateType);
}
}
public String resolveOutputPathMultiModule(String module, String templateType,
TemplateContext context) {
// 多模块场景:每个模块有独立的 src 目录
String basePath = module + "/src/main/java";
String packagePath = context.getParentPackage().replace('.', '/');
return String.format(
"%s/%s/%s/%s/%s.java",
basePath, packagePath, module, templateType + "s", context.getClassName()
);
}
}覆盖策略
代码生成器的文件覆盖策略决定了当目标文件已存在时的处理方式:
public enum OverrideStrategy {
SKIP, // 跳过:保留已有文件,不生成
OVERRIDE, // 覆盖:直接覆盖已有文件
BACKUP, // 备份:将原文件重命名为 .bak 后再生成
MERGE // 差异合并:保留用户修改区域,仅覆盖生成区域
}覆盖策略处理器:
public class FileWriteHandler {
private static final Logger log = LoggerFactory.getLogger(FileWriteHandler.class);
public void writeFile(File targetFile, String content, OverrideStrategy strategy) {
if (!targetFile.exists()) {
FileUtils.writeStringToFile(targetFile, content, StandardCharsets.UTF_8);
return;
}
switch (strategy) {
case SKIP:
log.info("跳过已存在文件: {}", targetFile.getPath());
break;
case OVERRIDE:
FileUtils.writeStringToFile(targetFile, content, StandardCharsets.UTF_8);
log.info("覆盖文件: {}", targetFile.getPath());
break;
case BACKUP:
File backupFile = new File(targetFile.getPath() + ".bak."
+ LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")));
FileUtils.copyFile(targetFile, backupFile);
FileUtils.writeStringToFile(targetFile, content, StandardCharsets.UTF_8);
log.info("已备份至: {}, 重新生成: {}", backupFile.getPath(), targetFile.getPath());
break;
case MERGE:
mergeWithUserCode(targetFile, content);
break;
}
}
/**
* 差异合并:保留用户手动修改的区域
* 通过标记注释 <!-- begin-generated --> / <!-- end-generated --> 来界定可覆盖区域
*/
private void mergeWithUserCode(File targetFile, String newContent) {
try {
String existingContent = FileUtils.readFileToString(targetFile, StandardCharsets.UTF_8);
String merged = performMerge(existingContent, newContent);
FileUtils.writeStringToFile(targetFile, merged, StandardCharsets.UTF_8);
} catch (IOException e) {
throw new GenerationException("合并文件失败: " + targetFile.getPath(), e);
}
}
private String performMerge(String existing, String generated) {
// 使用自定义区域标记进行合并
// 用户代码放在 @Customize @EndCustomize 注释块之间,不会被覆盖
Pattern pattern = Pattern.compile(
"// @Customize[\\s\\S]*?// @EndCustomize",
Pattern.MULTILINE
);
Matcher matcher = pattern.matcher(existing);
List<String> userBlocks = new ArrayList<>();
while (matcher.find()) {
userBlocks.add(matcher.group());
}
if (userBlocks.isEmpty()) {
return generated; // 无用户自定义代码,直接覆盖
}
// 将用户代码块插入到生成的代码末尾
StringBuilder merged = new StringBuilder(generated);
merged.append("\n\n");
merged.append(" // ===== 用户自定义代码 =====\n");
for (String block : userBlocks) {
merged.append(block).append("\n");
}
return merged.toString();
}
}低代码平台引擎
数据模型设计
低代码平台的数据模型是表单、页面、流程的基础,用 JSON Schema 描述实体定义。
实体定义模型:
{
"entityCode": "sys_user",
"entityName": "系统用户",
"tableName": "sys_user",
"fields": [
{
"fieldCode": "username",
"fieldName": "用户名",
"fieldType": "string",
"componentType": "input",
"maxLength": 50,
"required": true,
"unique": true,
"defaultValue": null,
"placeholder": "请输入用户名",
"rules": [
{ "type": "required", "message": "用户名不能为空" },
{ "type": "minLength", "value": 2, "message": "用户名至少2个字符" },
{ "type": "pattern", "value": "^[a-zA-Z0-9_]+$", "message": "仅支持字母数字下划线" }
]
},
{
"fieldCode": "email",
"fieldName": "邮箱",
"fieldType": "string",
"componentType": "input",
"maxLength": 100,
"required": false,
"rules": [
{ "type": "pattern", "value": "^\\w+@\\w+\\.\\w+$", "message": "邮箱格式不正确" }
]
},
{
"fieldCode": "age",
"fieldName": "年龄",
"fieldType": "integer",
"componentType": "number",
"minValue": 0,
"maxValue": 150,
"required": false
},
{
"fieldCode": "birthday",
"fieldName": "生日",
"fieldType": "date",
"componentType": "datePicker",
"required": false
},
{
"fieldCode": "status",
"fieldName": "状态",
"fieldType": "integer",
"componentType": "select",
"required": true,
"defaultValue": 1,
"options": [
{ "label": "启用", "value": 1 },
{ "label": "禁用", "value": 0 }
]
},
{
"fieldCode": "deptId",
"fieldName": "所属部门",
"fieldType": "integer",
"componentType": "treeSelect",
"required": false,
"relation": {
"type": "manyToOne",
"targetEntity": "sys_dept",
"targetField": "deptName",
"foreignKey": "dept_id"
}
}
],
"relations": [
{
"relationType": "oneToMany",
"targetEntity": "sys_user_role",
"targetField": "userId",
"cascade": "ALL"
}
]
}Java 实体定义类:
public class EntityDefinition {
private String id;
private String entityCode; // 实体编码
private String entityName; // 实体名称
private String tableName; // 数据库表名
private String description; // 描述
private List<FieldDefinition> fields;
private List<RelationDefinition> relations;
private JSONSchema schema; // JSON Schema 描述
}
public class FieldDefinition {
private String fieldCode; // 字段编码
private String fieldName; // 字段名称
private FieldType fieldType; // string, integer, number, boolean, date, datetime, array, object
private String componentType; // input, number, select, datePicker, radio, checkbox, switch, treeSelect, upload
private boolean required;
private boolean unique;
private Object defaultValue;
private String placeholder;
private List<ValidateRule> rules; // 校验规则
private Integer maxLength;
private Integer minValue;
private Integer maxValue;
private List<OptionItem> options; // 选项列表(select/radio/checkbox 使用)
private RelationInfo relation; // 关联关系
private FieldLayout layout; // 布局信息
}
public enum FieldType {
STRING, INTEGER, NUMBER, BOOLEAN, DATE, DATETIME, ARRAY, OBJECT, FILE, IMAGE
}
public class ValidateRule {
private String type; // required, minLength, maxLength, pattern, min, max, custom
private Object value;
private String message;
}
public class RelationInfo {
private RelationType type; // oneToOne, manyToOne, oneToMany, manyToMany
private String targetEntity;
private String targetField;
private String foreignKey;
private String cascade; // ALL, PERSIST, MERGE, REMOVE, NONE
}JSON Schema 生成:
public class JsonSchemaGenerator {
public JSONSchema generateSchema(EntityDefinition entity) {
JSONSchema schema = new JSONSchema();
schema.setTitle(entity.getEntityName());
schema.setType("object");
schema.setProperties(new HashMap<>());
for (FieldDefinition field : entity.getFields()) {
JSONSchemaProperty prop = new JSONSchemaProperty();
switch (field.getFieldType()) {
case STRING:
prop.setType("string");
if (field.getMaxLength() != null) {
prop.setMaxLength(field.getMaxLength());
}
if (field.getPlaceholder() != null) {
prop.setDescription(field.getPlaceholder());
}
break;
case INTEGER:
prop.setType("integer");
if (field.getMinValue() != null) prop.setMinimum(field.getMinValue());
if (field.getMaxValue() != null) prop.setMaximum(field.getMaxValue());
break;
case NUMBER:
prop.setType("number");
break;
case BOOLEAN:
prop.setType("boolean");
break;
case DATE:
prop.setType("string");
prop.setFormat("date");
break;
case DATETIME:
prop.setType("string");
prop.setFormat("date-time");
break;
case ARRAY:
prop.setType("array");
prop.setItems(new JSONSchemaProperty());
break;
}
if (field.getRules() != null) {
for (ValidateRule rule : field.getRules()) {
switch (rule.getType()) {
case "required":
schema.addRequired(field.getFieldCode());
break;
case "pattern":
prop.setPattern((String) rule.getValue());
break;
case "minLength":
prop.setMinLength((Integer) rule.getValue());
break;
case "maxLength":
prop.setMaxLength((Integer) rule.getValue());
break;
}
}
}
schema.getProperties().put(field.getFieldCode(), prop);
}
return schema;
}
}表单引擎
表单引擎负责将 JSON Schema 转换为可交互的 UI 表单。
核心架构:
JSON Schema (数据模型定义)
│
▼
FormSchemaTransformer (Schema → FormConfig)
│
├── LayoutEngine 布局渲染
├── FieldRenderer 字段组件渲染
├── ValidationEngine 校验引擎
└── LinkageEngine 联动规则引擎
│
▼
最终 UI 表单(Vue/React)表单配置对象:
// TypeScript 表单配置类型
interface FormConfig {
modelName: string
labelWidth?: number
layout: LayoutConfig
fields: FormFieldConfig[]
rules: Record<string, FormValidationRule[]>
linkages: LinkageRule[]
events: FormEvent[]
}
interface FormFieldConfig {
fieldCode: string
fieldName: string
componentType: ComponentType
props: Record<string, any> // 组件属性
colSpan: number // 栅格列数
order: number
visible: boolean
disabled: boolean
readonly: boolean
defaultValue: any
}
type ComponentType =
| 'input'
| 'textarea'
| 'number'
| 'select'
| 'multiSelect'
| 'radio'
| 'checkbox'
| 'switch'
| 'datePicker'
| 'dateRange'
| 'timePicker'
| 'upload'
| 'imageUpload'
| 'editor'
| 'treeSelect'
| 'cascader'
| 'rate'
| 'slider'
| 'custom'JSON Schema 转 UI 渲染:
<template>
<el-form
ref="formRef"
:model="formData"
:rules="formConfig.rules"
:label-width="formConfig.labelWidth || 100"
>
<el-row :gutter="20">
<template v-for="field in visibleFields" :key="field.fieldCode">
<el-col :span="field.colSpan || 24">
<el-form-item
:label="field.fieldName"
:prop="field.fieldCode"
v-show="field.visible"
>
<component
:is="getComponent(field.componentType)"
v-model="formData[field.fieldCode]"
v-bind="field.props"
:disabled="field.disabled || field.readonly"
@change="handleFieldChange(field.fieldCode, $event)"
/>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import type { FormConfig, FormFieldConfig } from './types'
const props = defineProps<{
formConfig: FormConfig
initialData?: Record<string, any>
}>()
const formRef = ref()
const formData = reactive(props.initialData || {})
// 组件映射表
const componentMap: Record<string, string> = {
input: 'el-input',
textarea: 'el-input',
number: 'el-input-number',
select: 'el-select',
radio: 'el-radio-group',
checkbox: 'el-checkbox-group',
switch: 'el-switch',
datePicker: 'el-date-picker',
treeSelect: 'el-tree-select',
cascader: 'el-cascader',
upload: 'el-upload',
editor: 'el-input',
}
const getComponent = (type: string) => {
return componentMap[type] || 'el-input'
}
// 联动处理
const visibleFields = computed(() => {
const linkages = props.formConfig.linkages || []
const results: FormFieldConfig[] = []
for (const field of props.formConfig.fields) {
let visible = field.visible !== false
for (const linkage of linkages) {
if (linkage.targetField === field.fieldCode) {
visible = evaluateLinkage(linkage, formData)
}
}
results.push({ ...field, visible })
}
return results.filter(f => f.visible !== false)
})
const handleFieldChange = (fieldCode: string, value: any) => {
// 触发联动逻辑
const linkages = props.formConfig.linkages || []
for (const linkage of linkages) {
if (linkage.sourceField === fieldCode) {
// 联动目标字段自动更新
if (linkage.type === 'setValue') {
formData[linkage.targetField] = linkage.getValue?.(value)
}
}
}
}
function evaluateLinkage(linkage: any, data: any): boolean {
const sourceValue = data[linkage.sourceField]
switch (linkage.operator) {
case 'equals':
return sourceValue === linkage.value
case 'notEquals':
return sourceValue !== linkage.value
case 'in':
return linkage.values?.includes(sourceValue) ?? false
case 'notEmpty':
return sourceValue != null && sourceValue !== ''
default:
return true
}
}
</script>校验引擎:
// 校验引擎实现
class ValidationEngine {
private validators: Map<string, ValidatorFunction> = new Map()
constructor() {
// 注册内置校验器
this.register('required', (value, rule) => {
if (value === null || value === undefined || value === '') {
return { valid: false, message: rule.message || '该字段不能为空' }
}
return { valid: true }
})
this.register('minLength', (value, rule) => {
if (typeof value === 'string' && value.length < rule.value) {
return { valid: false, message: rule.message || `最少${rule.value}个字符` }
}
return { valid: true }
})
this.register('pattern', (value, rule) => {
if (typeof value === 'string' && !new RegExp(rule.value).test(value)) {
return { valid: false, message: rule.message || '格式不正确' }
}
return { valid: true }
})
}
register(name: string, fn: ValidatorFunction) {
this.validators.set(name, fn)
}
validate(formData: Record<string, any>, fields: FieldDefinition[]): ValidationResult {
const errors: Record<string, string> = {}
for (const field of fields) {
const value = formData[field.fieldCode]
const rules = field.rules || []
for (const rule of rules) {
const validator = this.validators.get(rule.type)
if (validator) {
const result = validator(value, rule)
if (!result.valid) {
errors[field.fieldCode] = result.message
break
}
}
}
}
return {
valid: Object.keys(errors).length === 0,
errors
}
}
}联动规则引擎:
// 联动规则定义
interface LinkageRule {
id: string
sourceField: string // 触发联动的字段
targetField: string // 被联动影响的字段
type: LinkageType // show, hide, setValue, clearValue, enable, disable, setOptions
operator: 'equals' | 'notEquals' | 'in' | 'notEmpty' | 'greaterThan' | 'lessThan'
value?: any // 触发条件值
values?: any[] // 多值条件
getValue?: (sourceValue: any) => any // 联动设置值函数
}
type LinkageType = 'show' | 'hide' | 'setValue' | 'clearValue' | 'enable' | 'disable' | 'setOptions'
// 联动规则示例配置
const linkageRules: LinkageRule[] = [
// 当类型为"企业"时,显示企业名称字段
{
id: 'L001',
sourceField: 'userType',
targetField: 'companyName',
type: 'show',
operator: 'equals',
value: 'enterprise'
},
// 当选择省份时,自动设置城市选项
{
id: 'L002',
sourceField: 'province',
targetField: 'city',
type: 'setOptions',
operator: 'notEmpty',
getValue: async (province) => {
const cities = await fetchCities(province)
return cities
}
},
// 当勾选"与收货地址一致"时,自动填充地址字段
{
id: 'L003',
sourceField: 'sameAsShipping',
targetField: 'billingAddress',
type: 'setValue',
operator: 'equals',
value: true,
getValue: (_, formData) => formData.shippingAddress
}
]流程引擎
流程引擎支持可视化的 BPMN 设计、节点配置、条件分支和审批链。
核心数据模型:
// 流程定义
public class ProcessDefinition {
private String id;
private String processKey; // 流程编码
private String processName; // 流程名称
private String category; // 分类
private String description;
private String bpmnXml; // BPMN 2.0 XML
private List<ProcessNode> nodes; // 节点列表
private List<ProcessEdge> edges; // 连线列表
private int version;
private boolean deployed;
}
// 流程节点
public class ProcessNode {
private String id; // 节点 ID (bpmn:element id)
private String name; // 节点名称
private NodeType type; // startEvent, endEvent, userTask, serviceTask, exclusiveGateway, parallelGateway
private NodeConfig config; // 节点配置
private Position position; // 画布位置
private List<Assignment> assignees; // 审批人/办理人
}
public enum NodeType {
START_EVENT, // 开始事件
END_EVENT, // 结束事件
USER_TASK, // 用户任务(人工审批)
SERVICE_TASK, // 服务任务(自动执行)
EXCLUSIVE_GATEWAY, // 排他网关(条件分支)
PARALLEL_GATEWAY, // 并行网关
INCLUSIVE_GATEWAY // 包容网关
}
// 节点配置
public class NodeConfig {
private String formKey; // 绑定的表单
private String dueDate; // 期限配置
private int priority; // 优先级
private String skipExpression; // 跳过表达式
private List<ConditionExpression> conditions; // 条件分支配置
private CounterSignConfig counterSign; // 会签配置
}
// 条件表达式
public class ConditionExpression {
private String sequenceFlowId; // 连线 ID
private String expression; // 条件表达式,如 ${amount > 1000}
private String description; // 条件描述
}
// 会签配置
public class CounterSignConfig {
private CounterSignType type; // sequential(依次), parallel(并行)
private String completionCondition; // 完成条件: all(全部), majority(多数), percentage(百分比)
private int completionPercentage; // 百分比值(当 completionCondition=percentage 时)
}
// 连线
public class ProcessEdge {
private String id;
private String sourceNodeId; // 源节点
private String targetNodeId; // 目标节点
private String label; // 连线标签
private ConditionExpression condition; // 条件(排他网关使用)
}BPMN 设计器集成示例:
// BPMN 设计器配置
import BpmnModeler from 'bpmn-js/lib/Modeler'
class ProcessDesigner {
private modeler: BpmnModeler
private container: HTMLElement
constructor(container: HTMLElement) {
this.container = container
this.modeler = new BpmnModeler({
container,
keyboard: { bindTo: document },
propertiesPanel: {
parent: '#properties-panel'
},
additionalModules: [
// 自定义扩展模块
CustomPalette,
CustomContextPad,
CustomRenderer
],
moddleExtensions: {
// 自定义属性扩展
custom: CustomExtension
}
})
}
async createNewDiagram() {
const blankXml = this.getBlankProcessXml()
await this.modeler.importXML(blankXml)
}
async openProcess(processDefinition: ProcessDefinition) {
await this.modeler.importXML(processDefinition.bpmnXml)
this.bindModelEvents()
}
async saveProcess(): Promise<ProcessDefinition> {
const { xml } = await this.modeler.saveXML({ format: true })
const definitions = this.modeler.getDefinitions()
// 提取节点和连线信息
const nodes = this.extractNodes(definitions)
const edges = this.extractEdges(definitions)
return {
id: definitions.id,
processKey: definitions.get('processKey'),
processName: definitions.get('name'),
bpmnXml: xml,
nodes,
edges,
version: 1,
deployed: false
}
}
private extractNodes(definitions: any): ProcessNode[] {
const process = definitions.rootElements[0]
const flowElements = process.get('flowElements') || []
return flowElements
.filter((el: any) => el.$type !== 'bpmn:SequenceFlow')
.map((el: any) => ({
id: el.id,
name: el.name || '',
type: this.mapNodeType(el.$type),
position: {
x: el.di?.x || 0,
y: el.di?.y || 0
},
config: this.extractNodeConfig(el)
}))
}
private extractEdges(definitions: any): ProcessEdge[] {
const process = definitions.rootElements[0]
const flows = process.get('flowElements') || []
return flows
.filter((el: any) => el.$type === 'bpmn:SequenceFlow')
.map((el: any) => ({
id: el.id,
sourceNodeId: el.sourceRef?.id,
targetNodeId: el.targetRef?.id,
label: el.name || '',
condition: el.conditionExpression
? { expression: el.conditionExpression.body }
: undefined
}))
}
private mapNodeType(bpmnType: string): NodeType {
const typeMap: Record<string, NodeType> = {
'bpmn:StartEvent': NodeType.START_EVENT,
'bpmn:EndEvent': NodeType.END_EVENT,
'bpmn:UserTask': NodeType.USER_TASK,
'bpmn:ServiceTask': NodeType.SERVICE_TASK,
'bpmn:ExclusiveGateway': NodeType.EXCLUSIVE_GATEWAY,
'bpmn:ParallelGateway': NodeType.PARALLEL_GATEWAY,
'bpmn:InclusiveGateway': NodeType.INCLUSIVE_GATEWAY
}
return typeMap[bpmnType] || NodeType.USER_TASK
}
private getBlankProcessXml(): string {
return `<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
xmlns:dc="http://www.omg.org/spec/DD/20100524/DC"
targetNamespace="http://lowcode/process">
<process id="Process_1" isExecutable="true">
<startEvent id="StartEvent_1" />
<endEvent id="EndEvent_1" />
</process>
<bpmndi:BPMNDiagram id="BpmnDiagram_1">
<bpmndi:BPMNPlane id="BpmnPlane_1" bpmnElement="Process_1">
<bpmndi:BPMNShape id="StartEvent_1_di" bpmnElement="StartEvent_1">
<dc:Bounds x="170" y="200" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="EndEvent_1_di" bpmnElement="EndEvent_1">
<dc:Bounds x="470" y="200" width="36" height="36" />
</bpmndi:BPMNShape>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>`
}
}流程审批链配置:
// 审批链配置
interface ApprovalChain {
nodes: ApprovalNode[]
}
interface ApprovalNode {
id: string
name: string
type: 'sequential' | 'parallel' | 'condition'
assigneeType: 'user' | 'role' | 'org' | 'dynamic'
assignees: string[] // 审批人/角色 ID 列表
formPermissions: FormPermission[] // 表单字段权限
timeout?: number // 超时时间(小时)
timeoutAction?: 'autoPass' | 'autoReject' | 'notify'
counterSign?: CounterSignSetting
}
interface FormPermission {
fieldCode: string
readable: boolean
writable: boolean
}
interface CounterSignSetting {
mode: 'all' | 'majority' | 'percentage'
percentage?: number
}
// 审批链节点配置示例
const approvalChain: ApprovalChain = {
nodes: [
{
id: 'node_1',
name: '部门经理审批',
type: 'sequential',
assigneeType: 'role',
assignees: ['role_dept_manager'],
formPermissions: [
{ fieldCode: 'amount', readable: true, writable: false },
{ fieldCode: 'reason', readable: true, writable: false }
],
timeout: 48,
timeoutAction: 'notify'
},
{
id: 'node_2',
name: '财务审核',
type: 'parallel',
assigneeType: 'role',
assignees: ['role_finance'],
formPermissions: [
{ fieldCode: 'amount', readable: true, writable: true },
{ fieldCode: 'budgetCode', readable: true, writable: true }
],
counterSign: {
mode: 'majority'
}
},
{
id: 'node_3',
name: '总经理审批',
type: 'condition',
assigneeType: 'role',
assignees: ['role_general_manager'],
condition: '${amount >= 50000}',
formPermissions: [
{ fieldCode: '*', readable: true, writable: true }
]
}
]
}页面引擎
页面引擎提供可视化拖拉拽设计能力,支持组件拖放、布局配置和数据绑定。
页面数据模型:
// 页面定义
interface PageDefinition {
id: string
pageCode: string
pageName: string
pageType: 'list' | 'form' | 'detail' | 'dashboard' | 'custom'
layout: LayoutDefinition
components: ComponentInstance[]
dataSource: DataSourceBinding
events: PageEvent[]
lifecycleHooks: LifecycleHook[]
}
// 布局定义
interface LayoutDefinition {
type: 'flex' | 'grid' | 'absolute'
columns?: number // 栅格列数
gap?: number // 间距
padding?: number
backgroundColor?: string
children: LayoutRow[]
}
interface LayoutRow {
id: string
height?: string | number
cols: LayoutCol[]
}
interface LayoutCol {
id: string
span: number // 栅格占位(1-24)
offset?: number
components: string[] // 组件 ID 列表
}
// 组件实例
interface ComponentInstance {
id: string
componentKey: string // 组件标识
type: 'basic' | 'business' | 'custom'
props: Record<string, any> // 组件属性
dataBinding?: DataBinding // 数据绑定
events: ComponentEvent[] // 组件事件
style: Record<string, any> // 样式覆盖
condition?: string // 显示条件
}
// 数据绑定
interface DataBinding {
type: 'entity' | 'api' | 'static' | 'expression'
entityCode?: string // 实体编码
apiPath?: string // API 路径
staticData?: any // 静态数据
expression?: string // 表达式
params?: Record<string, string> // 请求参数映射
transform?: string // 数据转换函数
}
// 页面事件
interface PageEvent {
id: string
trigger: EventTrigger // 触发条件
action: EventAction // 执行动作
condition?: string // 执行条件表达式
}
type EventTrigger =
| { type: 'pageLoad' }
| { type: 'componentEvent'; componentId: string; eventName: string }
| { type: 'dataChange'; fieldCode: string }
| { type: 'custom'; name: string }
interface EventAction {
type: 'navigate' | 'openDialog' | 'closeDialog' | 'refreshData'
| 'callApi' | 'setData' | 'showMessage' | 'confirm' | 'print' | 'custom'
config: Record<string, any>
}页面渲染引擎:
<template>
<div class="page-engine" :style="pageStyle">
<!-- 遍历布局行 -->
<div
v-for="row in layout.rows"
:key="row.id"
class="layout-row"
:style="{ height: row.height }"
>
<el-row :gutter="layout.gap || 16">
<el-col
v-for="col in row.cols"
:key="col.id"
:span="col.span"
:offset="col.offset || 0"
>
<div
v-for="compId in col.components"
:key="compId"
class="component-wrapper"
>
<component
:is="getComponentRenderer(compId)"
v-bind="getComponentProps(compId)"
@event="handleComponentEvent"
/>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { PageDefinition, ComponentInstance } from './types'
const props = defineProps<{
pageDefinition: PageDefinition
pageData: Record<string, any>
}>()
const emit = defineEmits<{
event: [payload: any]
}>()
const layout = computed(() => props.pageDefinition.layout)
const pageStyle = computed(() => ({
padding: layout.value.padding + 'px' || '0',
backgroundColor: layout.value.backgroundColor || 'transparent'
}))
// 组件渲染器注册表
const componentRegistry = new Map<string, any>()
function registerComponent(key: string, component: any) {
componentRegistry.set(key, component)
}
function getComponentRenderer(compId: string): any {
const instance = getComponentInstance(compId)
return componentRegistry.get(instance?.componentKey || '')
}
function getComponentProps(compId: string): Record<string, any> {
const instance = getComponentInstance(compId)
if (!instance) return {}
let props = { ...instance.props }
// 数据绑定处理
if (instance.dataBinding) {
const boundData = resolveDataBinding(instance.dataBinding)
props = { ...props, ...boundData }
}
return props
}
function getComponentInstance(compId: string): ComponentInstance | undefined {
return props.pageDefinition.components.find(c => c.id === compId)
}
function resolveDataBinding(binding: DataBinding): Record<string, any> {
switch (binding.type) {
case 'entity':
// 从实体数据源获取数据
return { data: props.pageData[binding.entityCode || ''] }
case 'api':
// API 数据已在页面加载时预取
return { data: props.pageData[binding.apiPath || ''] }
case 'static':
return { data: binding.staticData }
case 'expression':
// 执行表达式获取数据
return evaluateExpression(binding.expression || '', props.pageData)
default:
return {}
}
}
function evaluateExpression(expression: string, context: any): Record<string, any> {
try {
const fn = new Function('context', `with(context) { return ${expression} }`)
const result = fn(context)
return { data: result }
} catch {
return {}
}
}
function handleComponentEvent(payload: any) {
// 查找匹配的事件处理
const { componentId, eventName, data } = payload
const events = props.pageDefinition.events || []
for (const event of events) {
if (event.trigger.type === 'componentEvent'
&& event.trigger.componentId === componentId
&& event.trigger.eventName === eventName) {
executeAction(event.action, data)
}
}
emit('event', payload)
}
function executeAction(action: EventAction, data: any) {
switch (action.type) {
case 'navigate':
// 路由跳转
window.location.href = action.config.url
## 低代码平台架构
### 前后端分离架构
低代码平台采用前后端分离架构,前端负责设计器与渲染器,后端负责数据持久化、业务逻辑和 API 服务。┌─────────────────────────────────────────────────────────┐ │ 前端(浏览器) │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 设计器(Designer) │ │ 渲染器(Renderer)│ │ 管理后台 │ │ │ │ - 页面设计器 │ │ - 表单渲染 │ │ - 数据模型 │ │ │ │ - 流程设计器 │ │ - 页面渲染 │ │ - 组件管理 │ │ │ │ - 表单设计器 │ │ - 流程执行 │ │ - 权限管理 │ │ │ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ └─────────┼──────────────────┼──────────────────┼───────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ API 网关(Gateway) │ │ 认证 / 授权 / 限流 / 路由 │ └─────────────────────────────────────────────────────────┘ │ │ │ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────┐ │ 后端服务 │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 元数据服务 │ │ 数据服务 │ │ 流程服务 │ │ │ │ - 实体定义 │ │ - 动态 CRUD │ │ - 流程部署 │ │ │ │ - 字段定义 │ │ - 数据校验 │ │ - 任务办理 │ │ │ │ - 关联关系 │ │ - 数据导入导出│ │ - 审批流转 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ │ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │ │ │ 用户服务 │ │ 文件服务 │ │ 通知服务 │ │ │ │ - 组织架构 │ │ - 上传下载 │ │ - 站内信 │ │ │ │ - 角色权限 │ │ - 图片处理 │ │ - 邮件通知 │ │ │ └──────────────┘ └──────────────┘ └──────────────┘ │ └─────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────┐ │ 数据存储 │ │ MySQL / PostgreSQL / Redis / MinIO │ └─────────────────────────────────────────────────────────┘
**后端动态 CRUD 核心实现:**
```java
@RestController
@RequestMapping("/api/dynamic/{entityCode}")
public class DynamicEntityController {
private final DynamicEntityService dynamicService;
@PostMapping("/page")
public Result<PageResult<Map<String, Object>>> page(
@PathVariable String entityCode,
@RequestBody DynamicQueryDTO query) {
EntityDefinition entity = dynamicService.getEntity(entityCode);
PageResult<Map<String, Object>> result = dynamicService.page(entity, query);
return Result.success(result);
}
@GetMapping("/{id}")
public Result<Map<String, Object>> get(
@PathVariable String entityCode,
@PathVariable Long id) {
Map<String, Object> data = dynamicService.findById(entityCode, id);
return Result.success(data);
}
@PostMapping
public Result<Long> create(
@PathVariable String entityCode,
@RequestBody Map<String, Object> data) {
EntityDefinition entity = dynamicService.getEntity(entityCode);
// 自动校验
ValidationResult result = validationEngine.validate(data, entity.getFields());
if (!result.isValid()) {
return Result.error(result.getErrors());
}
Long id = dynamicService.create(entity, data);
return Result.success(id);
}
@PutMapping("/{id}")
public Result<Void> update(
@PathVariable String entityCode,
@PathVariable Long id,
@RequestBody Map<String, Object> data) {
dynamicService.update(entityCode, id, data);
return Result.success();
}
@DeleteMapping("/{id}")
public Result<Void> delete(
@PathVariable String entityCode,
@PathVariable Long id) {
dynamicService.delete(entityCode, id);
return Result.success();
}
}动态建表 SQL 生成:
public class DynamicTableGenerator {
public String generateCreateTableSQL(EntityDefinition entity) {
StringBuilder sql = new StringBuilder();
sql.append("CREATE TABLE `").append(entity.getTableName()).append("` (\n");
sql.append(" `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键',\n");
for (FieldDefinition field : entity.getFields()) {
sql.append(" `").append(field.getFieldCode()).append("` ");
sql.append(mapFieldType(field));
sql.append(" COMMENT '").append(field.getFieldName()).append("'");
sql.append(",\n");
}
sql.append(" `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',\n");
sql.append(" `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',\n");
sql.append(" `deleted` tinyint DEFAULT 0 COMMENT '逻辑删除',\n");
sql.append(" PRIMARY KEY (`id`)\n");
sql.append(") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='").append(entity.getEntityName()).append("'");
return sql.toString();
}
private String mapFieldType(FieldDefinition field) {
switch (field.getFieldType()) {
case STRING:
int len = field.getMaxLength() != null ? field.getMaxLength() : 255;
return String.format("varchar(%d) %s", len, field.isRequired() ? "NOT NULL" : "DEFAULT NULL");
case INTEGER:
return field.isRequired() ? "int NOT NULL" : "int DEFAULT NULL";
case NUMBER:
return field.isRequired() ? "decimal(18,2) NOT NULL" : "decimal(18,2) DEFAULT NULL";
case BOOLEAN:
return field.isRequired() ? "tinyint NOT NULL DEFAULT 0" : "tinyint DEFAULT NULL";
case DATE:
return field.isRequired() ? "date NOT NULL" : "date DEFAULT NULL";
case DATETIME:
return field.isRequired() ? "datetime NOT NULL" : "datetime DEFAULT NULL";
default:
return "varchar(255) DEFAULT NULL";
}
}
}组件库机制
组件库按层级分为基础组件、业务组件和自定义组件三层架构:
// 组件注册接口
interface ComponentMeta {
key: string // 组件唯一标识
name: string // 组件显示名称
category: ComponentCategory // 组件分类
version: string // 组件版本
icon: string // 组件图标
description: string // 组件描述
// 设计时元数据
designMeta: {
propsSchema: JSONSchema // 属性配置的 JSON Schema
defaultProps: Record<string, any> // 默认属性值
resizable: boolean // 是否可调整大小
draggable: boolean // 是否可拖拽
childrenAcceptable: boolean // 是否可包含子组件
}
// 运行时渲染器
renderer: ComponentRenderer // 运行时渲染函数/组件
// 预览缩略图
thumbnail?: string
}
enum ComponentCategory {
LAYOUT = 'layout', // 布局组件: Row, Col, Container, Tab, Card
BASIC = 'basic', // 基础组件: Input, Select, DatePicker, Button
DISPLAY = 'display', // 展示组件: Table, List, Carousel, Statistic
FORM = 'form', // 表单组件: Form, FormItem, Switch, Upload
BUSINESS = 'business', // 业务组件: UserPicker, DeptTree, RegionCascader
CUSTOM = 'custom' // 自定义组件
}组件注册与加载:
class ComponentLibrary {
private components: Map<string, ComponentMeta> = new Map()
private loadedPlugins: Map<string, ComponentPlugin> = new Map()
/**
* 注册内置组件
*/
registerBuiltinComponents() {
// 布局组件
this.register({
key: 'row',
name: '栅格行',
category: ComponentCategory.LAYOUT,
icon: 'row-icon',
description: '栅格布局行容器',
designMeta: {
propsSchema: {
type: 'object',
properties: {
gutter: { type: 'integer', default: 16, description: '栅格间距' },
justify: { type: 'string', enum: ['start', 'end', 'center', 'around', 'between'] }
}
},
defaultProps: { gutter: 16 },
draggable: true,
childrenAcceptable: true
},
renderer: GridRowRenderer
})
// 基础输入组件
this.register({
key: 'input',
name: '文本输入',
category: ComponentCategory.BASIC,
icon: 'input-icon',
description: '文本输入框',
designMeta: {
propsSchema: {
type: 'object',
properties: {
placeholder: { type: 'string', description: '占位文本' },
maxLength: { type: 'integer', description: '最大长度' },
clearable: { type: 'boolean', default: true }
}
},
defaultProps: { placeholder: '请输入', clearable: true },
draggable: true,
childrenAcceptable: false
},
renderer: InputRenderer
})
// 表格组件
this.register({
key: 'table',
name: '数据表格',
category: ComponentCategory.DISPLAY,
icon: 'table-icon',
description: '数据表格展示',
designMeta: {
propsSchema: {
type: 'object',
properties: {
columns: { type: 'array', description: '列定义' },
pagination: { type: 'boolean', default: true },
pageSize: { type: 'integer', default: 10 }
}
},
defaultProps: { pagination: true, pageSize: 10 },
draggable: true,
childrenAcceptable: false
},
renderer: TableRenderer
})
// 业务组件示例:用户选择器
this.register({
key: 'userPicker',
name: '用户选择器',
category: ComponentCategory.BUSINESS,
icon: 'user-icon',
description: '选择系统用户',
designMeta: {
propsSchema: {
type: 'object',
properties: {
multiple: { type: 'boolean', default: false },
deptFilter: { type: 'string', description: '部门筛选' }
}
},
defaultProps: { multiple: false },
draggable: true,
childrenAcceptable: false
},
renderer: UserPickerRenderer
})
}
/**
* 注册组件
*/
register(meta: ComponentMeta) {
if (this.components.has(meta.key)) {
console.warn(`组件 ${meta.key} 已被注册,将被覆盖`)
}
this.components.set(meta.key, meta)
}
/**
* 按分类获取组件
*/
getByCategory(category: ComponentCategory): ComponentMeta[] {
return Array.from(this.components.values())
.filter(c => c.category === category)
}
/**
* 获取所有组件(供设计器组件面板使用)
*/
getAll(): ComponentMeta[] {
return Array.from(this.components.values())
}
/**
* 加载自定义组件插件
*/
async loadPlugin(pluginUrl: string): Promise<void> {
try {
const plugin: ComponentPlugin = await import(pluginUrl)
const components = plugin.getComponents()
for (const comp of components) {
this.register(comp)
}
this.loadedPlugins.set(pluginUrl, plugin)
} catch (e) {
console.error(`加载组件插件失败: ${pluginUrl}`, e)
}
}
/**
* 获取组件渲染器
*/
getRenderer(key: string): ComponentRenderer | undefined {
return this.components.get(key)?.renderer
}
}扩展点设计
低代码平台通过扩展点机制支持功能的灵活扩展,主要扩展点包括:
/**
* 扩展点注册中心
*/
public class ExtensionRegistry {
private Map<String, List<ExtensionPoint>> extensions = new ConcurrentHashMap<>();
private static final ExtensionRegistry INSTANCE = new ExtensionRegistry();
public static ExtensionRegistry getInstance() {
return INSTANCE;
}
/**
* 注册扩展点实现
*/
public void register(String pointId, ExtensionPoint implementation) {
extensions.computeIfAbsent(pointId, k -> new CopyOnWriteArrayList<>())
.add(implementation);
}
/**
* 获取扩展点实现列表
*/
public <T extends ExtensionPoint> List<T> getExtensions(String pointId, Class<T> type) {
return extensions.getOrDefault(pointId, Collections.emptyList())
.stream()
.filter(type::isInstance)
.map(type::cast)
.collect(Collectors.toList());
}
}
/**
* 扩展点接口
*/
public interface ExtensionPoint {
String getId();
String getName();
int getOrder(); // 执行顺序
}
/**
* 扩展点:自定义组件
*/
public interface CustomComponentExtension extends ExtensionPoint {
ComponentMeta getComponentMeta();
ComponentRenderer getRenderer();
}
/**
* 扩展点:自定义插件
*/
public interface CustomPluginExtension extends ExtensionPoint {
void onDesignerInit(DesignerContext context);
void onPageLoad(PageContext context);
void onDataSubmit(SubmitContext context);
}
/**
* 扩展点:自定义动作
*/
public interface CustomActionExtension extends ExtensionPoint {
String getActionType();
void execute(ActionContext context);
}
/**
* 扩展点:自定义校验
*/
public interface CustomValidatorExtension extends ExtensionPoint {
String getValidatorType();
ValidationResult validate(Object value, ValidateRule rule, ValidationContext context);
}扩展点注册示例:
@Component
public class BuiltinExtensionRegistrar implements InitializingBean {
@Autowired
private List<ExtensionPoint> allExtensions;
@Override
public void afterPropertiesSet() {
ExtensionRegistry registry = ExtensionRegistry.getInstance();
for (ExtensionPoint extension : allExtensions) {
if (extension instanceof CustomComponentExtension) {
registry.register("custom-component", extension);
} else if (extension instanceof CustomPluginExtension) {
registry.register("custom-plugin", extension);
} else if (extension instanceof CustomActionExtension) {
registry.register("custom-action", extension);
} else if (extension instanceof CustomValidatorExtension) {
registry.register("custom-validator", extension);
}
}
}
}前端扩展点 API 设计:
// 前端扩展点注册
interface ExtensionAPI {
// 注册自定义组件
registerComponent(meta: ComponentMeta): void
// 注册自定义插件
registerPlugin(plugin: LowCodePlugin): void
// 注册自定义动作
registerAction(type: string, handler: ActionHandler): void
// 注册自定义校验器
registerValidator(name: string, validator: CustomValidator): void
// 注册自定义事件
registerEvent(eventName: string, handler: EventHandler): void
}
// 插件接口
interface LowCodePlugin {
name: string
version: string
// 生命周期钩子
onInstall?(api: ExtensionAPI): void
onUninstall?(): void
onDesignerReady?(context: DesignerContext): void
onPageRender?(context: PageContext): void
}
// 自定义动作处理器
type ActionHandler = (context: ActionContext) => Promise<void> | void
interface ActionContext {
actionId: string
actionConfig: Record<string, any>
pageData: Record<string, any>
formData: Record<string, any>
componentInstances: Map<string, any>
}
// 自定义校验器
interface CustomValidator {
name: string
validate: (value: any, rule: any, formData: Record<string, any>) => ValidationResult
}
// 自定义动作示例:调用外部 API
const externalApiAction: ActionHandler = async (context) => {
const { actionConfig, pageData } = context
const { url, method = 'POST', dataMapping } = actionConfig
// 将页面数据映射为请求参数
const requestData: Record<string, any> = {}
for (const [targetKey, sourceKey] of Object.entries(dataMapping)) {
requestData[targetKey] = pageData[sourceKey as string]
}
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(requestData)
})
return response.json()
}
// 注册自定义动作
extensionAPI.registerAction('callExternalApi', externalApiAction)
// 自定义校验器示例:身份证号校验
const idCardValidator: CustomValidator = {
name: 'idCard',
validate: (value: string) => {
if (!value) return { valid: true }
const pattern = /^[1-9]\d{5}(18|19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[\dXx]$/
if (!pattern.test(value)) {
return { valid: false, message: '身份证号格式不正确' }
}
// 校验码验证
const factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
const checkCodes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
let sum = 0
for (let i = 0; i < 17; i++) {
sum += parseInt(value[i]) * factors[i]
}
const expectedCheckCode = checkCodes[sum % 11]
if (value[17].toUpperCase() !== expectedCheckCode) {
return { valid: false, message: '身份证号校验码不正确' }
}
return { valid: true }
}
}
// 注册自定义校验器
extensionAPI.registerValidator('idCard', idCardValidator)运行时与设计时分离
低代码平台的核心架构原则是运行时与设计时严格分离,确保设计器操作不影响已发布的线上应用。
┌──────────────────────┐ ┌──────────────────────┐
│ 设计时(Design Time) │ │ 运行时(Runtime) │
│ │ │ │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ 可视化设计器 │ │ │ │ 渲染引擎 │ │
│ │ - 组件拖放 │ │ 发布 │ │ - 组件渲染 │ │
│ │ - 属性配置 │──┼────────┼─>│ - 数据加载 │ │
│ │ - 布局编辑 │ │ │ │ - 事件处理 │ │
│ │ - 数据绑定 │ │ │ │ - 表单提交 │ │
│ └──────────────────┘ │ │ └──────────────────┘ │
│ │ │ │
│ ┌──────────────────┐ │ │ ┌──────────────────┐ │
│ │ 配置数据存储 │ │ │ │ 运行配置读取 │ │
│ │ - 草稿版本 │ │ │ │ - 已发布版本 │ │
│ │ - 历史版本 │ │ │ │ - 灰度版本 │ │
│ │ - 发布版本 │ │ │ │ - 缓存加速 │ │
│ └──────────────────┘ │ │ └──────────────────┘ │
└──────────────────────┘ └──────────────────────┘版本管理与发布:
public class PageVersionManager {
/**
* 保存草稿(设计时操作)
*/
public Long saveDraft(String pageCode, PageDefinition definition, String operator) {
PageVersion draft = new PageVersion();
draft.setPageCode(pageCode);
draft.setContent(JsonUtils.toJson(definition));
draft.setVersion(getNextDraftVersion(pageCode));
draft.setStatus(VersionStatus.DRAFT);
draft.setOperator(operator);
pageVersionMapper.insert(draft);
return draft.getId();
}
/**
* 发布版本(草稿 → 已发布)
*/
public void publish(String pageCode, Long versionId, String operator) {
PageVersion version = pageVersionMapper.selectById(versionId);
version.setStatus(VersionStatus.PUBLISHED);
version.setPublishTime(LocalDateTime.now());
version.setPublisher(operator);
pageVersionMapper.updateById(version);
// 清理运行时缓存,使新版本生效
cacheManager.evictPageCache(pageCode);
}
/**
* 回滚到指定历史版本
*/
public void rollback(String pageCode, Long targetVersionId, String operator) {
PageVersion target = pageVersionMapper.selectById(targetVersionId);
PageDefinition definition = JsonUtils.parse(target.getContent(), PageDefinition.class);
// 创建新的草稿版本(内容与历史版本一致)
PageVersion rollbackVersion = new PageVersion();
rollbackVersion.setPageCode(pageCode);
rollbackVersion.setContent(JsonUtils.toJson(definition));
rollbackVersion.setVersion(getNextDraftVersion(pageCode));
rollbackVersion.setStatus(VersionStatus.DRAFT);
rollbackVersion.setOperator(operator);
rollbackVersion.setSourceVersionId(targetVersionId);
pageVersionMapper.insert(rollbackVersion);
}
/**
* 灰度发布(仅对指定用户/组织生效)
*/
public void grayscalePublish(String pageCode, Long versionId,
GrayscaleRule rule, String operator) {
PageVersion version = pageVersionMapper.selectById(versionId);
version.setStatus(VersionStatus.GRAYSCALE);
version.setGrayscaleRule(JsonUtils.toJson(rule));
pageVersionMapper.updateById(version);
// 写入灰度发布缓存
cacheManager.setGrayscaleRule(pageCode, rule);
}
/**
* 获取运行时页面配置
*/
public PageDefinition getRuntimeDefinition(String pageCode, String userId) {
// 1. 检查灰度发布
GrayscaleRule rule = cacheManager.getGrayscaleRule(pageCode);
if (rule != null && rule.matches(userId)) {
PageVersion grayscaleVersion = pageVersionMapper
.findLatestByStatus(pageCode, VersionStatus.GRAYSCALE);
if (grayscaleVersion != null) {
return JsonUtils.parse(grayscaleVersion.getContent(), PageDefinition.class);
}
}
// 2. 获取最新发布版本(缓存优先)
PageDefinition cached = cacheManager.getPageCache(pageCode);
if (cached != null) {
return cached;
}
// 3. 从数据库加载并缓存
PageVersion published = pageVersionMapper
.findLatestByStatus(pageCode, VersionStatus.PUBLISHED);
if (published == null) {
throw new PageNotPublishedException("页面尚未发布: " + pageCode);
}
PageDefinition definition = JsonUtils.parse(published.getContent(), PageDefinition.class);
cacheManager.setPageCache(pageCode, definition);
return definition;
}
}运行时 vs 设计时组件适配:
// 设计时组件包装器(增加拖拽、选中、属性编辑功能)
const withDesignerWrapper = (WrappedComponent: any) => {
return defineComponent({
props: {
componentId: String,
selected: Boolean,
hovered: Boolean,
designProps: Object
},
setup(props, { slots, attrs }) {
const isSelected = computed(() => props.selected)
const isHovered = computed(() => props.hovered)
return () => h('div', {
class: {
'designer-component-wrapper': true,
'is-selected': isSelected.value,
'is-hovered': isHovered.value
},
'data-component-id': props.componentId,
onClick: (e: MouseEvent) => {
e.stopPropagation()
// 通知设计器选中该组件
designerContext.selectComponent(props.componentId)
},
onMouseenter: () => designerContext.hoverComponent(props.componentId),
onMouseleave: () => designerContext.unhoverComponent(props.componentId)
}, [
// 选中时显示操作工具栏
isSelected.value && h('div', { class: 'component-toolbar' }, [
h('el-button', { size: 'small', onClick: () => designerContext.copyComponent(props.componentId) }, '复制'),
h('el-button', { size: 'small', onClick: () => designerContext.deleteComponent(props.componentId) }, '删除')
]),
// 实际的组件渲染
h(WrappedComponent, {
...attrs,
...props.designProps,
'data-component-id': props.componentId
}, slots)
])
}
})
}
// 运行时组件渲染器(纯净模式,无设计器包装)
const RuntimeComponentRenderer = defineComponent({
props: {
componentKey: String,
componentProps: Object,
dataBinding: Object
},
setup(props) {
const renderer = componentLibrary.getRenderer(props.componentKey)
if (!renderer) {
return () => h('div', { class: 'component-error' }, `未知组件: ${props.componentKey}`)
}
return () => h(renderer, {
...props.componentProps,
data: resolveBindingData(props.dataBinding)
})
}
})设计时与运行时的核心区别对比:
| 维度 | 设计时 | 运行时 |
|---|---|---|
| 页面配置 | 从草稿表读取,可编辑 | 从发布表读取,只读 |
| 组件行为 | 包裹设计器包装层,支持拖拽/选中/编辑 | 纯净渲染,无额外包装 |
| 数据源 | 模拟数据或静态数据 | 真实 API 数据 |
| 错误处理 | 显示错误详情,支持调试 | 优雅降级,显示友好提示 |
| 性能要求 | 中等(设计器可接受延迟) | 高(用户直接使用) |
| 存储策略 | 频繁保存草稿版本 | 读缓存,不写 |
| 多租户 | 按租户隔离设计数据 | 按租户加载运行时配置 |
| 安全策略 | 需管理员权限 | 按用户权限加载 |
api.js.ftl:
```html
import request from '@/utils/request'
import type { PageResult, ${className}VO, ${className}DTO } from './types'
/**
* 分页查询${tableComment}
*/
export function get${className}Page(params: ${className}DTO & { page: number; size: number }): Promise<PageResult<${className}VO>> {
return request.get('/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}/page', { params })
}
/**
* 查询${tableComment}详情
*/
export function get${className}Detail(id: number): Promise<${className}VO> {
return request.get(`/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}/${id}`)
}
/**
* 新增${tableComment}
*/
export function add${className}(data: ${className}DTO): Promise<void> {
return request.post('/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}', data)
}
/**
* 修改${tableComment}
*/
export function update${className}(id: number, data: ${className}DTO): Promise<void> {
return request.put(`/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}/${id}`, data)
}
/**
* 删除${tableComment}
*/
export function delete${className}(id: number): Promise<void> {
return request.delete(`/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}/${id}`)
}
/**
* 获取${tableComment}列表
*/
export function get${className}List(params: ${className}DTO): Promise<${className}VO[]> {
return request.get('/<#if modulePackage??>${modulePackage}/</#if>${classNameLower}/list', { params })
}