美文网首页
技术组件(三)-业务账单(自定文件模版)工具

技术组件(三)-业务账单(自定文件模版)工具

作者: 爱编程的凯哥 | 来源:发表于2019-11-03 17:09 被阅读0次

需求

  1. 灵活配置账单,10行代码批量生成商户账单.

场景概述

一个代理商,下面有n个收单商户,要生成下面每个收单商户的每天的交易流水账单文件

实现能力

  1. 能通过模版文件配置修改账单内容
  2. 修改账单内容和结构只需修改配置文件sql
  3. 数据读取通过分页实现
  4. 对不同数据源的支持
  5. 支持多库数据组合生成账单的场景
  6. 支持自定义特殊字段的转换
  7. 支持文件的后置处理,可自定义存放位置

源码地址:

https://gitee.com/kaiyang_taichi/bill-Plugins.git

使用方法:

  1. 导入pom,因为未deploy到公有仓库,需要使用,可以自行下载源码编译
<dependency>
            <groupId>cn.bese.bill.template.plugins</groupId>
            <artifactId>bill-plugins</artifactId>
            <version>1.0-SNAPSHOT</version>
   </dependency>
  1. 编写配置文件:

例:

sql1: SELECT * FROM HUSKY2.MERCHANT where merchant_type in (${init.0})

sql2: select r,${sql3.Merchant_no} t,m.MERCHANT_NO,m.MERCHANT_NAME,m.POS_CATI,m.POS_SERIAL_NUMBER,m.TRX_TYPE,
 m.TRADE_SERIAL_NO,m.CREATE_TIME,m.CARD_NO,m.TRADE_AMOUNT,m.STATUS,m.CARD_TYPE,m.MERCHANT_FEE,'' shuangmian,m.AGENT_NO,'' AGENT_NAME,'' so  from (
  SELECT row_number() over(ORDER BY mr.id DESC) as r,mr.*
  FROM OFFLINE.TBL_OFFLINE_ORDER  mr
  where mr.MERCHANT_NO=${sql3.Merchant_no} and mr.status='SUCCESS'
  ) m where m.r>${sys.pageIndex} fetch first ${sys.pageSize}  rows only

sql3: select ym.* from (
      SELECT row_number() over(ORDER BY mr.id DESC) as r,m.* FROM HUSKY2.MERCHANT_RELA_NEW  mr
      inner join HUSKY2.MERCHANT m on mr.SUb_NO = m.merchant_no and m.merchant_type='MERCHANT'
      where mr.PARENT_NO=${file.2}) ym where ym.r>${sys.pageIndex} fetch first ${sys.pageSize} rows only

file-name: /Users/kai.yang/Desktop/yeepay/bill-plugins/bills/orders/${sys.yyyy}/${sys.MM}/${sql1.MERCHANT_NO}/${sql1.MERCHANT_NAME}/交易_${sys.yyyy}${sys.MM}${sys.dd}.csv


transfers:
   - AGENT_NAME->class:com.example.plugns.demoweb.config.bill.AgentNameHandler
   - STATUS->map:SUCCESS|成功

file-templates:
   - 标题:商户交易数据
   - 商户名称:${file.3}
   - 商户编号|商户名称|终端编号|SN号|产品类型|交易号|交易日期|交易时间|交易对方银行卡号|交易金额|交易状态|卡类型|手续费|小额双免|代理商编号|代理商名称|S0出款状态
   - ${sql2.t}|${sql2.MERCHANT_NO}|${sql2.MERCHANT_NAME}|${sql2.POS_CATI}|${sql2.POS_SERIAL_NUMBER}|${sql2.PRODUCT_CODE}|${sql2.TRADE_SERIAL_NO}|${sql2.CREATE_TIME}|${sql2.CARD_NO}|${sql2.TRADE_AMOUNT}|${sql2.STATUS}|${sql2.CARD_TYPE}|${sql2.MERCHANT_FEE}|${sql2.INPUT_TYPE}|${sql2.AGENT_NO}|${sql2.AGENT_NAME}|${sql2.so}

null-file-templates: sql2 -> no data today!

file-content-format-class: com.example.plugns.demoweb.config.bill.FileContentTransferHandler

save-after-class: com.example.plugns.demoweb.config.bill.SaveBillConfig

