- 操作结果\n */\nexport function addObj(obj?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}\',\n method: \'post\',\n data: obj\n })\n}\n\n/**\n * 获取详情数据\n * @param obj - 查询参数对象(包含ID等)\n * @returns Promise<数据详情>\n */\nexport function getObj(obj?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}/details\',\n method: \'get\',\n params: obj\n })\n}\n\n/**\n * 批量删除数据\n * @param ids - 要删除的ID数组\n * @returns Promise<操作结果>\n */\nexport function delObjs(ids?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}\',\n method: \'delete\',\n data: ids\n })\n}\n\n/**\n * 更新数据\n * @param obj - 要更新的数据对象\n * @returns Promise<操作结果>\n */\nexport function putObj(obj?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}\',\n method: \'put\',\n data: obj\n })\n}\n\n// ========== 树形CRUD接口 ==========\n\n/**\n * 获取树形列表数据\n * @param query - 查询参数对象\n * @returns Promise<树形数据>\n */\nexport function fetchTreeList(query?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}/tree\',\n method: \'get\',\n params: query\n })\n}\n\n/**\n * 获取树节点详情\n * @param obj - 查询参数对象(包含ID等)\n * @returns Promise<树节点详情>\n */\nexport function getTreeObj(obj?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}/tree/details\',\n method: \'get\',\n params: obj\n })\n}\n\n/**\n * 新增树节点\n * @param obj - 要新增的树节点对象\n * @returns Promise<操作结果>\n */\nexport function addTreeObj(obj?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}/tree\',\n method: \'post\',\n data: obj\n })\n}\n\n/**\n * 更新树节点\n * @param obj - 要更新的树节点对象\n * @returns Promise<操作结果>\n */\nexport function putTreeObj(obj?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}/tree\',\n method: \'put\',\n data: obj\n })\n}\n\n/**\n * 删除树节点\n * @param ids - 要删除的树节点ID数组\n * @returns Promise<操作结果>\n */\nexport function delTreeObjs(ids?: Object) {\n return request({\n url: \'/${moduleName}/${functionName}/tree\',\n method: \'delete\',\n data: ids\n })\n}\n\n// ========== 工具函数 ==========\n\n/**\n * 验证字段值唯一性\n * @param rule - 验证规则对象\n * @param value - 要验证的值\n * @param callback - 验证回调函数\n * @param isEdit - 是否为编辑模式\n *\n * @example\n * // 在表单验证规则中使用\n * fieldName: [\n * {\n * validator: (rule, value, callback) => {\n * validateExist(rule, value, callback, form.${pk.attrName} !== \'\');\n * },\n * trigger: \'blur\',\n * },\n * ]\n */\nexport function validateExist(rule: any, value: any, callback: any, isEdit: boolean) {\n // 编辑模式下跳过验证\n if (isEdit) {\n return callback();\n }\n\n // 查询是否存在相同值\n getObj({ [rule.field]: value }).then((response) => {\n const result = response.data;\n if (result !== null && result.length > 0) {\n callback(new Error(\'数据已经存在\'));\n } else {\n callback();\n }\n });\n}\n','2026-04-08 17:38:24','2026-04-08 17:38:24','0',1,'admin','admin');
INSERT INTO `gen_template` (`id`, `template_name`, `generator_path`, `template_desc`, `template_code`, `create_time`, `update_time`, `del_flag`, `tenant_id`, `create_by`, `update_by`) VALUES (43,'表格V2026001','${frontendPath}/src/views/${moduleName}/${functionName}/index.vue','表格V2026001','#foreach($field in $childFieldList)\n#if($field.primaryPk == \'1\')\n#set($childPkField = $field)\n#break\n#end\n#end\n\n \n
\n \n \n
\n \n \n \n \n {{ node.label }}\n \n \n \n \n \n \n \n \n \n \n \n \n
\n \n \n \n#if($queryList)\n
\n \n#foreach($field in $queryList)\n#if($field.attrName != ${childField})\n#if($field.queryFormType == \'select\')\n \n \n#if($field.fieldDict)\n \n#else\n \n#end\n \n \n#elseif($field.queryFormType == \'date\')\n \n \n \n#elseif($field.queryFormType == \'daterange\')\n \n \n \n#elseif($field.queryFormType == \'datetime\')\n \n \n \n#elseif($field.queryFormType == \'datetimerange\')\n \n \n \n#elseif($field.formType == \'radio\')\n \n \n#if($field.fieldDict)\n \n {{ item.label }}\n \n#else\n \n ${field.fieldComment}\n \n#end\n \n \n#else\n \n \n \n#end\n#end\n#end\n \n \n 查询\n \n 重置\n \n \n \n#end\n\n
\n \n \n 新增\n \n \n 导入\n \n \n 删除\n \n \n
\n \n\n
\n \n \n#foreach($field in $gridList)\n#if($field.fieldDict)\n \n \n \n \n \n#elseif(${field.formType} == \'upload-img\')\n \n \n \n \n \n#else\n \n#end\n#end\n \n \n \n 编辑\n \n \n 删除\n \n \n \n \n\n
\n
\n \n \n\n
\n
\n\n
\n
\n\n\n\n','2026-04-08 17:38:24','2026-04-08 17:38:24','0',1,'admin','admin');
diff --git a/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GeneratorServiceImpl.java b/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GeneratorServiceImpl.java
index 2711daed4..358363eee 100755
--- a/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GeneratorServiceImpl.java
+++ b/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/service/impl/GeneratorServiceImpl.java
@@ -52,6 +52,8 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringBootVersion;
import org.springframework.stereotype.Service;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.*;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
@@ -449,6 +451,7 @@ public class GeneratorServiceImpl implements GeneratorService {
dataModel.put(DataModelConstants.BACKEND_PATH, table.getBackendPath());
dataModel.put(DataModelConstants.FRONTEND_PATH, table.getFrontendPath());
+ setEntityModel(dataModel, table);
// 设置子表
String childTableName = table.getChildTableName();
@@ -488,12 +491,16 @@ public class GeneratorServiceImpl implements GeneratorService {
}
/**
- * 判断当前是否是 SpringBoot3 版本
+ * 判断当前是否为 SpringBoot 3 及以上版本(含 SpringBoot 4)。
+ *
+ * 模板变量 {@code $isSpringBoot3} 沿用历史命名未改,实际语义是「是否使用
+ * SpringBoot 3 之后的注解和包路径」,目前覆盖 SpringBoot 3.x 与 4.x。新引入
+ * 更高主版本时,请在此处补齐前缀,避免模板渲染走到 SpringBoot 2 分支。
*
- * @return true/fasle
+ * @return SpringBoot 主版本 >= 3 返回 true
*/
private boolean isSpringBoot3() {
- return StrUtil.startWith(SpringBootVersion.getVersion(), "3");
+ return StrUtil.startWithAny(SpringBootVersion.getVersion(), "3", "4");
}
/**
@@ -506,6 +513,101 @@ public class GeneratorServiceImpl implements GeneratorService {
return StrUtil.removeAll(table.getModuleName(), StrUtil.DASHED);
}
+ /**
+ * 设置实体生成路径和实体包名。
+ *
+ * 默认与业务模块同包,实体落到 {@code backendPath} 自身(即 biz 模块)。当
+ * {@code backendPath} 末尾形如 {@code xxx-biz} 且同级目录存在 {@code xxx-api}
+ * 模块时,自动切换为:实体写入 {@code xxx-api},包名调整为
+ * {@code ..api.entity},便于在多个 biz 模块之间共享实体
+ * 而不引入循环依赖。
+ *
+ * @param dataModel 数据模型,写入 entityPath、entityPackage、entityPackagePath、
+ * entityClassName、apiPath、hasApiModule
+ * @param table 表配置
+ */
+ private void setEntityModel(Map dataModel, GenTable table) {
+ String modulePackage = StrUtil.format("{}.{}", table.getPackageName(), normalizedModuleName(table));
+ String entityPackage = StrUtil.format("{}.entity", modulePackage);
+ String entityPath = normalizePathSeparator(table.getBackendPath());
+ String apiPath = StrUtil.EMPTY;
+ boolean hasApiModule = false;
+
+ Optional resolvedApiPath = resolveApiPath(table.getBackendPath());
+ if (resolvedApiPath.isPresent()) {
+ apiPath = resolvedApiPath.get();
+ entityPath = apiPath;
+ entityPackage = StrUtil.format("{}.api.entity", modulePackage);
+ hasApiModule = true;
+ }
+
+ dataModel.put(DataModelConstants.HAS_API_MODULE, hasApiModule);
+ dataModel.put(DataModelConstants.API_PATH, apiPath);
+ dataModel.put(DataModelConstants.ENTITY_PATH, entityPath);
+ dataModel.put(DataModelConstants.ENTITY_PACKAGE, entityPackage);
+ dataModel.put(DataModelConstants.ENTITY_PACKAGE_PATH, entityPackage.replace(StrUtil.DOT, StrUtil.SLASH));
+ dataModel.put(DataModelConstants.ENTITY_CLASS_NAME,
+ StrUtil.format("{}.{}Entity", entityPackage, table.getClassName()));
+ }
+
+ /**
+ * 通过 biz 模块路径推断同级 api 模块路径。
+ *
+ * 仅当末尾目录形如 {@code xxx-biz} 且同级 {@code xxx-api} 目录真实存在时
+ * 才返回 api 模块路径,否则返回空 {@link Optional},调用方继续沿用 biz
+ * 同包实体生成策略。返回值已统一为 {@code /} 分隔符,避免 Windows 下与
+ * 模板拼接产生混合分隔符。
+ *
+ * @param backendPath 后端生成路径(biz 模块的绝对或相对路径)
+ * @return 同级 api 模块路径;推断失败时返回 {@link Optional#empty()}
+ */
+ private Optional resolveApiPath(String backendPath) {
+ if (StrUtil.isBlank(backendPath)) {
+ return Optional.empty();
+ }
+
+ Path bizPath = Path.of(normalizePathSeparator(backendPath)).normalize();
+ Path bizFileName = bizPath.getFileName();
+ if (Objects.isNull(bizFileName)) {
+ return Optional.empty();
+ }
+
+ String bizDirectory = bizFileName.toString();
+ if (!StrUtil.endWith(bizDirectory, "-biz")) {
+ return Optional.empty();
+ }
+
+ Path parent = bizPath.getParent();
+ if (Objects.isNull(parent)) {
+ return Optional.empty();
+ }
+
+ Path apiPath = parent.resolve(StrUtil.removeSuffix(bizDirectory, "-biz") + "-api").normalize();
+ if (!Files.isDirectory(apiPath)) {
+ return Optional.empty();
+ }
+
+ return Optional.of(normalizePathSeparator(apiPath.toString()));
+ }
+
+ /**
+ * 归一化路径分隔符。
+ *
+ * 借助 {@link FileUtil#normalize(String)} 将反斜杠统一替换为 {@code /} 并清理
+ * 冗余 {@code ..} 与重复分隔符,确保后续 {@code Path.of} 解析以及模板字符串
+ * 拼接(如 {@code ${entityPath}/src/main/java/...})在 Windows 与类 Unix 平台
+ * 上得到一致的路径形态。
+ *
+ * @param path 原始路径
+ * @return 归一化后的路径;空入参返回空字符串
+ */
+ private String normalizePathSeparator(String path) {
+ if (StrUtil.isBlank(path)) {
+ return StrUtil.EMPTY;
+ }
+ return FileUtil.normalize(path);
+ }
+
/**
* 将表字段按照类型分组并存储到数据模型中
*
diff --git a/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/util/DataModelConstants.java b/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/util/DataModelConstants.java
index cf331dd4f..f1a32ba82 100644
--- a/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/util/DataModelConstants.java
+++ b/pigx-visual/pigx-codegen/src/main/java/com/pig4cloud/pigx/codegen/util/DataModelConstants.java
@@ -61,6 +61,18 @@ public class DataModelConstants {
public static final String FRONTEND_PATH = "frontendPath";
+ public static final String API_PATH = "apiPath";
+
+ public static final String ENTITY_PATH = "entityPath";
+
+ public static final String ENTITY_PACKAGE = "entityPackage";
+
+ public static final String ENTITY_PACKAGE_PATH = "entityPackagePath";
+
+ public static final String ENTITY_CLASS_NAME = "entityClassName";
+
+ public static final String HAS_API_MODULE = "hasApiModule";
+
// 子表信息
public static final String CHILD_FIELD_LIST = "childFieldList";