订单初版—1.分布式订单系统的简要设计文档

大纲

1.订单系统核心业务流程

2.Spring Cloud Alibaba在订单业务中的落地方案

3.面向分布式全链路故障设计的高可靠架构方案

4.分布式订单系统的技术栈与代码规范

1.订单系统核心业务流程

(1)生成订单时序图

(2)支付订单流程图

(3)取消订单流程图

这里主要介绍生单和退款两个核心链路:一是订单正向核心链路,二是订单逆向核心链路。

(1)生成订单时序图

(2)支付订单流程图

(3)取消订单流程图

2.Spring Cloud Alibaba在订单业务中的落地方案

(1)基于Dubbo + Nacos实现全RPC调用链路

(2)基于Seata实现正向核心链路的数据强一致性

(3)基于RocketMQ延迟消息取消超时未支付订单

(4)基于Builder和Template模式构建对象和发通知

(5)基于Redisson分布式锁解决并发预支付问题

(6)订单履约强一致解决方案

(7)订单履约幂等性解决方案

(8)基于Seata实现逆向核心链路数据强一致性

SCA包含了:Dubbo、Nacos、Sentinel、Seata、RocketMQ等组件,SCA技术栈在订单业务中的落地方案包括如下:

(1)基于Dubbo + Nacos实现全RPC调用链路

(2)基于Seata实现正向核心链路数据强一致性

针对生单 -> 优惠券锁定 -> 库存锁定等正向核心链路流程,使用Seata的AT模式确保涉及分布式事务的正向核心链路的数据强一致性。

(3)基于RocketMQ延迟消息取消超时未支付订单

通过Redisson分布式锁 + Elastic-Job实现补偿。

(4)基于Builder和Template模式构建对象和发通知

基于Builder模式实现复杂订单对象的构建、复杂查询规则对象的构建,基于Template模式实现物流配送的通知。

(5)基于Redisson分布式锁解决并发预支付问题

(6)订单履约强一致解决方案

基于RocketMQ的柔性分布式事务解决方案。

(7)订单履约幂等性解决方案

基于Redisson分布式锁 + 前置状态检查实现幂等。

(8)基于Seata实现逆向核心链路数据强一致性

针对取消订单 -> 取消履约 -> 释放资产等逆向核心链路流程,使用Seata的AT模式确保涉及分布式事务的逆向核心链路的数据强一致性。

简单介绍完SCA在复杂订单业务中的基础技术方案,接下来简单介绍在分布式架构中面向分布式全链路故障而设计的高可靠架构方案。

3.面向分布式全链路故障设计的高可靠架构方案

(1)订单系统Dubbo服务高并发压力优化

(2)订单系统引入Spring Cloud Alibaba Sentinel

(3)大促活动网关层限流解决方案

(4)订单系统自适应流控解决方案

(5)订单集群柔性限流解决方案

(6)订单核心链路雪崩场景保护方案

(7)防恶意刷单自动发现与黑名单方案

(8)库存系统异构存储的TCC分布式事务解决方案

(9)仓储系统老旧代码的Saga分布式事务解决方案

(10)物流系统多数据库的XA分布式事务解决方案

(11)Nacos+ZooKeeper双注册中心高可用方案

(12)Dubbo+Nacos多机房部署流量Mesh管控方案

分布式系统的技术难点,其实就是分布式系统链路长、故障多,如果任何一个环节出了故障就会导致系统出问题。所以要分析全链路里会有哪些问题,要用哪些方案来确保系统运行稳定。

分布式订单系统就采用了如下方案来保证系统稳定:

Dubbo服务高并发压力生产优化、大促活动网关层限流解决方案、订单集群柔性限流解决方案、订单系统自适应流控解决方案、订单系统核心链路雪崩解决方案、防恶意刷单与黑名单解决方案、库存系统异构存储架构TCC分布式事务解决方案、仓储系统老旧代码Saga分布式事务解决方案、物流系统多数据库XA分布式事务解决方案、Nacos + ZooKeeper双注册中心高可用方案、Dubbo + Nacos多机房部署流量Mesh管控方案。

(1)订单系统Dubbo服务高并发压力优化

首先针对订单系统核心接口逐个进行高并发压测,然后逐步分析Linux OS、Dubbo线程池、数据库连接池、数据库索引和SQL语句、业务逻辑效率等各个环节存在的问题。并得出在机器负载可控情况下,订单系统可以接受的最大并发压力。

(2)订单系统引入Spring Cloud Alibaba Sentinel

此时需要将Sentinel客户端引入订单系统工程,同时完成Sentinel Dashboard的搭建。

(3)大促活动网关层限流解决方案

首先演示在大促场景下,瞬时高并发是如何击垮订单系统的,同时评估出订单集群部署下的最大可抗压力,然后设计网关大促限流方案。可以基于Spring Cloud Gateway实现一个订单系统前置网关,对订单系统集群部署做负载均衡 + 部署网关集群。通过在网关引入Sentinel来实现限流,在流量入口处保护订单系统不被击垮。

(4)订单系统自适应流控解决方案

