编码技巧——校验器(职责链+抽象模版)

2023-11-20

日常开发中可能遇到这样的业务场景:请求从入口进来,需要经过层层的校验,通过校验后才会执行业务操作(写操作、RPC、异步消息...);这里前置的多层校验流程中,从类型上看,部分是基本参数校验,部分是包含业务逻辑的校验,并且部分校验是可以并行,部分是有依赖关系的;此外,从代码的通用性来说,对于流程相对固定的业务逻辑,完全可以提前定义好多个校验器,通过配置,可动态的拔插这些预制的校验器以及其顺序;本篇介绍用户下单前业务订单多层校验场景的代码示例;

1. 业务场景

本篇介绍一个业务场景:支付流程中,从端发起,一般会先到业务订单模块,通过一系列的校验和业务订单处理后,才会真正的与订单中台交互,而订单中台屏蔽对各种支付方(蚂蚁金服、微信支付、网上银行等)的交互;这里给出下单前业务侧前置校验(支付限购、实名制、未成年限制等)的方案及代码示例;

2. 代码示例

代码主要包括:校验器接口、职责链入参和返参(上下文)、执行器(顺序/并行);校验器抽象模板(汗基本校验逻辑)和其实现类;

(1)代码结构

(2)校验器接口IChecker 

/**
 * 校验器
 */
public interface IChecker {

    /**
     * 校验行为
     * @param checkParam
     * @param checkContext
     */
    void action(CheckParam checkParam, CheckContext checkContext);

    /**
     * 校验器code
     * @return
     */
    String getCheckCode();

}

(3)IChecker的参数(校验器入参、校验结果和职责链的上下文)

校验器入参

/**
 * 订单校验参数
 */
@Slf4j
@Data
public class CheckParam {

    /**
     * uid
     */
    private String uid;

    /**
     * 设备号
     */
    private String deviceId;

    /**
     * 支付插件版本
     */
    private String sdkVersion;

    /**
     * appId
     */
    private String appId;

    /**
     * 订单金额
     */
    private Long orderAmount;

    /**
     * 交易类型
     */
    private Integer tradeType;

    /**
     * 商品id
     */
    private String productId;

    /**
     * 周期内限购次数(0代表不限购)
     */
    private Integer limitNum;

    /**
     * 限购开始时间
     */
    private Date cycleStart;

    /**
     * 限购结束时间
     */
    private Date cycleEnd;

    /* 省略 */

}

校验结果

/**
 * 校验结果
 */
@Data
public class CheckResult {

    private String code;

    private String msg;

    public boolean isSuccess() {
        return TradeLimitCodeEnum.SUCCESS.getCode().equals(this.getCode());
    }

    public static CheckResult success() {
        return CheckResult.builder().code(TradeLimitCodeEnum.SUCCESS.getCode()).msg(TradeLimitCodeEnum.SUCCESS.getMsg()).build();
    }

    public static CheckResult fail(TradeLimitCodeEnum tradeLimitCodeEnum, String msg) {
        return CheckResult.builder().code(tradeLimitCodeEnum.getCode()).msg(msg).build();
    }
    
    /* 省略 */
}

职责链上下文

/**
 * 校验上下文
 */
@Data
public class CheckContext {

    /**
     * 存储各Checker的校验结果 key:check_code
     */
    private Map<String, CheckResult> checkResultMap = Maps.newConcurrentMap();

    public void setResult(String checkerName, CheckResult checkResult) {
        checkResultMap.put(checkerName, checkResult);
    }

}

(4)校验器配置-初始化几种校验链路

/**
 * 校验器配置
 */
@Configuration
public class CheckerConfig {

    // 校验器code
    public static final String ORDER_ACCOUNT_CHECKER_CODE = "order_account_checker";

    public static final String PAY_ACCOUNT_CHECKER_CODE = "pay_account_checker";

    public static final String ORDER_LIMIT_CHECKER_CODE = "order_limit_checker";

    // 配置下单校验器链
    public static final List<String> ORDER_CHECKER_PIPELINE = Lists.newArrayList(CheckerConfig.ORDER_ACCOUNT_CHECKER_CODE, CheckerConfig.ORDER_LIMIT_CHECKER_CODE);

    // 配置支付校验器链
    public static final List<String> PAY_CHECKER_PIPELINE = Lists.newArrayList(CheckerConfig.PAY_ACCOUNT_CHECKER_CODE);

    // 所有的校验器会自动注入这个List
    @Resource
    private List<IChecker> iCheckers;

    // 订单校验器自动注入
    @Bean
    public List<IChecker> orderCheckers() {
        return buildCheckFilterBase(CheckerConfig.ORDER_CHECKER_PIPELINE);
    }

