Pre Merge pull request !838 from Lau/futuer/boot4

This commit is contained in:
Lau 2026-03-19 05:38:31 +00:00 committed by Gitee
commit 6353d248aa
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
15 changed files with 477 additions and 1 deletions

View File

@ -0,0 +1,21 @@
package org.dromara.common.translation.collection;
import java.lang.annotation.*;
/**
* 集合翻译声明到需要翻译的实体上
*
* @author 秋辞未寒
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
public @interface TranslationCollection {
/**
* 其他条件 例如: 字典type(sys_user_gender)
*/
String other() default "";
}

View File

@ -0,0 +1,20 @@
package org.dromara.common.translation.collection;
import com.fasterxml.jackson.annotation.JacksonAnnotationsInside;
import tools.jackson.databind.annotation.JsonSerialize;
import java.lang.annotation.*;
/**
* 序列化器绑定注解
*
* @author 秋辞未寒
*/
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@JacksonAnnotationsInside
@JsonSerialize(using = TranslationCollectionSerializer.class)
public @interface TranslationCollectionBinding {
}

View File

@ -0,0 +1,53 @@
package org.dromara.common.translation.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ClassUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.reflect.AnnotationUtils;
import org.dromara.common.translation.core.handler.TranslationCollectionHandler;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
import tools.jackson.databind.SerializationContext;
import tools.jackson.databind.ValueSerializer;
import java.util.Collection;
/**
* 集合翻译序列化器
*
* @author 秋辞未寒
*/
@Slf4j
public class TranslationCollectionSerializer extends ValueSerializer<TranslationCollectionWrapper<?>> {
private static final TranslationCollectionHandler TRANSLATION_HANDLER = new TranslationCollectionHandler();
@Override
public void serialize(TranslationCollectionWrapper<?> value, JsonGenerator gen, SerializationContext provider) throws JacksonException {
if (CollUtil.isEmpty(value)) {
return;
}
Class<?> elementType = value.getElementType();
if (elementType == null) {
elementType = ClassUtil.getClass(CollUtil.getFirst(value));
}
// 获取包装原内容很重要如果原样使用的话会无限递归
Collection<?> collection = value.getCollection();
TranslationCollection annotation = AnnotationUtils.getAnnotation(elementType, TranslationCollection.class);
if (annotation != null ) {
try {
collection = TRANSLATION_HANDLER.process((Collection<Object>) collection, annotation.other());
} catch (Exception e) {
log.error("翻译处理异常value: {}", value, e);
// 出现异常时输出原始值而不是中断序列化
collection = value.getCollection();
}
}
gen.writeStartArray();
for (Object o : collection) {
gen.writePOJO(o);
}
gen.writeEndArray();
}
}

View File

@ -0,0 +1,126 @@
package org.dromara.common.translation.collection;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ClassUtil;
import lombok.Getter;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Objects;
/**
* 翻译集合包装器
*
* @param <E> 元素类型
*
* @author 秋辞未寒
*/
@Getter
@TranslationCollectionBinding
public class TranslationCollectionWrapper<E> implements Collection<E> {
private final Collection<E> collection;
private final Class<E> elementType;
/**
* 不建议直接使用构造函数去构建包装器
*/
private TranslationCollectionWrapper(Collection<E> collection, Class<E> elementType) {
this.collection = collection;
this.elementType = elementType;
}
/**
* 包装集合成翻译集合
* @param collection 待包装的集合
* @return 包装后的集合类型
* @param <E> 元素类型
*/
public static <E> TranslationCollectionWrapper<E> form(Collection<E> collection) {
return form(collection,null);
}
/**
* 包装集合成翻译集合 推荐使用该函数可以减少不必要的迭代器取值~
* @param collection 待包装的集合
* @param elementType 元素Class实例
* @return 包装后的集合类型
* @param <E> 元素类型
*/
public static <E> TranslationCollectionWrapper<E> form(Collection<E> collection, Class<E> elementType) {
if (Objects.isNull(collection)) {
collection = Collections.emptyList();
}
if (Objects.isNull(elementType)) {
elementType = ClassUtil.getClass(CollUtil.getFirst(collection));
}
return new TranslationCollectionWrapper<>(collection,elementType);
}
@Override
public int size() {
return collection.size();
}
@Override
public boolean isEmpty() {
return collection.isEmpty();
}
@Override
public boolean contains(Object o) {
return collection.contains(o);
}
@Override
public Iterator<E> iterator() {
return collection.iterator();
}
@Override
public Object[] toArray() {
return collection.toArray();
}
@Override
public <T> T[] toArray(T[] a) {
return collection.toArray(a);
}
@Override
public boolean add(E e) {
return collection.add(e);
}
@Override
public boolean remove(Object o) {
return collection.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
return collection.containsAll(c);
}
@Override
public boolean addAll(Collection<? extends E> c) {
return collection.addAll(c);
}
@Override
public boolean removeAll(Collection<?> c) {
return collection.removeAll(c);
}
@Override
public boolean retainAll(Collection<?> c) {
return collection.retainAll(c);
}
@Override
public void clear() {
collection.clear();
}
}