首先演示订单系统单实例流量超载的问题。然后根据订单系统部署机器的配置、高压下的机器负载、业务逻辑的复杂度,基于Sentinel设计订单系统的自适应流控方案。也就是根据机器负载、响应时间、请求速率,自适应调整机器的流量阈值,从而实现柔性流控效果。最后演示大流量下,流量被网关层限流后穿透到订单层,各个机器上的自适应流控效果。

(5)订单集群柔性限流解决方案

首先针对订单系统各核心接口,评估出集群模式下每个接口的最大负载压力。然后演示出集群模式下的接口,在访问超载时引发的问题。接着基于Sentinel设计各个核心接口的柔性限流方案。最后对订单接口进行超压力访问,演示接口级的柔性流控效果。

(6)订单核心链路雪崩场景保护方案

首先演示订单核心链路的服务雪崩场景,单服务崩溃是如何引发服务链路全面崩溃的。接着对订单核心链路的各个服务,基于Sentinel设计服务链路防雪崩方案,避免核心链路任何一个服务崩溃引发的服务链路雪崩问题。最后演示单服务崩溃时,整个订单服务的防雪崩效果。

订单核心链路的各个服务有:订单服务、库存服务、营销服务、仓储服务、物流服务、风控服务。

(7)防恶意刷单自动发现与黑名单方案

首先演示单用户恶意刷单行为和效果,接着基于基于Sentinel设计自动识别用户恶意刷单的方案,将恶意刷单的用户ID自动加入访问控制黑名单。从而实现自动化识别 + 防止恶意刷单 + 黑名单控制的机制。

(8)库存系统异构存储的TCC分布式事务解决方案

常见的库存架构是Redis + 数据库异构存储架构。由于订单系统又会强依赖库存系统,所以库存系统的异构存储架构的分布式事务解决方案,通常是基于Seata的TCC模式来实现的。

(9)仓储系统老旧代码的Saga分布式事务解决方案

由于仓储系统的逻辑通常会非常复杂,而且会包含多个服务协同工作,所以对这类系统做分布式事务改造的难度比较大。仓储系统的多服务链路老旧代码的分布式事务解决方案,通常是基于Seata的Saga模式来实现的。

(10)物流系统多数据库的XA分布式事务解决方案

(11)Nacos+ZooKeeper双注册中心高可用方案

(12)Dubbo+Nacos多机房部署流量Mesh管控方案

4.分布式订单系统的技术栈与代码规范

(1)订单系统的技术栈

(2)各层方法的命名规范

(3)领域模型POJO类命名规范

(4)统一异常处理规范

(5)统一返回结果处理规范

(6)Service层开发规范

(1)订单系统的技术栈

(2)各层方法的命名规范

开发规范基于阿⾥巴巴的《Java开发⼿册》:

一.总的原则是⽤动词做前缀,名词在后⾯

二.获取单个对象时使⽤get做前缀

三.获取多个对象时使⽤list做前缀如listOrders

四.获取统计值时使⽤count做前缀

五.插⼊数据时使⽤save/insert做前缀

六.删除数据时使⽤remove/delete做前缀

七.修改数据时使⽤update做前缀

(3)领域模型POJO类命名规范

一.数据对象:xxxDO,xxx即为数据表名

二.Controller层返回结果,展示对象⽤xxxVO

请求⼊参的命名格式⽤xxxRequest,查询条件封装对象⽤xxxQuery。

三.Dubbo API层返回结果,返回对象⽤xxxDTO

请求的命名格式为xxxRequest,查询条件封装对象⽤xxxQuery。

四.业务层内部的数据传输对象⼀般⽤xxxDTO,不做强制规定。

每个POJO类都会继承AbstractObject类,方便不同POJO之间的属性克隆。

kotlin 复制代码
//基础POJO类
//浅克隆:
//复制对象时仅仅复制对象本身,包括基本属性;
//但该对象的属性引用其他对象时,该引用对象不会被复制;
//即拷贝出来的对象与被拷贝出来的对象中的属性引用的对象是同一个;
//深克隆:
//复制对象本身的同时,也复制对象包含的引用指向的对象;
//即修改被克隆对象的任何属性都不会影响到克隆出来的对象。
@SuppressWarnings({"rawtypes", "unchecked"})
public abstract class AbstractObject {
    //深度克隆
    //@param targetClazz 目标对象的Class类型
    //@return 目标对象实例
    public <T> T clone(Class<T> targetClazz) {
        try {
            T target = targetClazz.newInstance();
            BeanCopierUtil.copyProperties(this, target);
            return getTarget(target);
        } catch (Exception e) {
            throw new RuntimeException("error", e);
        }
    }

    //浅度克隆
    //@param target 目标对象实例
    //@return 目标对象实例
    public <T> T clone(T target) {
        try {
            BeanCopierUtil.copyProperties(this, target);
            return getTarget(target);
        } catch (Exception e) {
            throw new RuntimeException("error", e);
        }
    }
    ...
}

(4)统一异常处理规范

一.定义⼀个GlobalExceptionHandler组件

