1. 代码生成器在生成的时候可以手动选择是否是业务数据类代码(会自动继承基础业务类,合并字段,实现业务接口标记,这个标记会将右上角选择公司作为当前业务数据数据权限进行过滤,办理业务时,如果操作人归属同一个公司的多个部门,会前置校验后弹窗选择归属部门后才能正常办理业务)
2. 文件上传的地方做了一个改动,如果上传文件的 hash 和已存在的附件相同,不会重复上传,会复用相同hash 的附件(要加一个字段,加字段脚本提供两个版本的 patch 脚本 mysql:根目录/sql/mysql/patch.sql dm:根目录/sql/dm/patch.sql )
This commit is contained in:
3
sql/dm/patch.sql
Normal file
3
sql/dm/patch.sql
Normal 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
3
sql/mysql/patch.sql
Normal 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);
|
||||
@@ -19,7 +19,7 @@ public class BusinessDataPermissionConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public DeptDataPermissionRuleCustomizer sysDeptDataPermissionRuleCustomizer() {
|
||||
public DeptDataPermissionRuleCustomizer businessDeptDataPermissionRuleCustomizer() {
|
||||
return rule -> {
|
||||
// dept
|
||||
rule.addDeptColumn("demo_contract", "dept_id");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"); // 创建时间,还是需要返回的
|
||||
}
|
||||
|
||||
|
||||
@@ -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<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 格式校验可能会报错
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@ public interface FileService {
|
||||
/**
|
||||
* 保存文件,并返回文件的访问路径
|
||||
*
|
||||
* <p>会根据文件内容计算哈希值,若已存在相同哈希的文件,则直接复用,不重复上传。</p>
|
||||
* @param content 文件内容
|
||||
* @param name 文件名称,允许空
|
||||
* @param directory 目录,允许空
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -11,7 +11,12 @@ 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;
|
||||
@@ -24,7 +29,6 @@ import com.alibaba.excel.annotation.*;
|
||||
#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" != "")##处理枚举值
|
||||
@@ -77,6 +91,35 @@ public class ${table.className}DO extends BaseDO {
|
||||
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
|
||||
|
||||
## 特殊:主子表专属逻辑(非 ERP 模式)
|
||||
#if ( $voType == 20 && $subTables && $subTables.size() > 0 && $table.templateType != 11 )
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 '文件表';
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user