技術組件(三)-業務賬單(自定文件模版)工具

需求

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

總結

寫的有點急,細節處理有很多沒處理到位,但已基本實現了大多數生成賬單的場景.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。