AR模块中通用对账的优化尝试

背景:

用户在唯品会下单后,是可以自由选择不同支付方式进行支付的,支付后,支付系统会将一笔收款单传送给AR,AR财务可以从此处看到收款情况。但是,真实的资金是按照不同支付方式,由银行或者其他渠道与资金系统交互的。在数据传输中,会有可能出现短款,即支付系统有收款单数据,但是资金系统未收到款。这就需要有一种对账的功能,将我们这边的收款情况和银行的流水做匹配。目前是银行将各自的流水文件每天传送给支付系统,再由支付将银行流水文件传到vos上。AR这边每天去拉取vos上的文件,进行解析后,与AR系统的收退款单数据做比较。最后将比较结果做成报表给到用户分析。

目前的问题点:

早期的时候,因为支付方式不对,对接的渠道少,同时每个银行给到的对账单文件和格式都不一样,由此每一种对账方式都是单独的一套代码,目前仅这种支付方式对账的任务就已经达到了23个,相关数据库表46个。而且后续每新增一个支付方式,按照之前的代码框架,要新增一个任务两个表和一堆配置信息和报表,需要至少3天的开发和测试工作量。这种设计显得非常不合理,为后续运维拓展更加方便,同时减轻数据库压力,需要优化成易拓展运维的通用形式。

前期的业务和代码分析:

通用点:所有的支付方式都需要从vos上拉取文件,解析数据后,存入到接口表中。然后根据支付流水号去抓取AR收款单数据后,将金额进行比较,最后输出结果表。

不同点:银行给的对账单文件格式都不一样:第一是对账单格式和名称不一样,有.csv,.txt,.xlsx,.zip,.xls,.gzip,.bin文件,每种文件的解码方式不一样,而且有的对账单文件前几列是无法解析的。这一部分无法做成完全的通用,但是可以将每种解码方式都写好后,再根据文件的格式去走不通的解码即可。然后文件前面多少列无需解析也可以通过配置文件指定。第二是字段名称含义和顺序不一样,但是银行给的对账文件上大多数字段用户并不关注,同时我们对账用到的字段仅有3个:收退款标识,支付流水号,金额。所以没必要将每种对账方式解析出的数据都落到不同表中,仅需要新增一个attribute字段足够多的通用接口表,用于将vos上拉到的文件字段全部落到通用接口表中即可。同时增加一个配置,指定每种对账方式的三个对账关键字段的位置即可。

代码关键片段:

拉取文件

java 复制代码
	public List<UnionPayObj> downLoadFile(List<ArCfgLookup> lookups){
		List<UnionPayObj> unionPayObjs = new ArrayList<>();

		for(ArCfgLookup arCfgLookup:lookups){
			if(arCfgLookup.getStartDate().equals(arCfgLookup.getEndDate())){
				continue;
			}
			ArVosSFTPSesionObj vosObj = getSftpSessionObj(arCfgLookup.getTagFlag());
			vosObj.setStartDate(FcsArDateUtil.convertDateToString2(arCfgLookup.getStartDate()));
			String fileName =vosObj.getStartDate().concat("-").concat(arCfgLookup.getAttrbiute1());
			vosObj.setFilename(fileName);
			creatLocalPath(vosObj);

			VosFileDownHandler vosFileDownHandler = VosFileDownHandlerFactory.INSTANCE.getVosHandler(vosObj.getVosBucket(),
					vosObj.getHost(), vosObj.getAccessKey(), vosObj.getSecretKey());
			try{
				if(!arCfgLookup.getDescription().contains("TEST")){
					vosFileDownHandler.dowload(vosObj.getFilename(), vosObj.getLocalPath());
				}
				if(arCfgLookup.getStartDate().before(arCfgLookup.getEndDate())){
					arCfgLookup.setStartDate(FcsArDateUtil.addDayByDate(arCfgLookup.getStartDate(), 1));
					//endDate要另起任务去根据当前时间轮训更新
				}
				arCfgLookupService.update(arCfgLookup);
				unionPayObjs.add(new UnionPayObj(fileName,arCfgLookup,vosObj));
			} catch (Exception e) {
				log.info("下载对账文件失败:"+vosObj.getFilename()+"地址:"+vosObj.getLocalPath());
			}
		}
		return unionPayObjs;
	}```


按照类型解析

```java
	public void parsingData(List<UnionPayObj> unionPayObjs){
		log.info("开始解析数据");
		for(UnionPayObj unionPayObj:unionPayObjs){
			List<List<String>> results = null;
			if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".txt")){
				results = parsingTxt(unionPayObj);
			}else if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".csv")){
				results = parsingCsv(unionPayObj);
			}else if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".bin")){
				results = parsingBin(unionPayObj);
			}else if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".xlsx")){
				results = parsingXlsx(unionPayObj);
			}else if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".xls")){
				results = parsingXls(unionPayObj);
			}else if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".zip")){
				results = parsingZip(unionPayObj);
			}else if(unionPayObj.getFileName().toLowerCase(Locale.ROOT).endsWith(".gzip")){
				results = parsingGzip(unionPayObj);
			}
			arIntCommonImportService.convToArIntCommon(results,unionPayObj.getArCfgLookup());
		}

	}