参数:

  1. 模版key配置方法:

    1. sql*: 模版主要内容,就是我们平时的sql语句,你可以根据所用数据库语言自己规范sql方言.多个sql可以组合使用,key为sql+(自定义数码,只用来区分sql没有特殊先后顺序)例子中:
      sql1--> 查询出指定类型的所有商户,本例中为了查出所有代理商
      sql3(先跳过sql2,因为sql2以sql3的结果作为了查询条件)-->遍历sql1的每个代理商,分页查出每个代理商对应的所有子商户
      sql2-->在每个文件中,分页查询sql3中每个子商户的交易数据,汇总生成文件内容
      sql1、sql2、sql3 其实就是我们平时写账单的三个步骤的sql语句,此处通过模版key的方式灵活替换

    2. file-name : 最后生成的文件名称,如例子;
      file-name: /Users/kai.yang/Desktop/yeepay/bill-plugins/bills/orders/${sys.yyyy}/${sys.MM}/${sql1.MERCHANT_NO}/${sql1.MERCHANT_NAME}/交易_${sys.yyyy}${sys.MM}${sys.dd}.csv
      其中所有${*}定义的参数,都可以在模版中通过${file.*}获取到,这里的index 从0开始.

    3. transfers:定义的转换器,可以对一些特殊字段进行后置处理.默认有两种转换器:
      1. map型: map:SUCCESS|成功,定义你SUCCESS到成功的映射,自动替换,场景如数据库枚举值,文件中转换为中文.
      2. class型: class:com.example.plugns.demoweb.config.bill.AgentNameHandler自定义转换类,只要出现你指定的字段,就会根据你定义的转换类进行替换.此类要继承TransferValueHandler接口

    4. file-templates: 文件模版,最终的csv文件模版定义.用yml文件的 -表示换行,注意点,最终的文件内容暂时只能通过一个sql主体出数据,否则系统无法组合分页.如本例中,最终数据从sql2中产出,本行模版不能有其他sql替换符,但可以有其他系统内置参数.

      image.png
    5. null-file-templates: 空文件模版配置,指获取的主sql数据为空时,文件展示的内容,不配的话只展示表头,否则根据你配置写文件.如例子中,当sql2数据为空时,文件内容为:
      no data today!

    6.file-content-format-class ,整行内容处理类,使用较少.作用是你可以对每一行数据都可以做整体的特殊处理,不过场景不多.

    1. save-after-class :文件后置处理类,如果你需要对最后的文件做相应的处理,如发送邮件,或保存到其他服务器的,可以通过此配置实现,继承SaveAfterProcessConfig接口:
public class SaveBillConfig implements SaveAfterProcessConfig {

    @Override
    public boolean afterProcess(File file, String fileName, Object[] fileParams) {
        System.out.println("文件存储后置处理");
        return true;
    }
}

  1. 系统内置参数说明:
    1. ${init.*}:以init开头的参数为,executer启动时传入的初始化参数,单个 executer上下文全局唯一,不会更改.可用于一些固定的外部参数,如时间范围、业务类型等等.

    2. ${sys.*}: 为系统内定参数模式,不需要外不指定,有自己的实现逻辑,可直接使用,其中:
      ${sys.pageIndex}: 分页页码参数,在sql中使用,系统会自动从0开始自增
      ${sys.pageSize}: 分页每页数据条数默认配置,默认200,也可自定义
      ${sys.yyyy}: 系统年份获取参数,取系统年份,格式如:2019
      ${sys.MM}:系统年份获取月份,取系统年份,格式如:09
      ${sys.dd}: 系统天:格式:23
      处理代码在cn.base.bill.template.plugins.config.SysParamConfig中,有需要可自行调整:

    3. ${file.*}:获取最终文件名中的指定参数,在单个文件不变的参数上下文传递时可以使用(但缺陷是文件目录会多出此参数,后续有机会可以优化,加入文件级别的上下文).例如:
      file-name: /Users/kai.yang/Desktop/yeepay/bill-plugins/bills/orders/${sys.yyyy}/${sys.MM}/${sql1.MERCHANT_NO}/${sql1.MERCHANT_NAME}/交易_${sys.yyyy}${sys.MM}${sys.dd}.csv
      但这里的fiile参数只取file-name配置中的${}中的参数,所以此例汇总 ${file.2}就是对应的${MERCHANT_NO}获取当前文件中的月份字段值(小标从0开始).

    4. ${sql*.*}: 重点的sql参数,在文件模版key中,已经说过sqln就是对应指定的sql,如${sql2.MERCHANT_NO}就是对应的sql2中的MERCHNAT_NO字段.

  1. 代码启动:
    配置文件配好后,10来行代码就可以生产你需要的账单了.
public class DemoController implements InitializingBean {

   /**
    * 配置的一个数据源
    */
   @Resource(name = "posDataSource")
   DataSource posDataSource;

   /**
    * 配置的第二个数据源
    */
   @Resource(name = "huskyDataSource")
   DataSource huskyDataSource;

   /**
    * 对应的执行器构造者,通过afterPropertiesSet方法初始化
    */
   BillPluginsExecuteBuilder orderBillPluginsExecuteBuilder;


