对接农业银行支付(微信和支付宝)的总结(四)

一、写在前面的话

首先感谢很多简友给我来消息,询问关于对接的源码;其次是有简友提醒我,有人在百度文库盗贴了我的文档;最后我想奉劝大家,如果有退款需求,建议不要对接农行,后面我会详细说明原因。

之前的文章也写了很多关于农行接口的出入参,其实他们本身也是有在线文档的,比我的文章里写的示例要清晰一些。(比杭州银行的离线Pdf或Doc要先进一步了,可惜它的其他方面体验差远了)

二、主要内容

  • 对账接口
  • 退款的资金来源

三、对账接口

需要特别指出,对账接口有很多个,如果你对接的是微信/支付宝方式的话,需要使用“微信支付宝交易对账单下载”。注意不要使用接口“农行交易对账单下载”。

农行的对账单接口.png

我相信,怎么使用http接口是很容易的,这里就不讲,需要指明的是日期字段的值,容易传错。
错在哪呢,其实它文档里也有写,谁让我们没看仔细呢。。。

可用来下载微信支付宝支付指定日期的对账单。
T 日对账单在 T+1 日 17:00 之后生成,因此请求对账单下载不应早于次日 17:00。

    1. 对账单日期与订单清算日期一致,微信支付宝三方订单清算日期在次日中午,所以如要获取支付完成日期为 2020/01/01,清算完成日期为 2020/01/02 的交易的对账单,请在 SettleDate 字段中上送 2020/01/02。
    1. 如果某个商户某天交易因为账户等原因清算失败,其对应交易对账单也将依次向后顺延,直到某天清算成功为止。
经我验证过,时间不用等到17点,上午就可以获取到了,具体几点开始有了账单的,就不想去细究了。

好了,关于对账接口,最后贴下他的格式,以及解析示例。

商户号|交易类型|订单编号|交易时间|交易金额|商户账号|商户动账金额|客户账号|账户类型|商户回佣手续费|商户分期手续费|会计日期|主机流水号|9014流水号|原订单号^^
103881909993435|weixinpay|B13220406092802072109|20220406092803|0.01|19015601949001842|0.01|otDNot8jM13dT_rhF9_OOVpRiO1M|OTHERS|0|0.00|20220407|242190361|46ECEP01092733452084|B13220406092802072109^^
103881909993435|weixinpay|132204061003230B71896|20220406100323|0.01|19015601949001842|0.01|otDNot8jM13dT_rhF9_OOVpRiO1M|OTHERS|0|0.00|20220407|242190361|46ECEP01100210191273|132204061003230B71896^^
103881909993435|weixinpay|132204061017200722B71|20220406101720|0.01|19015601949001842|0.01|otDNot8jM13dT_rhF9_OOVpRiO1M|OTHERS|0|0.00|20220407|242190361|46ECEP01101514287394|132204061017200722B71^^
103881909993435|WeiXinRefund|R112204061758490B72381|20220406175849|0.01|19015601040025213|0.01|||0|0.00|20220407|242190361|46ECEP01175501714434|1122040B6175627072375^^
103881909993435|weixinpay|B13220406094924072229|20220406094924|0.01|19015601949001842|0.01|otDNot8jM13dT_rhF9_OOVpRiO1M|OTHERS|0|0.00|20220407|242190361|46ECEP01094430716845|B13220406094924072229^^
103881909993435|weixinpay|1122040B6175627072375|20220406175627|0.01|19015601949001842|0.01|otDNotxTIe0B7VT1vGxbIbXqIQu0|OTHERS|0|0.00|20220407|242190361|46ECEP01175058463786|1122040B6175627072375

最后,贴出解析农行账单的伪代码,关于对账,有空我再写一系列的文章。

  • 大致的意思就是逐行解析,转换为支付明细集合和退款明细集合,如果对账单没有给汇总(金额、笔数、手续费),则需要自己根据明细计算出汇总信息。
  • 解析的手段有两种,一是split分割,二是正则表达式匹配。
  • 概括之,这里就是对各个第三方支付的账单,统一转换为我们支付平台的对账单格式。所以这里的类BillParserAbcBank是实现了接口IBillParser,parser()方法的第一个入参就是对账单内容。
  • 以此类推,如果你要实现杭州银行、微信、支付、工商银行等的对账单,你只要仿照此类,也实现接口IBillParser接口就好。
  • 另外,需要注意Bean的命名,各个第三方支付都实现了同一个接口,供BeanFactory工厂类查找对应的实现类。