    // 支付校验器自动注入
    @Bean
    public List<IChecker> payCheckers() {
        return buildCheckFilterBase(CheckerConfig.PAY_CHECKER_PIPELINE);
    }

    // 根据配置 取各自检验器code列表下对应的校验器Bean列表
    private List<IChecker> buildCheckFilterBase(List<String> checkerPipeline) {
        List<IChecker> orderChecks = Lists.newArrayList();
        if (CollectionUtils.isEmpty(iCheckers)) {
            return orderChecks;
        }
        Map<String, IChecker> map = iCheckers.stream().collect(Collectors.toMap(IChecker::getCheckCode, iChecker -> iChecker));
        checkerPipeline.forEach(code -> {
            if (map.get(code) != null) {
                orderChecks.add(map.get(code));
            }
        });
        return orderChecks;
    }

}

(5)校验控制器/执行器

/**
 * 校验控制器
 */
@Slf4j
@Service
public class CheckManager {

    @Resource
    private List<IChecker> orderCheckers;
    @Resource
    private List<IChecker> payCheckers;

    private static final ThreadPoolExecutor ORDER_POOL = new ThreadPoolExecutor(20, 256,
                    15L, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(),
                    (ThreadFactory) Thread::new);

    private static final ThreadPoolExecutor PAY_POOL = new ThreadPoolExecutor(20, 128,
                    15L, TimeUnit.SECONDS,
                    new LinkedBlockingQueue<>(),
                    (ThreadFactory) Thread::new);

    /**
     * 下单校验
     * @param checkParam
     * @return
     */
    public CheckResult checkOrder(CheckParam checkParam) {
        return checkBase(checkParam, orderCheckers, ORDER_POOL, true);
    }

    /**
     * 支付校验
     * @param checkParam
     * @return
     */
    public CheckResult checkPay(CheckParam checkParam) {
        return checkBase(checkParam, payCheckers, PAY_POOL, false);
    }

    private CheckResult checkBase(CheckParam checkParam, List<IChecker> checkers, Executor executor, boolean isOrderCheck) {
        // 后续可以根据需要在上下文暂存各校验器公共依赖的基础数据
        CheckContext checkContext = new CheckContext();
        if (CollectionUtils.isNotEmpty(checkers)) {
            CompletableFuture[] futures = new CompletableFuture[checkers.size()];
            for (int i = 0; i < checkers.size(); i ++) {
                IChecker checker = checkers.get(i);
                try {
                    futures[i] = CompletableFuture.runAsync(() -> checker.action(checkParam, checkContext), executor);
                } catch (Exception e) {
                    log.error("check_action_error.", e);
                }
            }
            try {
				// 同步获取多个异步结果 超时1500ms
                CompletableFuture.allOf(futures).get(ConfigManager.getInteger("trade.limit.timeout", 1500), TimeUnit.MILLISECONDS);
            } catch (TimeoutException e) {
                log.error("check_timeout");
            } catch (Exception e) {
                log.error("CheckManager_error", e);
            }
        }
        return getCheckResult(checkContext, isOrderCheck);
    }

    private CheckResult getCheckResult(CheckContext checkContext, boolean isOrderCheck) {
        List<String> checkerPipeLine = isOrderCheck ? CheckerConfig.ORDER_CHECKER_PIPELINE : CheckerConfig.PAY_CHECKER_PIPELINE;
        for (String checkerCode : checkerPipeLine) {
            CheckResult checkResult = checkContext.getCheckResultMap().get(checkerCode);
            // 返回第一个被拦截的校验结果
            if (checkResult != null && !TradeLimitCodeEnum.SUCCESS.getCode().equals(checkResult.getCode())) {
                return checkResult;
            }
        }
        return CheckResult.success();
    }

}

Tips:链式校验若先后关系无依赖,可以并行异步执行校验逻辑,同步获取校验结果上下文,从而提升执行速度;

(6)校验器模板及校验器实现类

抽象模板

/**
 * 账号域校验器
 */
@Slf4j
public abstract class AbstractChecker {

    @Resource
    private PayRecordService payRecordService;

    protected boolean preCheck(CheckParam checkParam) {
        if (StringUtils.isBlank(checkParam.getUid()) && StringUtils.isEmpty(checkParam.getDeviceId())) {
            log.info("preCheck_without_uid or deviceId]");
            return true;
        }
        return false;
    }

    protected PayInfo buildPayInfo(CheckParam checkParam) {/*...*/}

    protected PayHistory buildPayHistory(CheckParam checkParam) {/*...*/}
    