java 复制代码
	public List<List<String>> parsingTxt(UnionPayObj unionPayObj) {
		log.info("开始解析数据txt文件");
		List<List<String>> txtData = new ArrayList<>();
		int inputStreamCache = 5 * 1024 * 1024;
		try (FileInputStream fileInputStream = new FileInputStream(unionPayObj.getArVosSftpSesionObj().getLocalPath() + "/" + unionPayObj.getFileName());
			 InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream, StandardCharsets.UTF_8);
			 BufferedReader reader = new BufferedReader(inputStreamReader, inputStreamCache)) {
			String line;
			int index= 0;
			while ((line = reader.readLine()) != null) {
				index ++;
				if(index<= Integer.parseInt(unionPayObj.getArCfgLookup().getAttrbiute5())){
					continue;
				}
				String[] split = line.split("[|\\t]");
				txtData.add(new ArrayList<>(Arrays.asList(split)));
			}
		} catch (IOException e) {
			log.error(eventName + unionPayObj.getFileName()  + e.getMessage());
		}
		return txtData;
	}

数据落表之前根据配置将对账关键字段捞出放到前3

java 复制代码
	public void getMainParam(List<String> sts,ArCfgLookup lookup){
		String payTypeSign = sts.get(Integer.parseInt(lookup.getAttrbiute2()));
		String payNumber = sts.get(Integer.parseInt(lookup.getAttrbiute3()));
		String amount = sts.get(Integer.parseInt(lookup.getAttrbiute4()));
		//根据金额来判断收退款
		if(lookup.getAttrbiute2().equals(lookup.getAttrbiute4())){
			payTypeSign = new BigDecimal(amount).signum() > 0 ? "收款" : "退款";
		}
		//此处插入为文件带下来的对账流水号,并不一定是真实流水号,对账时再调用osp查询
		List<String> newList = new ArrayList<>(sts.size());
		newList.add(lookup.getLookupCode());
		newList.add(payTypeSign);
		newList.add(payNumber);
		newList.add(amount);
		sts.addAll(0, newList);
	}

数据准备好后对账片段

java 复制代码
	public void processRecData(ArIntCommonImport arIntCommonImport,List<ArMainReconciliationResult> arMainReconciliationResults){
		List<ArIntRecIn> recInts = arIntRecInService.listByPayNumber(arIntCommonImport.getAttribute2());
		BigDecimal recAmount = getRecAmount(arIntCommonImport.getAttribute2());
		if(CollectionUtils.isEmpty(recInts)){
			arIntCommonImport.setProcessFlag("E");
			arIntCommonImport.setErrorMessage("收款单缺失!");
			return;
		}
		if(!FcsArNumberUtil.compareToOnBigDecimal(recAmount,new BigDecimal(arIntCommonImport.getAttribute3()))){
			arIntCommonImport.setProcessFlag("E");
			arIntCommonImport.setErrorMessage("收款单金额与对账单金额不等!收款单金额"+recAmount.toString());
			return;
		}
		ArIntRecIn recIn = recInts.get(0);
		arIntCommonImport.setProcessFlag("S");
		arIntCommonImport.setErrorMessage("对账成功!");
		ArMainReconciliationResult arMainReconciliationResult = buildReconsResult(arIntCommonImport,"收款",recIn.getOrderNum(),recIn.getGlobalId());
		arMainReconciliationResults.add(arMainReconciliationResult);
 	}

优化后的提升:

后续新增对账方式无需新增数据库表和任务和报表。在不新增文件类型的情况下,无需修改代码,仅在配置表上增加配置用于识别新的对账方式即可。用户查看对账结果的报表也无需新增,报表上新增对账方式后,按照筛选对账方式进行查看即可。

最后贴一下GIT地址代码对比

老的对账方式 仅给一个支付宝对账的示例

拉取对账文件 ARTASK下的ArIntfcAliPayMain.java

对账 ARTASK下的ArAliPayAccountExecMain.java

新的对账方式

拉取对账文件 ARTASK下的 ArCommonAccountFromVosMain.java

对账 ARTASK下的 ArExcelCommonImportMain.java

相关推荐
小蜗牛慢慢爬行1 小时前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
wm10432 小时前
java web springboot
java·spring boot·后端
龙少95433 小时前
【深入理解@EnableCaching】
java·后端·spring
溟洵5 小时前
Linux下学【MySQL】表中插入和查询的进阶操作(配实操图和SQL语句通俗易懂)
linux·运维·数据库·后端·sql·mysql
有书Show6 小时前
文章发稿平台哪个好用?哪个类型的媒体平台比较好过稿?
经验分享·媒体
小奥超人6 小时前
PDF无法打印!怎么办?
windows·经验分享·pdf·办公技巧·pdf加密解密
SomeB1oody8 小时前
【Rust自学】6.1. 定义枚举
开发语言·后端·rust
SomeB1oody8 小时前
【Rust自学】5.3. struct的方法(Method)
开发语言·后端·rust
啦啦右一9 小时前
Spring Boot | (一)Spring开发环境构建
spring boot·后端·spring