web系统字典统一中文翻译问题
几乎每个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系统字典统一中文翻译问题的更多相关文章
- Web Analytics 2.0 中文翻译 [ 系列索引 ]
引言 内容概述 第一章:网站分析2.0的新奇世界 第二章:选择你的网络分析灵魂伴侣的最佳策略 第三章:点击流分析的奇妙世界:指标 第四章:点击流分析的奇妙世界:实际的解决方案 第五章:荣耀之钥:度量成 ...
- 《Introduction to Tornado》中文翻译计划——第五章:异步Web服务
http://www.pythoner.com/294.html 本文为<Introduction to Tornado>中文翻译,将在https://github.com/alioth3 ...
- Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译)
# 禅与 Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译) - 原文 <https://githu ...
- 【转】关于HTTP中文翻译的讨论
http://www.ituring.com.cn/article/1817 讨论参与者共16位: 图灵谢工 杨博 陈睿杰 贾洪峰 李锟 丁雪丰 郭义 梁涛 吴玺喆 邓聪 胡金埔 臧秀涛 张伸 图钉派 ...
- Spark SQL 官方文档-中文翻译
Spark SQL 官方文档-中文翻译 Spark版本:Spark 1.5.2 转载请注明出处:http://www.cnblogs.com/BYRans/ 1 概述(Overview) 2 Data ...
- 苹果App Store审核指南中文翻译(2014.9.1更新)
转:http://www.cocoachina.com/appstore/20140901/9500.html CocoaChina对<苹果应用商店审核指南>中文翻译最近一次更新时间为20 ...
- [资料] Apache2 的 httpd.conf 经典中文翻译
[i=s] 本帖最后由 www.PHP888.com 于 2009-5-22 13:40 编辑 [/i] # 基于 NCSA 服务的配置文件. # #这是Apache服务器主要配置文件. #它包含服务 ...
- 【转】苹果App Store审核指南中文翻译(更新)
(注:<苹果应用商店审核指南>中文翻译最近一次更新为2013-03-04,文中红色部分是相对于2013-03-04版本的新增内容,绿色部分代表更改的内容,蓝色表示苹果相关官方文档的链接.) ...
- Next.js v4.1.4 文档中文翻译【转载】
最近想稍稍看下 React的 SSR框架 Next.js,因为不想看二手资料, 所以自己跑到 Github上看,Next.js的文档是英文的,看倒是大概也能看得懂, 但有些地方不太确定,而且英文看着毕 ...
- python wsgi PEP333 中文翻译
PEP 333 中文翻译 首先说明一下,本人不是专门翻译的,英文水平也不敢拿来献丑.只是这是两年前用python的时候为了自己学习方便而翻译的,记录着笔记自己看看而已.最近翻出来看看觉得还是放出来吧. ...
随机推荐
- MySQL-(InnoDB)事务和锁
在事务并行处理背景下,不同的事务之间因数据共享的状态变化,存在着某种依赖/隔离影响.即事务隔离级别. 事务隔离级别,官网的解释在这里. InnoDB提供 SQL:1992 标准描述的所有四种事务隔离级 ...
- [C++核心编程] 2、引用
文章目录 2 引用 2.1 引用的基本使用 2.2 引用注意事项 2.3 引用做函数参数 2.4 引用做函数返回值 2.5 引用的本质 2.6 常量引用 2 引用 2.1 引用的基本使用 **作用: ...
- OData WebAPI实践-OData与EDM
本文属于 OData 系列 引言 在 OData 中,EDM(Entity Data Model) 代表"实体数据模型",它是一种用于表示 Web API 中的结构化数据的格式.E ...
- 2020-09-07:Docker的四种网络类型?
福哥答案2020-09-07: 敲docker network ps命令,显示三种模式.1.bridge模式:使用–net =bridge指定,默认设置.桥接式网络模式(默认).容器的默认网络模式,d ...
- 【Qt 6】读写剪贴板
剪贴板是个啥就不用多介绍了,最直观的功能是实现应用程序之间数据共享.就是咱们常说的"复制"."粘贴"功能. 在 Qt 中,QClipboard 类提供了相关 A ...
- Python从零到壹丨图像增强的顶帽运算和底帽运算
摘要:这篇文章详细介绍了顶帽运算和底帽运算,它们将为后续的图像分割和图像识别提供有效支撑. 本文分享自华为云社区<[Python从零到壹] 四十九.图像增强及运算篇之顶帽运算和底帽运算>, ...
- 【密码学】为什么不推荐在对称加密中使用CBC工作模式
引言 这篇文章是我在公司内部分享中一部分内容的详细版本,如标题所言,我会通过文字.代码示例.带你完整的搞懂为什么我们不建议你使用cbc加密模式,用了会导致什么安全问题,即使一定要用需要注意哪些方面的内 ...
- 代码随想录算法训练营Day31 贪心算法| 理论基础 455.分发饼干 376. 摆动序列 53. 最大子序和
代码随想录算法训练营 理论基础 什么是贪心 贪心的本质是选择每一阶段的局部最优,从而达到全局最优. 每次拿最大的就是局部最优,最后拿走最大数额的钱就是推出全局最优. 贪心的套路(什么时候用贪心) 贪心 ...
- 代码随想录算法训练营Day17二叉树|110.平衡二叉树 257. 二叉树的所有路径 404.左叶子之和
优先掌握递归 110.平衡二叉树 题目链接:110.平衡二叉树 给定一个二叉树,判断它是否是高度平衡的二叉树. 本题中,一棵高度平衡二叉树定义为: 一个二叉树_每个节点_ 的左右两个子树的高度差的绝对 ...
- html+css简单易懂的轮播图实现
实现轮播图感觉好复杂啊,这个比较简单的实现了 但是还是没有怎么理解代码,只能先发出来慢慢学习学习了 话不多说,直接上代码 <!DOCTYPE html> <html lang=&qu ...