本文介绍了我在一些业务系统中遇到的错误提示问题,以及进行需求分析和设计实现的过程,欢迎进行交流和指点,一起进步。
作为程序员,或多或少,都经历过如下场景:
场景1:
我们的程序出小差了
我们的程序出小差了,请联系[email protected]
场景2:
公司名称已被占用
,这个提示太简短,要改一下;这种需求是合理的,可是类似的场景多了,程序员容易被打断,也会很烦躁;我们自然不能跟着产品经理的节奏,要想办法优化。
原始错误提示存在的问题:
历次经历过的优化方案:
公司名称必须5个字以上
,这是前端报的错,能不能也放后端搞?一个小小的错误提示,怎么这么多事?
那么对于错误提示,产品的真实需求是什么?我们具体分析一下:
每个产品不可避免都会出错,包括用户的无效操作导致出错、产品本身的缺陷导致出错,
对于这些错误,我们的产品要能:
要达到这些目的,我们的错误提示,不可避免的会经常变更:
边界确认:
综合到成本、性能、通用性等各方面的评估,最终选型和过程如下 :
错误码定义:
提供一个错误码维护后台,进行错误码和用户错误提示的配置维护能力,保存到MySQL数据库;
增加操作规范要求:
当错误码数据有变更时,维护后台会自动生成js 和 json两种格式的文件,可以选择上传到资源服务器、或阿里云oss、或aws-S3;
注:在项目配置里(或数据库里)根据不同产品配置,可以配置各自的静态资源文件存储目标位置。
前端封装一个SDK,读取对应的js或json静态资源文件,进行缓存并定时刷新(建议利用http的304协议机制判断和更新);
设计优点:
CREATE TABLE `t_errcodes` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`product_id` int NOT NULL COMMENT '产品标识',
`err_code` int NOT NULL COMMENT '错误码',
`rd_desc` varchar(1000) NOT NULL DEFAULT '' COMMENT '开发人员备注,不输出给前端',
`lang` varchar(10) NOT NULL DEFAULT 'zh-CN' COMMENT '所属语言',
`err_type` varchar(100) NOT NULL DEFAULT '' COMMENT '错误分类',
`show` tinyint NOT NULL DEFAULT 0 COMMENT '是否展示给用户',
`retry` tinyint NOT NULL DEFAULT 0 COMMENT '出错是否允许重试',
`process_desc` varchar(1000) NOT NULL DEFAULT '' COMMENT '用户错误处理文案',
`process_url` varchar(1000) NOT NULL DEFAULT '' COMMENT '用户错误引导链接',
`tag` varchar(50) NOT NULL DEFAULT '' COMMENT '标签',
`create_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_date` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `unq_err_code`(`lang`, `err_code`, `product_id`) USING BTREE
) ENGINE = InnoDB COMMENT = '错误码配置表';
在管理后台进行错误码编辑后,会自动为每个产品、每个语言,创建一个js文件和json文件,使用参考如下:
window.bn_globalErrorCode = {
"40001": {
"err_type": "about user",
"process_desc": "email already exists",
"process_url": "https://beinet.cn/help.html",
"show": 1,
"retry": 1
},
"40121": {
"err_type": "about login",
"process_desc": "password's length must longer than 6",
"process_url": null,
"show": 1,
"retry": 1
}
};
注:为了压缩减小体积,我在实际生产系统使用的是下面这种单行/数组格式,如:
window.bn_globalErrorCode = {"40001":["about user","email already exists","https://beinet.cn/help.html",1,1],"40121":["err_type": "about login","password's length must longer than 6",null,1,1]};
Javascript前端使用代码参考(针对单行/数组格式):
<script src="https://oss.beinet.cn/errorCode/errcode-40-en-US.js"></script>
<script>
function findUserDesc(code) {
let obj = window.bn_globalErrorCode[code];
if (!obj)
return '未配置此错误码,返回通用错误说明'; // 这里找产品出一个通用错误文案
if(obj[3] !== 1)
return '这个错误不显示报文错误'; // show的内容,根据你的具体业务场景使用
return obj[0] + ':' + obj[1];
}
let errCode = '40001';
alert(findUserDesc(errCode)); // 会弹出:email already exists
</script>
为方便开发人员初始化和产品人员使用,我在生产环境也部署了一些工具:
/actuator/enums
接口,可以遍历项目中所有enum类,并输出为json,package beinet.cn.frontstudy.actuator;
import org.reflections.Reflections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.ReadOperation;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;
/**
* 遍历项目中所有枚举类,并显示的端点类
*
* @author youbl
* @since 2023/04/08
*/
@Endpoint(id = "enums")
@Component
public class EnumListEndPoint {
@Autowired
private ApplicationContext context;
private Map<String, Object> enumMap;
private void Init() throws IllegalAccessException, InvocationTargetException {
Map<String, Object> beansWithAnnotation = context.getBeansWithAnnotation(SpringBootApplication.class);
if (!beansWithAnnotation.isEmpty()) {
Class<?> appClass = beansWithAnnotation.values().toArray()[0].getClass();
Reflections reflections = new Reflections(getScanPackageName(appClass));
Set<Class<? extends Enum>> enums = reflections.getSubTypesOf(Enum.class);
for (Class<? extends Enum> enumClass : enums) {
List<Method> methods = Arrays.stream(enumClass.getMethods())
.filter(m -> m.getName().startsWith("get") && Modifier.isPublic(m.getModifiers()) && !Modifier.isStatic(m.getModifiers()) && m.getParameterCount() == 0)
.filter(m -> !m.getName().equals("getClass") && !m.getName().equals("getDeclaringClass"))
.collect(Collectors.toList());
for (Method method : methods) {
method.setAccessible(true);
}
EnumObject enumObject = new EnumObject();
if (enumMap == null)
enumMap = new HashMap<>();
enumMap.put(enumClass.getTypeName(), enumObject);
Field[] values = enumClass.getFields();
for (Field enumItem : values) {
if (enumItem.getType() != enumClass)
continue;
enumItem.setAccessible(true);
Object enumValue = enumItem.get(null);
String code = enumItem.getName();
enumObject.Enums.put(code, getMap(enumValue, methods));
}
}
}
}
private String getScanPackageName(Class<?> appClass) {
SpringBootApplication anno = appClass.getAnnotation(SpringBootApplication.class);
if (anno != null) {
String[] packages = anno.scanBasePackages();
if (packages != null && packages.length > 0)
return packages[0];
}
return appClass.getPackage().getName();// .getPackageName();
}
@ReadOperation
public Map<String, Object> read() throws InvocationTargetException, IllegalAccessException {
if (enumMap == null) {
synchronized (this) {
if (enumMap == null)
Init();
if (enumMap == null)
enumMap = new HashMap<>();
}
}
return enumMap;
}
private Map<String, Object> getMap(Object enumInstance, List<Method> methods) throws InvocationTargetException, IllegalAccessException {
Map<String, Object> map = new HashMap<>();
for (Method method : methods) {
map.put(method.getName().substring(3), method.invoke(enumInstance));
}
return map;
}
public static class EnumObject {
public String Description;
public Map<String, Map<String, Object>> Enums = new HashMap<>();
}
}
后续有机会,我再整理一下整个错误码工程源码,并开源出来。