   @Override
   public void afterPropertiesSet() {
       //初始化构造者,
       //1。setBillConfigFilePath 指定配置文件路径
       //2。setDataSource指定数据源配置,参数(DataSource dataSource, String... keys),指定哪些sql的key对应哪个数据源
       // 本例子中配置了两个数据源,sql1、sql3对应huskyDataSource,sql2对应posDataSource数据源
       //3。最后调用init()方法启动builder
       orderBillPluginsExecuteBuilder = new BillPluginsExecuteBuilder()
           .setBillConfigFilePath("/biil-template/order-templates-demo-db2.yml")
           .setDataSource(huskyDataSource, "sql1", "sql3").setDataSource(posDataSource, "sql2").init();

   }

   @GetMapping("/test2")
   public String test2() throws SQLException {
       //params为配置执行器上下文的初始化参数,可通过${init.n}获得
       Object[] params = new Object[]{"MIDDLE_AGENT", "10040041322"};
       //最后执行generate生产所有文件
       orderBillPluginsExecuteBuilder.build(params).generate();
       return "ok";
   }
}

生产的账单例子,生成这个代理商下每个子商户的数据:


image.png

源码简介

此处先简单介绍下代码结构,有需要以后再细说.


image.png

看下源码机构图:

  1. config是对应上面说的系统内置参数的处理逻辑
  2. context 为组件上下文定义,里边有全局的一些缓存
  3. dao为数据库交互层,封装了sql的执行过程、分页实现都在这里
  4. format为对应参数格式化实现,默认有时间、和空值的处理
  5. model里定义的是实体模型
  6. parse是对yml配置文件的解析过程
  7. transfer为对应个别字段的特殊转换处理
  8. BillPluginsExecuteBuilder是对文件解析的入口,是Executor的构造者
    9 BillPluginsExecutor 是最终的执行类,所有核心逻辑的入口 从generate方法开始.

generate主要执行时序图:

image.png

其中主要流程分为两步:
第一步: 对文件名的解析;
第二步:针对每个文件,对file-templates文件模版的解析

原则就是,解析过程中如果有sql依赖,就先执行sql依赖(文件名目前执行1层sql依赖,内容支持两层,基本满足大多数场景).

对于sql的执行通过DefaultSqlCallerImpl进行封装,然后类似于jdbc的流式读取,在ResultRows结果集中处理分页逻辑


  /**
     * 遍历行,获取数据
     * 1。 对数据进行参数格式化,可用户自定义格式
     * 2。对于特殊参数进行转换处理,用户可自定义
     */
    public Map<String, String> next() throws SQLException {

        if (index >= rowMaps.size()) {
            if (isHasNext() && pageNoIndex != -1) {
                //存在下一页情况,先进行页码替换

                Object[] newParams = Arrays.copyOf(parsms, parsms.length);

                newParams[pageNoIndex] = Integer.valueOf(newParams[pageNoIndex].toString()) + pageSize;

                //更换下页码参数换成
                this.parsms=newParams;

                //当前页数据,索引清零
                index = 0;

                //下页查询
                ResultRows call = ((DefaultSqlCallerImpl) sqlCaller).call(newParams);
                this.rowMaps = call.getRowMaps();
                call.close(); //帮助gc
            }

            //此时只能返回null,说明没有值了
            if (index >= rowMaps.size()) {
                return null;
            }
        }

        return formartResult(rowMaps.get(index++));
    }

并通过formartResult方法进行参数的自定义格式化

  /**
     * 映射格式化
     */
    private Map<String, String> formartResult(Map<String, Object> resultMap) {

        Map<String, String> result = new HashMap<>();
        if (MapUtils.isNotEmpty(resultMap)) {
            resultMap.forEach((k, v) -> {

                //1。固定类型格式化
                String formatValue = FormaterRegistry.getFormater(typeMaps.get(k)).format(v);

                //2。对于特殊参数的转换处理
                TransfersConfig transferConfig = BillPluginsContext.getTransferConfig(k);
                if (transferConfig != null) {
                    switch (transferConfig.getTransferTypeEnums()) {
                        case MAP:
                            String transferValue = transferConfig.getTransferMap().get(formatValue.toUpperCase());
                            result.put(k, StringUtils.isEmpty(transferValue) ? formatValue : transferValue);
                            break;
                        case Class_TRANSFER:
                            result.put(k, transferConfig.getTransferType().transfer(formatValue,resultMap));
                            break;
                        default:
                            result.put(k, formatValue);
                            break;
                    }
                } else {
                    result.put(k, formatValue);
                }
            });

        }
        return result;
    }

总结

写的有点急,细节处理有很多没处理到位,但已基本实现了大多数生成账单的场景.

相关文章

网友评论

      本文标题:技术组件(三)-业务账单(自定文件模版)工具

      本文链接:https://www.haomeiwen.com/subject/hfjfuctx.html