package com.imyeyu.api.util; import com.fasterxml.jackson.databind.JsonNode; import com.imyeyu.api.annotation.MultilingualField; import com.imyeyu.api.modules.common.entity.Multilingual; import com.imyeyu.api.modules.common.service.MultilingualService; import com.imyeyu.java.TimiJava; import com.imyeyu.java.bean.Language; import com.imyeyu.spring.TimiSpring; import com.imyeyu.utils.StringInterpolator; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.io.Reader; import java.io.Writer; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.nio.file.Path; import java.time.temporal.TemporalAccessor; import java.util.ArrayList; import java.util.Collection; import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; /** * 统一回填响应对象中的多语言字段 * * @author 夜雨 * @since 2026-07-17 00:00 */ @Slf4j @Component @RequiredArgsConstructor public class MultilingualFieldBinder { private static final StringInterpolator INTERPOLATOR = new StringInterpolator(StringInterpolator.SIMPLE_OBJ); private static final Map, List> TRAVERSE_CACHE = new ConcurrentHashMap<>(); private static final Map, List> BINDING_CACHE = new ConcurrentHashMap<>(); private final MultilingualService multilingualService; /** * 根据当前请求语言回填对象中的多语言字段 * * @param body 响应对象 */ public void bind(Object body) { if (body == null) { return; } LinkedHashSet langIdSet = new LinkedHashSet<>(); // 第一遍只收集多语言 ID,避免递归回填时重复查询 collectLangId(body, new IdentityHashMap<>(), langIdSet); if (langIdSet.isEmpty()) { return; } Map multilingualMap = new LinkedHashMap<>(); for (Multilingual item : multilingualService.listByIdList(new ArrayList<>(langIdSet))) { multilingualMap.put(item.getId(), item); } // 第二遍执行字段回填,模板替换也在这一遍统一完成 fillValue(body, new IdentityHashMap<>(), multilingualMap, TimiSpring.getLanguage()); } /** * 递归扫描响应对象,提取所有待翻译字段依赖的多语言 ID * * @param current 当前对象 * @param visited 已访问对象,避免循环引用死递归 * @param langIdSet 收集到的多语言 ID */ private void collectLangId(Object current, IdentityHashMap visited, LinkedHashSet langIdSet) { switch (current) { case null -> { return; } case Collection collection -> { if (visited.put(current, Boolean.TRUE) != null) { return; } for (Object item : collection) { collectLangId(item, visited, langIdSet); } return; } case Map map -> { if (visited.put(current, Boolean.TRUE) != null) { return; } for (Object item : map.values()) { collectLangId(item, visited, langIdSet); } return; } default -> { } } if (current.getClass().isArray()) { if (visited.put(current, Boolean.TRUE) != null) { return; } int length = Array.getLength(current); for (int i = 0; i < length; i++) { collectLangId(Array.get(current, i), visited, langIdSet); } return; } if (isSimpleValueType(current.getClass())) { return; } if (visited.put(current, Boolean.TRUE) != null) { return; } for (BindingField field : getBindingFields(current.getClass())) { String langId = getStringValue(field.langIdField(), current); if (TimiJava.isNotEmpty(langId)) { langIdSet.add(langId); } } for (Field field : getTraverseFields(current.getClass())) { collectLangId(getFieldValue(field, current), visited, langIdSet); } } /** * 递归回填响应对象中的多语言展示字段 * * @param current 当前对象 * @param visited 已访问对象,避免循环引用死递归 * @param multilingualMap 本次响应涉及的多语言缓存 * @param language 当前请求语言 */ private void fillValue(Object current, IdentityHashMap visited, Map multilingualMap, Language.Enum language) { if (current == null) { return; } if (current instanceof Collection collection) { if (visited.put(current, Boolean.TRUE) != null) { return; } for (Object item : collection) { fillValue(item, visited, multilingualMap, language); } return; } if (current instanceof Map map) { if (visited.put(current, Boolean.TRUE) != null) { return; } for (Object item : map.values()) { fillValue(item, visited, multilingualMap, language); } return; } if (current.getClass().isArray()) { if (visited.put(current, Boolean.TRUE) != null) { return; } int length = Array.getLength(current); for (int i = 0; i < length; i++) { fillValue(Array.get(current, i), visited, multilingualMap, language); } return; } if (isSimpleValueType(current.getClass())) { return; } if (visited.put(current, Boolean.TRUE) != null) { return; } for (BindingField field : getBindingFields(current.getClass())) { String langId = getStringValue(field.langIdField(), current); if (TimiJava.isEmpty(langId)) { continue; } Multilingual multilingual = multilingualMap.get(langId); if (multilingual == null) { continue; } String value = multilingual.getValue(language); if (TimiJava.isEmpty(value)) { value = multilingual.getZhCN(); } if (TimiJava.isEmpty(value)) { continue; } Map argMap = getArgMapValue(field.argMapField(), current); if (!argMap.isEmpty()) { value = INTERPOLATOR.inject(value, argMap); } if (TimiJava.isEmpty(value)) { continue; } setFieldValue(field.targetField(), current, value); } for (Field field : getTraverseFields(current.getClass())) { fillValue(getFieldValue(field, current), visited, multilingualMap, language); } } private List getBindingFields(Class clazz) { return BINDING_CACHE.computeIfAbsent(clazz, key -> { Map fieldMap = new LinkedHashMap<>(); List declaredFieldList = getAllFields(key); for (Field field : declaredFieldList) { fieldMap.put(field.getName(), field); } List result = new ArrayList<>(); for (Field field : declaredFieldList) { MultilingualField annotation = field.getAnnotation(MultilingualField.class); if (annotation == null) { continue; } Field langIdField = fieldMap.get(annotation.value()); if (langIdField == null) { log.warn("MultilingualField 配置错误,类 {} 不存在字段 {}", key.getName(), annotation.value()); continue; } Field argMapField = null; if (TimiJava.isNotEmpty(annotation.argMap())) { argMapField = fieldMap.get(annotation.argMap()); if (argMapField == null) { log.warn("MultilingualField 配置错误,类 {} 不存在字段 {}", key.getName(), annotation.argMap()); continue; } if (!Map.class.isAssignableFrom(argMapField.getType())) { log.warn("MultilingualField 配置错误,类 {} 字段 {} 不是 Map", key.getName(), annotation.argMap()); continue; } } result.add(new BindingField(field, langIdField, argMapField)); } return result; }); } /** * 生成需要继续向下遍历的字段列表 * * @param clazz 目标类型 * @return 可递归字段列表 */ private List getTraverseFields(Class clazz) { return TRAVERSE_CACHE.computeIfAbsent(clazz, key -> { List result = new ArrayList<>(); for (Field field : getAllFields(key)) { if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) { continue; } if (field.getType().isArray() || Collection.class.isAssignableFrom(field.getType()) || Map.class.isAssignableFrom(field.getType())) { field.setAccessible(true); result.add(field); continue; } if (isSimpleValueType(field.getType())) { continue; } field.setAccessible(true); result.add(field); } return result; }); } private List getAllFields(Class clazz) { List result = new ArrayList<>(); Class current = clazz; while (current != null && current != Object.class) { for (Field field : current.getDeclaredFields()) { if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) { continue; } field.setAccessible(true); result.add(field); } current = current.getSuperclass(); } return result; } /** * 仅接受字符串类型的 langId 字段 * * @param field 字段 * @param target 对象 * @return 多语言 ID */ private String getStringValue(Field field, Object target) { Object value = getFieldValue(field, target); if (value instanceof String string) { return string; } return null; } /** * 仅读取 Map 类型模板参数字段,未配置时返回空 Map * * @param field 字段 * @param target 对象 * @return 模板参数 */ @SuppressWarnings("unchecked") private Map getArgMapValue(Field field, Object target) { if (field == null) { return Map.of(); } Object value = getFieldValue(field, target); if (value instanceof Map map) { return (Map) map; } return Map.of(); } private Object getFieldValue(Field field, Object target) { try { return field.get(target); } catch (IllegalAccessException e) { throw new IllegalStateException("读取字段失败: %s#%s".formatted(target.getClass().getName(), field.getName()), e); } } private void setFieldValue(Field field, Object target, Object value) { try { field.set(target, value); } catch (IllegalAccessException e) { throw new IllegalStateException("写入字段失败: %s#%s".formatted(target.getClass().getName(), field.getName()), e); } } private boolean isSimpleValueType(Class clazz) { if (clazz.isPrimitive() || clazz.isEnum() || clazz.isAnnotation()) { return true; } if (CharSequence.class.isAssignableFrom(clazz) || Number.class.isAssignableFrom(clazz) || Boolean.class == clazz || Character.class == clazz || UUID.class == clazz || TemporalAccessor.class.isAssignableFrom(clazz) || Class.class == clazz || JsonNode.class.isAssignableFrom(clazz) || File.class.isAssignableFrom(clazz) || Path.class.isAssignableFrom(clazz) || InputStream.class.isAssignableFrom(clazz) || OutputStream.class.isAssignableFrom(clazz) || Reader.class.isAssignableFrom(clazz) || Writer.class.isAssignableFrom(clazz)) { return true; } Package currentPackage = clazz.getPackage(); if (currentPackage == null) { return false; } String packageName = currentPackage.getName(); return packageName.startsWith("java.") || packageName.startsWith("javax.") || packageName.startsWith("jakarta.") || packageName.startsWith("sun.") || packageName.startsWith("com.fasterxml.jackson."); } private record BindingField(Field targetField, Field langIdField, Field argMapField) { } }