通过对该组件添加@RestControllerAdvice注解,让该组件成为默认的Controller全局异常处理增强组件。在这个组件中,会分别对系统级别未知系统、客户端异常、服务端异常都做了统⼀处理。

typescript 复制代码
//默认的Controller全局异常处理增强组件
@RestControllerAdvice
@Order
public class GlobalExceptionHandler {
    // =========== 系统级别未知异常 =========
    @ExceptionHandler(value = Exception.class)
    public JsonResult<Object> handle(Exception e) {
        log.error("[ 系统未知错误 ]", e);
        return JsonResult.buildError(CommonErrorCodeEnum.SYSTEM_UNKNOWN_ERROR);
    }

    // =========== 客户端异常 =========
    //1001 HTTP请求方法类型错误
    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public JsonResult<Object> handle(HttpRequestMethodNotSupportedException e) {
        log.error("[客户端HTTP请求方法错误]", e);
        return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_HTTP_METHOD_ERROR);
    }

    //1002 客户端请求体参数校验不通过
    @ExceptionHandler(value = MethodArgumentNotValidException.class)
    public JsonResult<Object> handle(MethodArgumentNotValidException e) {
        log.error("[客户端请求体参数校验不通过]", e);
        String errorMsg = this.handle(e.getBindingResult().getFieldErrors());
        return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_BODY_CHECK_ERROR.getErrorCode(), errorMsg);
    }

    private String handle(List<FieldError> fieldErrors) {
        StringBuilder sb = new StringBuilder();
        for (FieldError obj : fieldErrors) {
            sb.append(obj.getField());
            sb.append("=[");
            sb.append(obj.getDefaultMessage());
            sb.append("]  ");
        }
        return sb.toString();
    }

    //1003 客户端请求体JSON格式错误或字段类型不匹配
    @ExceptionHandler(value = HttpMessageNotReadableException.class)
    public JsonResult<Object> handle(HttpMessageNotReadableException e) {
        log.error("[客户端请求体JSON格式错误或字段类型不匹配]", e);
        return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_BODY_FORMAT_ERROR);
    }

    //1004 客户端URL中的参数类型错误
    @ExceptionHandler(value = BindException.class)
    public JsonResult<Object> handle(BindException e) {
        log.error("[客户端URL中的参数类型错误]", e);
        FieldError fieldError = e.getBindingResult().getFieldError();
        String errorMsg = null;
        if (fieldError != null) {
            errorMsg = fieldError.getDefaultMessage();
            if (errorMsg != null && errorMsg.contains("java.lang.NumberFormatException")) {
                errorMsg = fieldError.getField() + "参数类型错误";
            }
        }
        if (errorMsg != null && !"".equals(errorMsg)) {
            return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_PATH_VARIABLE_ERROR.getErrorCode(), errorMsg);
        }
        return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_PATH_VARIABLE_ERROR);
    }

    //1005 客户端请求参数校验不通过
    @ExceptionHandler(value = ConstraintViolationException.class)
    public JsonResult<Object> handle(ConstraintViolationException e) {
        log.error("[客户端请求参数校验不通过]", e);
        Iterator<ConstraintViolation<?>> it = e.getConstraintViolations().iterator();
        String errorMsg = null;
        if (it.hasNext()) {
            errorMsg = it.next().getMessageTemplate();
        }
        if (errorMsg != null && !"".equals(errorMsg)) {
            return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_PARAM_CHECK_ERROR.getErrorCode(), errorMsg);
        }
        return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_PARAM_CHECK_ERROR);
    }

    //1006 客户端请求缺少必填的参数
    @ExceptionHandler(value = MissingServletRequestParameterException.class)
    public JsonResult<Object> handle(MissingServletRequestParameterException e) {
        log.error("[客户端请求缺少必填的参数]", e);
        String errorMsg = null;
        String parameterName = e.getParameterName();
        if (!"".equals(parameterName)) {
            errorMsg = parameterName + "不能为空";
        }
        if (errorMsg != null) {
            return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_PARAM_REQUIRED_ERROR.getErrorCode(), errorMsg);
        }
        return JsonResult.buildError(CommonErrorCodeEnum.CLIENT_REQUEST_PARAM_REQUIRED_ERROR);
    }

    // =========== 服务端异常 =========
    //2001 业务方法参数检查不通过
    @ExceptionHandler(value = IllegalArgumentException.class)
    public JsonResult<Object> handle(IllegalArgumentException e) {
        log.error("[业务方法参数检查不通过]", e);
        return JsonResult.buildError(CommonErrorCodeEnum.SERVER_ILLEGAL_ARGUMENT_ERROR);
    }

    //系统自定义业务异常
    @ExceptionHandler(value = BaseBizException.class)
    public JsonResult<Object> handle(BaseBizException e) {
        log.error("[ 业务异常 ]", e);
        return JsonResult.buildError(e.getErrorCode(), e.getErrorMsg());
    }
}

二.继承基础业务异常类BaseBizException

在业务代码开发中,可以直接抛出这个异常类,也可以在具体的业务代码⼯程新建⼀个类继承BaseBizException,然后抛出⾃定义的异常类。⽐如在订单中⼼,新建⼀个OrderBizException⾃定义类,然后继承BaseBizException,最后在业务代码中抛出OrderBizException。

