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

需求

  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;
    }

总结

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343