    protected void buildResult(String checkerCode, Result accountResult, CheckContext checkContext) { 
        // 账号限购、实名制...
    }

}

模板实现类——支付环节账号校验

/**
 * 支付环节账号校验---实名制、未成年
 */
@Slf4j
@Component
public class PayByAccountChecker extends AbstractChecker implements IChecker {

    @Resource
    private SubAccountService subAccountService;
	
	// 标识当前校验器的code
    @Override
    public String getCheckCode() {
        return CheckerConfig.PAY_ACCOUNT_CHECKER_CODE;
    }
	
    @Override
    public void action(CheckParam checkParam, CheckContext checkContext) {
        Result result = null;
        try {
			// 校验开关
            if (!ConfigManager.getBoolean("pay.account.check.switch", true)) {
                return;
            }
			// 基本参数校验
            if (preCheck(checkParam)) {
                return;
            }
			// 支付插件版本判断
            if (StringUtils.isNotBlank(checkParam.getSdkVersion()) && APKVersionComparetor.compareVersion(checkParam.getSdkVersion(), "4.8.0") < 0
                    && ConfigManager.get("pay.account.check.lowapk.apptype", "5").contains(checkParam.getAppBigType())) {
                checkContext.setResult(getCheckCode(), CheckResult.fail(TradeLimitCodeEnum.FORBID_PAY, "支付安全插件版本较低,请前往应用商店升级"));
                return;
            }
			// 对支付环节账号的校验逻辑
            result = subAccountService.checkPayLimit(buildPayInfo(checkParam), buildPayHistory(checkParam));
        } catch (Exception e) {
            log.error("PayByAccountChecker_action_error.", e);
        }
        buildResult(getCheckCode(), result, checkContext);
    }

}

模板实现类——支付限购校验

/**
* 支付限购
*/
@Slf4j
@Component
public class OrderBuyLimitChecker extends AbstractChecker implements IChecker {

	@Resource
    private PayRecordService payRecordService;

	// 校验器对应的code
	@Override
	public String getCheckCode() {
		return CheckerConfig.ORDER_LIMIT_CHECKER_CODE;
	}
	
	@Override
	public void action(CheckParam checkParam, CheckContext checkContext) {
		String result = TradeLimitCodeEnum.SUCCESS.getCode();
		try {
			// 功能开关 
			if (!ConfigManager.getBoolean("buy.limit.check.switch", true) || isInBuyLimitWhiteList(checkParam.getAppId())) {
				return;
			}
			// 前置校验
			if (checkParam(checkParam)) {
				return;
			}
			// 支付限购校验逻辑
			if (payRecordService.queryNumberOfPay(checkParam) >= checkParam.getLimitNum()) {
				result = TradeLimitCodeEnum.BUY_LIMIT.getCode();
			}
		} catch (Exception e) {
			log.error("PayLimitChecker_action_error.", e);
		}
		buildResult(getCheckCode(), result, checkContext);
	}

	/* ... */

}

以上便是相关的所有代码;还有优化的空间,如:校验器加上order顺序属性、动态配置校验器的数量和顺序等;

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

编码技巧——校验器(职责链+抽象模版) 的相关文章