typescript 复制代码
//基础自定义业务异常
public class BaseBizException extends RuntimeException {
    //默认错误码
    private static final String DEFAULT_ERROR_CODE = "-1";
    private String errorCode;
    private String errorMsg;

    public BaseBizException(String errorMsg) {
        super(errorMsg);
        this.errorCode = DEFAULT_ERROR_CODE;
        this.errorMsg = errorMsg;
    }

    public BaseBizException(String errorCode, String errorMsg) {
        super(errorMsg);
        this.errorCode = errorCode;
        this.errorMsg = errorMsg;
    }

    public BaseBizException(BaseErrorCodeEnum baseErrorCodeEnum) {
        super(baseErrorCodeEnum.getErrorMsg());
        this.errorCode = baseErrorCodeEnum.getErrorCode();
        this.errorMsg = baseErrorCodeEnum.getErrorMsg();
    }

    public BaseBizException(String errorCode, String errorMsg, Object... arguments) {
        super(MessageFormat.format(errorMsg, arguments));
        this.errorCode = errorCode;
        this.errorMsg = MessageFormat.format(errorMsg, arguments);
    }

    public BaseBizException(BaseErrorCodeEnum baseErrorCodeEnum, Object... arguments) {
        super(MessageFormat.format(baseErrorCodeEnum.getErrorMsg(), arguments));
        this.errorCode = baseErrorCodeEnum.getErrorCode();
        this.errorMsg = MessageFormat.format(baseErrorCodeEnum.getErrorMsg(), arguments);
    }

    public String getErrorCode() {
        return errorCode;
    }

    public void setErrorCode(String errorCode) {
        this.errorCode = errorCode;
    }

    public String getErrorMsg() {
        return errorMsg;
    }

    public void setErrorMsg(String errorMsg) {
        this.errorMsg = errorMsg;
    }
}

//订单中心自定义业务异常类
public class OrderBizException extends BaseBizException {
    public OrderBizException(String errorMsg) {
        super(errorMsg);
    }

    public OrderBizException(String errorCode, String errorMsg) {
        super(errorCode, errorMsg);
    }

    public OrderBizException(BaseErrorCodeEnum baseErrorCodeEnum) {
        super(baseErrorCodeEnum);
    }

    public OrderBizException(String errorCode, String errorMsg, Object... arguments) {
        super(errorCode, errorMsg, arguments);
    }

    public OrderBizException(BaseErrorCodeEnum baseErrorCodeEnum, Object... arguments) {
        super(baseErrorCodeEnum, arguments);
    }
}

@Service
public class OrderServiceImpl implements OrderService {
    ...
    //风控检查
    private void checkRisk(CreateOrderRequest createOrderRequest) {
        //调用风控服务进行风控检查
        CheckOrderRiskRequest checkOrderRiskRequest = createOrderRequest.clone(CheckOrderRiskRequest.class);
        JsonResult<CheckOrderRiskDTO> jsonResult = riskApi.checkOrderRisk(checkOrderRiskRequest);
        if (!jsonResult.getSuccess()) {
            throw new OrderBizException(jsonResult.getErrorCode(), jsonResult.getErrorMessage());
        }
    }
    ...
}

三.提供BaseErrorCodeEnum错误信息枚举接⼝

各个业务服务在⾃定义错误枚举信息时,会继承该接口。在抛出业务异常时,统⼀使⽤错误枚举信息,可以很好地集中管理错误信息并定义错误码。

