代码生成器
代码生成器
业务用途
「代码生成器」面向平台内的开发提效场景:当你在里建好一张表(lc_table_meta + lc_column_meta)后,可以一键预览这张表对应的标准五层后端代码(Entity / Mapper / Service / ServiceImpl / Controller)和三段前端代码(Vue 列表页 / Vue 表单页 / Vue API 模块)。
它的价值在于:
- 即时预览:不用离开平台就能看到某张表「骨架代码」长什么样,方便确认字段类型、命名是否合理。
- 一键落盘:勾选配置后,
generateAndWriteFiles会按约定路径把代码直接写进平台自身的backend/frontend源码目录,作为平台二次开发的新增模块。
与「应用导出」的区别
代码生成器用 StringBuilder 字符串拼接生成 8 段代码,不经过 FreeMarker 模板,产物是 Map<String, String>。它服务的不是「导出独立工程」,而是「在平台内快速产出 CRUD 骨架」。若需把整个应用导出成可独立部署的 zip,请看 。
涉及文件
| 角色 | 文件路径 |
|---|---|
| 控制器 | backend/src/main/java/com/lowcode/controller/GeneratorController.java |
| 生成服务 | backend/src/main/java/com/lowcode/generator/CodeGeneratorService.java |
| 表元数据实体 | backend/src/main/java/com/lowcode/entity/LcTableMeta.java |
| 字段元数据实体 | backend/src/main/java/com/lowcode/entity/LcColumnMeta.java |
| 前端页面 | frontend/src/views/generator/index.vue |
| 数据服务 | backend/src/main/java/com/lowcode/service/SchemaService.java(提供 getTableList) |
后端实现(关键代码)
端点一览
GeneratorController 挂在 /api/generator 下,共 4 个端点:
| 方法 | 路径 | 说明 |
|---|---|---|
POST | /api/generator/preview/{tableId} | 预览代码,返回 Map<String,String>(8 段代码) |
POST | /api/generator/generate/{tableId} | 生成并写入文件到平台 backend/frontend 目录 |
POST | /api/generator/download/{tableId} | 生成代码并返回 codeMap + files 文件名集合 |
GET | /api/generator/table/list | 获取可生成代码的表列表(委托 SchemaService.getTableList()) |
三个写操作都接收可选的 GeneratorConfig(请求体),为空时走默认值。
控制器
// GeneratorController.java
@RestController
@RequestMapping("/api/generator")
public class GeneratorController {
@Autowired private CodeGeneratorService codeGeneratorService;
@Autowired private SchemaService schemaService;
@PostMapping("/preview/{tableId}")
public Result<Map<String, String>> previewCode(
@PathVariable Long tableId,
@RequestBody(required = false) CodeGeneratorService.GeneratorConfig config) {
if (config == null) config = new CodeGeneratorService.GeneratorConfig();
Map<String, String> codeMap = codeGeneratorService.generateCode(tableId, config);
return Result.success(codeMap);
}
@PostMapping("/generate/{tableId}")
public Result<Map<String, String>> generateCode(
@PathVariable Long tableId,
@RequestBody(required = false) CodeGeneratorService.GeneratorConfig config) {
if (config == null) config = new CodeGeneratorService.GeneratorConfig();
return Result.success(codeGeneratorService.generateAndWriteFiles(tableId, config));
}
@GetMapping("/table/list")
public Result<?> getTableList() {
return Result.success(schemaService.getTableList());
}
}生成服务核心流程
CodeGeneratorService.generateCode(tableId, config) 的步骤:
tableMetaMapper.selectById(tableId)取表元数据,不存在抛RuntimeException("表不存在")。columnMetaMapper按tableId+sort升序取字段列表;为空抛「表不存在任何字段」。- 用 Hutool
StrUtil推导className(优先取tableMeta.className,否则tableName蛇形转大驼峰)和varName(首字母小写)。 - 组装
dataModel:packageName/moduleName/businessName/tableName/tableComment/className/varName/author/columns/pkColumn/importPackages。 - 依次调用 8 个
generateXxxCode(dataModel),把结果塞进Map,key 为entity/mapper/service/serviceImpl/controller/vueIndex/vueForm/vueApi。
// CodeGeneratorService.java
Map<String, Object> dataModel = new HashMap<>();
dataModel.put("packageName", config.getPackageName());
dataModel.put("className", className);
dataModel.put("varName", varName);
dataModel.put("columns", columns);
dataModel.put("pkColumn", columns.stream()
.filter(c -> "Y".equals(c.getIsPk())).findFirst().orElse(columns.get(0)));
// 过滤需要导入的包
List<String> importPackages = columns.stream()
.map(this::getJavaImport).filter(StrUtil::isNotBlank).distinct().collect(Collectors.toList());
dataModel.put("importPackages", importPackages);
Map<String, String> result = new HashMap<>();
result.put("entity", generateEntityCode(dataModel));
result.put("mapper", generateMapperCode(dataModel));
result.put("service", generateServiceCode(dataModel));
result.put("serviceImpl", generateServiceImplCode(dataModel));
result.put("controller", generateControllerCode(dataModel));
result.put("vueIndex", generateVueIndexCode(dataModel));
result.put("vueForm", generateVueFormCode(dataModel));
result.put("vueApi", generateVueApiCode(dataModel));Java 类型导入推导
getJavaImport 按字段的 javaType 简单名映射全限定包名,只导非 java.lang 的类型:
private String getJavaImport(LcColumnMeta column) {
switch (column.getJavaType()) {
case "BigDecimal": return "java.math.BigDecimal";
case "Date": return "java.util.Date";
case "LocalDateTime": return "java.time.LocalDateTime";
case "LocalDate": return "java.time.LocalDate";
case "LocalTime": return "java.time.LocalTime";
default: return "";
}
}生成产物示例(Entity 片段)
generateEntityCode 拼出的实体继承 BaseEntity,带 @Data / @EqualsAndHashCode(callSuper = true) / @TableName:
package com.lowcode.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import com.lowcode.common.BaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
/**
* 采购订单实体类
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("pms_purchase_order")
public class PmsPurchaseOrder extends BaseEntity {
/**
* 订单编号
*/
private String orderNo;
/**
* 采购金额
*/
private BigDecimal totalPrice;
}生成的 Controller 挂在 /api/{varName},提供 list / page / getById / add / update / delete 六个标准接口;生成的 Vue API 调用 request.get('/{varName}/list') 等(注意路径不带 /api,由 axios baseURL 补齐)。
GeneratorConfig 与文件写入
public static class GeneratorConfig {
private String packageName = "com.lowcode";
private String moduleName = "system";
private String author = "lowcode";
private String backendPath = "../backend/src/main/java/com/lowcode/";
private String frontendPath = "../frontend/src/";
// getter / setter ...
}generateAndWriteFiles 在 generateCode 之后,按 backendPath + entity/<类名>.java、frontendPath + views/<moduleName>/<varName>/index.vue 等路径用 FileWriter 覆盖写入,并在 codeMap 里塞 status=success / error。
落盘路径约定
后端(5 个文件):
entity/<ClassName>.javamapper/<ClassName>Mapper.javaservice/I<ClassName>Service.javaservice/impl/<ClassName>ServiceImpl.javacontroller/<ClassName>Controller.java
前端(3 个文件,目录为 moduleName/varName):
views/<moduleName>/<varName>/index.vueviews/<moduleName>/<varName>/form.vueapi/<varName>.ts
前端实现
入口页 frontend/src/views/generator/index.vue,三步交互:
- 选表:
onMounted调request.get('/generator/table/list')拉表列表,el-select单选(label 形如pms_purchase_order (采购订单))。 - 配置:表单绑定
generatorConfig(packageName/moduleName/author,默认com.lowcode/system/lowcode)。 - 预览:点「生成代码」调
request.post('/generator/preview/' + id, generatorConfig),返回Map转el-tabs,每个 tab 展示一段代码,附「复制代码」按钮。