View File

@ -2,6 +2,10 @@ package org.dromara.common.translation.core;
import org.dromara.common.translation.annotation.TranslationType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 翻译接口 (实现类需标注 {@link TranslationType} 注解标明翻译类型)
*
@ -17,4 +21,15 @@ public interface TranslationInterface<T> {
* @return 返回键对应的翻译值
*/
T translation(Object key, String other);
/**
*
* 按翻译键执行转换
*
* @param keys 需要被翻译的键(注意可能为空)
* @param other 其他参数
* @return 返回键对应的翻译值
*/
List<T> translation(List<Object> keys, String other);
}

View File

@ -0,0 +1,175 @@
package org.dromara.common.translation.core.handler;
import cn.hutool.core.collection.CollectionUtil;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.core.TranslationInterface;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* 翻译集合处理器 - 默认实现
* 用于批量翻译集合对象中添加了 {@link Translation} 注解的字段
*
* @author Lau
*/
@Slf4j
public class TranslationCollectionHandler {
public Collection<Object> process(Collection<Object> value, String other) throws Exception {
// 过滤非空对象并转为 List保证后续操作的顺序性和可索引性
List<Object> items = StreamUtils.filter(value, Objects::nonNull);
if (CollectionUtil.isEmpty(items)) {
return value;
}
// 获取所有需要翻译的字段及其元数据
List<TranslationFieldInfo> fieldInfos = extractTranslationFields(items.get(0));
if (fieldInfos.isEmpty()) {
return items;
}
// 逐个字段执行批量翻译并回填
fieldInfos.forEach(fieldInfo -> {
try {
// 提取原始值列表 source 字段获取
List<Object> sourceValues = extractSourceValues(items, fieldInfo.sourceField());
// 执行批量翻译
List<?> translatedValues = fieldInfo.translator().translation(sourceValues, fieldInfo.annotation().other());
// 验证翻译结果数量一致性
if (translatedValues.size() != sourceValues.size()) {
log.error("翻译异常,待翻译列表与翻译后列表长度不一致 - 字段:{}, 源数量:{}, 翻译数量:{}",
fieldInfo.targetFieldName(), sourceValues.size(), translatedValues.size());
return;
}
// 回填翻译结果到原对象使用已缓存的 targetField避免重复反射
setTranslatedValues(items, fieldInfo.targetField(), translatedValues);
} catch (Exception e) {
log.error("翻译失败 - fieldName: {}, type: {}",
fieldInfo.targetFieldName(), fieldInfo.annotation().type(), e);
}
});
return items;
}
/**
* 从对象中提取所有带有 {@link Translation} 注解的字段的元数据
*
* @param firstItem 第一个对象用于反射分析
* @return 翻译字段元数据列表
*/
private List<TranslationFieldInfo> extractTranslationFields(Object firstItem) {
// 使用 ReflectUtils 获取所有带 @Translation 注解的字段
Field[] translationFields = ReflectUtils.getFields(firstItem.getClass(),
field -> field.isAnnotationPresent(Translation.class));
if (translationFields.length == 0) {
return Collections.emptyList();
}
List<TranslationFieldInfo> fieldInfos = new ArrayList<>(translationFields.length);
for (Field field : translationFields) {
Translation annotation = field.getAnnotation(Translation.class);
String mapperFieldName = annotation.mapper();
// 确定源字段名优先使用 mapper 指定的字段否则使用当前字段
String sourceFieldName = mapperFieldName.isEmpty() ? field.getName() : mapperFieldName;
// 仅处理同对象的字段名映射优先走 Field 直取提升性能
Field sourceField = ReflectUtils.getField(firstItem.getClass(), sourceFieldName);
TranslationInterface<?> translator = TranslationHandler.TRANSLATION_MAPPER.get(annotation.type());
if (translator == null) {
continue;
}
// 创建字段信息记录source 为值来源target 为翻译结果存放位置
fieldInfos.add(new TranslationFieldInfo(
sourceFieldName,
sourceField,
field.getName(),
field,
annotation,
translator
));
}
return fieldInfos;
}
/**
* 从对象集合中提取指定字段的值
*
* @param items 对象集合
* @param sourceField 源字段对象若为空则不取值直接返回全 null 列表
* @return 原始值列表
*/
private List<Object> extractSourceValues(List<Object> items, Field sourceField) {
int size = items.size();
if (sourceField == null) {
return new ArrayList<>(Collections.nCopies(size, null));
}
List<Object> values = new ArrayList<>(size);
for (Object item : items) {
values.add(ReflectUtils.getFieldValue(item, sourceField));
}
return values;
}
/**
* 将翻译后的值设置回原对象集合
* <p>
* 直接使用已缓存的 Field 对象避免重复反射获取
* </p>
*
* @param items 对象集合
* @param targetField 目标字段已从 TranslationFieldInfo 中获取
* @param translatedValues 翻译后的值列表
*/
private void setTranslatedValues(List<Object> items, Field targetField, List<?> translatedValues) {
if (targetField == null) {
return;
}
// 使用 fori 循环按索引回填保证顺序一致性且避免迭代器开销
for (int i = 0; i < items.size(); i++) {
ReflectUtils.setFieldValue(items.get(i), targetField, translatedValues.get(i));
}
}
/**
* 翻译字段信息记录Record
* <p>
* 封装翻译过程中需要的字段信息明确区分源字段值来源和目标字段值存放
* </p>
*
* @param sourceFieldName 源字段名从这里获取原始值
* @param sourceField 源字段对象 mapper 为单级属性则可复用以提升性能
* @param targetFieldName 目标字段名翻译后的值存放这里
* @param targetField 目标字段对象用于反射设置值
* @param annotation 翻译注解包含翻译类型mapper 等配置
* @param translator 翻译实现类 type 解析缓存
*/
private record TranslationFieldInfo(
String sourceFieldName,
Field sourceField,
String targetFieldName,
Field targetField,
Translation annotation,
TranslationInterface<?> translator
) {}
}