swift 复制代码
public enum OrderErrorCodeEnum implements BaseErrorCodeEnum {
    //正向订单错误码105开头
    USER_ID_IS_NULL("105001", "用户ID不能为空"),
    ORDER_NO_TYPE_ERROR("105002", "订单号类型错误"),
    CREATE_ORDER_REQUEST_ERROR("105003", "生单请求参数错误"),
    ORDER_ID_IS_NULL("105004", "订单号不能为空"),
    BUSINESS_IDENTIFIER_IS_NULL("105005", "业务线标识不能为空"),
    BUSINESS_IDENTIFIER_ERROR("105006", "业务线标识错误"),
    ORDER_TYPE_IS_NULL("105007", "订单类型不能为空"),
    ORDER_TYPE_ERROR("105008", "订单类型错误"),
    SELLER_ID_IS_NULL("105009", "卖家ID不能为空"),
    USER_ADDRESS_ERROR("105010", "地址信息错误"),
    DELIVERY_TYPE_IS_NULL("105011", "配送类型不能为空"),
    USER_LOCATION_IS_NULL("105011", "地理位置不能为空"),
    ORDER_RECEIVER_IS_NULL("105012", "收货人不能为空"),
    ORDER_CHANNEL_IS_NULL("105013", "下单渠道信息不能为空"),
    CLIENT_IP_IS_NULL("105014", "客户端IP不能为空"),
    ORDER_ITEM_IS_NULL("105015", "订单商品信息不能为空"),
    ORDER_ITEM_PARAM_ERROR("105016", "订单商品信息错误"),
    ORDER_AMOUNT_IS_NULL("105017", "订单费用信息不能为空"),
    ORDER_AMOUNT_TYPE_IS_NULL("105018", "订单费用类型不能为空"),
    ORDER_AMOUNT_TYPE_PARAM_ERROR("105019", "订单费用类型错误"),
    ORDER_ORIGIN_PAY_AMOUNT_IS_NULL("105020", "订单支付原价不能为空"),
    ORDER_SHIPPING_AMOUNT_IS_NULL("105021", "订单运费不能为空"),
    ORDER_REAL_PAY_AMOUNT_IS_NULL("105022", "订单实付金额不能为空"),
    ORDER_DISCOUNT_AMOUNT_IS_NULL("105023", "订单优惠券抵扣金额不能为空"),
    ORDER_PAYMENT_IS_NULL("105024", "订单支付信息不能为空"),
    PAY_TYPE_PARAM_ERROR("105025", "支付类型错误"),
    ACCOUNT_TYPE_PARAM_ERROR("105026", "账户类型错误"),
    PRODUCT_SKU_CODE_ERROR("105027", "商品{0}不存在"),
    CALCULATE_ORDER_AMOUNT_ERROR("105028", "计算订单价格失败"),
    ORDER_CHECK_REAL_PAY_AMOUNT_FAIL("105029", "订单验价失败"),
    ORDER_NOT_ALLOW_INFORM_WMS_RESULT("105029", "订单不允许通知物流配送结果"),
    ORDER_NOT_ALLOW_TO_DELIVERY("105030", "订单不允许配送"),
    ORDER_NOT_ALLOW_TO_SIGN("105031", "订单不允许签收"),
    SKU_CODE_IS_NULL("105032", "skuCode 不能为空"),
    AFTER_SALE_ID_IS_NULL("105033", "售后ID不能为空"),
    LACK_ITEM_IS_NULL("105034", "缺品项不能为空"),
    LACK_NUM_IS_LT_0("105035", "缺品数量不能小于0"),
    ORDER_NOT_FOUND("105036", "查无此订单"),
    LACK_ITEM_NOT_IN_ORDER("105037", "缺品商品并未下单"),
    LACK_NUM_IS_GE_SKU_ORDER_ITEM_SIZE("105038", "缺品数量不能大于或等于下单商品数量"),
    ORDER_NOT_ALLOW_TO_LACK("105039", "订单不允许发起缺品"),
    REGION_ID_IS_NULL("105040", "区域ID不能为空"),
    ORDER_PAY_AMOUNT_ERROR("105041", "订单支付金额错误"),
    ORDER_PRE_PAY_ERROR("105042", "订单支付发生错误"),
    ORDER_PRE_PAY_EXPIRE_ERROR("105042", "已经超过支付订单的截止时间"),
    ORDER_PAY_CALLBACK_ERROR("105043", "订单支付回调发生错误"),
    ORDER_INFO_IS_NULL("105044", "订单信息不存在"),
    ORDER_CALLBACK_PAY_AMOUNT_ERROR("105045", "订单支付金额错误"),
    ORDER_CANCEL_PAY_CALLBACK_ERROR("105046", "接收到支付回调时,订单已经取消"),
    ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_SAME_ERROR("105047", "接收到支付回调的时候订单已经取消,且支付回调为同种支付方式"),
    ORDER_CANCEL_PAY_CALLBACK_PAY_TYPE_NO_SAME_ERROR("105047", "接收到支付回调的时候订单已经取消,且支付回调非同种支付方式"),
    ORDER_CANCEL_PAY_CALLBACK_REPEAT_ERROR("105048", "不同支付方式下的重复支付回调"),
    ORDER_CANNOT_REMOVE("105046", "订单不允许删除"),
    ORDER_NOT_ALLOW_TO_ADJUST_ADDRESS("105047", "订单不允许调整配送地址"),
    ORDER_DELIVERY_NOT_FOUND("105048", "订单配送记录不存在"),
    ORDER_DELIVERY_ADDRESS_HAS_BEEN_ADJUSTED("105049", "订单配送地址已被调整过"),
    ORDER_FULFILL_ERROR("105050", "订单履约失败"),
    AFTER_SALE_NOT_FOUND("105051", "查无此售后单"),
    AFTER_SALE_CANNOT_REVOKE("105052", "售后单无法撤销"),
    ORDER_STATUS_ERROR("105049", "订单状态异常"),
    ORDER_PAY_STATUS_IS_PAID("105050", "订单已经是已完成支付状态"),
    RETURN_GOODS_REQUEST_IS_NULL("105051", "手动退货入参不能为空"),
    AFTER_SALE_TIME_IS_NULL("105052", "申请售后时间不能为空"),
    ORDER_CURRENT_TYPE_IS_NULL("105053", "当前订单状态不能为空"),
    SKU_IS_NULL("105054", "sku列表不能为空"),
    RETURN_GOODS_NUM_IS_NULL("105055", "退货数量不能为空"),
    RETURN_GOODS_CODE_IS_NULL("105056", "退货原因选项不能为空"),
    AFTER_SALE_TYPE_IS_NULL("105057", "售后类型不能为空"),
    ORDER_STATUS_CHANGED("105058", "当前订单状态已变更,请重新刷新"),
    ORDER_STATUS_CANCELED("105058", "当前订单已取消"),
    ORDER_STATUS_IS_NULL("105059", "当前订单状态不能为空"),
    ORDER_PAY_TRADE_NO("105060", "当前订单已产生支付流水号,不能取消"),
    CANCEL_REQUEST_IS_NULL("105061", "取消订单入参不能为空"),
    CANCEL_TYPE_IS_NULL("105062", "取消订单类型不能为空"),
    CANCEL_ORDER_ID_IS_NULL("105063", "取消订单ID不能为空"),
    CANCEL_USER_ID_IS_NULL("105064", "取消订单用户ID不能为空"),
    CANCEL_ORDER_REPEAT("105065", "取消订单重复"),
    CANCEL_ORDER_FULFILL_ERROR("105066", "调用履约系统失败"),
    PROCESS_REFUND_REPEAT("105067", "处理退款重复"),
    CALCULATING_REFUND_AMOUNT_FAILED("105071", "取消订单用户ID不能为空"),
    PROCESS_REFUND_FAILED("105072", "退款前准备工作失败"),
    SEND_MQ_FAILED("105073", "发送MQ消息失败"),
    CONSUME_MQ_FAILED("105074", "消费MQ消息失败"),
    ORDER_REFUND_AMOUNT_FAILED("105075", "调用支付退款接口失败"),
    PROCESS_PAY_REFUND_CALLBACK_REPEAT("105076", "处理支付退款回调重复"),
    PROCESS_PAY_REFUND_CALLBACK_FAILED("105077", "处理支付退款回调失败"),
    PROCESS_PAY_REFUND_CALLBACK_BATCH_NO_IS_NULL("105078", "处理支付退款回调批次号不能为空"),
    PROCESS_PAY_REFUND_CALLBACK_STATUS_NO_IS_NUL("105079", "处理支付退款回调退款状态不能为空"),
    PROCESS_PAY_REFUND_CALLBACK_FEE_NO_IS_NUL("105080", "处理支付退款回调退款金额不能为空"),
    PROCESS_PAY_REFUND_CALLBACK_TOTAL_FEE_NO_IS_NUL("105081", "处理支付退款回调退款总额不能为空"),
    PROCESS_PAY_REFUND_CALLBACK_SIGN_NO_IS_NUL("105082", "处理支付退款回调签名不能为空"),
    PROCESS_PAY_REFUND_CALLBACK_TRADE_NO_IS_NUL("105083", "处理支付退款回调交易号不能为空"),
    PROCESS_AFTER_SALE_RETURN_GOODS("105084", "处理售后退款重复"),
    PROCESS_PAY_REFUND_CALLBACK_AFTER_SALE_ID_IS_NULL("105085", "处理支付退款回调售后订单号不能为空"),
    PROCESS_PAY_REFUND_CALLBACK_AFTER_SALE_REFUND_TIME_IS_NULL("105086", "处理支付退款回调售后退款时间不能为空"),
    REPEAT_CALLBACK("105087", "重复的退款回调"),
    REPEAT_CALL("105088", "当前接口被重复调用"),
    REFUND_MONEY_REPEAT("105089", "执行退款操作重复"),
    ORDER_CANNOT_REPEAT_OPERATE("105090", "当前订单不能重复操作"),
    PROCESS_APPLY_AFTER_SALE_CANNOT_REPEAT("105091", "不能重复发起售后"),
    CUSTOMER_AUDIT_CANNOT_REPEAT("105092", "不能重复发起客服审核"),
    AFTER_SALE_ITEM_CANNOT_NULL("105093", "售后商品信息不能为空"),

