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


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

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

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

    代码关键片段:
    拉取文件

    	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());
    		}
    
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    	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;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

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

    	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);
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    数据准备好后对账片段

    	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);
     	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    最后贴一下GIT地址代码对比
    老的对账方式 仅给一个支付宝对账的示例
    拉取对账文件 ARTASK下的ArIntfcAliPayMain.java
    对账 ARTASK下的ArAliPayAccountExecMain.java

    新的对账方式
    拉取对账文件 ARTASK下的 ArCommonAccountFromVosMain.java
    对账 ARTASK下的 ArExcelCommonImportMain.java

  • 相关阅读:
    Centos Linux 7系统基础配置
    【华为机试真题 JAVA】字符串加密-100
    88 合并两个有序数组
    nacos和eruka的区别
    【promptulate专栏】ChatGPT框架——两行代码构建一个强大的论文总结助手
    反射第一部分,获取Class对象,获取Constructor对象
    AIGC|数字时代巨变,创新潮流涌现,万亿市值风口已开!
    十三、企业开发(5)
    企业电脑监控软件
    excel卡住了还没保存怎么办?
  • 原文地址:https://blog.csdn.net/vipshop_fin_dev/article/details/138089567