几乎每个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
}

web系统字典统一中文翻译问题的更多相关文章

  1. Web Analytics 2.0 中文翻译 [ 系列索引 ]

    引言 内容概述 第一章:网站分析2.0的新奇世界 第二章:选择你的网络分析灵魂伴侣的最佳策略 第三章:点击流分析的奇妙世界:指标 第四章:点击流分析的奇妙世界:实际的解决方案 第五章:荣耀之钥:度量成 ...

  2. 《Introduction to Tornado》中文翻译计划——第五章:异步Web服务

    http://www.pythoner.com/294.html 本文为<Introduction to Tornado>中文翻译,将在https://github.com/alioth3 ...

  3. Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译)

    # 禅与 Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译) - 原文 <https://githu ...

  4. 【转】关于HTTP中文翻译的讨论

    http://www.ituring.com.cn/article/1817 讨论参与者共16位: 图灵谢工 杨博 陈睿杰 贾洪峰 李锟 丁雪丰 郭义 梁涛 吴玺喆 邓聪 胡金埔 臧秀涛 张伸 图钉派 ...

  5. Spark SQL 官方文档-中文翻译

    Spark SQL 官方文档-中文翻译 Spark版本:Spark 1.5.2 转载请注明出处:http://www.cnblogs.com/BYRans/ 1 概述(Overview) 2 Data ...

  6. 苹果App Store审核指南中文翻译(2014.9.1更新)

    转:http://www.cocoachina.com/appstore/20140901/9500.html CocoaChina对<苹果应用商店审核指南>中文翻译最近一次更新时间为20 ...

  7. [资料] Apache2 的 httpd.conf 经典中文翻译

    [i=s] 本帖最后由 www.PHP888.com 于 2009-5-22 13:40 编辑 [/i] # 基于 NCSA 服务的配置文件. # #这是Apache服务器主要配置文件. #它包含服务 ...

  8. 【转】苹果App Store审核指南中文翻译(更新)

    (注:<苹果应用商店审核指南>中文翻译最近一次更新为2013-03-04,文中红色部分是相对于2013-03-04版本的新增内容,绿色部分代表更改的内容,蓝色表示苹果相关官方文档的链接.) ...

  9. Next.js v4.1.4 文档中文翻译【转载】

    最近想稍稍看下 React的 SSR框架 Next.js,因为不想看二手资料, 所以自己跑到 Github上看,Next.js的文档是英文的,看倒是大概也能看得懂, 但有些地方不太确定,而且英文看着毕 ...

  10. python wsgi PEP333 中文翻译

    PEP 333 中文翻译 首先说明一下,本人不是专门翻译的,英文水平也不敢拿来献丑.只是这是两年前用python的时候为了自己学习方便而翻译的,记录着笔记自己看看而已.最近翻出来看看觉得还是放出来吧. ...

随机推荐

  1. RHEL 7配置HAProxy实现Web负载均衡

    本文将简单介绍使用HAProxy实现web负载均衡,主要内容包括基于权重的轮询.为HAProxy配置https.配置http重定向为https.配置HAProxy使用独立日志. 一.测试环境 HAPr ...

  2. Jmeter-测试报告模板分享

    1.jmeter-results-detail-report_21 <?xml version="1.0"?> <!-- ~ Licensed to the Ap ...

  3. ctfshow菜狗杯(一)

    CTFshow菜狗杯,web签到 传参. 需要注意的是传参的时候要对中文字符进行编码输出. 得到flag. 第二关 come-to_s1gn 打开页面源代码 这里好像给了一半的flag,另一半好像说在 ...

  4. Django date

    date根据给定格式对一个日期变量进行格式化. 可用的格式字符串: 格式化字符 描述 示例输出a 'a.m.'或'p.m.' 'a.m.'A 'AM'或'PM' 'AM'b 月份,文字形式,3个字母, ...

  5. Can't uninstall 'Pillow'. No files were found to uninstall.

    Can't uninstall 'Pillow'. No files were found to uninstall. Pillow卸载不掉的解决办法 1.进入python所在路径,进入scripts ...

  6. Error in nextTick: "TypeError: Right-hand side of 'instanceof' is not an object"

    发生这种情况,直接去查看 props 对象是否  类型正确 props 有 大概两种 写法吧, 一种就是对象形 ,一种是数组形 // 对象形props: { show: { type: Boolean ...

  7. Midjourney|文心一格prompt教程[Text Prompt(上篇)]:品牌log、App、徽章、插画、头像场景生成,各种风格选择:科技风、运动风

    Midjourney|文心一格prompt教程[Text Prompt(上篇)]:品牌log.App.徽章.插画.头像场景生成,各种风格选择:科技风.运动风 1.撰写 Text Prompt 注意事项 ...

  8. [安全开发] SQL注入扫描(一股子GPT味~)

    实际上大部分都是它写的,它真我哭 SQL注入扫描就是一种用于检测和预防SQL注入攻击的工具.它通过模拟SQL注入攻击的方式,向目标网站发送特定的SQL查询语句,以验证目标网站是否存在SQL注入漏洞.S ...

  9. 你还在用Object.equals()方法吗?

    前言 当<阿里巴巴Java开发手册>发布后,我也是仔细进行了阅读,想从中找出一些"标准",让自己的代码质量提高.手册中对 Object 的 equals 方法的使用进行 ...

  10. 逍遥自在学C语言 | break-循环的中断与跳转

    前言 在C语言中,break语句是一种控制流语句,它用于终止当前所在的循环结构(for.while.do-while)或者switch语句,从而跳出循环或者结束switch语句的执行. 一.人物简介 ...