web系統字典統一中文翻譯問題

2023-07-25 15:02:11

幾乎每個web系統都離不開各種狀態碼。訂單新建,待支付,未支付,已支付,待發貨。
訊息已讀未讀,任務待標記待審批已審批待流轉已完成未完成。等等。
複雜一點的,會有多級狀態碼。

狀態碼超出3個的,一般都會納入統一的字典管理。字典系統作為一個獨立的微服務部署。
使用Redis作為快取。其它系統使用字典的時候只需接入該服務,呼叫相應介面即可。

這本身沒什麼問題,也沒什麼可講的。
但在字典翻譯的時候還是會出現一些五花八門的問題。

問題

資料庫裡儲存的是字典的code碼,但是前端展示的時候得是中文。字典翻譯指的是,後端介面將資料庫查詢出來的code轉為中文的過程。

如果沒有統一的管理,它就會有一些問題。

比如有的字典表欄位只需存一個值,有的呢,需要儲存多個,通過,或者_進行分隔。有的呢,又是單獨的表儲存。

那麼,此時對映到物件就分成了String('1,2,3')或者集合List<String>

還有,List<Object>,這種屬於巢狀物件,物件A裡面有個集合,集合元素為B物件,B物件裡面有字典需要翻譯。

有的呢,前端只需要展示中文,有的時候,前端既需要展示中文,同時也需要字典原始code碼來做一些條件判斷。

有的時候呢,對於字典碼String('1,2,3'),前端需要後端返回字典1,字典2,字典3,有的時候呢,前端更想後端返回字典1\n字典2\n字典3直接換行。

還有的呢,多級字典碼,資料庫存的是全路徑1_2_3,前端需要返回中文的全路徑,比如一級_二級_三級,有的呢,只需要返回葉子節點,三級

更有甚者,有的專案呢,比較亂,沒有約定好。
比如二級字典碼101_202本身就帶了_,當前端在傳遞多級字典全路徑的時候,使用了_作分割符,本來應該返回101,101_202變成了101_101_202

實現

首先在介面程式碼當中自行查詢字典服務,然後翻譯,這樣子肯定是不優雅的。

更好的方法是,統一進行處理。

一是在springboot序列化輸出的時候進行,看具體的序列化框架。

比如fastjson

FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter() {
            @Override
            public void write(Object object, Type type, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                try {
                    // todo
                } catch (Exception e) {
                    log.error("response dict converter error", e);
                }
                super.write(object, type, contentType, outputMessage);
            }
        };

又或者jackson

@Configuration
public class DictConfig {
    
    @Autowired
    private DictService dictService;
    
    @Bean
    public MappingJackson2HttpMessageConverter converter() {
        return new MappingJackson2HttpMessageConverter() {

            @Override
            protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
                try {
                    // todo
                } catch (Exception e) {
                    log.error("response dict converter error", e);
                }
                super.writeInternal(object, type, outputMessage);
            }
            
        };
    }
}

二是通過AOP切面統一進行

定義一個字典註解

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DictNest {

    /**
     * 使用哪個分組, 即字典code
     * @return
     */
    String group();

    /**
     * 多級標籤組裝連線符,預設為'_'
     * @return
     */
    String joinStr() default "_";

    /***
     * 資料庫裡多級字典連線符,預設為'_'
     * @return
     */
    String splitStr() default "_";

    /**
     * 是否只返回葉子節點,預設為false即返回全路徑,如果設定為true則只返回最後一級
     * 舉個例子,字典值為二級字典102_206
     * 如果此值設定為false返回給前端 102中文_206中文
     * 如果此值設定為true返回給前端  106中文
     * @return
     */
    boolean onlyLeaf() default false;
    
    /**
     * 把字典值指定給此屬性
     * @return
     */
    String field() default ""; 
}

對屬性簡單說明一下

group()即字典code。

splitStr()即資料庫存放多個字典時的拼接符,在翻譯的時候根據此對字串進行分割。這裡應與字元code的連線符區別開來。

joinStr(),多個字典code在翻譯成中文後,進行拼接的連線符。