View File

@ -5,6 +5,7 @@ import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.reflect.ReflectUtils;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.collection.TranslationCollection;
import org.dromara.common.translation.core.TranslationInterface;
import tools.jackson.core.JacksonException;
import tools.jackson.core.JsonGenerator;
@ -57,6 +58,10 @@ public class TranslationHandler extends ValueSerializer<Object> {
*/
@Override
public void serialize(Object value, JsonGenerator gen, SerializationContext ctxt) throws JacksonException {
if (value != null) {
gen.writePOJO(value);
return;
}
TranslationInterface<?> trans = TRANSLATION_MAPPER.get(translation.type());
if (ObjectUtil.isNotNull(trans)) {
// 如果映射字段不为空 则取映射字段的值

View File

@ -6,6 +6,9 @@ import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.List;
import java.util.stream.Collectors;
/**
* 部门翻译实现
*
@ -33,4 +36,9 @@ public class DeptNameTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public List<String> translation(List<Object> key, String other) {
return key.stream().map(k -> translation(k, other)).collect(Collectors.toList());
}
}

View File

@ -7,6 +7,9 @@ import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.List;
import java.util.stream.Collectors;
/**
* 字典翻译实现
*
@ -32,4 +35,8 @@ public class DictTypeTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public List<String> translation(List<Object> key, String other) {
return key.stream().map(k -> translation(k, other)).collect(Collectors.toList());
}
}

View File

@ -1,11 +1,15 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import lombok.AllArgsConstructor;
import org.dromara.common.core.service.UserService;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import java.util.*;
import java.util.stream.Collectors;
/**
* 用户昵称翻译实现
*
@ -33,4 +37,9 @@ public class NicknameTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public List<String> translation(List<Object> key, String other) {
return key.stream().map(k -> translation(k, other)).collect(Collectors.toList());
}
}

View File

@ -6,6 +6,9 @@ import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.List;
import java.util.stream.Collectors;
/**
* OSS翻译实现
*
@ -33,4 +36,9 @@ public class OssUrlTranslationImpl implements TranslationInterface<String> {
}
return null;
}
@Override
public List<String> translation(List<Object> key, String other) {
return key.stream().map(k -> translation(k, other)).collect(Collectors.toList());
}
}

View File

@ -1,12 +1,17 @@
package org.dromara.common.translation.core.impl;
import cn.hutool.core.convert.Convert;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.service.UserService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.common.translation.core.TranslationInterface;
import lombok.AllArgsConstructor;
import java.util.*;
import java.util.stream.Collectors;
/**
* 用户名翻译实现
*
@ -29,4 +34,17 @@ public class UserNameTranslationImpl implements TranslationInterface<String> {
public String translation(Object key, String other) {
return userService.selectUserNameById(Convert.toLong(key));
}
@Override
public List<String> translation(List<Object> key, String other) {
List<Long> ids = key.stream().filter(Objects::nonNull).map(Convert::toLong).collect(Collectors.toList());
List<UserDTO> userDTOList = userService.selectListByIds(ids);
Map<Long, String> userIdToUserName = StreamUtils.toMap(userDTOList, UserDTO::getUserId, UserDTO::getUserName);
List<String> translationList = new ArrayList<>();
for (Long id : ids) {
String s = userIdToUserName.get(id);
translationList.add(s);
}
return translationList;
}
}

View File

@ -6,6 +6,7 @@ import org.apache.fesod.sheet.annotation.format.DateTimeFormat;
import org.dromara.common.excel.annotation.ExcelNotation;
import org.dromara.common.excel.annotation.ExcelRequired;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.collection.TranslationCollection;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.demo.domain.TestDemo;
import io.github.linpeilie.annotations.AutoMapper;
@ -24,6 +25,7 @@ import java.util.Date;
*/
@Data
@ExcelIgnoreUnannotated
@TranslationCollection
@AutoMapper(target = TestDemo.class)
public class TestDemoVo implements Serializable {

View File

@ -9,6 +9,7 @@ import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.translation.collection.TranslationCollectionWrapper;
import org.dromara.demo.domain.TestDemo;
import org.dromara.demo.domain.bo.TestDemoBo;
import org.dromara.demo.domain.vo.TestDemoVo;
@ -53,7 +54,7 @@ public class TestDemoServiceImpl implements ITestDemoService {
public PageResult<TestDemoVo> queryPageList(TestDemoBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TestDemo> lqw = buildQueryWrapper(bo);
Page<TestDemoVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return PageResult.build(result.getRecords(), result.getTotal());
return PageResult.build(TranslationCollectionWrapper.form(result.getRecords()), result.getTotal());
}
/**

View File

@ -10,6 +10,9 @@ import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.service.IFlwCategoryService;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
/**
* 流程分类名称翻译实现
*
@ -35,4 +38,9 @@ public class CategoryNameTranslationImpl implements TranslationInterface<String>
public String translation(Object key, String other) {
return flwCategoryService.selectCategoryNameById(Convert.toLong(key));
}
@Override
public List<String> translation(List<Object> key, String other) {
return key.stream().map(k -> translation(k, other)).collect(Collectors.toList());
}
}