需求分析案例:全局错误码设计

news2024/11/23 20:52:19

本文介绍了我在一些业务系统中遇到的错误提示问题,以及进行需求分析和设计实现的过程,欢迎进行交流和指点,一起进步。

1、需求起源

作为程序员,或多或少,都经历过如下场景:

  • 场景1:

    • 产品经理:xxx,用户反馈说收到一个看不懂的错误,你排查一下是什么问题:
      在这里插入图片描述
    • 程序员:这是哪个接口报的错?
    • 产品经理:我去问问用户,另外,你这个错误能不能写明白一点,让我们可以看得懂?
    • 程序员:要排查一下,某个变量没数据了,我改一下:我们的程序出小差了
    • 产品经理:啥叫变量变数据?错误提示后面把你的邮箱加上,出问题就找你吧,请联系xxx@beinet.cn
    • 程序员:好,我改:我们的程序出小差了,请联系xxx@beinet.cn
    • 产品经理:看你回邮件挺辛苦的,把邮箱改成客服的吧,要改成yyy@beinet.cn
    • 程序员:好,我改
  • 场景2:

    • 产品经理:我们的产品,注册时的错误提示:公司名称已被占用,这个提示太简短,要改一下;
    • 程序员:好,我改一下,发布上线了;
    • 产品经理:政策变了,不让用公司这个词,公司要改成团队,你再改一下;
    • 程序员:好,我改
    • 产品经理:又那啥了,再改一下,改成xxxx
    • 程序员:我能不能不改啊……

这种需求是合理的,可是类似的场景多了,程序员容易被打断,也会很烦躁;我们自然不能跟着产品经理的节奏,要想办法优化。
原始错误提示存在的问题:

  • 开发人员定义的错误文字,被直接展示给用户,导致用户体验较差或其它误会;
  • 政策变化或产品需要,导致一些敏感词需要替换;
  • 产品定义错误文字,硬编码在代码(前端或后端 )里,而修改错误提示,需要开发介入和发布。

历次经历过的优化方案:

  • 第1次:服务端把所有的错误提示,放在一个枚举类里,这样每次都只需要去这个文件里修改,再发布就好了;
    但是每次还要发布啊,尤其是服务节点多了,时间还不短;
  • 第2次:每个错误提示,分配一个错误编号,并写入数据库,代码里定义一个全局变量,定时读取数据库,比如每分钟更新一次;
    而所有返回错误提示的地方,全部改用错误编号,去全局变量里查找错误提示并返回;
    这样,产品经理要改提示,程序员用SQL刷库就好了,再也不需要发布了;
    可是,为什么改个提示,还要我程序员来操作啊?
  • 第3次:搞一个后台,可以编辑这个数据库表,并开放权限给产品经理;
    这下安静了,改提示,再也不需要我们程序员介入了。
    什么?前端也会报错,比如公司名称必须5个字以上,这是前端报的错,能不能也放后端搞?
  • 第4次:前端也改造一下,前端的每个错误提示也改成错误编号,根据错误编号从后端的数据库里查找对应的错误提示,进行展示;
    这回没啥事了吧。
    啥?有些错误提示,要增加一个链接,引导用户去修复这个错误?要加字段?
    这个获取错误提示的API又应该属于哪个业务的领域?
    每个业务又都有自己的错误提示啊?
    数据库挂了,用户那边显示不了错误,而且会不会读取错误码超时,连累其它业务?

一个小小的错误提示,怎么这么多事?

2、需求分析

那么对于错误提示,产品的真实需求是什么?我们具体分析一下:
每个产品不可避免都会出错,包括用户的无效操作导致出错、产品本身的缺陷导致出错,
对于这些错误,我们的产品要能:

  • 提供正确的反馈:告知用户操作成功还是失败;
  • 能帮助用户理解和引导解决问题:提供易于理解的、明确的错误信息,让用户知道出了什么问题,如何解决;
  • 提升用户体验:错误提示应该要站在用户角度,让用户感到被关注、被尊重和被帮助,可以提升用户的满意度和忠诚度;
  • 符合法律法规和业界明示暗示的规则。

要达到这些目的,我们的错误提示,不可避免的会经常变更:

  • 错误提示不易于理解,被内部发现,或外部投诉,导致需要调整;
  • 错误提示有政策风险、环境要求等,需要进行调整;
  • 错误提示需要可扩展,如增加引导链接、增加图片、可配置某些错误不显示,支持国际化多语言能力支持