<!-- generator/index.vue 关键片段 -->
<el-select v-model="selectedTable" placeholder="请选择要生成代码的表" filterable>
<el-option v-for="table in tableList" :key="table.id"
:label="`${table.tableName} (${table.tableComment})`" :value="table" />
</el-select>
<el-form :model="generatorConfig" :inline="true">
<el-form-item label="包名"><el-input v-model="generatorConfig.packageName" /></el-form-item>
<el-form-item label="模块名"><el-input v-model="generatorConfig.moduleName" /></el-form-item>
<el-form-item label="作者"><el-input v-model="generatorConfig.author" /></el-form-item>
</el-form>// 预览
const res = await request.post(`/generator/preview/${selectedTable.value.id}`, generatorConfig)
codeMap.value = new Map(Object.entries(res.data))
前端只用了 preview
generator/index.vue 目前只调 /generator/preview/{tableId} 做预览,没有调 /generate/{tableId}(落盘)。如需在平台内直接生成文件,需自行加按钮调 request.post('/generator/generate/' + id, config)。
操作步骤
- 确保后端(
:52856)与前端(:3000)已启动,并已登录。 - 先在「表设计器」里建好一张表并保存字段(否则预览会报「表不存在任何字段」)。
- 进入「代码生成器」页面(路由对应
views/generator/index.vue)。 - 在下拉里选目标表,按需改包名 / 模块名 / 作者。
- 点「生成代码」,下方出现 7 个 Tab(Entity / Mapper / Service / ServiceImpl / Controller / Vue 列表页 / Vue API),可逐个复制使用。
- (可选)若要落盘到平台工程,调
POST /api/generator/generate/{tableId},文件会写到GeneratorConfig.backendPath/frontendPath指定的相对路径(默认../backend/.../../frontend/...,需保证运行目录有写权限)。

常见问题
预览报「表不存在任何字段」
该表在 lc_column_meta 里没有字段记录。回到表设计器,添加字段并保存后再预览。
生成的 Controller 路径里为什么没有 /api 前缀?
生成器拼的是 @RequestMapping("/api/" + varName),已包含 /api。而前端生成的 request.get('/' + varName + '/list') 不带 /api,由 axios baseURL 补齐——这与平台 一致,不要在前端再手写 /api。
generate 写文件失败 / 找不到目录
GeneratorConfig.backendPath 默认是相对路径 ../backend/src/main/java/com/lowcode/,依赖后端进程的工作目录。生产环境建议改为绝对路径,或改用 直接拿 zip。
想改生成模板怎么办?
代码生成器没有用模板文件,逻辑全在 CodeGeneratorService 的 generateXxxCode 方法里用 StringBuilder 拼接。要改样式只能改这些方法的源码。若需要更强的模板化能力(FreeMarker),请使用 。
