对龙果支付系统的简单了解
龙果支付系统的代码下载地址码云/Roncoo,虽然官网上说的功能很炫酷,但实际上其实我觉的还是挺酷的,只是功能没有他们说的那么全。目前我只浏览了一下支付业务,系统中看到了支付宝和微信的扫码支付和刷卡支付、微信的小程序支付,测试了微信的扫码和刷卡支付。
我对这个系统的了解
现在有如下几个角色:
平台:龙果支付系统,
商户:使用龙果支付系统的用户,比如某公司的商城系统使用该系统,商户就是某公司
用户:使用商户系统的用户
当前龙果支付系统实现的功能:
- 商户使用平台,用户浏览商户商品购买,向商户的第三方账户(微信、支付宝)付款,
- 商户使用扫描设备获取用户付款码,调用平台支付,商户第三方账号向用户收款,
- 平台的流水记录与第三方(微信、支付宝)账单进行对账,账单对应不上的放入差错池
一些不全的功能:
- 结算,将商户在平台的账户余额提到商户的银行卡中,这里没有这项功能,只是将平台中账户的操作历史(加款、减款)的金额汇总,得出可结算的余额。
- 微信H5支付,没有这段代码。
- 微信小程序支付,平台中有小程序支付代码,没有调用案例,从微信开发文档中看,好像是需要小程序的appid,我没有测试。
- 可以对账,虽然 spring 配置有定时任务,但是不能定时启动对账,因为程序入口运行一次就结束了。
- 结算,只能账户金额汇总,同样不能定时结算,需要自己改。
项目分析与部署
可以先参考这两个教程:
分析
根据第一个教程中可以了解到系统所使用的技术,我只看了龙果支付系统的支付业务,我就说一下我在支付业务中使用的技术:
- maven + eclipse,要了解 maven 的聚合、继承、依赖、插件,虽然我的 maven 很渣,一般应用没有问题
- spring + mybatis,系统中 mybatis 的用法跟我学的不太一样,但是差不多能理解
- activemq,消息中间件,没有学过,可以花两三个小时入门,我做了这个 ActiveMQ 笔记
- ngrok,内网穿透,将本地 web 应用发布到外网上,可以自己搭建外网穿透,但是需要云服务器,我用腾讯云的学生优惠
- mysql、tomcat、微信和支付宝接入开发文档
这个项目使用的 jdk7,虽然 maven 项目,我之前用 maven 的 tomcat 插件运行不起来,这里用的 eclipse 配置的本地 tomcat 容器运行,后来主要研究支付业务就没看 怎么用 tomcat 插件运行支付系统。
了解几个概念:
- 长款短款:实际收到的钱比应该收到的钱多是长款,反之短款。
- 微信里的扫码支付就是支付宝的即时到账,都是用户拿着手机扫二维码付款;微信里的刷卡支付就是支付宝的条码支付,都是商家用扫码条形码的机器扫描用户手机上的付款码
项目结构功能
|-- roncoo-pay //龙果支付系统,父工程,管理jar包依赖的版本 | |-- roncoo-pay-common-core //整个项目用到的工具类枚举类等公共类资源和依赖的公共jar包 | |-- roncoo-pay-service //支付系统的核心业务工程,依赖common-core工程 | |-- roncoo-pay-web-gateway //给商户提供可以请求的支付接口,依赖service和core工程 | |-- roncoo-pay-web-boss //支付系统管理员用的管理后台,依赖service和core工程 | |-- roncoo-pay-web-merchant //商家用的管理后台,依赖service和core工程 | |-- roncoo-pay-app-reconciliation //对账应用工程,依赖service和core工程 | |-- roncoo-pay-app-settlement //结算应用工程,依赖service和core工程 | |-- roncoo-pay-app-notify //将交易结果通知商户系统的工程,依赖service和core工程 | |-- roncoo-pay-app-order-polling //第三方交易结果查询,更新本地数据库,依赖service和core工程 |-- roncoo-pay-web-sample-shop //模拟商户请求gateway支付接口的示例
支付平台接入简介
接入步骤:
- 需要 roncoo-pay-web-boss 启动,用户名密码:admin/123456,管理员给支付平台,添加支付产品,这里添加一个编码为 ALLPAY,描述:所有支付,给ALLPAY添加支付方式,我这里添加了微信的扫码支付、刷卡支付、小程序支付,支付宝的即时到账、条码支付。
- 创建一个商户,boss 会给商户创建一系列账号什么的,就是资金账户,然后给这个商户设置支付配置,选择刚才的 ALLPAY,这样商户就可以使用微信和支付宝的那几种支付,还要设置商户的收款渠道,商户收款款就是:商户使用平台微信支付,平台向微信请求,请求时需要微信账户的 appid 什么的微信配置,商户收款这时候就是获取的商户自己的微信配置,这样用户付款直接打入商户;平台收款就是使用平台配置的微信配置,用户付款打入平台,平台中商户的账号余额增加,但是不能提现到商户的银行卡,因为系统没有实现。。。
- 修改相关配置,启动 activemq,roncoo-pay-app-order-polling、roncoo-pay-app-notify、roncoo-pay-web-gateway、roncoo-pay-web-sample-shop,这些都可以在本地运行,但是 roncoo-pay-web-gateway 需要内网穿透发布到外网,又或者都发布到外网服务器。
- 正常情况下点击打开 shop,点击微信扫码支付或微信刷卡支付是可以的,后面详细介绍。
项目部署
- git 下载源码,导入 eclipse,和视频教程是一样的,这里用到 maven 知识,上面提的知识不懂的,就当作浏览小学作文就行,不要认真看。
- 本地仓库安装支付宝的 jar 包,这里是我的推测,因为他那个教程有段时间了,现在支付宝官方接入文档中有支付 jar 包的 maven 依赖:所以我觉得应该只要把那个添加到 pom 文件中就行,所以很多地方需要看第三方支付的接口文档进行了解,我这里只测试微信,就不做那个处理了。
- 有红叉 maven update project,如果 pom 文件这样的错:“Artifact has not been packaged yet” 就可以不用管他了,可以看这里
- 项目没有问题,本地测试就不用 maven 安装了,eclipse 配置一下本地的 tomcat,将 boss 应用放入 tomcat 运行,一般页面能浏览 boss,那种以独立 jar 方式运行的,就是打包成jar,然后用 java -jar 运行,当然也可以在 eclipse 中找到 main 入口,直接在 eclipse 中 run 运行。下面时支付宝接入文档中提到的 maven:
支付流程代码分析
这里以微信的扫码支付为例进行分析:
准备工作
- 首先在 boss 系统中设置好支付产品,我这里用之前那个 ALLPAY,里面包含了微信的扫码支付和刷卡支付方式,产品上线
- 给商户选择支付产品 ALLPAY,选择收款渠道,如果是商家收款,需要商户添加自己的微信配置,在微信支付开发平台获取的appid、商户号、密钥等;平台收款修改 service 配置文件中 weixinpay_config.properties,如果支付产品有支付宝的支付方式需要配置支付宝,这里只测微信,这些配置在哪获取?为什么用这些配置,就需要你了解微信支付接入开发文档
修改相关配置。
- weixinpay_config.properties:①notify_url,微信交易结束后,微信服务器通知交易结果,这个就是微信请求的 url,默认的是请求 gateway 工程中 ScanPayNotifyController中的 notify 方法,所以需要将 gateway 工程放到外网上,并修改 notify_url,可以让微信访问到。这里用内网穿透使微信可以访问gateway。②order_query_url,向平台查询交易订单状态的地址,修改服务器地址就行,我这里是本地 tomcat,就修改那个 localhost:8080 就可以。③bill_type,微信账单下载的类型,这里 SUCCESS 就行,下载成功交易的微信账单,用来对账。
- reconciliation_config.properties:dir,微信交易账单下载保存在本地的位置。
- mq_config.properties:ActiveMQ 的 url,用户名、密码,我用本地的 activemq,默认没有用户名密码,可以空着
- pay_config.properties:shop 工程,测试接口的案例,需要修改刚才申请的商户账号的 payKey、paySecret,这些可以在 boss 管理后台获取,或者商户登陆商户后台 merchant,查看自己的信息。修改扫码支付、条码支付请求地址:只需要在8080后面添加上 gateway 工程名就行,如:scanPayUrl=http://localhost:8080/roncoo-pay-web-gateway/scanPay/initPay,后台通知结果 url 就不用改了,因为没有写相应的控制类,前台页面跳转通知要改:returnUrl=http://localhost:8080/roncoo-pay-web-sample-shop,就是支付成功后跳转的页面。
启动
前面准备工作都做好了,这里开始在本地运行测试
- 启动 ActiveMQ
- 启动 roncoo-pay-app-notify 会加载数据库所有没有通知完商户的数据,然后放入线程池继续通知,通知完线程池就开始等待,监听 tradeQueueName.notify=tradeNotify 的通知,该通知由 gateway 工程发出,用来通知商户后台系统交易结果。
- 启动 roncoo-pay-app-order-polling,会启动线程池,监听 orderQueryQueueName.query=orderQuery 的通知,该通知由 gateway 工程发出,用来向第三方查询交易结果,这里就是向微信查询交易结果,更改平台数据库交易结果。
启动 roncoo-pay-web-gateway,支付网关,给商户提供支付调用的接口,有三个关键控制类:
- ScanPayController:微信扫码支付和支付宝即时到账的控制类,发送支付请求,该类请求第三方获取二维码,返回二维码页面给客户端
- F2FPayController:微信刷卡支付和支付宝条码支付的控制类,商户扫描设备扫描用户付款码,发起收款请求,该类向第三方发起收款请求,返回支付结果。
- ScanPayNotifyController:后台通知类,第三方交易结果如微信交易结果发送通知,请求的就是该类,该类收到请求,向商户发送后台通知,更新平台交易结果,响应微信服务器,微信服务器停止通知。
- 启动 ngrok 内网穿透,将 gateway 发布到公网。
- 启动 shop 商城测试工程,测试 gateway 支付接口的案例。
微信扫码支付测试分析
--> 打开 shop 页面
点击左边第一个微信支付,shop 工程会发送一个表单请求:
上面代码的意思就是将参数签名,然后拼接出一个 form 表单,和表单提交 js 代码,就是 buildRequest 字符串,然后返回 toPay.jsp ,这个页面会加载这段代码,然后向 gateway 提交支付请求,这些不是重点,重点在于支付请求发送的参数和 url 地址,这里请求要用 utf-8 ,不然乱码,验证签名会失败。自己花点时间整理请求参数,重点是后面的分析。
--> gateway 工程的请求处理
扫码请求处理类 ScanPayController 的 initPay 方法响应请求,处理请求参数,获取用户支付配置,根据配置判断是否校验请求 ip,然后校验签名,根据商户参数中是否有 payWayCode(支付渠道:微信、支付宝)判断,
- 如果没有,返回给消费者页面,然消费者选择,这个就是 shop 中的网关支付,这时候 先生成交易订单,消费者选择后,生成交易流水记录,向第三方发起预支付请求,返回预付款二维码
- 如果有,调用 RpTradePaymentManagerServiceImpl 交易订单管理服务类的 initDirectScanPay 方法,这个类是交易处理的核心类,我们发起的微信扫码支付,这里肯定就是 payWayCode=WEIXIN,然后根据值选择返回相应的二维码页面。
--> rpTradePaymentManagerService.initDirectScanPay 方法内部处理
- 获取用户支付配置
RpUserPayConfig rpUserPayConfig = rpUserPayConfigService.getByPayKey(payKey); if (rpUserPayConfig == null) { throw new UserBizException(UserBizException.USER_PAY_CONFIG_ERRPR, "用户支付配置有误"); }
- 根据用户支付配置中选择的支付产品和参数中的 payWayCode 支付渠道,获取支付方式,这里是微信扫码方式
if (PayWayEnum.WEIXIN.name().equals(payWayCode)) { //条件查询支付方式记录 payWay = rpPayWayService.getByPayWayTypeCode(rpUserPayConfig.getProductCode(), payWayCode, PayTypeEnum.SCANPAY.name()); payType = PayTypeEnum.SCANPAY;//扫码支付 } else if (PayWayEnum.ALIPAY.name().equals(payWayCode)) { payWay = rpPayWayService.getByPayWayTypeCode(rpUserPayConfig.getProductCode(), payWayCode, PayTypeEnum.DIRECT_PAY.name()); payType = PayTypeEnum.DIRECT_PAY; } if (payWay == null) { throw new UserBizException(UserBizException.USER_PAY_CONFIG_ERRPR, "用户支付配置有误"); }
- 根据商户信息和订单号获取交易订单记录,如果没有就创建交易记录插入到数据库,如果有且未支付就更新订单金额:
String merchantNo = rpUserPayConfig.getUserNo();// 商户编号 RpUserInfo rpUserInfo = rpUserInfoService.getDataByMerchentNo(merchantNo); if (rpUserInfo == null) { throw new UserBizException(UserBizException.USER_IS_NULL, "用户不存在"); } RpTradePaymentOrder rpTradePaymentOrder = rpTradePaymentOrderDao.selectByMerchantNoAndMerchantOrderNo(merchantNo, orderNo); if (rpTradePaymentOrder == null) { rpTradePaymentOrder = sealRpTradePaymentOrder(merchantNo, rpUserInfo.getUserName(), productName, orderNo, orderDate, orderTime, orderPrice, payWayCode, PayWayEnum.getEnum(payWayCode).getDesc(), payType, rpUserPayConfig.getFundIntoType(), orderIp, orderPeriod, returnUrl, notifyUrl, remark, field1, field2, field3, field4, field5); rpTradePaymentOrderDao.insert(rpTradePaymentOrder); } else { if (TradeStatusEnum.SUCCESS.name().equals(rpTradePaymentOrder.getStatus())) { throw new TradeBizException(TradeBizException.TRADE_ORDER_ERROR, "订单已支付成功,无需重复支付"); } if (rpTradePaymentOrder.getOrderAmount().compareTo(orderPrice) != 0) { rpTradePaymentOrder.setOrderAmount(orderPrice);// 如果金额不一致,修改金额为最新的金额 } }
执行 getScanPayResultVo 方法,该方法主要生成交易流水记录,根据支付渠道发起预支付请求,获取二维码,发送订单通知,返回预支付请求结果。过程如下:
- 获取支付渠道,更新交易订单的支付方式,主要是网关支付的时候没有确定支付方式,这里可以更新支付方式:
ScanPayResultVo scanPayResultVo = new ScanPayResultVo(); String payWayCode = payWay.getPayWayCode();// 支付方式 PayTypeEnum payType = null; if (PayWayEnum.WEIXIN.name().equals(payWay.getPayWayCode())) { payType = PayTypeEnum.SCANPAY; } else if (PayWayEnum.ALIPAY.name().equals(payWay.getPayWayCode())) { payType = PayTypeEnum.DIRECT_PAY; } //这边更新交易订单支付方式的原因就是网关支付的时候是先生成订单,在提供用户选择支付方式,这时候要更新支付方式 rpTradePaymentOrder.setPayTypeCode(payType.name()); rpTradePaymentOrder.setPayTypeName(payType.getDesc()); rpTradePaymentOrder.setPayWayCode(payWay.getPayWayCode()); rpTradePaymentOrder.setPayWayName(payWay.getPayWayName()); rpTradePaymentOrderDao.update(rpTradePaymentOrder);
- 生成交易流水记录:
RpTradePaymentRecord rpTradePaymentRecord = sealRpTradePaymentRecord(rpTradePaymentOrder.getMerchantNo(), rpTradePaymentOrder.getMerchantName(), rpTradePaymentOrder.getProductName(), rpTradePaymentOrder.getMerchantOrderNo(), rpTradePaymentOrder.getOrderAmount(), payWay.getPayWayCode(), payWay.getPayWayName(), payType, rpTradePaymentOrder.getFundIntoType(), BigDecimal.valueOf(payWay.getPayRate()), rpTradePaymentOrder.getOrderIp(), rpTradePaymentOrder.getReturnUrl(), rpTradePaymentOrder.getNotifyUrl(), rpTradePaymentOrder.getRemark(), rpTradePaymentOrder.getField1(), rpTradePaymentOrder.getField2(), rpTradePaymentOrder.getField3(), rpTradePaymentOrder.getField4(), rpTradePaymentOrder.getField5()); rpTradePaymentRecordDao.insert(rpTradePaymentRecord);
- 根据 payWayCode 这里是 WEIXIN ,微信支付,根据资金流入方向,这里商家收款,获取商家自己在微信的配置信息,然后封装成微信预支付 xml 请求字符串,其中包含从 service 中获取的 notify_url,就是微信交易结果通知的 url,交易完成后,微信服务器会向这个 url 发送请求,后面再说,这里向微信支付发送 post 请求,微信支付服务器响应返回预支付信息,验证返回签名,正确就将预支付信息封装到返回实体类:
String appid = ""; String mch_id = ""; String partnerKey = ""; if (FundInfoTypeEnum.MERCHANT_RECEIVES.name().equals(rpTradePaymentOrder.getFundIntoType())) {// 商户收款 // 根据资金流向获取微信的配置信息 RpUserPayInfo rpUserPayInfo = rpUserPayInfoService.getByUserNo(rpTradePaymentOrder.getMerchantNo(), payWayCode); appid = rpUserPayInfo.getAppId(); mch_id = rpUserPayInfo.getMerchantId(); partnerKey = rpUserPayInfo.getPartnerKey(); } else if (FundInfoTypeEnum.PLAT_RECEIVES.name().equals(rpTradePaymentOrder.getFundIntoType())) {// 平台收款 //这里是平台收款,获取平台 service 工程中 weixinpay_config.properties 中的微信配置 appid = WeixinConfigUtil.readConfig("appId"); mch_id = WeixinConfigUtil.readConfig("mch_id"); partnerKey = WeixinConfigUtil.readConfig("partnerKey"); } WeiXinPrePay weiXinPrePay = sealWeixinPerPay(appid, mch_id, rpTradePaymentOrder.getProductName(), rpTradePaymentOrder.getRemark(), rpTradePaymentRecord.getBankOrderNo(), rpTradePaymentOrder.getOrderAmount(), rpTradePaymentOrder.getOrderTime(), rpTradePaymentOrder.getOrderPeriod(), WeiXinTradeTypeEnum.NATIVE, rpTradePaymentRecord.getBankOrderNo(), "", rpTradePaymentOrder.getOrderIp()); String prePayXml = WeiXinPayUtils.getPrePayXml(weiXinPrePay, partnerKey); LOG.info("扫码支付,微信请求报文:{}", prePayXml); // 调用微信支付的功能,获取微信支付code_url Map<String, Object> prePayRequest = WeiXinPayUtils.httpXmlRequest(WeixinConfigUtil.readConfig("prepay_url"), "POST", prePayXml); LOG.info("扫码支付,微信返回报文:{}", prePayRequest.toString()); if (WeixinTradeStateEnum.SUCCESS.name().equals(prePayRequest.get("return_code")) && WeixinTradeStateEnum.SUCCESS.name().equals(prePayRequest.get("result_code"))) { String weiXinPrePaySign = WeiXinPayUtils.geWeiXintPrePaySign(appid, mch_id, weiXinPrePay.getDeviceInfo(), WeiXinTradeTypeEnum.NATIVE.name(), prePayRequest, partnerKey); String codeUrl = String.valueOf(prePayRequest.get("code_url")); LOG.info("预支付生成成功,{}", codeUrl); if (prePayRequest.get("sign").equals(weiXinPrePaySign)) {//验证签名 rpTradePaymentRecord.setBankReturnMsg(prePayRequest.toString()); rpTradePaymentRecordDao.update(rpTradePaymentRecord); scanPayResultVo.setCodeUrl(codeUrl);// 用于生成二维码 scanPayResultVo.setPayWayCode(PayWayEnum.WEIXIN.name()); scanPayResultVo.setProductName(rpTradePaymentOrder.getProductName()); scanPayResultVo.setOrderAmount(rpTradePaymentOrder.getOrderAmount()); } else { throw new TradeBizException(TradeBizException.TRADE_WEIXIN_ERROR, "微信返回结果签名异常"); } } else { throw new TradeBizException(TradeBizException.TRADE_WEIXIN_ERROR, "请求微信异常"); }
- 发送通知,交易生成,由 roncoo-pay-app-order-polling 监听,polling 收到通知,放入线程池,执行,先查询交易流水状态,如果是 WAITING_PAY,流水记录创建时的状态,等待支付,就会向微信服务器发起交易查询,如果交易成功,更新本地交易记录和流水记录的状态为成功,如果微信服务器查询失败,没有超过最大通知次数,则放入线程池继续通知,就是继续向微信服务器查询交易状态,具体细节忘了,大概是这样,下面代码是通知 order-polling
rpNotifyService.orderSend(rpTradePaymentRecord.getBankOrderNo());
- 返回微信扫码页面,生成二维码,同时页面不断向支付系统发送订单结果查询,一旦交易订单状态为成功,页面就会跳转
--> 微信结果通知
微信服务器通知支付系统,gateway 工程的 ScanPayNotifyController 控制类收到通知并响应,处理方法是 notify,作用是,解析请求,验证签名更新系统内订单状态,发送商家通知,notify 收到通知,向商家通知,通知频率为 60/120/300/900,商家需要回复 success 字符串,来终止通知。
- 解析返回通知结果,将通知参数放入 map 中
//解析返回通知结果 Map<String , String> notifyMap = new HashMap<String , String >(); if (PayWayEnum.WEIXIN.name().equals(payWayCode)){ InputStream inputStream = httpServletRequest.getInputStream();// 从request中取得输入流 notifyMap = WeiXinPayUtils.parseXml(inputStream); }else if (PayWayEnum.ALIPAY.name().equals(payWayCode)){ Map<String, String[]> requestParams = httpServletRequest.getParameterMap(); notifyMap = AliPayUtil.parseNotifyMsg(requestParams); }
- completeScanPay 用来验证通知签名更新数据库,发送商家通知,加款,获取响应第三方的字符串,支付宝是返回 success 或 fail
String completeWeiXinScanPay = rpTradePaymentManagerService.completeScanPay(payWayCode ,notifyMap);
- 响应第三方服务器
if (!StringUtil.isEmpty(completeWeiXinScanPay)){ if (PayWayEnum.WEIXIN.name().equals(payWayCode)){ httpServletResponse.setContentType("text/xml"); } httpServletResponse.getWriter().print(completeWeiXinScanPay); }
completeScanPay 里面的内容:
- 根据渠道验证签名
签名正确,根据交易结果选择执行成功或失败的方法,这两个方法内部都是
- 更新数据库交易状态
- 拼接通知参数,给 activemq 发送通知 tradeNotify。
- 如果是交易成功而且是平台收款,那就对商家账户进行加款
- 根据渠道拼凑第三方服务器响应数据。
--> roncoo-pay-app-notify,收到 tradeNotify 通知,向商家发起通知,代码不分析了。
对账分析
roncoo-pay-app-reconciliation 是对账工程,对账就是,拿微信来说,将微信服务器上下载的微信支付交易的账单数据与平台系统的交易订单进行比对,看看有没有交易错误的信息,系统会将错误信息放入差错池,就是一个差错数据表,当前系统是不支持定时自动进行对账,但是他在 spring 配置中设置了自动计时任务。
--> 执行对账任务
正常情况下,spring 执行定时任务,运行 ReconciliationTask 类的 main 方法,但是计时任务不能启动,所以想体验的就直接运行 main 方法可以,这里定时任务 10 点 15 分触发的原因是,微信会在早上 9 点,对昨天的交易进行整理,产生账单,微信推荐的是在 10 点以后在下载账单,这里运行 main 方法。
--> 遍历对账接口,完成对账
- 获取当前对账的接口信息和对账日期,这里写了微信和支付宝两个接口信息,所以只能实现微信和支付的对账,这里以微信对账为例
// 判断接口是否正确 ReconciliationInterface reconciliationInter = (ReconciliationInterface) reconciliationInterList.get(num); if (reconciliationInter == null) { LOG.info("对账接口信息" + reconciliationInter + "为空"); continue; } // 获取需要对账的对账单时间(当前时间减去微信配置中的对账周期1,即前天的日期,原因为微信在每日9点生成前天的账单,建议商户在10点后下载前天账单) Date billDate = DateUtil.addDay(new Date(), -reconciliationInter.getBillDay()); // 获取对账渠道 String interfaceCode = reconciliationInter.getInterfaceCode();
- 查询对账记录,根据当前的对账日期和对账接口渠道查询状态不为错误和失败的对账记录,不存在则没有对过帐,创建对账记录:
/** step1:判断是否对过账 **/ RpAccountCheckBatch batch = new RpAccountCheckBatch(); Boolean checked = validateBiz.isChecked(interfaceCode, billDate); if (checked) { LOG.info("账单日[" + sdf.format(billDate) + "],支付方式[" + interfaceCode + "],已经对过账,不能再次发起自动对账。"); continue; } // 创建对账记录 batch.setCreater("reconciliationSystem"); batch.setCreateTime(new Date()); batch.setBillDate(billDate); batch.setBatchNo(buildNoService.buildReconciliationNo()); batch.setBankType(interfaceCode);
- 根据对账接口渠道执行相应文件下载类的账单下载方法,下载对应的账单,比如执行 WeiXinFileDown 的 fileDown 方法,下载
/** step2:对账文件下载 **/ File file = null; try { LOG.info("ReconciliationFileDownBiz,对账文件下载开始");//在微信配置文件中规定下载微信支付成功的账单 file = fileDownBiz.downReconciliationFile(interfaceCode, billDate); if (file == null) { continue; } LOG.info("对账文件下载结束"); } catch (Exception e) { LOG.error("对账文件下载异常:", e); batch.setStatus(BatchStatusEnum.FAIL.name()); batch.setRemark("对账文件下载异常"); batchService.saveData(batch); continue; }
- 根据不同的对账接口的渠道,解析不同的对账文件,包装成账单列表 list:
/** step3:解析对账文件 **/ List<ReconciliationEntityVo> bankList = null; try { LOG.info("=ReconciliationFileParserBiz=>对账文件解析开始>>>"); // 解析文件,微信在这里解析的是成功支付的账单 bankList = parserBiz.parser(batch, file, billDate, interfaceCode); // 如果下载文件异常,退出 if (BatchStatusEnum.ERROR.name().equals(batch.getStatus())) { continue; } LOG.info("对账文件解析结束"); } catch (Exception e) { LOG.error("对账文件解析异常:", e); batch.setStatus(BatchStatusEnum.FAIL.name()); batch.setRemark("对账文件解析异常"); batchService.saveData(batch); continue; }
- 执行 check 对账,check 是对账的核心代码
/** step4:对账流程 **/ try { checkBiz.check(bankList, interfaceCode, batch); } catch (Exception e) { LOG.error("对账异常:", e); batch.setStatus(BatchStatusEnum.FAIL.name()); batch.setRemark("对账异常"); batchService.saveData(batch); continue; }
check 内部代码:
- 先检查账单列表是否为空,空的话就结束对账,不为空查询平台系统中当前对账日期的当前渠道的所有交易记录和所有交易成功的记录,初始化差错列表:
// 判断bankList是否为空 if (bankList == null) { bankList = new ArrayList<ReconciliationEntityVo>(); } // 查询平台bill_date,interfaceCode成功的交易 List<RpTradePaymentRecord> platSucessDateList = reconciliationDataGetBiz.getSuccessPlatformDateByBillDate(batch.getBillDate(), interfaceCode); // 查询平台bill_date,interfaceCode所有的交易 List<RpTradePaymentRecord> platAllDateList = reconciliationDataGetBiz.getAllPlatformDateByBillDate(batch.getBillDate(), interfaceCode); // 查询平台缓冲池中所有的数据 List<RpAccountCheckMistakeScratchPool> platScreatchRecordList = rpAccountCheckMistakeScratchPoolService.listScratchPoolRecord(null); // 差错list /** 第三方成功交易账单中在平台交易记录中不存在,平台漏单,将交易记录放入差错池 **/ List<RpAccountCheckMistake> mistakeList = new ArrayList<RpAccountCheckMistake>(); // 需要放入缓冲池中平台长款list /** 平台成功交易记录中没有匹配到第三方交易成功的记录,需要放入差错缓冲池 **/ List<RpAccountCheckMistakeScratchPool> insertScreatchRecordList = new ArrayList<RpAccountCheckMistakeScratchPool>(); // 需要从缓冲池中移除的数据 /** 在数据库差错缓存池中,如果第三方成功支付的账单匹配到缓冲池的账单,需要移除缓冲池,这里好像应该还有更新订单状态操作,但是代码没有写 **/ List<RpAccountCheckMistakeScratchPool> removeScreatchRecordList = new ArrayList<RpAccountCheckMistakeScratchPool>();
- 以平台交易成功的记录为准遍历匹配微信账单列表,匹配不到的记录放入差错缓存池,匹配到的判断数据是否正确不正确的,将错误信息放入差错池,更新对账记录。
- 再以微信账单为准,遍历平台的所有交易流水记录,匹配平台系统订单状态不为成功的记录,就是状态错误,将错误信息放入差错池,没有匹配到的再匹配差错缓存池列表,匹配到了校验信息,将错误信息放入差错池,将缓存池匹配到的记录放入移除列表,仍没有匹配到,将错误放入差错池(漏单)。
--> 清理对账缓存池中三天前的记录,并将三天前的记录放入到差错池
结算分析
结算工程是 roncoo-pay-app-settlement,和对账工程一样,是不能定时运行的,可以主动运行 main 测试,后台调用结账功能和主动运行 main 一样的,结果都是实现了商户账号的可以提现的余额汇总,银行卡提现的功能是没有实现的。
总结
这个系统内容还是挺丰富,建议花点时间研究研究,还有一个支付系统 XxPay 聚合支付,也挺有意思的,XxPay Spring boot版本。