From 619c12bbba9cb476d48f77295bed362696013e53 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Wed, 16 Jul 2025 17:19:08 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BB=A3=E7=A0=81=E7=94=9F=E6=88=90?= =?UTF-8?q?=E5=99=A8=E5=9C=A8=E7=94=9F=E6=88=90=E7=9A=84=E6=97=B6=E5=80=99?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E6=89=8B=E5=8A=A8=E9=80=89=E6=8B=A9=E6=98=AF?= =?UTF-8?q?=E5=90=A6=E6=98=AF=E4=B8=9A=E5=8A=A1=E6=95=B0=E6=8D=AE=E7=B1=BB?= =?UTF-8?q?=E4=BB=A3=E7=A0=81=EF=BC=88=E4=BC=9A=E8=87=AA=E5=8A=A8=E7=BB=A7?= =?UTF-8?q?=E6=89=BF=E5=9F=BA=E7=A1=80=E4=B8=9A=E5=8A=A1=E7=B1=BB=EF=BC=8C?= =?UTF-8?q?=E5=90=88=E5=B9=B6=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=B8=9A=E5=8A=A1=E6=8E=A5=E5=8F=A3=E6=A0=87=E8=AE=B0=EF=BC=8C?= =?UTF-8?q?=E8=BF=99=E4=B8=AA=E6=A0=87=E8=AE=B0=E4=BC=9A=E5=B0=86=E5=8F=B3?= =?UTF-8?q?=E4=B8=8A=E8=A7=92=E9=80=89=E6=8B=A9=E5=85=AC=E5=8F=B8=E4=BD=9C?= =?UTF-8?q?=E4=B8=BA=E5=BD=93=E5=89=8D=E4=B8=9A=E5=8A=A1=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E6=95=B0=E6=8D=AE=E6=9D=83=E9=99=90=E8=BF=9B=E8=A1=8C=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=EF=BC=8C=E5=8A=9E=E7=90=86=E4=B8=9A=E5=8A=A1=E6=97=B6?= =?UTF-8?q?=EF=BC=8C=E5=A6=82=E6=9E=9C=E6=93=8D=E4=BD=9C=E4=BA=BA=E5=BD=92?= =?UTF-8?q?=E5=B1=9E=E5=90=8C=E4=B8=80=E4=B8=AA=E5=85=AC=E5=8F=B8=E7=9A=84?= =?UTF-8?q?=E5=A4=9A=E4=B8=AA=E9=83=A8=E9=97=A8=EF=BC=8C=E4=BC=9A=E5=89=8D?= =?UTF-8?q?=E7=BD=AE=E6=A0=A1=E9=AA=8C=E5=90=8E=E5=BC=B9=E7=AA=97=E9=80=89?= =?UTF-8?q?=E6=8B=A9=E5=BD=92=E5=B1=9E=E9=83=A8=E9=97=A8=E5=90=8E=E6=89=8D?= =?UTF-8?q?=E8=83=BD=E6=AD=A3=E5=B8=B8=E5=8A=9E=E7=90=86=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=EF=BC=89=202.=20=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E7=9A=84?= =?UTF-8?q?=E5=9C=B0=E6=96=B9=E5=81=9A=E4=BA=86=E4=B8=80=E4=B8=AA=E6=94=B9?= =?UTF-8?q?=E5=8A=A8=EF=BC=8C=E5=A6=82=E6=9E=9C=E4=B8=8A=E4=BC=A0=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E7=9A=84=20hash=20=E5=92=8C=E5=B7=B2=E5=AD=98?= =?UTF-8?q?=E5=9C=A8=E7=9A=84=E9=99=84=E4=BB=B6=E7=9B=B8=E5=90=8C=EF=BC=8C?= =?UTF-8?q?=E4=B8=8D=E4=BC=9A=E9=87=8D=E5=A4=8D=E4=B8=8A=E4=BC=A0=EF=BC=8C?= =?UTF-8?q?=E4=BC=9A=E5=A4=8D=E7=94=A8=E7=9B=B8=E5=90=8Chash=20=E7=9A=84?= =?UTF-8?q?=E9=99=84=E4=BB=B6=EF=BC=88=E8=A6=81=E5=8A=A0=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E5=AD=97=E6=AE=B5=EF=BC=8C=E5=8A=A0=E5=AD=97=E6=AE=B5=E8=84=9A?= =?UTF-8?q?=E6=9C=AC=E6=8F=90=E4=BE=9B=E4=B8=A4=E4=B8=AA=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84=20patch=20=E8=84=9A=E6=9C=AC=20mysql=EF=BC=9A?= =?UTF-8?q?=E6=A0=B9=E7=9B=AE=E5=BD=95/sql/mysql/patch.sql=20dm=EF=BC=9A?= =?UTF-8?q?=E6=A0=B9=E7=9B=AE=E5=BD=95/sql/dm/patch.sql=20=20=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/dm/patch.sql | 3 ++ sql/mysql/patch.sql | 3 ++ .../BusinessDataPermissionConfiguration.java | 2 +- ...sinessDataPermissionAutoConfiguration.java | 2 +- .../admin/codegen/CodegenController.java | 21 +++++--- .../infra/dal/dataobject/file/FileDO.java | 7 ++- .../infra/dal/mysql/file/FileMapper.java | 10 ++++ .../infra/service/codegen/CodegenService.java | 11 ++++ .../service/codegen/CodegenServiceImpl.java | 17 ++++-- .../service/codegen/inner/CodegenBuilder.java | 10 +++- .../service/codegen/inner/CodegenEngine.java | 30 +++++++---- .../infra/service/file/FileService.java | 1 + .../infra/service/file/FileServiceImpl.java | 18 +++++-- .../codegen/java/controller/controller.vm | 22 +++++--- .../src/main/resources/codegen/java/dal/do.vm | 53 +++++++++++++++++-- .../main/resources/codegen/vue3/api/api.ts.vm | 18 +++++-- .../service/file/FileServiceImplTest.java | 40 ++++++++++++++ .../src/test/resources/sql/create_tables.sql | 1 + .../contract/vo/DemoContractSaveReqVO.java | 12 ++--- yudao-server/pom.xml | 6 +++ 20 files changed, 236 insertions(+), 51 deletions(-) create mode 100644 sql/dm/patch.sql create mode 100644 sql/mysql/patch.sql diff --git a/sql/dm/patch.sql b/sql/dm/patch.sql new file mode 100644 index 00000000..4ecdf651 --- /dev/null +++ b/sql/dm/patch.sql @@ -0,0 +1,3 @@ +ALTER TABLE infra_file ADD hash VARCHAR(64); +COMMENT ON COLUMN infra_file.hash IS '文件哈希值(SHA-256)'; +CREATE INDEX idx_infra_file_hash ON infra_file(hash); \ No newline at end of file diff --git a/sql/mysql/patch.sql b/sql/mysql/patch.sql new file mode 100644 index 00000000..750ec693 --- /dev/null +++ b/sql/mysql/patch.sql @@ -0,0 +1,3 @@ +-- 1. 附件信息表新增上传文件 Hash 字段,如果上传文件 hash 重复直接复用不进行重复上传 +ALTER TABLE infra_file ADD COLUMN hash VARCHAR(64) COMMENT '文件哈希值(SHA-256)'; +CREATE INDEX idx_infra_file_hash ON infra_file(hash); \ No newline at end of file diff --git a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java index 4a0b19d5..81ff3d4b 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-business/src/main/java/cn/iocoder/yudao/framework/business/framework/BusinessDataPermissionConfiguration.java @@ -19,7 +19,7 @@ public class BusinessDataPermissionConfiguration { } @Bean - public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() { + public DeptDataPermissionRuleCustomizer businessDeptDataPermissionRuleCustomizer() { return rule -> { // dept rule.addDeptColumn("demo_contract", "dept_id"); diff --git a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java index 1a806c43..5a88fd50 100644 --- a/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java +++ b/yudao-framework/yudao-spring-boot-starter-biz-data-permission/src/main/java/cn/iocoder/yudao/framework/datapermission/config/YudaoBusinessDataPermissionAutoConfiguration.java @@ -35,7 +35,7 @@ public class YudaoBusinessDataPermissionAutoConfiguration { } @Bean - public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi, List customizers) { + public DeptDataPermissionRule deptBusinessDataPermissionRule(PermissionCommonApi permissionApi, List customizers) { // Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用 // 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误 try { diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java index bf987d8b..c577c8fa 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/codegen/CodegenController.java @@ -34,7 +34,6 @@ import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserNickname; import static cn.iocoder.yudao.module.infra.framework.file.core.utils.FileTypeUtils.writeAttachment; @@ -125,21 +124,29 @@ public class CodegenController { @Operation(summary = "预览生成代码") @GetMapping("/preview") - @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @Parameters({ + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024"), + @Parameter(name = "isBusiness", description = "是否业务基类", example = "false") + }) @PreAuthorize("@ss.hasPermission('infra:codegen:preview')") - public CommonResult> previewCodegen(@RequestParam("tableId") Long tableId) { - Map codes = codegenService.generationCodes(tableId); + public CommonResult> previewCodegen(@RequestParam("tableId") Long tableId, + @RequestParam(value = "isBusiness", required = false, defaultValue = "false") Boolean isBusiness) { + Map codes = codegenService.generationCodes(tableId, isBusiness); return success(CodegenConvert.INSTANCE.convert(codes)); } @Operation(summary = "下载生成代码") @GetMapping("/download") - @Parameter(name = "tableId", description = "表编号", required = true, example = "1024") + @Parameters({ + @Parameter(name = "tableId", description = "表编号", required = true, example = "1024"), + @Parameter(name = "isBusiness", description = "是否业务基类", example = "false") + }) @PreAuthorize("@ss.hasPermission('infra:codegen:download')") public void downloadCodegen(@RequestParam("tableId") Long tableId, + @RequestParam(value = "isBusiness", required = false, defaultValue = "false") Boolean isBusiness, HttpServletResponse response) throws IOException { - // 生成代码 - Map codes = codegenService.generationCodes(tableId); + // 生成代码,传递 isBusiness + Map codes = codegenService.generationCodes(tableId, isBusiness); // 构建 zip 包 String[] paths = codes.keySet().toArray(new String[0]); ByteArrayInputStream[] ins = codes.values().stream().map(IoUtil::toUtf8Stream).toArray(ByteArrayInputStream[]::new); diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java index f340d9e3..b0cc337a 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/file/FileDO.java @@ -26,7 +26,7 @@ import lombok.*; public class FileDO extends BaseDO { /** - * 编号,数据库自增 + * 编号 */ @TableId(type = IdType.ASSIGN_ID) private Long id; @@ -57,4 +57,9 @@ public class FileDO extends BaseDO { */ private Integer size; + /** + * 文件哈希值(SHA-256) + */ + private String hash; + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java index 11233a45..60998a1e 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/file/FileMapper.java @@ -23,4 +23,14 @@ public interface FileMapper extends BaseMapperX { .orderByDesc(FileDO::getId)); } + /** + * 根据哈希值查询文件 + * @param hash 文件哈希值 + * @return 文件DO,若不存在返回null + */ + default FileDO selectByHash(String hash) { + return selectOne(new LambdaQueryWrapperX() + .eq(FileDO::getHash, hash)); + } + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java index 7adc9f7f..64835408 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenService.java @@ -86,6 +86,17 @@ public interface CodegenService { * @param tableId 表编号 * @return 生成结果。key 为文件路径,value 为对应的代码内容 */ + /** + * 执行指定表的代码生成,支持业务基类继承 + * @param tableId 表编号 + * @param isBusiness 是否业务基类 + * @return 生成结果 + */ + Map generationCodes(Long tableId, Boolean isBusiness); + + /** + * 兼容原有接口,默认 isBusiness=false + */ Map generationCodes(Long tableId); /** diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java index d00971a8..c50ebcc2 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/CodegenServiceImpl.java @@ -243,7 +243,10 @@ public class CodegenServiceImpl implements CodegenService { } @Override - public Map generationCodes(Long tableId) { + /** + * 执行指定表的代码生成,支持业务基类继承 + */ + public Map generationCodes(Long tableId, Boolean isBusiness) { // 校验是否已经存在 CodegenTableDO table = codegenTableMapper.selectById(tableId); if (table == null) { @@ -275,8 +278,16 @@ public class CodegenServiceImpl implements CodegenService { } } - // 执行生成 - return codegenEngine.execute(table, columns, subTables, subColumnsList); + // 执行生成,传递 isBusiness + return codegenEngine.execute(table, columns, subTables, subColumnsList, isBusiness != null && isBusiness); + } + + /** + * 兼容原有接口,默认 isBusiness=false + */ + @Override + public Map generationCodes(Long tableId) { + return generationCodes(tableId, false); } @Override diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java index 06f0478f..80c846a9 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenBuilder.java @@ -4,6 +4,7 @@ import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.ReflectUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BusinessBaseDO; import cn.iocoder.yudao.module.infra.convert.codegen.CodegenConvert; import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenTableDO; @@ -67,6 +68,7 @@ public class CodegenBuilder { * {@link cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO} 的字段 */ public static final Set BASE_DO_FIELDS = new HashSet<>(); + public static final Set BUSINESS_BASE_DO_FIELDS = new HashSet<>(); /** * 新增操作,不需要传递的字段 */ @@ -87,12 +89,18 @@ public class CodegenBuilder { static { Arrays.stream(ReflectUtil.getFields(BaseDO.class)).forEach(field -> BASE_DO_FIELDS.add(field.getName())); BASE_DO_FIELDS.add(TENANT_ID_FIELD); + Arrays.stream(ReflectUtil.getFields(BusinessBaseDO.class)).forEach(field -> BUSINESS_BASE_DO_FIELDS.add(field.getName())); + BUSINESS_BASE_DO_FIELDS.add(TENANT_ID_FIELD); // 处理 OPERATION 相关的字段 CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); LIST_OPERATION_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); - LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的 LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BASE_DO_FIELDS); + CREATE_OPERATION_EXCLUDE_COLUMN.addAll(BUSINESS_BASE_DO_FIELDS); + UPDATE_OPERATION_EXCLUDE_COLUMN.addAll(BUSINESS_BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.addAll(BUSINESS_BASE_DO_FIELDS); + LIST_OPERATION_RESULT_EXCLUDE_COLUMN.addAll(BUSINESS_BASE_DO_FIELDS); + LIST_OPERATION_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是可能需要传递的 LIST_OPERATION_RESULT_EXCLUDE_COLUMN.remove("createTime"); // 创建时间,还是需要返回的 } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java index 364e6f8e..5880d47d 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/codegen/inner/CodegenEngine.java @@ -24,6 +24,7 @@ import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat; import cn.iocoder.yudao.framework.excel.core.convert.DictConvert; import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; +import cn.iocoder.yudao.framework.mybatis.core.dataobject.BusinessBaseDO; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.infra.dal.dataobject.codegen.CodegenColumnDO; @@ -52,7 +53,6 @@ import static cn.hutool.core.text.CharSequenceUtil.*; /** * 代码生成的引擎,用于具体生成代码 * 目前基于 {@link org.apache.velocity.app.Velocity} 模板引擎实现 - * * 考虑到 Java 模板引擎的框架非常多,Freemarker、Velocity、Thymeleaf 等等,所以我们采用 hutool 封装的 {@link cn.hutool.extra.template.Template} 抽象 * * @author 芋道源码 @@ -62,7 +62,6 @@ public class CodegenEngine { /** * 后端的模板配置 - * * key:模板在 resources 的地址 * value:生成的路径 */ @@ -98,7 +97,6 @@ public class CodegenEngine { /** * 后端的配置模版 - * * key1:UI 模版的类型 {@link CodegenFrontTypeEnum#getType()} * key2:模板在 resources 的地址 * value:生成的路径 @@ -190,7 +188,6 @@ public class CodegenEngine { /** * 是否使用 jakarta 包,用于解决 Spring Boot 2.X 和 3.X 的兼容性问题 - * * true - 使用 jakarta.validation.constraints.* * false - 使用 javax.validation.constraints.* */ @@ -199,7 +196,6 @@ public class CodegenEngine { /** * 是否为 yudao-cloud 项目,用于解决 Boot 和 Cloud 的 api 模块兼容性问题 - * * true - 需要有 yudao-module-xxx-api 模块 * false - 不需要有,使用 api、enum 包即可 */ @@ -247,6 +243,9 @@ public class CodegenEngine { // DO 类,独有字段 globalBindingMap.put("BaseDOClassName", BaseDO.class.getName()); globalBindingMap.put("baseDOFields", CodegenBuilder.BASE_DO_FIELDS); + // BusinessDO 类,独有字段 + globalBindingMap.put("BusinessBaseDOClassName", BusinessBaseDO.class.getName()); + globalBindingMap.put("businessBaseDOFields", CodegenBuilder.BUSINESS_BASE_DO_FIELDS); globalBindingMap.put("QueryWrapperClassName", LambdaQueryWrapperX.class.getName()); globalBindingMap.put("BaseMapperClassName", BaseMapperX.class.getName()); // Util 工具类 @@ -264,22 +263,28 @@ public class CodegenEngine { /** * 生成代码 - * * @param table 表定义 * @param columns table 的字段定义数组 * @param subTables 子表数组,当且仅当主子表时使用 * @param subColumnsList subTables 的字段定义数组 * @return 生成的代码,key 是路径,value 是对应代码 */ + /** + * 代码生成,支持业务基类继承 + * @param isBusiness 是否业务基类 + */ public Map execute(CodegenTableDO table, List columns, - List subTables, List> subColumnsList) { + List subTables, List> subColumnsList, boolean isBusiness) { // 1.1 初始化 bindMap 上下文 Map bindingMap = initBindingMap(table, columns, subTables, subColumnsList); + // 传递 isBusiness 到模板 + bindingMap.put("isBusiness", isBusiness); // 1.2 获得模版 Map templates = getTemplates(table.getFrontType()); // 2. 执行生成 - Map result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序 + // 有序 + Map result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); templates.forEach((vmPath, filePath) -> { // 2.1 特殊:主子表专属逻辑 if (isSubTemplate(vmPath)) { @@ -303,6 +308,14 @@ public class CodegenEngine { return result; } + /** + * 兼容原有接口,默认 isBusiness=false + */ + public Map execute(CodegenTableDO table, List columns, + List subTables, List> subColumnsList) { + return execute(table, columns, subTables, subColumnsList, false); + } + private void generateCode(Map result, String vmPath, String filePath, Map bindingMap) { filePath = formatFilePath(filePath, bindingMap); @@ -351,7 +364,6 @@ public class CodegenEngine { /** * 格式化生成后的代码 - * * 因为尽量让 vm 模版简单,所以统一的处理都在这个方法。 * 如果不处理,Vue 的 Pretty 格式校验可能会报错 * diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index a3976629..40b1d5bc 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -27,6 +27,7 @@ public interface FileService { /** * 保存文件,并返回文件的访问路径 * + *

会根据文件内容计算哈希值,若已存在相同哈希的文件,则直接复用,不重复上传。

* @param content 文件内容 * @param name 文件名称,允许空 * @param directory 目录,允许空 diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 59876b12..2fbdd150 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -78,13 +78,22 @@ public class FileServiceImpl implements FileService { } private FileDO uploadFile(byte[] content, String name, String directory, String type) throws Exception { - // 1.1 处理 type 为空的情况 + // 1.1 计算文件哈希 + String hash = DigestUtil.sha256Hex(content); + + // 1.2 查找是否已存在相同hash的文件,存在则直接复用 + FileDO exist = fileMapper.selectByHash(hash); + if (exist != null) { + return exist; + } + + // 1.3 处理 type 为空的情况 if (StrUtil.isEmpty(type)) { type = FileTypeUtils.getMineType(content, name); } - // 1.2 处理 name 为空的情况 + // 1.4 处理 name 为空的情况 if (StrUtil.isEmpty(name)) { - name = DigestUtil.sha256Hex(content); + name = hash; } if (StrUtil.isEmpty(FileUtil.extName(name))) { // 如果 name 没有后缀 type,则补充后缀 @@ -102,7 +111,8 @@ public class FileServiceImpl implements FileService { String url = client.upload(content, path, type); FileDO entity = new FileDO().setConfigId(client.getId()) .setName(name).setPath(path).setUrl(url) - .setType(type).setSize(content.length); + .setType(type).setSize(content.length) + .setHash(hash); // 3. 保存到数据库 fileMapper.insert(entity); diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/controller/controller.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/controller/controller.vm index 9f63e240..ec9f3282 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/controller/controller.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/controller/controller.vm @@ -4,6 +4,9 @@ import org.springframework.web.bind.annotation.*; import ${jakartaPackage}.annotation.Resource; import org.springframework.validation.annotation.Validated; #if ($sceneEnum.scene == 1)import org.springframework.security.access.prepost.PreAuthorize;#end +#if($isBusiness && $isBusiness == true) +import ${basePackage}.framework.business.interceptor.BusinessControllerMarker; +#end import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.Parameter; @@ -39,7 +42,12 @@ import ${basePackage}.module.${table.moduleName}.service.${table.businessName}.$ ##二级的 businessName 暂时不算在 HTTP 路径上,可以根据需要写 @RequestMapping("/${table.moduleName}/${simpleClassName_strikeCase}") @Validated +## 支持业务基类标记:isBusiness=true 时继承 BusinessControllerMarker +#if($isBusiness && $isBusiness == true) +public class ${sceneEnum.prefixClass}${table.className}Controller implements BusinessControllerMarker { +#else public class ${sceneEnum.prefixClass}${table.className}Controller { +#end @Resource private ${table.className}Service ${classNameVar}Service; @@ -129,7 +137,7 @@ public class ${sceneEnum.prefixClass}${table.className}Controller { #if ($voType == 10) return success(BeanUtils.toBean(list, ${respVOClass}.class)); #else - return success(list); + return success(list); #end } @@ -256,15 +264,15 @@ public class ${sceneEnum.prefixClass}${table.className}Controller { } #end - @GetMapping("/${subSimpleClassName_strikeCase}/get") - @Operation(summary = "获得${subTable.classComment}") - @Parameter(name = "id", description = "编号", required = true) + @GetMapping("/${subSimpleClassName_strikeCase}/get") + @Operation(summary = "获得${subTable.classComment}") + @Parameter(name = "id", description = "编号", required = true) #if ($sceneEnum.scene == 1) @PreAuthorize("@ss.hasPermission('${permissionPrefix}:query')") #end - public CommonResult<${subTable.className}DO> get${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { - return success(${classNameVar}Service.get${subSimpleClassName}(id)); - } + public CommonResult<${subTable.className}DO> get${subSimpleClassName}(@RequestParam("id") ${subPrimaryColumn.javaType} id) { + return success(${classNameVar}Service.get${subSimpleClassName}(id)); + } #end #end diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/dal/do.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/dal/do.vm index 6dcf1f1f..9c718296 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/dal/do.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/java/dal/do.vm @@ -11,20 +11,24 @@ import java.time.LocalDateTime; #end #end import com.baomidou.mybatisplus.annotation.*; +## 导入基类 +#if($isBusiness && $isBusiness == true) +import ${basePackage}.framework.mybatis.core.dataobject.BusinessBaseDO; +#else import ${BaseDOClassName}; +#end ## 处理 Excel 导出 + Schema 注解(仅 DO 模式) #if ($voType == 20) import io.swagger.v3.oas.annotations.media.Schema; import com.alibaba.excel.annotation.*; #foreach ($column in $columns) #if ("$!column.dictType" != "")## 有设置数据字典 - import ${DictFormatClassName}; - import ${DictConvertClassName}; + import ${DictFormatClassName}; + import ${DictConvertClassName}; #break #end #end #end - /** * ${table.classComment} DO * @@ -43,15 +47,25 @@ import com.alibaba.excel.annotation.*; @Schema(description = "${sceneEnum.name} - ${table.classComment} Response VO") @ExcelIgnoreUnannotated #end +/** + * 支持业务基类继承:isBusiness=true 时继承 BusinessBaseDO,否则继承 BaseDO + */ +#if($isBusiness && $isBusiness == true) +public class ${table.className}DO extends BusinessBaseDO { +#else public class ${table.className}DO extends BaseDO { +#end ## 特殊:树表专属逻辑 #if ( $table.templateType == 2 ) public static final Long ${treeParentColumn_javaField_underlineCase.toUpperCase()}_ROOT = 0L; #end + +## 字段定义,分支避免嵌套,保证 Velocity 兼容性 +#if($isBusiness == true) #foreach ($column in $columns) -#if (!${baseDOFields.contains(${column.javaField})})##排除 BaseDO 的字段 + #if (!${businessBaseDOFields.contains(${column.javaField})}) /** * ${column.columnComment} #if ("$!column.dictType" != "")##处理枚举值 @@ -75,6 +89,35 @@ public class ${table.className}DO extends BaseDO { #end ## 3. 处理字段定义 private ${column.javaType} ${column.javaField}; + #end +#end +#else +#foreach ($column in $columns) + #if (!${baseDOFields.contains(${column.javaField})}) + /** + * ${column.columnComment} + #if ("$!column.dictType" != "")##处理枚举值 + * + * 枚举 {@link TODO ${column.dictType} 对应的类} + #end + */ + #if (${column.primaryKey})##处理主键 + @TableId#if (${column.javaType} == 'String')(type = IdType.INPUT)#else(type = IdType.ASSIGN_ID)#end + #end +#if ($voType == 20) +## 1. 处理 Swagger 注解 + @Schema(description = "${column.columnComment}"#if (!${column.nullable}), requiredMode = Schema.RequiredMode.REQUIRED#end#if ("$!column.example" != ""), example = "${column.example}"#end) +## 2. 处理 Excel 导出 +#if ("$!column.dictType" != "")##处理枚举值 + @ExcelProperty(value = "${column.columnComment}", converter = DictConvert.class) + @DictFormat("${column.dictType}") // TODO 代码优化:建议设置到对应的 DictTypeConstants 枚举类中 +#else + @ExcelProperty("${column.columnComment}") +#end +#end +## 3. 处理字段定义 + private ${column.javaType} ${column.javaField}; + #end #end #end @@ -94,7 +137,7 @@ public class ${table.className}DO extends BaseDO { * ${subTable.classComment} */ @Schema(description = "${subTable.classComment}") - @TableField(exist = false) + @TableField(exist = false) private ${subTable.className}DO ${subClassNameVars.get($index)}; #end #end diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/api/api.ts.vm b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/api/api.ts.vm index c3044fb8..a252884c 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/api/api.ts.vm +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/codegen/vue3/api/api.ts.vm @@ -15,6 +15,16 @@ export interface ${simpleClassName}VO { #end #end } +## 主键类型动态生成,优先查找列名为 id 的类型,找不到则默认 string +#set($idType = "string") +#foreach($column in $columns) + #if($column.javaField == "id") + #if($column.javaType.toLowerCase() == "long" || $column.javaType.toLowerCase() == "integer" || $column.javaType.toLowerCase() == "short" || $column.javaType.toLowerCase() == "double" || $column.javaType.toLowerCase() == "bigdecimal") + #set($idType = "number") + #end + #end +#end +type IdType = $idType; // ${table.classComment} API export const ${simpleClassName}Api = { @@ -31,7 +41,7 @@ export const ${simpleClassName}Api = { #end // 查询${table.classComment}详情 - get${simpleClassName}: async (id: number) => { + get${simpleClassName}: async (id: IdType) => { return await request.get({ url: `${baseURL}/get?id=` + id }) }, @@ -46,7 +56,7 @@ export const ${simpleClassName}Api = { }, // 删除${table.classComment} - delete${simpleClassName}: async (id: number) => { + delete${simpleClassName}: async (id: IdType) => { return await request.delete({ url: `${baseURL}/delete?id=` + id }) }, @@ -102,12 +112,12 @@ export const ${simpleClassName}Api = { }, // 删除${subTable.classComment} - delete${subSimpleClassName}: async (id: number) => { + delete${subSimpleClassName}: async (id: IdType) => { return await request.delete({ url: `${baseURL}/${subSimpleClassName_strikeCase}/delete?id=` + id }) }, // 获得${subTable.classComment} - get${subSimpleClassName}: async (id: number) => { + get${subSimpleClassName}: async (id: IdType) => { return await request.get({ url: `${baseURL}/${subSimpleClassName_strikeCase}/get?id=` + id }) }, #end diff --git a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java index c109b25a..514e2798 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java +++ b/yudao-module-infra/yudao-module-infra-server/src/test/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImplTest.java @@ -306,4 +306,44 @@ public class FileServiceImplTest extends BaseDbUnitTest { assertTrue(path.matches("\\d{8}/test_\\d+\\.jpg")); } + /** + * 如果上传文件 hash 一致 SHA256 值,则复用已存在的文件 + * 不做重复上传 + */ + @Test + public void testCreateFile_withSameHash() throws Exception { + // 准备参数 + byte[] content = ResourceUtil.readBytes("file/erweima.jpg"); + String name = "单测文件名"; + String directory = randomString(); + String type = "image/jpeg"; + // mock Master 文件客户端 + FileClient client = mock(FileClient.class); + when(fileConfigService.getMasterFileClient()).thenReturn(client); + String url = randomString(); + AtomicReference pathRef = new AtomicReference<>(); + when(client.upload(same(content), argThat(path -> { + assertTrue(path.matches(directory + "/\\d{8}/" + name + "_\\d+.jpg")); + pathRef.set(path); + return true; + }), eq(type))).thenReturn(url); + when(client.getId()).thenReturn(10L); + + // 首次上传 + String result1 = fileService.createFile(content, name, directory, type); + assertEquals(result1, url); + + // 再次上传同样的内容,应该复用已存在的文件 + String result2 = fileService.createFile(content, name, directory, type); + assertEquals(result2, url); + + // 校验数据 + FileDO file = fileMapper.selectOne(FileDO::getUrl, url); + assertEquals(10L, file.getConfigId()); + assertEquals(pathRef.get(), file.getPath()); + assertEquals(url, file.getUrl()); + assertEquals(type, file.getType()); + assertEquals(content.length, file.getSize()); + } + } diff --git a/yudao-module-infra/yudao-module-infra-server/src/test/resources/sql/create_tables.sql b/yudao-module-infra/yudao-module-infra-server/src/test/resources/sql/create_tables.sql index d4b19c92..0bca9a28 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/test/resources/sql/create_tables.sql +++ b/yudao-module-infra/yudao-module-infra-server/src/test/resources/sql/create_tables.sql @@ -45,6 +45,7 @@ CREATE TABLE IF NOT EXISTS "infra_file" ( "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, "deleted" bit NOT NULL DEFAULT FALSE, "tenant_id" bigint not null default '0', + "hash" varchar(64) DEFAULT NULL COMMENT '文件哈希值(SHA-256)', PRIMARY KEY ("id") ) COMMENT '文件表'; diff --git a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java index 84395532..ab520a95 100644 --- a/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java +++ b/yudao-module-template/yudao-module-template-server/src/main/java/cn/iocoder/yudao/module/template/controller/admin/contract/vo/DemoContractSaveReqVO.java @@ -1,11 +1,11 @@ package cn.iocoder.yudao.module.template.controller.admin.contract.vo; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.*; -import java.util.*; -import jakarta.validation.constraints.*; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import lombok.Data; + import java.math.BigDecimal; -import org.springframework.format.annotation.DateTimeFormat; import java.time.LocalDateTime; @Schema(description = "管理后台 - 合同新增/修改 Request VO") @@ -36,19 +36,15 @@ public class DemoContractSaveReqVO { private String remark; @Schema(description = "公司ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "4180") - @NotNull(message = "公司ID不能为空") private Long companyId; @Schema(description = "公司名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") - @NotEmpty(message = "公司名称不能为空") private String companyName; @Schema(description = "部门ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1707") - @NotNull(message = "部门ID不能为空") private Long deptId; @Schema(description = "部门名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "张三") - @NotEmpty(message = "部门名称不能为空") private String deptName; @Schema(description = "岗位ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "26779") diff --git a/yudao-server/pom.xml b/yudao-server/pom.xml index 53e5e0fa..56ca68bf 100644 --- a/yudao-server/pom.xml +++ b/yudao-server/pom.xml @@ -114,6 +114,12 @@ + + + cn.iocoder.cloud + yudao-module-template-server + ${revision} +