几乎每个web系统都离不开各种状态码。订单新建,待支付,未支付,已支付,待发货。
消息已读未读,任务待标记待审批已审批待流转已完成未完成。等等。
复杂一点的,会有多级状态码。
状态码超出3个的,一般都会纳入统一的字典管理。字典系统作为一个独立的微服务部署。
使用Redis作为缓存。其它系统使用字典的时候只需接入该服务,调用相应接口即可。
这本身没什么问题,也没什么可讲的。
但在字典翻译的时候还是会出现一些五花八门的问题。
问题
数据库里存储的是字典的code码,但是前端展示的时候得是中文。字典翻译指的是,后端接口将数据库查询出来的code转为中文的过程。
如果没有统一的管理,它就会有一些问题。
比如有的字典表字段只需存一个值,有的呢,需要存储多个,通过,
或者_
进行分隔。有的呢,又是单独的表存储。
那么,此时映射到对象就分成了String('1,2,3')
或者集合List
。
还有,List
,这种属于嵌套对象,对象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
大佬们自行重构一下代码。
效果
先定义一个测试对象,赋上测试值
@Data
public class TestVO {
@DictNest(group = "test_state")
private String connectionStatus = "1";
@DictNest(group = "test_state")
private List connectionStatusList = new ArrayList(){{
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 subs = new ArrayList(){{
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
}