field(),它必須是當前物件的一個屬性欄位名,為空時中文替換掉code,不為空時,將翻譯好的中文賦給此屬性欄位,原欄位字典保留不變。

onlyLeaf() 是否只返回葉子節點,預設為false即返回全路徑,如果設定為true則只返回最後一級。

定義一個切面


import com.github.pagehelper.Page;
import com.google.common.collect.Maps;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @projectName: test
 * @package: com.test
 * @className: 
 * @author: nyp
 * @date: 2022/11/1 14:11
 * @version: 1.0
 */
@Aspect
@Component
@Slf4j
public class CommonAspect2 {

    @Resource
    private DictApi dictApi;

    @Pointcut("execution(public * com.test.controller.*.*(..))")
    public void LogAspect() {
    }

   
    @AfterReturning(value = "LogAspect()", returning = "returnValue")
    public void logResultVOInfo(JoinPoint joinPoint, Object returnValue) throws Exception {
        List<Object> data = new ArrayList<>();
	// 這裡自己改造一下,這裡根據公司自定義返回程式碼來解析的
        if (returnValue instanceof PageResult) {
            Object data2 = ((PageResult<?>) returnValue).getData();
            if (data2 instanceof Page) {
                Page pageInfo = (Page) data2;
                data = pageInfo.getResult();
            } else {          
		// ingore
            }
        } else if (returnValue instanceof Result) {
            Object data2 = ((Result<?>) returnValue).getData();
            data = new ArrayList<>();
            if (data2 instanceof ArrayList) {
                List<Object> finalData = data;
                ((ArrayList<?>) data2).forEach(e -> finalData.add(e));
            } else {
                data.add(data2);
            }
        }
        try {
            if (data == null) {
                return;
            }
            for (Object e : data) { (e == null) {
                    break;
                }
                Field[] fields = e.getClass().getDeclaredFields();
                Class clazz = e.getClass();
                extracted(e, fields, clazz);
            }
        } catch (Exception exception) {
            log.error("", exception);
        }
    }

    private void extracted(Object e, Field[] fields, Class clazz) throws Exception {
        for (Field field : fields) {
            if (field.getGenericType().toString().contains("List")) {
                Method method = getMethod(clazz, field.getName(), "get");
                List<Object> subList = (List<Object>) method.invoke(e);
                if (subList == null) {
                    continue;
                }
                List<String> chineseList = new ArrayList<>();
                for (Object obj : subList) {
                    if(obj instanceof String){
                        List<String> subObjs = Arrays.stream(((String) obj).split(",")).collect(Collectors.toList());

                        for (String subObj : subObjs){
                            Annotation[] annotations = field.getDeclaredAnnotations();
                            for (Annotation a : annotations) {
                                if(a instanceof DictNest){
                                    DictNest dictNest = field.getAnnotation(DictNest.class);
                                    String chinese = getDictMap(dictNest, subObj);
                                    chineseList.add(chinese);
                                }
                            }
                        }
                    } else {
                        extracted(obj, obj.getClass().getDeclaredFields(), obj.getClass());
                    }
                }
                if(!CollectionUtils.isEmpty(chineseList)){
                    field.setAccessible(true);
                    field.set(e, chineseList);
                }
            } else{
                annotation(e, fields, field);
            }
        }
    }

    private void annotation(Object e, Field[] fields, Field field) throws IllegalAccessException {
        Annotation[] annotations = field.getDeclaredAnnotations();
        for (Annotation a : annotations) {
            // 字典翻譯
            dict(e, fields, field, a);
        }
    }

    private void dict(Object e, Field[] fields, Field field, Annotation a) throws IllegalAccessException {
        if (a instanceof DictNest) {
            DictNest dictNest = field.getAnnotation(DictNest.class);
            field.setAccessible(true);
            Object value = field.get(e);
            if (value == null) {
                return;
            }
            String chinese = getDictMap(dictNest, value.toString());
            if (chinese == null) {
                return;
            }
            // 是否需要把值賦給指定的屬性
            String otherFiled = dictNest.field();
            if (StringUtils.isBlank(otherFiled)) {
                field.set(e, chinese);
            } else {
                for (Field other : fields) {
                    if (other.getName().equals(otherFiled)) {
                        other.setAccessible(true);
                        other.set(e, chinese);
                    }
                }
            }
        }
    }