    //通用异常
    COLLECTION_PARAM_CANNOT_BEYOND_MAX_SIZE("108001", "[{0}]大小不能超过{1}"),
    ENUM_PARAM_MUST_BE_IN_ALLOWABLE_VALUE("108002", "[{0}]的取值必须为{1}"),
    DELIVERY_TYPE_ERROR("105080", "配送类型错误"),
    ;

    private String errorCode;
    private String errorMsg;
    ...
}

(5)统一返回结果处理规范

规范一: Web层各个Controller组件可统⼀使⽤JsonResult组件作为返回值,JsonResult主要是定义了统⼀返回给前端的Json格式。

其中,success字段表示请求是否成功,data字段表业务数据。请求成功时才会返回业务数据,请求失败时data是null。当success字段为false时,表示请求失败,此时errorCode与errorMessage才会有值,errorCode与errorMessage分别表示错误码与错误提示信息。

typescript 复制代码
//统一的Spring mvc响应结果封装对象
public class JsonResult<T> implements Serializable {
    private static final long serialVersionUID = 1L;
    private static final boolean REQUEST_SUCCESS = true;//请求成功
    private static final boolean REQUEST_FAIL = false;//请求失败
    private static final String DEFAULT_ERROR_CODE = "-1";//默认错误码
    private Boolean success;//请求是否成功
    private T data;//业务数据
    private String errorCode;//错误码
    private String errorMessage;//错误提示语