边界确认:

  • 不管前端后端,都应该关注业务实现,出现错误就抛出去,怎么展示给用户,展示什么,都不应该是具体业务内的职责;
    这也符合职责单一原则
  • 由专门的后台服务,负责每个业务的错误,转换为具体的用户提示,输出给前端
  • 前端可以专门封装一个模块或SDK,输入错误,找后台转换,并按业务规则进行展示

3、设计与实现

综合到成本、性能、通用性等各方面的评估,最终选型和过程如下 :

  • 错误码定义:

    • 定义产品ID,每个产品或业务定义不同的ID,以区分不同产品;
      注:建议结合公司的立项流程,使用那边定义的项目编号;比如Android客户端定义为30
    • 每个产品的不同模块,再定义模块ID,以区分产品里的不同模块;
      比如Android客户端里的SDK模块定义为2
    • 每个模块的程序员,在编码时为每个错误,定义一个错误码编号,如用户输入的密码长度不足,编号定为123;
      则:Android客户端定义为30,SDK模块定义为2,再加错误码编号123,此时完整的错误码为302123,
      对于程序员来说,看到这个错误码302123,就知道是Android设备端SDK模块抛出的 密码长度不足的错误;
    • 注:这个错误码定义的规范只是我这边的建议,你们可以根据各自的项目实际情况自行定义,方便理解和跟踪即可。
  • 提供一个错误码维护后台,进行错误码和用户错误提示的配置维护能力,保存到MySQL数据库;
    增加操作规范要求:

    • 程序员新增错误码时,他必须去维护后台添加该错误码编号,及对应的开发说明;
    • 出现新的错误码时,产品经理或运营人员,必须去配置相应的用户文案和引导链接等信息;
    • 可以增加监控,扫描所有模块里的错误码,出现未配置的错误码时进行钉钉群告警。
  • 当错误码数据有变更时,维护后台会自动生成js 和 json两种格式的文件,可以选择上传到资源服务器、或阿里云oss、或aws-S3;
    注:在项目配置里(或数据库里)根据不同产品配置,可以配置各自的静态资源文件存储目标位置。

  • 前端封装一个SDK,读取对应的js或json静态资源文件,进行缓存并定时刷新(建议利用http的304协议机制判断和更新);

    • 当遇到错误时,根据代码返回的错误码编号,去查找静态资源文件里的对应错误提示,进行展示给用户;
    • 如果未找到错误提示(遗漏配置),则展示通用的错误提示,如:我们的系统打了个盹,请稍候再试。
      在这里插入图片描述

设计优点:

  • 没有API,仅静态资源文件,几乎不需要考虑性能问题,成本问题也几乎可以忽略;
  • 只提供一个管理后台的配置页面和一张表,可以夹杂在统一的管理后台中,迁移方便;
  • 提供js和json两种格式,通用性强;
  • 支持各种需要对用户展示错误提示和错误引导的场景,包括:浏览器前端、Windows客户端、iOs客户端、Android客户端等等;
  • 可扩展性强,只要保证原有的格式不变,在后面新增字段,均没有任何影响;
  • 可SaaS化,通过产品标识进行租户区分和错误码生成;
  • 提升排错能力,可以通过配置是否展示错误码给用户,当用户截图反馈问题时,可以根据错误码快速定位哪个产品哪个模块出错;
    尤其是微服务化时,链路的哪个模块抛出的错误一目了解;
    建议增加链路跟踪ID展示(可以虚化放在背景图中,不影响用户体验)

3.1、数据库设计参考

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 = '错误码配置表';

3.2、静态资源文件生成规则参考

在管理后台进行错误码编辑后,会自动为每个产品、每个语言,创建一个js文件和json文件,使用参考如下:

  • 文件命名规则:
    • js文件命名为【errcode-产品标识-语言.js】,如:
      产品标识为Android的40,语言为en-US,则文件名为 errcode-40-en-US.js
    • json文件命名为【产品标识-errcode-语言.json】,如:
      产品标识为Android的40,语言为en-US,则文件名为 errcode-40-en-US.json
  • 注:你可以根据自己的情况,使用产品+模块+语言来定义文件名,如果错误码少,也可以全局就一个错误码文件。

3.3、静态资源文件使用参考

  • js格式文件内容参考:
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>
  • json格式文件内容参考:
    json格式文件,与js文件相比,只少了前面的全局变量【window.bn_globalErrorCode】定义

4、自动导出与导入