    public String getDictMap(DictNest dictNest, String keys) {
        Map<String, String> map = Maps.newHashMap();
	// 這裡應該換成自己的字典查詢方式
        Map<Object, Object> dict = dictApi.findByGroupCode(dictNest.group());
        if (dict == null) {
            return null;
        }
        for (Map.Entry<Object, Object> entry : dict.entrySet()) {
            if (null != entry.getKey() && null != entry.getValue()) {
                map.put(entry.getKey().toString(), entry.getValue().toString());
            }
        }
        List<String> dictResult = new ArrayList<>();
        String[] keyList = keys.split(dictNest.splitStr());
        if (!dictNest.onlyLeaf()) {
            for (String key : keyList) {
                dictResult.add(map.get(key));
            }
        } else {
            dictResult.add(map.get(keyList[keyList.length - 1]));
        }
        return dictResult.stream().collect(Collectors.joining(dictNest.joinStr()));
    }

    private Method getMethod(Class clazz, String fieldName, String type) throws NoSuchMethodException {
        Method method;
        String methodName = type + fieldName.substring(0, 1).toUpperCase(Locale.ROOT) + fieldName.substring(1);
        if ("set".equals(type)) {
            method = clazz.getDeclaredMethod(methodName, String.class);
        } else {
            method = clazz.getDeclaredMethod(methodName);
        }
        method.setAccessible(true);
        return method;
    }
}

大佬們自行重構一下程式碼。

效果

先定義一個測試物件,賦上測試值

@Data
public class TestVO {

    @DictNest(group = "test_state")
    private String connectionStatus = "1";
    @DictNest(group = "test_state")
    private List<String> connectionStatusList = new ArrayList<String>(){{
        add("1");
        add("2");
    }};
    @DictNest(group = "test_state", splitStr = ",")
    private String connectionStatusStr = "1,2";

    @DictNest(group = "test_state", joinStr = "\n" , splitStr = ",")
    private String connectionStatusStr2 = "1,2";

    @DictNest(group = "test_result", joinStr = "\n", field = "stateStr")
    private String state = "101_205";

    private String stateStr;

    @DictNest(group = "test_result", onlyLeaf = true)
    private String stateStr2 = "101_205";

    private List<Sub> subs = new ArrayList<Sub>(){{
        add(new Sub("1"));
        add(new Sub("2"));
    }};

    @Data
    @AllArgsConstructor
    public static class Sub{
        @DictNest(group = "lg_conn_state")
        private String connectionStatus;
    }
}

這裡面有單個的字典,有String和List型別的多個註解,有多級字典,分別返回全路徑和葉子節點。
有同時返回字典code及中文。有巢狀物件,裡面有字典。

如果沒加字典註解,原始返回值:

{
	"code":"200",
	"data":{
		"connectionStatus":"1",
		"connectionStatusList":[
			"1",
			"2"
		],
		"connectionStatusStr":"1,2",
		"connectionStatusStr2":"1,2",
		"state":"101_205",
		"stateStr":null,
		"stateStr2":"101_205",
		"subs":[
			{
				"connectionStatus":"1"
			},
			{
				"connectionStatus":"2"
			}
		]
	},
	"msg":"success",
	"success":true
}

加了字典註解最終的效果:

{
	"code":"200",
	"data":{
		"connectionStatus":"完成",
		"connectionStatusList":[
			"完成",
			"未完成"
		],
		"connectionStatusStr":"完成_未完成",
		"connectionStatusStr2":"完成\n未完成",
		"state":"101_205",
		"stateStr":"一級字典名稱\n二級字典名稱",
		"stateStr2":"二級字典名稱",
		"subs":[
			{
				"connectionStatus":"完成"
			},
			{
				"connectionStatus":"未完成"
			}
		]
	},
	"msg":"success",
	"success":true
}