    public JsonResult() {

    }

    public JsonResult(Boolean success, T data, String errorCode, String errorMessage) {
        this.success = success;
        this.data = data;
        this.errorCode = errorCode;
        this.errorMessage = errorMessage;
    }

    //成功,不用返回数据
    public static <T> JsonResult<T> buildSuccess() {
        return new JsonResult<>(REQUEST_SUCCESS, null, null, null);
    }

    //成功,返回数据
    public static <T> JsonResult<T> buildSuccess(T data) {
        return new JsonResult<>(REQUEST_SUCCESS, data, null, null);
    }

    //失败,固定状态码
    public static <T> JsonResult<T> buildError(String errorMsg) {
        return new JsonResult<>(REQUEST_FAIL, null, DEFAULT_ERROR_CODE, errorMsg);
    }

    //失败,自定义错误码和信息
    //@param errorCode 错误码
    //@param errorMsg  错误提示
    public static <T> JsonResult<T> buildError(String errorCode, String errorMsg) {
        return new JsonResult<>(REQUEST_FAIL, null, errorCode, errorMsg);
    }

    //失败,枚举类定义错误码和信息
    public static <T> JsonResult<T> buildError(BaseErrorCodeEnum baseErrorCodeEnum) {
        return new JsonResult<>(REQUEST_FAIL, null, baseErrorCodeEnum.getErrorCode(), baseErrorCodeEnum.getErrorMsg());
    }
    ...
}

规范二: Controller中的⽅法也可以不返回JsonResult组件,⽽返回原样的对象,最后会由框架中的GlobalResponseBodyAdvice组件来统⼀拦截处理。

这个组件通过实现ResponseBodyAdvice接口 + 添加@RestControllerAdvice注解,实现了全局Controller⽅法的默认的返回结果格式的统⼀处理。其中,处理逻辑都在beforeBodyWrite()⽅法中。

typescript 复制代码
//默认的Controller全局响应结果处理增强组件
@RestControllerAdvice
@Order
public class GlobalResponseBodyAdvice implements ResponseBodyAdvice<Object> {
    //是否支持advice功能
    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
        Class<?> declaringClass = returnType.getDeclaringClass();
        if (declaringClass.equals(ApiResourceController.class) || declaringClass.equals(Swagger2Controller.class)) {
            return false;
        }
        if (declaringClass.equals(BasicErrorController.class)) {
            return false;
        }
        return true;
    }

    //处理response的具体业务方法
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (!selectedContentType.equalsTypeAndSubtype(MediaType.APPLICATION_JSON)) {
            return body;
        }
        if (body instanceof JsonResult || body instanceof JsonMap) {
            return body;
        } else if (body instanceof String) {
            try {
                HttpServletResponse httpServletResponse = ServletUtil.getResponse();
                if (httpServletResponse != null) {
                    ServletUtil.writeJsonMessage(httpServletResponse, JsonResult.buildSuccess(body));
                    return null;
                }
            } catch (Exception e) {
                log.warn("响应字符串对象给前端异常", e);
            }
            return JsonUtil.object2Json(JsonResult.buildSuccess(body));
        }
        return JsonResult.buildSuccess(body);
    }
}

规范三: 如果某接⼝就想返回原样的对象,不想让返回结果被JsonResult封装,那么可以让Controller⽅法返回一个JsonMap对象。

GlobalResponseBodyAdvice.beforeBodyWrite()⽅法会对类型为JsonMap的body进行直接返回。

java 复制代码
//自定义Map实现,完全兼容java.util.HashMap
public class JsonMap<K, V> extends HashMap<K, V> {
    public JsonMap(int initialCapacity, float loadFactor) {
        super(initialCapacity, loadFactor);
    }

    public JsonMap(int initialCapacity) {
        super(initialCapacity);
    }

    public JsonMap() {

    }

    public JsonMap(Map<? extends K, ? extends V> m) {
        super(m);
    }
}

(6)Service层开发规范

业务异常规范:

一.Service层需要中断执行的逻辑时,统⼀抛BaseBizException或其⼦类业务异常

二.DAO层不要显式地抛任何业务异常,统⼀由Service来捕捉并抛异常

三.Controller层不需要显式通过try catch来捕捉Service层抛出的业务异常,因为会由GlobalExceptionHandler组件来进行统⼀处理

四.抛业务异常时,建议对异常信息定义⼀个枚举值,这个枚举类要继承BaseErrorCodeEnum

事务处理规范:

对于⾮查询接⼝,必须在Service实现类的⽅法上添加如下的@Transactional注解。

python 复制代码
@Transactional(rollbackFor = Exception.class)

(7)DAO层开发规范

一.Mybatis Plus的配置使⽤规范

arduino 复制代码
使⽤mybatisplus 3.x版本;
mybatisplus相关依赖在demo-eshop-common中都已经添加好了;
mybatisplus插件的配置参考com.demo.eshop.order.config.MybatisPlusConfig组件;
mybatisplus其它配置参考application.yml中mybatis-plus开头的配置项;