随机推荐

  • vue+diff:计算两个时间的差值diff方法momentjs和dayjs

    前言 在项目中计算时间的时候 会遇到一个问题 那就是日期跨天问题 我们的开始时间和结束时间拿到了毫秒 然后时间 比如 11 50 1 50 这个如果按小时来算 就没有到1天 24小时 但是他又是过了一天 所以会需要这个diff计算 具体操作
  • 在聚会中常玩数七的游戏,七的倍数和带有七的数字都不能说,比如14,27,28。请找出1~100的不能说的数字。...

    利用ES5的filter高阶函数来实现 var arr 1 2 3 4 5 6 7 17 27 21 22 28 100 r arr filter function x return x 10 7 x 7 0 alert r 7 14 17
  • 在python中创建excel文件并写入数据

    python的包xlwt和xlsxwriter都是比较方便创建excel文件并写入数据的 工具 python3 0 首先 需要安装好相应的包 pip install xlwt 或pip install xlsxwriter xlwt中 通过
  • 谈谈JS异步处理(Promise、generator、async)

    大家都知道nodejs很快 为什么会这么快呢 原因就是node采用异步回调的方式来处理需要等待的事件 使得代码会继续往下执行不用在某个地方等待着 但是也有一个不好的地方 当我们有很多回调的时候 比如这个回调执行完需要去执行下个回调 然后接着
  • Jira、Confluence 备份 迁移

    Jira 备份 迁移 全量打包文件和数据库 将打包好的文件放到迁移的服务器 创建数据库排序规则为utf8 bin并导入备份脚本 服务器创建jira用户 gt useradd jira jira服务文件夹赋权 gt chown R jira
  • Vue中的import from

    Vue中的import from 大家都知道 import from 是用来引入一些文件的 在vue中 可能有 js文件 json文件 vue文件 在JS和JSON文件引入的时候 往往需要写入一些 例如数组 export const a 例
  • JavaScript string中includes、startsWith和endsWith的使用

    文章目录 前言 一 includes 二 startsWith 三 endsWith 总结 前言 JavaScript string的这三个方法都是根据参数返回true或false 一 includes includes 方法判断一个字符串
  • 3D点云处理:Opencv Pcl实现深度图转点云(附源码)

    文章目录 0 测试效果 1 代码实现 文章目录 3D视觉个人学习目录 0 测试效果 处理结果 1 代码实现 文章中提供的深度图像 深度图像一般以 tiff和 png保存 可以通过Opencv中的 c v i m r
  • docker入门---最全笔记

    前言 小编我将用CSDN记录软件开发求学之路上亲身所得与所学的心得与知识 有兴趣的小伙伴可以关注一下 也许一个人独行 可以走的很快 但是一群人结伴而行 才能走的更远 让我们在成长的道路上互相学习 让我们共同进步 欢迎关注 目录 前言 一 D
  • FFMPEG进阶系列02-ffmpeg命令详解3

    文章目录 ffmpeg 的封装转换 ffmpeg的编转码 ffmpeg 的基本编转码原理 过滤器链 filter chain 码率 帧率和文件大小 帧率 帧率和文件大小 调整视频分辨率 调整视频分辨率 scale filter调整分辨率 裁
  • Go Web编程实战(10)----模板引擎库text/template包的使用

    目录 前言 模板引擎 定义模板文件 解析模板文件 渲染模板 实战使用模板 创建 tmpl文件 创建文件用于解析与渲染模板 前言 在Go语言中 模板引擎库text template包主要用于处理任意格式的文本内容 同时还提供了html tem
  • IP网址可访问,域名网址无法访问

    可以通过修改DNS排查问题 一 修改DNS的好处 适当提高上网速度 更换DNS可以访问某些因为域名解析存在问题而不能访问的网站 可以屏蔽运营商的广告 还可以帮助您避免被钓鱼的危险 二 修改DNS带来的副作用 无法访问页面或者访问的页面不是你
  • Ubuntu 20.04 配置深度学习开发环境

    目录 写在前面 Dependency 1 安装Anaconda 1 1 下载安装包 1 2 进入安装文件夹 执行安装脚本 1 3 环境变量的配置与更新 1 4 测试安装 1 5 创建虚拟环境 2 安装英伟达驱动 法一 命令行安装 法二 GU
  • 1264 - Out of range value for column 'id' at row 1

    1 我用的是mysql 在数据插入是报错 原因是我插入的值 超过了数据库中类型和长度设置 1 1 我的插入语句 注意 id 的值 INSERT INTO test id sex name username password classes
  • Vue —— 锚点导航

    一个页面中分为多块 例如 目录一 目录二 目录三等 这就需要加上一个锚点导航的需求 提高用户的操作性 原生的写法 div class wrapper ul li a href catalogue1 目录一 a li li a href ca
  • SpringCloud概述

    SpringCloud概述 1 SpringCloud是什么 2 SpringCloud和SpringBoot关系 3 Dubbo和SpringCloud技术选型 4 SpringCloud作用 1 SpringCloud是什么 现代化的J
  • How to compile rocksdb with lz4 support

    On CentOS 6 x or 7 x you can do the following to easily install lz4 using the package manager As root sudo su is your fr
  • 137-----JS基础-----类的操作

    一 代码 不算难 如果后续操作到类的话 可以直接使用下面封装好的接口到自己的tool中
  • 线性回归建模及模型诊断

    目录 一 建模背景及目的及数据源说明 二 描述性分析 2 1 连续自变量与连续因变量的相关性分析 2 2 二分类变量与连续变量的相关性分析 2 3 多分类变量与连续变量的相关性分析 三 模型建立与诊断 3 1 一元线形回归及模型解读 3 2
  • 编码技巧——校验器(职责链+抽象模版)

    日常开发中可能遇到这样的业务场景 请求从入口进来 需要经过层层的校验 通过校验后才会执行业务操作 写操作 RPC 异步消息 这里前置的多层校验流程中 从类型上看 部分是基本参数校验 部分是包含业务逻辑的校验 并且部分校验是可以并行 部分是有