feat(codegen): 代码生成新增 AI 配置推断并迁移模板资源
- 模板与外部文件移至 resources/codegen、excel、ip2region - 新增 AI 配置推断接口、表单与返回对象 - 字典项与菜单 SQL、IP 工具、日志配置调整 - docker-compose 与英文 README 更新 - 版本 4.11.0
This commit is contained in:
@@ -8,6 +8,7 @@ import com.youlai.boot.codegen.property.CodegenProperties;
|
||||
import com.youlai.boot.common.enums.ActionTypeEnum;
|
||||
import com.youlai.boot.common.enums.LogModuleEnum;
|
||||
import com.youlai.boot.codegen.service.CodegenService;
|
||||
import com.youlai.boot.codegen.model.form.AiFillConfigForm;
|
||||
import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.model.query.TableQuery;
|
||||
import com.youlai.boot.codegen.model.vo.CodegenPreviewVO;
|
||||
@@ -111,4 +112,16 @@ public class CodegenController {
|
||||
throw new BusinessException("代码生成压缩包响应写入失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// AI 辅助:未开启 AI 时调用返回业务异常
|
||||
|
||||
@Operation(summary = "AI 填充代码生成配置")
|
||||
@PostMapping("/{tableName}/ai-config")
|
||||
public Result<GenConfigForm> fillConfig(
|
||||
@Parameter(description = "表名", example = "sys_user") @PathVariable String tableName,
|
||||
@RequestBody(required = false) @Valid AiFillConfigForm form
|
||||
) {
|
||||
String requirement = form == null ? null : form.getRequirement();
|
||||
return Result.success(genTableService.aiFillConfig(tableName, requirement));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class GenTable extends BaseEntity {
|
||||
private String author;
|
||||
|
||||
/**
|
||||
* 页面类型 classic|curd
|
||||
* 页面类型 classic|crud
|
||||
*/
|
||||
private String pageType;
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.youlai.boot.codegen.model.form;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* AI 填充生成配置入参
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.1.0
|
||||
*/
|
||||
@Schema(description = "AI 填充生成配置入参")
|
||||
@Data
|
||||
public class AiFillConfigForm {
|
||||
|
||||
@Schema(description = "补充需求描述,可为空", example = "状态字段用下拉框,创建时间不出现在表单")
|
||||
private String requirement;
|
||||
}
|
||||
@@ -19,36 +19,36 @@ import java.util.List;
|
||||
@Data
|
||||
public class GenConfigForm {
|
||||
|
||||
@Schema(description = "主键",example = "1")
|
||||
@Schema(description = "主键", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "表名",example = "sys_user")
|
||||
@Schema(description = "表名", example = "sys_user")
|
||||
@NotBlank(message = "表名不能为空")
|
||||
private String tableName;
|
||||
|
||||
@Schema(description = "业务名",example = "用户")
|
||||
@Schema(description = "业务名", example = "用户")
|
||||
@NotBlank(message = "业务名不能为空")
|
||||
private String businessName;
|
||||
|
||||
@Schema(description = "模块名",example = "system")
|
||||
@Schema(description = "模块名", example = "system")
|
||||
@NotBlank(message = "模块名不能为空")
|
||||
@Pattern(regexp = "[a-zA-Z][a-zA-Z0-9_]*", message = "模块名只能包含字母、数字和下划线,且以字母开头")
|
||||
private String moduleName;
|
||||
|
||||
@Schema(description = "包名",example = "com.youlai")
|
||||
@Schema(description = "包名", example = "com.youlai")
|
||||
@NotBlank(message = "包名不能为空")
|
||||
@Pattern(regexp = "([a-zA-Z_][a-zA-Z0-9_]*\\.)*[a-zA-Z_][a-zA-Z0-9_]*", message = "包名格式不正确")
|
||||
private String packageName;
|
||||
|
||||
@Schema(description = "实体名",example = "User")
|
||||
@Schema(description = "实体名", example = "User")
|
||||
@NotBlank(message = "实体名不能为空")
|
||||
@Pattern(regexp = "[a-zA-Z][a-zA-Z0-9]*", message = "实体名只能包含字母和数字,且以字母开头")
|
||||
private String entityName;
|
||||
|
||||
@Schema(description = "作者",example = "youlaitech")
|
||||
@Schema(description = "作者", example = "youlaitech")
|
||||
private String author;
|
||||
|
||||
@Schema(description = "上级菜单ID",example = "1")
|
||||
@Schema(description = "上级菜单ID", example = "1")
|
||||
private Long parentMenuId;
|
||||
|
||||
@Schema(description = "字段配置列表")
|
||||
@@ -60,7 +60,7 @@ public class GenConfigForm {
|
||||
@Schema(description = "前端应用名")
|
||||
private String frontendAppName;
|
||||
|
||||
@Schema(description = "页面类型 classic|curd", example = "classic")
|
||||
@Schema(description = "页面类型 classic|crud", example = "classic")
|
||||
private String pageType;
|
||||
|
||||
@Schema(description = "要移除的表前缀,如: sys_", example = "sys_")
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.youlai.boot.codegen.model.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI 推断的代码生成配置
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 0.1.0
|
||||
*/
|
||||
@Data
|
||||
public class GenConfigAiVO {
|
||||
|
||||
/** 业务名 */
|
||||
private String businessName;
|
||||
|
||||
/** 字段配置列表 */
|
||||
private List<FieldAiVO> fieldConfigs;
|
||||
|
||||
/**
|
||||
* AI 推断的字段配置
|
||||
*/
|
||||
@Data
|
||||
public static class FieldAiVO {
|
||||
|
||||
/** 列名,须与入参一致 */
|
||||
private String columnName;
|
||||
|
||||
/** 字段描述 */
|
||||
private String fieldComment;
|
||||
|
||||
/** 表单类型枚举名 */
|
||||
private String formType;
|
||||
|
||||
/** 查询类型枚举名 */
|
||||
private String queryType;
|
||||
|
||||
/** 是否必填 */
|
||||
private Integer isRequired;
|
||||
|
||||
/** 是否在列表显示 */
|
||||
private Integer isShowInList;
|
||||
|
||||
/** 是否在表单显示 */
|
||||
private Integer isShowInForm;
|
||||
|
||||
/** 是否在查询条件显示 */
|
||||
private Integer isShowInQuery;
|
||||
|
||||
/** 字典类型编码 */
|
||||
private String dictType;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,7 @@ public class CodegenProperties {
|
||||
public static class TemplateConfig {
|
||||
|
||||
/**
|
||||
* 模板路径 (e.g. /templates/codegen/controller.java.vm)
|
||||
* 模板路径,相对模板根 classpath:codegen (e.g. backend/controller.java.vm)
|
||||
*/
|
||||
private String templatePath;
|
||||
|
||||
|
||||
@@ -36,4 +36,13 @@ public interface GenTableService extends IService<GenTable> {
|
||||
*/
|
||||
void deleteGenConfig(String tableName);
|
||||
|
||||
/**
|
||||
* 用 AI 推断代码生成配置,未推断出的字段沿用原有默认值
|
||||
*
|
||||
* @param tableName 表名
|
||||
* @param requirement 补充需求描述,可为空
|
||||
* @return 回填了 AI 推断结果的配置
|
||||
*/
|
||||
GenConfigForm aiFillConfig(String tableName, String requirement);
|
||||
|
||||
}
|
||||
|
||||
@@ -85,10 +85,10 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
return templateConfig.getTemplatePath();
|
||||
}
|
||||
if ("API".equals(templateName)) {
|
||||
return "codegen/frontend/js/api.js.vm";
|
||||
return "frontend/js/api.js.vm";
|
||||
}
|
||||
if ("VIEW".equals(templateName)) {
|
||||
return "codegen/frontend/js/index.js.vue.vm";
|
||||
return "frontend/js/index.js.vue.vm";
|
||||
}
|
||||
return templateConfig.getTemplatePath();
|
||||
}
|
||||
@@ -352,14 +352,14 @@ public class CodegenServiceImpl implements CodegenService {
|
||||
bindMap.put("hasBigDecimal", hasBigDecimal);
|
||||
bindMap.put("hasRequiredField", hasRequiredField);
|
||||
|
||||
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("templates", TemplateConfig.ResourceMode.CLASSPATH));
|
||||
// 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.curd.vue.vm
|
||||
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig("codegen", TemplateConfig.ResourceMode.CLASSPATH));
|
||||
// 根据 ui 选择不同的前端页面模板:默认 index.vue.vm;封装版使用 index.crud.vue.vm
|
||||
String path = templatePath;
|
||||
if ("curd".equalsIgnoreCase(pageType)) {
|
||||
if ("crud".equalsIgnoreCase(pageType)) {
|
||||
if (path.endsWith("index.js.vue.vm")) {
|
||||
path = path.replace("index.js.vue.vm", "index.curd.js.vue.vm");
|
||||
path = path.replace("index.js.vue.vm", "index.crud.js.vue.vm");
|
||||
} else if (path.endsWith("index.vue.vm")) {
|
||||
path = path.replace("index.vue.vm", "index.curd.vue.vm");
|
||||
path = path.replace("index.vue.vm", "index.crud.vue.vm");
|
||||
}
|
||||
}
|
||||
Template template = templateEngine.getTemplate(path);
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.youlai.boot.codegen.service.impl;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.lang.Assert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.boot.YouLaiBootApplication;
|
||||
@@ -20,8 +21,11 @@ import com.youlai.boot.codegen.model.vo.TableMetaVO;
|
||||
import com.youlai.boot.codegen.model.entity.GenTable;
|
||||
import com.youlai.boot.codegen.model.entity.GenTableColumn;
|
||||
import com.youlai.boot.codegen.model.form.GenConfigForm;
|
||||
import com.youlai.boot.codegen.model.vo.GenConfigAiVO;
|
||||
import com.youlai.boot.codegen.service.GenTableService;
|
||||
import com.youlai.boot.codegen.service.GenTableColumnService;
|
||||
import com.youlai.boot.support.ai.AiChatService;
|
||||
import com.youlai.boot.support.ai.PromptLoader;
|
||||
import com.youlai.boot.system.model.dto.MenuCreateDTO;
|
||||
import com.youlai.boot.system.service.MenuService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -29,9 +33,14 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 数据库服务实现类
|
||||
@@ -43,10 +52,15 @@ import java.util.Objects;
|
||||
@RequiredArgsConstructor
|
||||
public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> implements GenTableService {
|
||||
|
||||
/** 系统提示词名称,对应 resources/prompts 下的相对路径 */
|
||||
private static final String SYSTEM_PROMPT_PATH = "codegen/system.md";
|
||||
|
||||
private final DatabaseMapper databaseMapper;
|
||||
private final CodegenProperties codegenProperties;
|
||||
private final GenTableColumnService genTableColumnService;
|
||||
private final CodegenConverter codegenConverter;
|
||||
private final AiChatService aiChatService;
|
||||
private final PromptLoader promptLoader;
|
||||
|
||||
@Value("${spring.profiles.active}")
|
||||
private String springProfilesActive;
|
||||
@@ -229,6 +243,137 @@ public class GenTableServiceImpl extends ServiceImpl<GenTableMapper, GenTable> i
|
||||
}
|
||||
}
|
||||
|
||||
//---------------------------------------------------
|
||||
// AI 辅助:未开启 AI 时调用返回业务异常
|
||||
//---------------------------------------------------
|
||||
|
||||
/**
|
||||
* 用 AI 推断代码生成配置
|
||||
*
|
||||
* @param tableName 表名 eg: sys_user
|
||||
* @param requirement 补充需求描述,可为空
|
||||
* @return 回填了 AI 推断结果的配置
|
||||
*/
|
||||
@Override
|
||||
public GenConfigForm aiFillConfig(String tableName, String requirement) {
|
||||
GenConfigForm config = getGenTableFormData(tableName);
|
||||
String systemPrompt = promptLoader.render(SYSTEM_PROMPT_PATH, buildAiPromptVariables());
|
||||
GenConfigAiVO aiConfig = aiChatService.chatEntity(systemPrompt, buildAiUserPrompt(config, requirement), GenConfigAiVO.class);
|
||||
|
||||
if (StrUtil.isNotBlank(aiConfig.getBusinessName())) {
|
||||
config.setBusinessName(aiConfig.getBusinessName().trim());
|
||||
}
|
||||
applyAiFieldConfigs(config, aiConfig.getFieldConfigs());
|
||||
return config;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造系统提示词的变量值,表单类型与查询类型清单取自枚举,新增枚举值无需改动提示词
|
||||
*
|
||||
* @return 变量值
|
||||
*/
|
||||
private Map<String, String> buildAiPromptVariables() {
|
||||
String formTypes = Arrays.stream(FormTypeEnum.values())
|
||||
.map(type -> type.name() + "(" + type.getLabel() + ")")
|
||||
.collect(Collectors.joining("|"));
|
||||
String queryTypes = Arrays.stream(QueryTypeEnum.values())
|
||||
.map(Enum::name)
|
||||
.collect(Collectors.joining("|"));
|
||||
return Map.of("formTypes", formTypes, "queryTypes", queryTypes);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按列名匹配回填字段配置
|
||||
*
|
||||
* @param config 原配置
|
||||
* @param aiFields AI 返回的字段列表
|
||||
*/
|
||||
private void applyAiFieldConfigs(GenConfigForm config, List<GenConfigAiVO.FieldAiVO> aiFields) {
|
||||
if (CollectionUtil.isEmpty(aiFields) || CollectionUtil.isEmpty(config.getFieldConfigs())) {
|
||||
return;
|
||||
}
|
||||
Map<String, GenConfigAiVO.FieldAiVO> aiFieldMap = aiFields.stream()
|
||||
.filter(field -> StrUtil.isNotBlank(field.getColumnName()))
|
||||
.collect(Collectors.toMap(GenConfigAiVO.FieldAiVO::getColumnName, Function.identity(),
|
||||
(existing, replacement) -> existing, LinkedHashMap::new));
|
||||
|
||||
config.getFieldConfigs().forEach(field -> {
|
||||
GenConfigAiVO.FieldAiVO aiField = aiFieldMap.get(field.getColumnName());
|
||||
if (aiField == null) {
|
||||
return;
|
||||
}
|
||||
if (StrUtil.isNotBlank(aiField.getFieldComment())) {
|
||||
field.setFieldComment(aiField.getFieldComment().trim());
|
||||
}
|
||||
if (StrUtil.isNotBlank(aiField.getDictType())) {
|
||||
field.setDictType(aiField.getDictType().trim());
|
||||
}
|
||||
field.setFormType(parseAiEnum(aiField.getFormType(), FormTypeEnum::valueOf, field.getFormType()));
|
||||
field.setQueryType(parseAiEnum(aiField.getQueryType(), QueryTypeEnum::valueOf, field.getQueryType()));
|
||||
field.setIsRequired(resolveAiValue(aiField.getIsRequired(), field.getIsRequired()));
|
||||
field.setIsShowInList(resolveAiValue(aiField.getIsShowInList(), field.getIsShowInList()));
|
||||
field.setIsShowInForm(resolveAiValue(aiField.getIsShowInForm(), field.getIsShowInForm()));
|
||||
field.setIsShowInQuery(resolveAiValue(aiField.getIsShowInQuery(), field.getIsShowInQuery()));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析枚举名,非法或未给出时沿用原值
|
||||
*
|
||||
* @param name 枚举名
|
||||
* @param parser 枚举解析器
|
||||
* @param fallback 原值
|
||||
* @return 枚举或原值
|
||||
*/
|
||||
private <T> T parseAiEnum(String name, Function<String, T> parser, T fallback) {
|
||||
if (StrUtil.isBlank(name)) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return parser.apply(name.trim());
|
||||
} catch (IllegalArgumentException e) {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AI 值缺失时沿用原值
|
||||
*
|
||||
* @param aiValue AI 值
|
||||
* @param fallback 原值
|
||||
* @return 生效值
|
||||
*/
|
||||
private <T> T resolveAiValue(T aiValue, T fallback) {
|
||||
return aiValue != null ? aiValue : fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造用户提示词
|
||||
*
|
||||
* @param config 当前配置
|
||||
* @param requirement 补充需求描述
|
||||
* @return 用户提示词
|
||||
*/
|
||||
private String buildAiUserPrompt(GenConfigForm config, String requirement) {
|
||||
List<Map<String, Object>> columns = new ArrayList<>();
|
||||
if (CollectionUtil.isNotEmpty(config.getFieldConfigs())) {
|
||||
config.getFieldConfigs().forEach(field -> {
|
||||
Map<String, Object> column = new LinkedHashMap<>();
|
||||
column.put("columnName", field.getColumnName());
|
||||
column.put("columnType", field.getColumnType());
|
||||
column.put("fieldComment", field.getFieldComment());
|
||||
column.put("isRequired", field.getIsRequired());
|
||||
columns.add(column);
|
||||
});
|
||||
}
|
||||
return """
|
||||
表名:%s
|
||||
当前业务名:%s
|
||||
补充说明:%s
|
||||
字段列表:
|
||||
%s
|
||||
""".formatted(config.getTableName(), config.getBusinessName(),
|
||||
StrUtil.blankToDefault(requirement, "无"), JSONUtil.toJsonStr(columns));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ import java.nio.file.StandardCopyOption;
|
||||
@Component
|
||||
public class IPUtils {
|
||||
|
||||
private static final String DB_PATH = "/data/ip2region.xdb";
|
||||
private static final String DB_PATH = "/ip2region/ip2region.xdb";
|
||||
private static Searcher searcher;
|
||||
|
||||
@PostConstruct
|
||||
|
||||
@@ -28,7 +28,7 @@ import java.util.stream.Collectors;
|
||||
public class RoleMenuServiceImpl extends ServiceImpl<RoleMenuMapper, RoleMenu> implements RoleMenuService {
|
||||
|
||||
/**
|
||||
* 权限缓存TTL:与 Caffeine 菜单缓存 expireAfterWrite=600s 对齐。
|
||||
* 权限缓存 TTL(秒)。
|
||||
* Hash 写入默认永不过期,直接改库(SQL 脚本/手工修数)不触发任何刷新点时会永久陈旧;
|
||||
* 加 TTL 后过期整 Key 消失,Read-Through 按角色逐个回源重建,实现最终一致自愈
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user