import cn.hutool.core.date.DateUtil;
import com.xxx.service.pay.constant.PayConstants;
import com.xxx.service.pay.domain.model.BillInfo;
import com.xxx.service.pay.domain.service.dto.BankTradeDTO;
import com.xxx.service.pay.utils.AmountUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

@Slf4j
@Component("billParser" + PayConstants.PayChannelType.AbcBank)
public class BillParserAbcBank implements IBillParser {
    private static final String BILL_OF_PAY_TYPE = "pay";

    private static final String BILL_OF_REFUND_TYPE = "refund";

    private static final Pattern PATTERN = Pattern.compile("(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)\\|(.*?)$");

    @Override
    public <T> void parser(T object, BillInfo billInfo, List<BankTradeDTO> payTradeDTOList, List<BankTradeDTO> refundTradeDTOList) throws Exception {
        String billContent = (String) object;

        String[] array = billContent.split("\\^\\^");

        if (array.length > 1) {
            this.parseDetailLine(array, payTradeDTOList, refundTradeDTOList, billInfo);
        }
    }

    /**
     * 解析明细数据.
     * <p>
     * 先删除明细数据的标题, 再循环遍历数据值
     * </p>
     *
     * @param array
     * @param payTradeDTOList
     * @param refundTradeDTOList
     */
    private void parseDetailLine(String[] array, List<BankTradeDTO> payTradeDTOList, List<BankTradeDTO> refundTradeDTOList, BillInfo billInfo) {
        int totalPayAmount = 0;
        int totalPayCount = 0;

        int totalRefundAmount = 0;
        int totalRefundCount = 0;

        int totalPayFee = 0;
        int totalRefundFee = 0;

        // 跳过第一行,下标从1开始
        for (int i = 1; i < array.length; i++) {
            // 正则匹配校验
            Matcher totalMatcher = PATTERN.matcher(array[i]);
            if (!totalMatcher.find()) {
                return;
            }

            String[] detailArray = array[i].split("\\|");
            // 交易类型
            String tradeType = detailArray[1].toLowerCase();
            // 对银行而言, 商户订单号
            String outTradeNo = detailArray[2];
            // 交易时间
            String tradeTime = detailArray[3];
            // 交易金额
            int amount = AmountUtils.changeY2F(detailArray[4]);
            // 手续费
            int fee = AmountUtils.changeY2F(detailArray[10]);
            // 平台流水号
            String tradeNo = detailArray[13];

            //支付订单
            if (tradeType.endsWith(BILL_OF_PAY_TYPE)) {
                BankTradeDTO payTradeDTO = BankTradeDTO.builder()
                        .tradeTime(DateUtil.parse(tradeTime))
                        .outTradeNo(outTradeNo)
                        .tradeNo(tradeNo)

                        .amount(amount)
                        .fee(fee)
                        .build();

                totalPayAmount += amount;
                totalPayFee += fee;
                totalPayCount++;
                payTradeDTOList.add(payTradeDTO);
            }

            if (tradeType.endsWith(BILL_OF_REFUND_TYPE)) {
                BankTradeDTO refundTradeDTO = BankTradeDTO.builder()
                        .tradeTime(DateUtil.parse(tradeTime))
                        .outTradeNo(outTradeNo)
                        .tradeNo(tradeNo)

                        .amount(Math.abs(amount))
                        .fee(fee)
                        .build();

                totalRefundAmount += amount;
                totalRefundFee += fee;
                totalRefundCount++;
                refundTradeDTOList.add(refundTradeDTO);
            }
        }

        billInfo.setBankPayTrade(totalPayAmount, totalPayCount, totalPayFee);

        billInfo.setBankRefundTrade(totalRefundAmount, totalRefundCount, totalRefundFee);
    }

}
  • 工厂类,使用了spring自身BeanFactory的getBean(),根据名字获取相应的解析器,这里也就是对应接口的IBillParser实现类们。
import com.xxx.service.pay.domain.model.BillInfo;
import com.xxx.service.pay.domain.service.dto.BankTradeDTO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * 账单解析工厂类
 */
@Slf4j
@Service
public class BillParserFactory implements BeanFactoryAware {

