需求
- 靈活配置賬單,10行代碼批量生成商戶賬單.
場景概述
一個代理商,下面有n個收單商戶,要生成下面每個收單商戶的每天的交易流水賬單文件
實現能力
- 能通過模版文件配置修改賬單內容
- 修改賬單內容和結構只需修改配置文件sql
- 數據讀取通過分頁實現
- 對不同數據源的支持
- 支持多庫數據組合生成賬單的場景
- 支持自定義特殊字段的轉換
- 支持文件的后置處理,可自定義存放位置
源碼地址:
https://gitee.com/kaiyang_taichi/bill-Plugins.git
使用方法:
- 導入pom,因為未deploy到公有倉庫,需要使用,可以自行下載源碼編譯
<dependency>
<groupId>cn.bese.bill.template.plugins</groupId>
<artifactId>bill-plugins</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 編寫配置文件:
例:
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
參數:
-
模版key配置方法:
sql*: 模版主要內容,就是我們平時的sql語句,你可以根據所用數據庫語言自己規范sql方言.多個sql可以組合使用,key為sql+(自定義數碼,只用來區分sql沒有特殊先后順序)例子中:
sql1--> 查詢出指定類型的所有商戶,本例中為了查出所有代理商
sql3(先跳過sql2,因為sql2以sql3的結果作為了查詢條件)-->遍歷sql1的每個代理商,分頁查出每個代理商對應的所有子商戶
sql2-->在每個文件中,分頁查詢sql3中每個子商戶的交易數據,匯總生成文件內容
sql1、sql2、sql3 其實就是我們平時寫賬單的三個步驟的sql語句,此處通過模版key的方式靈活替換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開始.transfers:定義的轉換器,可以對一些特殊字段進行后置處理.默認有兩種轉換器:
1. map型:map:SUCCESS|成功
,定義你SUCCESS到成功的映射,自動替換,場景如數據庫枚舉值,文件中轉換為中文.
2. class型:class:com.example.plugns.demoweb.config.bill.AgentNameHandler
自定義轉換類,只要出現你指定的字段,就會根據你定義的轉換類進行替換.此類要繼承TransferValueHandler接口-
file-templates: 文件模版,最終的csv文件模版定義.用yml文件的
-
表示換行,注意點,最終的文件內容暫時只能通過一個sql主體出數據,否則系統無法組合分頁.如本例中,最終數據從sql2中產出,本行模版不能有其他sql替換符,但可以有其他系統內置參數.
image.png null-file-templates: 空文件模版配置,指獲取的主sql數據為空時,文件展示的內容,不配的話只展示表頭,否則根據你配置寫文件.如例子中,當sql2數據為空時,文件內容為:
no data today!
6.file-content-format-class ,整行內容處理類,使用較少.作用是你可以對每一行數據都可以做整體的特殊處理,不過場景不多.
- save-after-class :文件后置處理類,如果你需要對最后的文件做相應的處理,如發送郵件,或保存到其他服務器的,可以通過此配置實現,繼承SaveAfterProcessConfig接口:
public class SaveBillConfig implements SaveAfterProcessConfig {
@Override
public boolean afterProcess(File file, String fileName, Object[] fileParams) {
System.out.println("文件存儲后置處理");
return true;
}
}
- 系統內置參數說明:
${init.*}
:以init開頭的參數為,executer啟動時傳入的初始化參數,單個 executer上下文全局唯一,不會更改.可用于一些固定的外部參數,如時間范圍、業務類型等等.${sys.*}
: 為系統內定參數模式,不需要外不指定,有自己的實現邏輯,可直接使用,其中:
${sys.pageIndex}
: 分頁頁碼參數,在sql中使用,系統會自動從0開始自增
${sys.pageSize}
: 分頁每頁數據條數默認配置,默認200,也可自定義
${sys.yyyy}
: 系統年份獲取參數,取系統年份,格式如:2019
${sys.MM}
:系統年份獲取月份,取系統年份,格式如:09
${sys.dd}
: 系統天:格式:23
處理代碼在cn.base.bill.template.plugins.config.SysParamConfig中,有需要可自行調整:${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開始).${sql*.*}
: 重點的sql參數,在文件模版key中,已經說過sqln就是對應指定的sql,如${sql2.MERCHANT_NO}
就是對應的sql2中的MERCHNAT_NO字段.
- 代碼啟動:
配置文件配好后,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";
}
}
生產的賬單例子,生成這個代理商下每個子商戶的數據:
源碼簡介
此處先簡單介紹下代碼結構,有需要以后再細說.
看下源碼機構圖:
- config是對應上面說的
系統內置參數
的處理邏輯 - context 為組件上下文定義,里邊有全局的一些緩存
- dao為數據庫交互層,封裝了sql的執行過程、分頁實現都在這里
- format為對應參數格式化實現,默認有時間、和空值的處理
- model里定義的是實體模型
- parse是對yml配置文件的解析過程
- transfer為對應個別字段的特殊轉換處理
- BillPluginsExecuteBuilder是對文件解析的入口,是Executor的構造者
9 BillPluginsExecutor 是最終的執行類,所有核心邏輯的入口 從generate方法開始.
generate主要執行時序圖:
其中主要流程分為兩步:
第一步: 對文件名的解析;
第二步:針對每個文件,對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;
}
總結
寫的有點急,細節處理有很多沒處理到位,但已基本實現了大多數生成賬單的場景.