为方便开发人员初始化和产品人员使用,我在生产环境也部署了一些工具:

  • 后端框架层实现 /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<>();
    }
}
  • 通过定时任务定时扫描与导入,如crontab、xxljob之类,定时扫描所有业务项目的api,实现自动的导入;
    因为导入时,只有程序员写的文字,为避免新增的错误码被用户看到,可以设置为告警,而不做导入;
  • 管理后台,增加多语言错误码比对能力,防止某些语言遗漏了配置,并支持批量导出和导入能力,方便内部员工操作。

后续有机会,我再整理一下整个错误码工程源码,并开源出来。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/751260.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Stable Diffusion 用2D图片制作3D动态壁纸

如果想让我们的2D图片动起来可以使用stable-diffusion-webui-depthmap-script插件在SD中进行加工让图片动起来。 这是一个可以从单个图像创建深度图,现在也可以生成3D立体图像对的插件,无论是并排还是浮雕。生成的结果可在3D或全息设备(如VR耳机或Looking Glass显示器)上查…

LabVIEW和Web Service交互方式?LabVIE本地项目如何发布到互联网上让外网访问

LabVIEW全称Laboratory Virtual Instrument Engineering Workbench&#xff0c; 是一种图形化编程语言(通常称为G语言)&#xff0c;即实验室虚拟仪器集成环境。LabVIEW 经过多年的持续创新&#xff0c;已经从单纯的仪器控制软件发展成为面向设计、测量和控制的综合性图形化开发…

【设计模式】循序渐进的理解观察者模式Spring事件机制的运用

文章目录 1. 概述2.循序渐进的理解观察者模式2.1 观察者模式概念引入2.2.观察者接口抽象2.3 被观察者接口抽象2.4 观察者模式的通用类图2.5.观察者模式的通用代码实现 3.Spring中的事件运用3.1.Spring事件中的几个角色介绍3.2.代码实现 4.总结 1. 概述 观察者模式&#xff08;…

RabbitMQ高阶使用

1. 问题 2. 延时任务 2.1 什么是延时任务 在当前时间往后延迟多少时间执行的任务 2.1.1 和定时任务区别 定时任务有明确的触发时间&#xff0c;延时任务没有定时任务有执行周期&#xff0c;而延时任务在某事件触发后一段时间内执行&#xff0c;没有执行周期定时任务一般执行的…

生命周期函数和wxs脚本

生命周期函数和wxs脚本 1. 生命周期函数1.1. 应用的生命周期函数1.2. 页面的生命周期函数 2. wxs脚本2.1. wxs与JavaScript的关系2.2. wxs内嵌脚本2.3. wxs外联脚本2.4. tips 1. 生命周期函数 1.1. 应用的生命周期函数 应用的生命周期函数&#xff1a;指小程序从启动 -> 运…

【每日算法】【226. 翻转二叉树】

☀️博客主页&#xff1a;CSDN博客主页 &#x1f4a8;本文由 我是小狼君 原创&#xff0c;首发于 CSDN&#x1f4a2; &#x1f525;学习专栏推荐&#xff1a;面试汇总 ❗️游戏框架专栏推荐&#xff1a;游戏实用框架专栏 ⛅️点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd;&…

熔断与降级的那些事

什么熔断 熔断&#xff08;Circuit Breaker&#xff09;是一种用于限制系统请求的机制&#xff0c;其目的是防止系统在发生故障或异常情况下继续执行无效的调用&#xff0c;从而避免雪崩效应和进一步的系统损害。 熔断器通常用于分布式系统中的微服务架构中&#xff0c;它负责…

taro3 微信小程序 createIntersectionObserver 监听无效

项目&#xff1a; taro3 vue3 官方文档 版本&#xff1a;3.x Taro.createIntersectionObserver(component, options) 创建并返回一个 IntersectionObserver 对象实例。在自定义组件或包含自定义组件的页面中&#xff0c;应使用 this.createIntersectionObserver([options]) …

FSMC外设—扩展外部SRAM

目录 FSMC—扩展外部SRAM 前言 SRAM SRAM控制原理 SRAM芯片外观 SRAM芯片的内部功能框架 SRAM信号线 SRAM的存储矩阵 地址译码器、列I/O及I/O数据电路 控制电路 SRAM的读写流程 FSMC FSMC简介 FSMC框图剖析 通讯引脚 存储器控制器 时钟控制逻辑 FSMC的地址映…

【USRP X410】LabVIEW参考架构软件,用于使用Ettus USRP X410对无线系统进行原型验证

