Merge branch 'main' of http://192.168.65.198:3000/toolchaintechnologycenter/spdm-backend
This commit is contained in:
@@ -0,0 +1,366 @@
|
||||
package com.sdm.common.common;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.sdm.common.entity.req.system.UserQueryReq;
|
||||
import com.sdm.common.entity.resp.system.CIDUserResp;
|
||||
import com.sdm.common.service.UserNameCacheService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestControllerAdvice
|
||||
@Slf4j
|
||||
public class UserNameResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||
|
||||
@Autowired
|
||||
private UserNameCacheService userNameCacheService;
|
||||
|
||||
@Override
|
||||
public boolean supports(MethodParameter returnType, Class converterType) {
|
||||
Class<?> controllerClass = returnType.getContainingClass();
|
||||
String className = controllerClass.getSimpleName();
|
||||
String fullClassName = controllerClass.getName();
|
||||
|
||||
// 排除SysUserController
|
||||
if ("SysUserController".equals(className) || fullClassName.contains("SysUserController")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只处理RestController的方法
|
||||
return returnType.getContainingClass().isAnnotationPresent(RestController.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(Object body, MethodParameter returnType,
|
||||
MediaType selectedContentType, Class selectedConverterType,
|
||||
ServerHttpRequest request, ServerHttpResponse response) {
|
||||
long startTime = System.currentTimeMillis();
|
||||
try {
|
||||
if (body == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
processBody(body);
|
||||
|
||||
long cost = System.currentTimeMillis() - startTime;
|
||||
log.debug("用户名转换完成,耗时: {}ms", cost);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("用户名转换处理失败", e);
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
private void processBody(Object body) {
|
||||
if (body == null) return;
|
||||
|
||||
// 如果是SdmResponse类型,处理其data字段
|
||||
if (isSdmResponse(body)) {
|
||||
processSdmResponse(body);
|
||||
} else if (body instanceof Collection) {
|
||||
((Collection<?>) body).forEach(this::processBody);
|
||||
} else if (body instanceof Page) {
|
||||
((Page<?>) body).getRecords().forEach(this::processBody);
|
||||
} else {
|
||||
processSingleObjectWithChildren(body);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理SdmResponse对象
|
||||
*/
|
||||
private void processSdmResponse(Object sdmResponse) {
|
||||
try {
|
||||
Field dataField = sdmResponse.getClass().getDeclaredField("data");
|
||||
dataField.setAccessible(true);
|
||||
Object data = dataField.get(sdmResponse);
|
||||
|
||||
if (data != null) {
|
||||
// 递归处理data字段 兼容处理双重嵌套的data结构
|
||||
processDoubleNestedData(data);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("解析SdmResponse的data字段失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理双重嵌套的data结构
|
||||
*/
|
||||
private void processDoubleNestedData(Object outerData) {
|
||||
if (outerData == null) return;
|
||||
|
||||
try {
|
||||
// 检查外层data是否还有内层data字段
|
||||
if (hasDataField(outerData)) {
|
||||
Object innerData = getDataValue(outerData);
|
||||
|
||||
if (innerData != null) {
|
||||
// 处理内层data
|
||||
processInnerData(innerData);
|
||||
}
|
||||
} else {
|
||||
// 如果没有内层data,直接处理外层data
|
||||
processBody(outerData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("处理双重嵌套data结构失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取data字段或data键的值
|
||||
*/
|
||||
private Object getDataValue(Object obj) {
|
||||
if (obj == null) return null;
|
||||
|
||||
try {
|
||||
// 情况1:如果是Map,通过get方法获取
|
||||
if (obj instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
return map.get("data");
|
||||
}
|
||||
|
||||
// 情况2:如果是普通对象,通过反射获取字段值
|
||||
Field dataField = obj.getClass().getDeclaredField("data");
|
||||
dataField.setAccessible(true);
|
||||
return dataField.get(obj);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("获取data值失败: {}", e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有data字段或data键
|
||||
*/
|
||||
private boolean hasDataField(Object obj) {
|
||||
if (obj == null) {
|
||||
return false;
|
||||
}
|
||||
// 情况1:如果是Map类型,检查是否有"data"键
|
||||
if (obj instanceof Map) {
|
||||
Map<?, ?> map = (Map<?, ?>) obj;
|
||||
return map.containsKey("data");
|
||||
}
|
||||
// 情况2:如果是普通Java对象,检查是否有data字段
|
||||
try {
|
||||
Field dataField = obj.getClass().getDeclaredField("data");
|
||||
return dataField != null;
|
||||
} catch (NoSuchFieldException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理内层data
|
||||
*/
|
||||
private void processInnerData(Object innerData) {
|
||||
if (innerData == null) return;
|
||||
|
||||
try {
|
||||
// 内层data可能是列表、分页或单个对象
|
||||
if (innerData instanceof Collection) {
|
||||
((Collection<?>) innerData).forEach(this::processBody);
|
||||
} else if (innerData instanceof Page) {
|
||||
((Page<?>) innerData).getRecords().forEach(this::processBody);
|
||||
} else if (hasRecordsField(innerData)) {
|
||||
// 处理分页结构的records字段
|
||||
processRecordsField(innerData);
|
||||
} else {
|
||||
// 单个对象
|
||||
processBody(innerData);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("处理内层data失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有records字段(分页结构)
|
||||
*/
|
||||
private boolean hasRecordsField(Object data) {
|
||||
try {
|
||||
Field recordsField = data.getClass().getDeclaredField("records");
|
||||
return recordsField != null;
|
||||
} catch (NoSuchFieldException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理records字段(分页数据)
|
||||
*/
|
||||
private void processRecordsField(Object data) {
|
||||
try {
|
||||
Field recordsField = data.getClass().getDeclaredField("records");
|
||||
recordsField.setAccessible(true);
|
||||
Object records = recordsField.get(data);
|
||||
|
||||
if (records instanceof Collection) {
|
||||
((Collection<?>) records).forEach(this::processBody);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("处理records字段失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 判断是否是SdmResponse类型
|
||||
*/
|
||||
private boolean isSdmResponse(Object obj) {
|
||||
if (obj == null) return false;
|
||||
|
||||
// 通过类名判断
|
||||
String className = obj.getClass().getSimpleName();
|
||||
if ("SdmResponse".equals(className)) {
|
||||
return true;
|
||||
}
|
||||
// 检查特定字段
|
||||
return hasSdmResponseFields(obj);
|
||||
}
|
||||
|
||||
private boolean hasSdmResponseFields(Object obj) {
|
||||
try {
|
||||
Class<?> clazz = obj.getClass();
|
||||
Field codeField = clazz.getDeclaredField("code");
|
||||
Field messageField = clazz.getDeclaredField("message");
|
||||
Field dataField = clazz.getDeclaredField("data");
|
||||
|
||||
return codeField != null && messageField != null && dataField != null;
|
||||
} catch (NoSuchFieldException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private void processSingleObjectWithChildren(Object obj) {
|
||||
if (obj == null) return;
|
||||
|
||||
try {
|
||||
// 收集不重复的userId
|
||||
Set<Long> userIds = collectUserIds(obj);
|
||||
if (!userIds.isEmpty()) {
|
||||
// 获取用户名称
|
||||
Map<Long, String> userNames = userNameCacheService.batchGetUserNames(userIds);
|
||||
setUserNamesToObject(obj, userNames);
|
||||
}
|
||||
// 递归处理children字段
|
||||
processChildrenField(obj);
|
||||
} catch (Exception e) {
|
||||
log.warn("处理对象 {} 的用户名失败: {}", obj.getClass().getSimpleName(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归处理children字段
|
||||
*/
|
||||
private void processChildrenField(Object obj) throws Exception {
|
||||
List<Field> fields = getAllFields(obj);
|
||||
|
||||
for (Field field : fields) {
|
||||
if ("children".equals(field.getName())) {
|
||||
field.setAccessible(true);
|
||||
Object children = field.get(obj);
|
||||
|
||||
if (children != null) {
|
||||
// 递归处理children
|
||||
processBody(children);
|
||||
}
|
||||
// 找到children字段后就可以退出循环了
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Set<Long> collectUserIds(Object obj) throws Exception {
|
||||
Set<Long> userIds = new HashSet<>();
|
||||
List<Field> fields = getAllFields(obj);
|
||||
|
||||
for (Field field : fields) {
|
||||
if (isUserIdField(field)) {
|
||||
field.setAccessible(true);
|
||||
Long userId = (Long) field.get(obj);
|
||||
userIds.add(userId);
|
||||
}
|
||||
}
|
||||
|
||||
return userIds;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对象的所有字段(包括父类)
|
||||
*/
|
||||
private List<Field> getAllFields(Object obj) {
|
||||
List<Field> fields = new ArrayList<>();
|
||||
Class<?> clazz = obj.getClass();
|
||||
|
||||
// 递归获取所有父类的字段
|
||||
while (clazz != null && clazz != Object.class) {
|
||||
fields.addAll(Arrays.asList(clazz.getDeclaredFields()));
|
||||
clazz = clazz.getSuperclass();
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
private boolean isUserIdField(Field field) {
|
||||
String fieldName = field.getName();
|
||||
return "creator".equals(fieldName) || "updater".equals(fieldName) || "userId".equals(fieldName);
|
||||
}
|
||||
|
||||
private void setUserNamesToObject(Object obj, Map<Long, String> userNames) throws Exception {
|
||||
List<Field> fields = getAllFields(obj);
|
||||
|
||||
for (Field field : fields) {
|
||||
if (isUserIdField(field)) {
|
||||
field.setAccessible(true);
|
||||
Long userId = (Long) field.get(obj);
|
||||
String userName = userNames.get(userId);
|
||||
if (userName != null) {
|
||||
// 设置对应的name字段
|
||||
String nameFieldName = field.getName() + "Name";
|
||||
try {
|
||||
Field nameField = getFieldRecursively(obj.getClass(), nameFieldName);
|
||||
nameField.setAccessible(true);
|
||||
nameField.set(obj, userName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// 如果没有对应的name字段,忽略
|
||||
log.debug("对象 {} 没有字段 {}", obj.getClass().getSimpleName(), nameFieldName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归获取字段(包括父类)
|
||||
*/
|
||||
private Field getFieldRecursively(Class<?> clazz, String fieldName) throws NoSuchFieldException {
|
||||
try {
|
||||
// 在当前类中查找
|
||||
return clazz.getDeclaredField(fieldName);
|
||||
} catch (NoSuchFieldException e) {
|
||||
// 如果在当前类中没找到,递归到父类中查找
|
||||
Class<?> superClass = clazz.getSuperclass();
|
||||
if (superClass != null && superClass != Object.class) {
|
||||
return getFieldRecursively(superClass, fieldName);
|
||||
} else {
|
||||
throw new NoSuchFieldException("字段 '" + fieldName + "' 在类 " + clazz.getName() + " 及其父类中未找到");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
51
common/src/main/java/com/sdm/common/config/CacheConfig.java
Normal file
51
common/src/main/java/com/sdm/common/config/CacheConfig.java
Normal file
@@ -0,0 +1,51 @@
|
||||
package com.sdm.common.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.cache.Cache;
|
||||
import org.springframework.cache.CacheManager;
|
||||
import org.springframework.cache.annotation.EnableCaching;
|
||||
import org.springframework.cache.concurrent.ConcurrentMapCache;
|
||||
import org.springframework.cache.interceptor.CacheErrorHandler;
|
||||
import org.springframework.cache.interceptor.SimpleCacheErrorHandler;
|
||||
import org.springframework.cache.support.SimpleCacheManager;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
@Slf4j
|
||||
public class CacheConfig {
|
||||
|
||||
/**
|
||||
* 用户名缓存配置
|
||||
*/
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
SimpleCacheManager cacheManager = new SimpleCacheManager();
|
||||
cacheManager.setCaches(Arrays.asList(
|
||||
// 用户名缓存
|
||||
new ConcurrentMapCache("userNames")
|
||||
// 可以添加其他缓存配置
|
||||
));
|
||||
return cacheManager;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CacheErrorHandler cacheErrorHandler() {
|
||||
return new SimpleCacheErrorHandler() {
|
||||
@Override
|
||||
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
|
||||
log.warn("缓存获取失败, key: {}, 错误: {}", key, exception.getMessage());
|
||||
// 不抛出异常,继续执行
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
|
||||
log.warn("缓存存入失败, key: {}, 错误: {}", key, exception.getMessage());
|
||||
// 不抛出异常,继续执行
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -30,4 +30,7 @@ public class PermConstants {
|
||||
public static final String ENCODE_FILE_FLAG = ".sdmaes";
|
||||
public static final String DECODE_FILE_FLAG = "AES_DECODE";
|
||||
public static final int REDIS_EXPIRE_TIME = 3600 * 48; //超期时间48小时
|
||||
// 分片碎文件的后缀
|
||||
public static final String CHUNK_TEMPFILE_SUFFIX=".temp";
|
||||
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ public class BaseEntity extends BaseBean {
|
||||
|
||||
@Schema(description = "更新者ID")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
public long updater;
|
||||
public Long updater;
|
||||
|
||||
@Schema(description = "更新者名称")
|
||||
public String updateName;
|
||||
public String updaterName;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
public String updateTime;
|
||||
@@ -31,7 +31,8 @@ public class BaseEntity extends BaseBean {
|
||||
|
||||
@Schema(description = "创建者ID")
|
||||
@JsonFormat(shape = JsonFormat.Shape.STRING)
|
||||
public long creator;
|
||||
public Long creator;
|
||||
public String creatorName;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
public String createTime;
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.sdm.common.entity.req.data;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Data
|
||||
public class ChunkUploadMinioFileReq {
|
||||
|
||||
// 接口5.1 前端生成的,用于发起统一一次审批流的凭证
|
||||
private String uploadTaskId;
|
||||
|
||||
// 接口5.1 返回的这个文件对应的业务数据主键id
|
||||
private Long businessId;
|
||||
|
||||
// 接口5.1 返回的 objectKey
|
||||
private String objectKey;
|
||||
|
||||
// 原始文件的名称
|
||||
private String sourceFileName;
|
||||
|
||||
// 当前为第几分片
|
||||
private Integer chunk;
|
||||
|
||||
// DigestUtils.md5Hex(chunkData[])
|
||||
private String chunkMd5;
|
||||
|
||||
// 分片总数
|
||||
private Integer chunkTotal;
|
||||
|
||||
// 分块文件传输对象
|
||||
private MultipartFile file;
|
||||
|
||||
// 单一文件维度。第一片请求不传,后面的请求必传,第一次请求成功后后端会返回,本次文件的父目录
|
||||
private String fileTempPath;
|
||||
|
||||
|
||||
}
|
||||
@@ -12,6 +12,12 @@ import java.util.List;
|
||||
@Schema(description = "文件上传请求参数")
|
||||
public class UploadFilesReq {
|
||||
|
||||
@Schema(description = "用户勾选的所有的文件的原始名称和大小,前端限制不能选择相同名称的文件,后端逻辑判断对应dirId下不能和历史文件名相同")
|
||||
private List<UploadFilesReq> sourceFiles;
|
||||
|
||||
@Schema(description = "本次新增数据的任务id,毫秒值时间戳即可")
|
||||
private String uploadTaskId;
|
||||
|
||||
@Schema(description = "文件路径")
|
||||
private String path;
|
||||
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.sdm.common.entity.resp.data;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
@Data
|
||||
public class BatchAddFileInfoResp {
|
||||
/**
|
||||
* 这次任务上传的id 时间戳
|
||||
*/
|
||||
private String uploadTaskId;
|
||||
/**
|
||||
* 文件名
|
||||
*/
|
||||
private String sourceFileName;
|
||||
/**
|
||||
* 文件fileId
|
||||
*/
|
||||
private Long businessId;
|
||||
/**
|
||||
* MinIO的文件object_key 绝对路径名
|
||||
*/
|
||||
private String objectKey;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.sdm.common.entity.resp.data;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ChunkUploadMinioFileResp {
|
||||
|
||||
// 本次分片上传的结果 true 成功,false 失败
|
||||
private Boolean result;
|
||||
|
||||
// 文件关联的业务数据Id
|
||||
private Long businessId;
|
||||
|
||||
// 分片上传的任务Id,用于回调处理的审批流创建
|
||||
private String uploadTaskId;
|
||||
|
||||
// 分片文件的临时目录,第一次请求后,每次都会返回
|
||||
private String fileTempPath;
|
||||
|
||||
// 失败的原因
|
||||
private String errMsg;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.sdm.common.service;
|
||||
|
||||
import com.sdm.common.common.SdmResponse;
|
||||
import com.sdm.common.entity.req.system.UserQueryReq;
|
||||
import com.sdm.common.entity.resp.system.CIDUserResp;
|
||||
import com.sdm.common.feign.inter.system.ISysUserFeignClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.cache.annotation.Cacheable;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserNameCacheService {
|
||||
|
||||
@Autowired
|
||||
private ISysUserFeignClient sysUserFeignClient;
|
||||
|
||||
/**
|
||||
* 批量获取用户名 - 带缓存 TODO 后续加入Redis的时候改成Redis
|
||||
*/
|
||||
@Cacheable(value = "userNames", key = "#userIds.toString()")
|
||||
public Map<Long, String> batchGetUserNames(Set<Long> userIds) {
|
||||
log.info("【缓存未命中】批量查询用户名,用户数量: {}", userIds.size());
|
||||
|
||||
// 批量调用用户服务
|
||||
SdmResponse<List<CIDUserResp>> response = sysUserFeignClient.listUserByIds(
|
||||
UserQueryReq.builder().userIds(new ArrayList<>(userIds)).build()
|
||||
);
|
||||
|
||||
Map<Long, String> userMap = response.getData().stream()
|
||||
.collect(Collectors.toMap(
|
||||
CIDUserResp::getUserId,
|
||||
CIDUserResp::getNickname
|
||||
));
|
||||
|
||||
return userMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缓存key的静态方法
|
||||
*/
|
||||
public static String generateKey(Set<Long> userIds) {
|
||||
if (userIds == null || userIds.isEmpty()) {
|
||||
return "empty";
|
||||
}
|
||||
// 排序后拼接成字符串
|
||||
List<Long> sortedList = new ArrayList<>(userIds);
|
||||
Collections.sort(sortedList);
|
||||
return sortedList.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user