    private BeanFactory beanFactory;

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        this.beanFactory = beanFactory;
    }

    public Object getService(String payInterface) {
        return beanFactory.getBean(payInterface);
    }


    public <T> void parser(T object, BillInfo billInfo, List<BankTradeDTO> payTradeDTOList, List<BankTradeDTO> refundTradeDTOList) throws Exception {

        String parserClassName = "billParser" + billInfo.getChannelType();

        IBillParser service;
        try {
            // 根据名字获取相应的解析器
            service = (IBillParser) this.getService(parserClassName);
        } catch (NoSuchBeanDefinitionException e) {
            log.error("根据解析器的名字, 没有找到相应的解析器, [parserClassName={}, mchId={}, channelType={}, billDate={}]",
                    parserClassName, billInfo.getMchId(), billInfo.getChannelType(), billInfo.getBillYmd());
            return;
        }
        // 使用相应的解析器解析文件
        service.parser(object, billInfo, payTradeDTOList, refundTradeDTOList);
    }
}
import com.xxx.service.pay.domain.model.BillInfo;
import com.xxx.service.pay.domain.service.dto.BankTradeDTO;

import java.util.List;

/**
 * 解析对账单为统一格式
 *
 */
public interface IBillParser {
    /**
     * 解析对账单
     *
     * @param object
     * @param billInfo
     * @param payTradeDTOList
     * @param refundTradeDTOList
     * @param <T>
     * @throws Exception
     */
    <T> void parser(T object, BillInfo billInfo, List<BankTradeDTO> payTradeDTOList, List<BankTradeDTO> refundTradeDTOList) throws Exception;
}

四、退款接口

农行也是对接了微信的api接口,所以我们要理解农行的退款机制,一定要先理解微信官方的api。这里还得吐槽下,它的对账机制没有杭州银行做得灵活。站在我们的角度,我们对接的是银行方,他们回复你的答案却是说“你对接的是微信”。真的是只做桥接啊!!

1、微信的退款接口

我们使用的老版接口,后面微信支付是有升级的,不过我们升级的意愿不大。

默认是使用未结算资金,
白话讲,就是当天收到了多少钱,这笔钱就是未结算的钱,这些钱才能用来抵扣退款给用户的钱。
  • 用户退款能否到账,取决于未结算的钱有多少,假使你可用余额有足够的钱也枉然。可能你会想,那我让财务充值吧?对不起,也不行!因为充值的钱到在可用余额里,并不能影响未结算资金。
  • 那农行就给你,刷一笔订单啊,让未结算金额有足够的钱了,就能够退款成功。我们的客户都是这么做的!!-- 这是什么脑回路,你首先不确定用户什么时候退款,退多少钱,第二个问题是让谁去刷单,这个订单的履约怎么做后续。还有更严重的问题,资金怎么对账呢?向公司的财务借钱去下单并支付,但是并无发货。
  • 另外一个问题是,你刷单,当天不退的话,意味着你后面想要退款,又得接着刷单。循环往复,不知道会多少人工成本在这里,且不去说退款的时效性非常差。
  • 最后说一句,农行的退款就是根本不考虑对接方的实际使用情况。退款还去依赖当天的收费,强势!!

2、农行的退款接口

没有退款资金来源的字段!!也就是说我们无法指定退款资金的来源,任由农行的退款机制。它使用的是微信默认方式,不让对接方选择。你说气人不气人~~

    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @Data
    public static class DicRequest {
        /**
         * 退款-Refund
         */
        private String trxType;

        /**
         * 订单日期: YYYY/MM/DD
         */
        private String orderDate;
        /**
         * 订单时间: HH:MM:SS
         */
        private String orderTime;

        /**
         * 平台支付流水号
         */
        private String orderNo;

        /**
         * 退款交易编号
         */
        private String newOrderNo;

        /**
         * 币种:156-人民币
         */
        private Integer currencyCode;

        /**
         * 退款金额,单位:元
         */
        private String trxAmount;

    }

五、劝退

本文对农行的退款出现的一个大坑做了具体分析,希望能帮助到要对接农行的朋友。奉劝需要对接退款的朋友,本文可以算是劝退篇了!!

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,386评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,142评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,704评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,702评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,716评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,573评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,314评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,230评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,680评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,873评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,991评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,706评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,329评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,910评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,038评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,158评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,941评论 2 355

推荐阅读更多精彩内容