LabVIEW参考架构软件&#xff0c;用于使用Ettus USRP X410对无线系统进行原型验证 设备 1 MHz to 7.2 GHz&#xff0c;400 MHz带宽&#xff0c;GPS驯服OCXO&#xff0c;USRP软件无线电设备 - Ettus USRP X410集成硬件和软件&#xff0c;可帮助您制作高性能无线系统的原型&…

探索非洲专线物流的新时代_国际物流供应链管理平台_箱讯科技

随着全球化的发展&#xff0c;非洲作为一个充满机遇和挑战的大陆&#xff0c;吸引着越来越多的企业和投资者。然而&#xff0c;由于非洲的地理复杂性和基础设施不完善&#xff0c;物流问题一直是制约非洲发展的瓶颈之一。为了解决这一问题&#xff0c;非洲专线物流应运而生。本…

分布式数据库HBase,它到底是怎么组成的?

原文链接&#xff1a;http://www.ibearzmblog.com/#/technology/info?id3f432a2451f5f9cb9a14d6e756036b67 前言 大数据的核心问题无非就是存储和计算这两个。Hadoop中的HDFS解决了数据存储的问题&#xff0c;而HBase就是在HDFS上构建&#xff0c;因此Hbase既能解决大数据存…

【广州华锐互动】AR远程巡检系统在设备维修保养中的作用

随着科技的不断发展&#xff0c;AR(增强现实)远程巡检系统在设备检修中发挥着越来越重要的作用。这种系统可以将AR技术与远程通信技术相结合&#xff0c;实现对设备检修过程的实时监控和远程指导&#xff0c;提高设备检修的效率和质量。 首先&#xff0c;AR远程巡检系统可以帮助…

004.PADS VX2.4常用快捷键及无模命令

1.常用快捷键&#xff1a; F2 布线(Layout) F3 布线(Router) F4 切换layer F6 选中一个导线按f6选中整个网络 TAB 旋转 CtrlA select All 全选 CtrlB sheet 切换到整线sheet可以的视图状态 CtrlC copy 复制选定对象(可以是多选或选一范围): 也可以在按住Ctrl同时拖动选定对象…

微信小程序音频播放失败:TypeError: Cannot read property ‘duration‘ of undefined

报错截图 最下面这个this.setData()报错可不用理会&#xff0c;是this取值的问题 解决 需要播放和暂停功能时&#xff0c;需要把audio以及他的src放在Page外面。不能缺少 audioCtx.onPlay() 和 audioCtx.onError()两个方法&#xff0c;且需要放在play()方法之前如果在wx.crea…

解决/usr/bin/ld: cannot find -l****解决

运行程序时出现了以下错误 在这里说明一下出现/usr/bin/ld: cannot find -l****其实都是出现了类似的问题&#xff0c;只是各自的文件不同 其中****即表示函式库文件名称&#xff0c;如上例的&#xff1a;libstdc.so、libluuid.so 其命名规则是&#xff1a;lib库名(即xxx).so …

pytorch深度学习 之一 神经网络梯度下降和线性回归

张量和随机运行&#xff0c;exp函数 import torch a torch.tensor([[1,2],[3,4]]) print(a) a torch.randn(size(10,3)) print(a) b a-a[0] print(torch.exp(b)[0].numpy())输出&#xff1a; tensor([[1, 2],[3, 4]]) tensor([[-1.0165, 0.3531, -0.0852],[-0.1065, -0.5…

【HCIA】06.静态路由

路由器的作用&#xff1a;通过路由器让不同广播域实现互联互通&#xff1b;路由可以指的是路由器&#xff0c;也可以是传递的一个动词&#xff0c;或者是一个路由条目信息。 在一个典型的数据通信网络中&#xff0c;往往存在多个不同的IP网段&#xff0c;数据在不同的IP网段之…

学会写作读后感

读书不是任务 有句俗话说:“清醒时做事&#xff0c;迷茫时读书&#xff0c;独处时思考&#xff0c;烦躁时运动”。 读书 不仅让我们 跨越时间&#xff0c;空间 去感受 作者 思想的力量&#xff0c;也连接了另一个世界——认知&#xff0c;想象&#xff0c;情感&#xff0c;美…

PDF转CAD后尺寸如何保持一致?这几种方法可以尝试一下

CAD文件是可编辑的&#xff0c;可以进行修改、添加和删除&#xff0c;这使得在CAD软件中进行编辑更加容易和灵活。这意味着&#xff0c;如果需要对图纸进行修改或者添加新的元素&#xff0c;可以直接在CAD软件中进行操作&#xff0c;而不需要重新制作整个图纸。那么将PDF文件转…