二.订单中⼼的MybatisPlusConfig组件示例

typescript 复制代码
//Mybatis Plus配置
@Configuration
public class MybatisPlusConfig {
    //通用字段自动填充配置
    @Bean
    public MetaObjectHandler metaObjectHandler() {
        return new MetaObjectHandler() {
            @Override
            public void insertFill(MetaObject metaObject) {
                this.strictInsertFill(metaObject, "gmtCreate", Date.class, new Date());
                this.strictInsertFill(metaObject, "gmtModified", Date.class, new Date());
            }

            @Override
            public void updateFill(MetaObject metaObject) {
                this.strictUpdateFill(metaObject, "gmtModified", Date.class, new Date());
            }
        };
    }

    //插件配置
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
        //分页插件
        mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
        return mybatisPlusInterceptor;
    }
}

三.application.yml中mybatis-plus开头的配置项

ruby 复制代码
mybatis-plus:
    configuration:
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
        map-underscore-to-camel-case: true
    mapper-locations: classpath:mapper/*.xml

// mybatis-plus.configuration.log-impl⽤于控制台开启⽇志输出,⼀般只在开发环境使⽤;
// mybatis-plus.configuration.map-underscore-to-camel-case表示是否开启下划线转驼峰,默认就是true;
// mybatis-plus.mapper-locations表示扫描mapper接⼝对应的xml⽂件的位置,默认也是在src/main/resources下的mapper⽬录下的;
// 更多配置,参考:https://mp.baomidou.com/config/#configlocation

四.单表操作规范

⼀般情况下单表操作⽆论单条记录还是批量记录的增删改查,全部使⽤Mybatis Plus框架的IService接口或BaseMapper接口。

只有多表关联查询时才需要在mapper.xml⽂件中编写SQL脚本,不要让⼀个SQL脚本⽤于多个功能多个场景,特别是对很多字段做<#if>判断。然后作为可能的查询条件,应该按不同功能需求定义多个⽅法。

五.DO通用字段处理规范

每个表都必须有三个必选字段 id、gmt_create、gmt_modified,其中gmt_create、gmt_modified这两个字段值都是让Mybatis Plus⾃动填充的,不需要在业务代码中显式对这两个字段进⾏操作。

不过除了前⾯的MybatisPlusConfig中的配置之外,还需要在DO类的这两个字段上添加相应的注解,⽐如OrderInfoDO类。

scala 复制代码
public class BaseEntity extends AbstractObject {
    //主键ID
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    //创建时间
    @TableField(fill = FieldFill.INSERT)
    private Date gmtCreate;

    //更新时间
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Date gmtModified;
    ...
}

//订单表
@Data
@TableName("order_info")
public class OrderInfoDO extends BaseEntity implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer businessIdentifier;//接入方业务线标识  1, "自营商城"
    private String orderId;//订单编号
    private String parentOrderId;//父订单编号
    private String businessOrderId;//接入方订单号
    private Integer orderType;//订单类型 1:一般订单  255:其它
    private Integer orderStatus;//订单状态 10:已创建, 30:已履约, 40:出库, 50:配送中, 60:已签收, 70:已取消, 100:已拒收, 255:无效订单
    private String cancelType;//订单取消类型
    private Date cancelTime;//订单取消时间
    private String sellerId;//卖家编号
    private String userId;//买家编号
    private Integer totalAmount;//交易总金额(以分为单位存储)
    private Integer payAmount;//交易支付金额
    private Integer payType;//交易支付方式
    private String couponId;//使用的优惠券编号
    private Date payTime;//支付时间
    private Date expireTime;//支付订单截止时间
    private String userRemark;//用户备注
    private Integer deleteStatus;//订单删除状态 0:未删除  1:已删除
    private Integer commentStatus;//订单评论状态 0:未发表评论  1:已发表评论
    private String extJson;//扩展信息
}
相关推荐
DKPT14 分钟前
Java桥接模式实现方式与测试方法
java·笔记·学习·设计模式·桥接模式
好奇的菜鸟2 小时前
如何在IntelliJ IDEA中设置数据库连接全局共享
java·数据库·intellij-idea
DuelCode3 小时前
Windows VMWare Centos Docker部署Springboot 应用实现文件上传返回文件http链接
java·spring boot·mysql·nginx·docker·centos·mybatis
优创学社23 小时前
基于springboot的社区生鲜团购系统
java·spring boot·后端
幽络源小助理3 小时前
SpringBoot基于Mysql的商业辅助决策系统设计与实现
java·vue.js·spring boot·后端·mysql·spring
猴哥源码3 小时前
基于Java+springboot 的车险理赔信息管理系统
java·spring boot
YuTaoShao4 小时前
【LeetCode 热题 100】48. 旋转图像——转置+水平翻转
java·算法·leetcode·职场和发展
Dcs4 小时前
超强推理不止“大”——手把手教你部署 Mistral Small 3.2 24B 大模型
java
Code blocks5 小时前
使用Jenkins完成springboot项目快速更新
java·运维·spring boot·后端·jenkins