需求
- 靈活配置賬單,10行代碼批量生成商戶賬單.
場景概述
一個(gè)代理商,下面有n個(gè)收單商戶,要生成下面每個(gè)收單商戶的每天的交易流水賬單文件
實(shí)現(xiàn)能力
- 能通過模版文件配置修改賬單內(nèi)容
- 修改賬單內(nèi)容和結(jié)構(gòu)只需修改配置文件sql
- 數(shù)據(jù)讀取通過分頁實(shí)現(xiàn)
- 對不同數(shù)據(jù)源的支持
- 支持多庫數(shù)據(jù)組合生成賬單的場景
- 支持自定義特殊字段的轉(zhuǎn)換
- 支持文件的后置處理,可自定義存放位置
源碼地址:
https://gitee.com/kaiyang_taichi/bill-Plugins.git
使用方法:
- 導(dǎo)入pom,因?yàn)槲磀eploy到公有倉庫,需要使用,可以自行下載源碼編譯
<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:
- 標(biāo)題:商戶交易數(shù)據(jù)
- 商戶名稱:${file.3}
- 商戶編號|商戶名稱|終端編號|SN號|產(chǎn)品類型|交易號|交易日期|交易時(shí)間|交易對方銀行卡號|交易金額|交易狀態(tài)|卡類型|手續(xù)費(fèi)|小額雙免|代理商編號|代理商名稱|S0出款狀態(tài)
- ${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
參數(shù):
-
模版key配置方法:
sql*: 模版主要內(nèi)容,就是我們平時(shí)的sql語句,你可以根據(jù)所用數(shù)據(jù)庫語言自己規(guī)范sql方言.多個(gè)sql可以組合使用,key為sql+(自定義數(shù)碼,只用來區(qū)分sql沒有特殊先后順序)例子中:
sql1--> 查詢出指定類型的所有商戶,本例中為了查出所有代理商
sql3(先跳過sql2,因?yàn)閟ql2以sql3的結(jié)果作為了查詢條件)-->遍歷sql1的每個(gè)代理商,分頁查出每個(gè)代理商對應(yīng)的所有子商戶
sql2-->在每個(gè)文件中,分頁查詢sql3中每個(gè)子商戶的交易數(shù)據(jù),匯總生成文件內(nèi)容
sql1、sql2吁讨、sql3 其實(shí)就是我們平時(shí)寫賬單的三個(gè)步驟的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
其中所有${*}
定義的參數(shù),都可以在模版中通過${file.*}
獲取到,這里的index 從0開始.transfers:定義的轉(zhuǎn)換器,可以對一些特殊字段進(jìn)行后置處理.默認(rèn)有兩種轉(zhuǎn)換器:
1. map型:map:SUCCESS|成功
,定義你SUCCESS到成功的映射,自動替換,場景如數(shù)據(jù)庫枚舉值,文件中轉(zhuǎn)換為中文.
2. class型:class:com.example.plugns.demoweb.config.bill.AgentNameHandler
自定義轉(zhuǎn)換類,只要出現(xiàn)你指定的字段,就會根據(jù)你定義的轉(zhuǎn)換類進(jìn)行替換.此類要繼承TransferValueHandler接口-
file-templates: 文件模版,最終的csv文件模版定義.用yml文件的
-
表示換行,注意點(diǎn),最終的文件內(nèi)容暫時(shí)只能通過一個(gè)sql主體出數(shù)據(jù),否則系統(tǒng)無法組合分頁.如本例中,最終數(shù)據(jù)從sql2中產(chǎn)出,本行模版不能有其他sql替換符,但可以有其他系統(tǒng)內(nèi)置參數(shù).
null-file-templates: 空文件模版配置,指獲取的主sql數(shù)據(jù)為空時(shí),文件展示的內(nèi)容,不配的話只展示表頭,否則根據(jù)你配置寫文件.如例子中,當(dāng)sql2數(shù)據(jù)為空時(shí),文件內(nèi)容為:
no data today!
6.file-content-format-class ,整行內(nèi)容處理類,使用較少.作用是你可以對每一行數(shù)據(jù)都可以做整體的特殊處理,不過場景不多.
- save-after-class :文件后置處理類,如果你需要對最后的文件做相應(yīng)的處理,如發(fā)送郵件,或保存到其他服務(wù)器的,可以通過此配置實(shí)現(xiàn),繼承SaveAfterProcessConfig接口:
public class SaveBillConfig implements SaveAfterProcessConfig {
@Override
public boolean afterProcess(File file, String fileName, Object[] fileParams) {
System.out.println("文件存儲后置處理");
return true;
}
}
- 系統(tǒng)內(nèi)置參數(shù)說明:
${init.*}
:以init開頭的參數(shù)為,executer啟動時(shí)傳入的初始化參數(shù),單個(gè) executer上下文全局唯一,不會更改.可用于一些固定的外部參數(shù),如時(shí)間范圍认臊、業(yè)務(wù)類型等等.${sys.*}
: 為系統(tǒng)內(nèi)定參數(shù)模式,不需要外不指定,有自己的實(shí)現(xiàn)邏輯,可直接使用,其中:
${sys.pageIndex}
: 分頁頁碼參數(shù),在sql中使用,系統(tǒng)會自動從0開始自增
${sys.pageSize}
: 分頁每頁數(shù)據(jù)條數(shù)默認(rèn)配置,默認(rèn)200,也可自定義
${sys.yyyy}
: 系統(tǒng)年份獲取參數(shù),取系統(tǒng)年份,格式如:2019
${sys.MM}
:系統(tǒng)年份獲取月份,取系統(tǒng)年份,格式如:09
${sys.dd}
: 系統(tǒng)天:格式:23
處理代碼在cn.base.bill.template.plugins.config.SysParamConfig中,有需要可自行調(diào)整:${file.*}
:獲取最終文件名中的指定參數(shù),在單個(gè)文件不變的參數(shù)上下文傳遞時(shí)可以使用(但缺陷是文件目錄會多出此參數(shù),后續(xù)有機(jī)會可以優(yōu)化,加入文件級別的上下文).例如:
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參數(shù)只取file-name配置中的${}
中的參數(shù),所以此例匯總${file.2}
就是對應(yīng)的${MERCHANT_NO}
獲取當(dāng)前文件中的月份字段值(小標(biāo)從0開始).${sql*.*}
: 重點(diǎn)的sql參數(shù),在文件模版key中,已經(jīng)說過sqln就是對應(yīng)指定的sql,如${sql2.MERCHANT_NO}
就是對應(yīng)的sql2中的MERCHNAT_NO字段.
- 代碼啟動:
配置文件配好后,10來行代碼就可以生產(chǎn)你需要的賬單了.
public class DemoController implements InitializingBean {
/**
* 配置的一個(gè)數(shù)據(jù)源
*/
@Resource(name = "posDataSource")
DataSource posDataSource;
/**
* 配置的第二個(gè)數(shù)據(jù)源
*/
@Resource(name = "huskyDataSource")
DataSource huskyDataSource;
/**
* 對應(yīng)的執(zhí)行器構(gòu)造者檬输,通過afterPropertiesSet方法初始化
*/
BillPluginsExecuteBuilder orderBillPluginsExecuteBuilder;
@Override
public void afterPropertiesSet() {
//初始化構(gòu)造者存皂,
//1宾符。setBillConfigFilePath 指定配置文件路徑
//2踢星。setDataSource指定數(shù)據(jù)源配置统刮,參數(shù)(DataSource dataSource, String... keys),指定哪些sql的key對應(yīng)哪個(gè)數(shù)據(jù)源
// 本例子中配置了兩個(gè)數(shù)據(jù)源污抬,sql1汞贸、sql3對應(yīng)huskyDataSource,sql2對應(yīng)posDataSource數(shù)據(jù)源
//3印机。最后調(diào)用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為配置執(zhí)行器上下文的初始化參數(shù)矢腻,可通過${init.n}獲得
Object[] params = new Object[]{"MIDDLE_AGENT", "10040041322"};
//最后執(zhí)行g(shù)enerate生產(chǎn)所有文件
orderBillPluginsExecuteBuilder.build(params).generate();
return "ok";
}
}
生產(chǎn)的賬單例子,生成這個(gè)代理商下每個(gè)子商戶的數(shù)據(jù):
源碼簡介
此處先簡單介紹下代碼結(jié)構(gòu),有需要以后再細(xì)說.
看下源碼機(jī)構(gòu)圖:
- config是對應(yīng)上面說的
系統(tǒng)內(nèi)置參數(shù)
的處理邏輯 - context 為組件上下文定義,里邊有全局的一些緩存
- dao為數(shù)據(jù)庫交互層,封裝了sql的執(zhí)行過程、分頁實(shí)現(xiàn)都在這里
- format為對應(yīng)參數(shù)格式化實(shí)現(xiàn),默認(rèn)有時(shí)間射赛、和空值的處理
- model里定義的是實(shí)體模型
- parse是對yml配置文件的解析過程
- transfer為對應(yīng)個(gè)別字段的特殊轉(zhuǎn)換處理
- BillPluginsExecuteBuilder是對文件解析的入口,是Executor的構(gòu)造者
9 BillPluginsExecutor 是最終的執(zhí)行類,所有核心邏輯的入口 從generate方法開始.
generate主要執(zhí)行時(shí)序圖:
其中主要流程分為兩步:
第一步: 對文件名的解析;
第二步:針對每個(gè)文件,對file-templates文件模版的解析
原則就是,解析過程中如果有sql依賴,就先執(zhí)行sql依賴(文件名目前執(zhí)行1層sql依賴,內(nèi)容支持兩層,基本滿足大多數(shù)場景).
對于sql的執(zhí)行通過DefaultSqlCallerImpl進(jìn)行封裝,然后類似于jdbc的流式讀取,在ResultRows結(jié)果集中處理分頁邏輯
/**
* 遍歷行多柑,獲取數(shù)據(jù)
* 1。 對數(shù)據(jù)進(jìn)行參數(shù)格式化咒劲,可用戶自定義格式
* 2顷蟆。對于特殊參數(shù)進(jìn)行轉(zhuǎn)換處理,用戶可自定義
*/
public Map<String, String> next() throws SQLException {
if (index >= rowMaps.size()) {
if (isHasNext() && pageNoIndex != -1) {
//存在下一頁情況,先進(jìn)行頁碼替換
Object[] newParams = Arrays.copyOf(parsms, parsms.length);
newParams[pageNoIndex] = Integer.valueOf(newParams[pageNoIndex].toString()) + pageSize;
//更換下頁碼參數(shù)換成
this.parsms=newParams;
//當(dāng)前頁數(shù)據(jù)腐魂,索引清零
index = 0;
//下頁查詢
ResultRows call = ((DefaultSqlCallerImpl) sqlCaller).call(newParams);
this.rowMaps = call.getRowMaps();
call.close(); //幫助gc
}
//此時(shí)只能返回null帐偎,說明沒有值了
if (index >= rowMaps.size()) {
return null;
}
}
return formartResult(rowMaps.get(index++));
}
并通過formartResult方法進(jìn)行參數(shù)的自定義格式化
/**
* 映射格式化
*/
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蛔屹。對于特殊參數(shù)的轉(zhuǎn)換處理
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;
}
總結(jié)
寫的有點(diǎn)急,細(xì)節(jié)處理有很多沒處理到位,但已基本實(shí)現(xiàn)了大多數(shù)生成賬單的場景.