1. 代码生成器在生成的时候可以手动选择是否是业务数据类代码(会自动继承基础业务类,合并字段,实现业务接口标记,这个标记会将右上角选择公司作为当前业务数据数据权限进行过滤,办理业务时,如果操作人归属同一个公司的多个部门,会前置校验后弹窗选择归属部门后才能正常办理业务)

2. 文件上传的地方做了一个改动,如果上传文件的 hash 和已存在的附件相同,不会重复上传,会复用相同hash 的附件(要加一个字段,加字段脚本提供两个版本的 patch 脚本 mysql:根目录/sql/mysql/patch.sql dm:根目录/sql/dm/patch.sql  )
This commit is contained in:
chenbowen
2025-07-16 17:19:08 +08:00
parent eaea76e955
commit 619c12bbba
20 changed files with 236 additions and 51 deletions

3
sql/dm/patch.sql Normal file
View File

@@ -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);

3
sql/mysql/patch.sql Normal file
View File

@@ -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);

View File

@@ -19,7 +19,7 @@ public class BusinessDataPermissionConfiguration {
}
@Bean
public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() {
public DeptDataPermissionRuleCustomizer businessDeptDataPermissionRuleCustomizer() {
return rule -> {
// dept
rule.addDeptColumn("demo_contract", "dept_id");

View File

@@ -35,7 +35,7 @@ public class YudaoBusinessDataPermissionAutoConfiguration {
}
@Bean
public DeptDataPermissionRule deptDataPermissionRule(PermissionCommonApi permissionApi, List<DeptDataPermissionRuleCustomizer> customizers) {
public DeptDataPermissionRule deptBusinessDataPermissionRule(PermissionCommonApi permissionApi, List<DeptDataPermissionRuleCustomizer> customizers) {
// Cloud 专属逻辑:优先使用本地的 PermissionApi 实现类,而不是 Feign 调用
// 原因:在创建租户时,租户还没创建好,导致 Feign 调用获取数据权限时,报“租户不存在”的错误
try {

View File

@@ -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<List<CodegenPreviewRespVO>> previewCodegen(@RequestParam("tableId") Long tableId) {
Map<String, String> codes = codegenService.generationCodes(tableId);
public CommonResult<List<CodegenPreviewRespVO>> previewCodegen(@RequestParam("tableId") Long tableId,
@RequestParam(value = "isBusiness", required = false, defaultValue = "false") Boolean isBusiness) {
Map<String, String> 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<String, String> codes = codegenService.generationCodes(tableId);
// 生成代码,传递 isBusiness
Map<String, String> 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);

View File

@@ -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;
}

View File

@@ -23,4 +23,14 @@ public interface FileMapper extends BaseMapperX<FileDO> {
.orderByDesc(FileDO::getId));
}
/**
* 根据哈希值查询文件
* @param hash 文件哈希值
* @return 文件DO若不存在返回null
*/
default FileDO selectByHash(String hash) {
return selectOne(new LambdaQueryWrapperX<FileDO>()
.eq(FileDO::getHash, hash));
}
}

View File

@@ -86,6 +86,17 @@ public interface CodegenService {
* @param tableId 表编号
* @return 生成结果。key 为文件路径value 为对应的代码内容
*/
/**
* 执行指定表的代码生成,支持业务基类继承
* @param tableId 表编号
* @param isBusiness 是否业务基类
* @return 生成结果
*/
Map<String, String> generationCodes(Long tableId, Boolean isBusiness);
/**
* 兼容原有接口,默认 isBusiness=false
*/
Map<String, String> generationCodes(Long tableId);
/**

View File

@@ -243,7 +243,10 @@ public class CodegenServiceImpl implements CodegenService {
}
@Override
public Map<String, String> generationCodes(Long tableId) {
/**
* 执行指定表的代码生成,支持业务基类继承
*/
public Map<String, String> 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<String, String> generationCodes(Long tableId) {
return generationCodes(tableId, false);
}
@Override

View File

@@ -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<String> BASE_DO_FIELDS = new HashSet<>();
public static final Set<String> 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"); // 创建时间,还是需要返回的
}

View File

@@ -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 {
/**
* 后端的配置模版
*
* key1UI 模版的类型 {@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<String, String> execute(CodegenTableDO table, List<CodegenColumnDO> columns,
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList, boolean isBusiness) {
// 1.1 初始化 bindMap 上下文
Map<String, Object> bindingMap = initBindingMap(table, columns, subTables, subColumnsList);
// 传递 isBusiness 到模板
bindingMap.put("isBusiness", isBusiness);
// 1.2 获得模版
Map<String, String> templates = getTemplates(table.getFrontType());
// 2. 执行生成
Map<String, String> result = Maps.newLinkedHashMapWithExpectedSize(templates.size()); // 有序
// 有序
Map<String, String> 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<String, String> execute(CodegenTableDO table, List<CodegenColumnDO> columns,
List<CodegenTableDO> subTables, List<List<CodegenColumnDO>> subColumnsList) {
return execute(table, columns, subTables, subColumnsList, false);
}
private void generateCode(Map<String, String> result, String vmPath,
String filePath, Map<String, Object> bindingMap) {
filePath = formatFilePath(filePath, bindingMap);
@@ -351,7 +364,6 @@ public class CodegenEngine {
/**
* 格式化生成后的代码
*
* 因为尽量让 vm 模版简单,所以统一的处理都在这个方法。
* 如果不处理Vue 的 Pretty 格式校验可能会报错
*

View File

@@ -27,6 +27,7 @@ public interface FileService {
/**
* 保存文件,并返回文件的访问路径
*
* <p>会根据文件内容计算哈希值,若已存在相同哈希的文件,则直接复用,不重复上传。</p>
* @param content 文件内容
* @param name 文件名称,允许空
* @param directory 目录,允许空

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<String> 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());
}
}

View File

@@ -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 '文件表';

View File

@@ -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")

View File

@@ -114,6 +114,12 @@
<!-- <artifactId>yudao-module-iot-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- IoT 物联网相关模块。默认注释,保证编译速度 -->
<dependency>
<groupId>cn.iocoder.cloud</groupId>
<artifactId>yudao-module-template-server</artifactId>
<version>${revision}</version>
</dependency>
<!-- spring boot 配置所需依赖 -->
<dependency>