农行95交易码接口对接全解析:从报文组装到生产上线
2026/9/15 5:11:11 网站建设 项目流程

前一阵子产品经理丢过来一张需求单,标题只有一行字:“95 农业银行接口对接”,备注栏写着“客户要求走农行接口,业务编号95,尽快拉通联调”。没有文档链接,没有字段说明,也没有测试账号。银行接口对接的需求,开局基本都长这样——业务侧只负责传递客户的一句话,剩下所有技术细节都要自己去渠道侧问、去文档中心翻、去联调环境踩,没人能替你走完这条路。

先说结论,这个“95”不是农行客服短号的某种缩写,而是农行接口报文里的交易码(TxnCode),代表“企业批量付款”这条业务链路。农行开放平台把不同业务能力拆成不同交易码,有的编号对应余额查询,有的编号对应单笔转账,而95这一组,负责的是批量交易从报文组装、签名上送、渠道转发、核心记账到结果通知的完整闭环。项目组后来都顺口叫它“95接口”,但你要清楚,它不是一个简单的Restful URL,而是一整套接口约定:请求地址、消息头、报文结构、签名算法、同步应答、异步通知,每一个环节都由交易码=95串起来。这篇文章就基于这次的实操经历,把从准备到联调、再从上线的关键节点都梳理一遍,给后面接农行接口的同学做个参考。

1. 接到这个需求时,我第一反应是:95到底是什么

一个需求单上只有一个编号,第一件要做的事不是写代码,而是把“95”这个编号的完整业务含义从农行文档里捞出来。这一步如果搞错了,后面所有配置都是空中楼阁。

1.1 需求单上的“95”:从文档里挖出来的交易码

农行的对公接口体系里,交易码是识别业务类型的第一把钥匙。有的交易码代表账户余额查询,有的代表交易明细下载,有的代表单笔支付,而我们对接的95,在我拿到的《企业网银开放接口说明书》里对应的是“批量付款及结果通知”。也就是说,客户那边一次性发一批付款指令,农行接收后进核心系统处理,处理完成后再通过异步通知把逐笔结果推回来。

这个编号并不算常见,很多做单笔支付的同行直到项目结束都没碰过它。但也正因为是批量场景,它的报文设计比普通查询类接口多了一大块:批量总笔数、总金额、明细清单、同批次的批次号、以及后续的批量结果通知。在字段层面,它和单笔接口完全不同;在调用方式上,它又保留了Restful接口的通用约定——POST一个JSON报文到固定URL,用证书做签名,服务端同步返回受理结果,真正的业务结果走异步回调。

所以在开始联调前,我先把交易码=95对应的文档页面完整读了三遍,把“请求报文”“响应报文”“通知报文”这三段字段全部摘出来,按照必填和非必填整理成字段映射表。这一步虽然枯燥,但能避免后面测试时反复因为少字段被拒。

1.2 农行对接的整体链路:从企业系统到核心系统

农行的接口链路,可以粗略分成四段:企业侧系统、农行的前置或开放平台网关、农行内部渠道系统、核心账务系统。企业侧系统就是我们自己的ERP、财务系统或者资金管理系统,负责组装报文、维护商户号/证书、接收回调;农行网关负责验签、鉴权、合法性检查,通过后转发到内部渠道;渠道系统根据交易码把请求路由到核心系统;核心系统完成记账后再沿原路把结果返回。

理解这条链路有一个直接好处:你能快速判断一个报错到底发生在哪一层。比如“验签失败”通常是企业侧证书或签名逻辑的问题;“渠道不存在”多半是渠道号配置错了;“交易币种与账户不一致”则是核心系统校验出来的账户问题。联调时最怕的就是不知道错误是哪一层返回的,然后瞎改一气。把链路画清楚(哪怕是画在纸上),对接效率会高非常多。

2. 先把对接四件套备齐:渠道号、密钥、证书、测试环境

银行接口不同于互联网开放平台,不是注册账号、拿个token就能调。农行侧那套准入流程,本质是双向身份认证加业务权限控制。你在生产环境能调哪些交易码,完全取决于农行给你开的渠道参数,而这套参数从申请到下发的周期,往往比写代码还长。

2.1 渠道号、商户号、网点编号,先理清这三者的关系

有开发第一次对接,看到“商户号”就直接填了客户的企业信用代码,然后一直报“渠道不存在”。其实农行参数体系分三层:

  • 渠道号:标识接入方式,比如银企直连、开放平台、前置机网银,各自有独立的渠道标识;
  • 商户号:也叫单位编号或客户号,是客户在农行侧开立账户时生成的关联标识,报文里通常用它定位是哪个企业在发起交易;
  • 网点编号:开户行的行政管理编码,一般不进报文,但申请接口权限时会在工单里用到。

三者关系可以粗略类比成:渠道号是你进哪个门,商户号是你在门后挂哪个账户,网点编号是账户落在哪个柜台。报文里核心用的是渠道号加商户号的组合,其他字段按文档要求填,不要自作主张。尤其注意一个常见坑:有人把农行网银登录页上那串客户号和接口报文里的商户号混在一起用,格式都对不上,白折腾半天。

2.2 双向证书:农行验你、你验农行

农行接口走的是HTTPS加双向证书认证。所谓双向,就是企业侧要用农行签发的证书证明“我是我”,同时农行服务端的证书也要能被企业侧信任。我们这次用的是RSA算法,密钥长度2048位,证书格式是PKCS#12(即.pfx文件),里面同时包含公钥和私钥。农行侧还有一个CA根证书,用于校验服务端身份。

实操中容易忽略的是:私钥证书和签名证书可能不是同一个东西。有的渠道会发两个文件,一个.pfx用于HTTPS建连时做客户端证书,一个.cer或.txt形式的公钥字符串用于报文签名验签。我这次一开始就吃了亏,拿.pfx里的私钥去签报文体,虽然签名能生成,但农行服务端验不过,日志一直报“签名校验失败”。后来仔细看文档才发现,报文签名要用单独的RSA私钥,HTTPS客户端证书是另外一套。这两个如果不分开,联调第一关就卡死。

2.3 测试环境参数清单:建一个配置类而不是写死

农行联调环境通常和生产的接入地址不同,证书也不同。我在项目里用配置类统一管理这些参数,而不是把值散落在代码里,这样后面切生产时只改配置,不动代码。

以我们这次的情况为例,整理成表大概是这样的:

配置项联调环境值(示例)生产环境值(示例)说明
接口基础地址https://ibsbjstar.ccb.com.cn/xxxhttps://ibsbjstar.abchina.com.cn/xxx不同交易的路径前缀可能相同,靠交易码区分
渠道号联调渠道号正式渠道号渠道号错误会直接报“渠道不存在”
商户号测试商户号生产商户号对应客户实际开立的账户
客户端证书test_client.pfxprod_client.pfx由农行渠道侧下发
签名私钥test_private.keyprod_private.key与农行留存公钥配对
连接超时3000ms5000ms交易提交接口不建议太长
读取超时10000ms15000ms避免线程长时间挂住

配置类写好后,再补一个环境枚举,切环境时只改一个参数。我习惯把联调和生产的配置分开成两个yaml/profile文件,防止联调时手滑把测试报文发到生产去。

3. 拆解95号接口的Restful协议:请求头、报文体与签名

地基建好了,接下来才是真正干活的阶段:把Restful接口请求组装出来。农行很多渠道已经把接口规格统一成了“HTTPS + POST + JSON”的形式,跟传统的XML报文或定长报文比起来友好不少,但签名、字段类型这些细节仍然决定成败。

3.1 请求地址与消息头的写法

95接口的调用地址在文档里是一个固定URL,比如以/api/gateway结尾。你只需要往这个URL POST一个JSON字符串,不需要把交易码拼在路径里,交易码在请求体里面。这样做的好处是网关入口统一,新交易上来基本不用运维改路由。

请求头有几个字段建议每次都显式设置:

  • Content-Type: application/json;charset=UTF-8
  • Accept: application/json
  • 如果有Http请求头扩展字段用来透传AppId或客户号,按文档要求加

这里最容易出问题的是字符集。有些农行网关默认按ISO-8859-1解析,如果你代码里没指定UTF-8,中文户名、用途字段到了服务端就变乱码。为了彻底规避,我通常在代码里把所有String类型都统一编码为UTF-8,并且在HTTP客户端层面设置Entity内容为UTF-8格式,而不是只改Content-Type头。

3.2 报文体的核心字段:交易码、商户号、批次号

95号接口的请求体分两部分:公共报文头和业务报文。公共报文头包含交易码、渠道号、商户号、请求流水号、请求时间、签名值等;业务报文部分则包含批次号、总笔数、总金额、付款账户、以及每条明细的收款账号、收款户名、金额、用途。

这里挑几个关键字段重点说明:

  • 交易码:固定填95,这个字段决定农行内部路由;
  • 请求流水号:企业侧生成的唯一流水,建议用“日期+随机数”的组合,农行侧会拿它做去重;
  • 批次号:批量交易的全局唯一批次标识,建议规则可读性强一点,比如yyyyMMddHHmmss+4位随机数;
  • 总金额:单位通常是分,必须用字符串类型传输。Java里如果用Long类型拼JSON,没问题;如果用了浮点数,精度就直接废了;
  • 明细列表:最多支持多少笔、单笔金额上限,以具体签约为准,批量接口通常都有笔数限制。

以JSON格式看,大致是这个结构:

{ "txnCode": "95", "channelNo": "渠道号", "merchantNo": "商户号", "reqSeqNo": "202506131030001234", "reqTime": "2025-06-13 10:30:00", "batchNo": "202506131030000001", "totalCount": 2, "totalAmount": "10000", "payAccountNo": "开户账号", "detailList": [ { "detailSeq": "1", "recvAccountNo": "收款账号1", "recvAccountName": "收款户名1", "amount": "6000", "purpose": "货款" }, { "detailSeq": "2", "recvAccountNo": "收款账号2", "recvAccountName": "收款户名2", "amount": "4000", "purpose": "服务费" } ] }

字段名前后顺序在文档中有严格定义,因为它直接关系到签名串的拼接。不要用JSON.stringify硬拼对象,建议严格按照文档列出的字段顺序构造LinkedHashMap,避免顺序不一致导致验签失败。

3.3 签名生成步骤:先拼字符串,再做SHA256withRSA

签名是整个对接环节里最容易出问题的部分,也是银行接口区别于普通开放平台的关键。农行Restful接口的签名逻辑通常可以概括为三步:

第一步,把请求报文里的核心字段按文档定义的顺序拼接成一个待签名字符串。这个顺序不一定是JSON里的顺序,有的渠道要求按“商户号+请求流水号+请求时间+批次号+总笔数+总金额”这样的顺序拼;有的渠道要求把整个业务报文JSON原样当作一个字段参与签名。没有统一规则,必须以文档为准。

第二步,用企业侧私钥对这个待签名字符串做SHA256withRSA签名。签名结果绝大多数情况是Base64编码的字符串,放入请求报文中的sign字段。

第三步,农行服务端用企业侧的公钥验签;同时农行用农行自己的私钥对响应做签名,企业侧用农行公钥去验。

我用Java手写过签名工具,核心逻辑大概长这样:

// 待签名字符串先按文档拼接 String content = merchantNo + reqSeqNo + reqTime + batchNo + totalCount + totalAmount; // 加载私钥 byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64); PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec); // 使用SHA256withRSA算法签名 Signature signature = Signature.getInstance("SHA256withRSA"); signature.initSign(privateKey); signature.update(content.getBytes(StandardCharsets.UTF_8)); byte[] signed = signature.sign(); // 输出Base64 String signValue = Base64.getEncoder().encodeToString(signed);

这一步的教训是:待签名字符串里绝对不允许带空格、换行等不可见字符。我排查过一个问题,两个系统用的同一套字段值,一边拼出来的字符串中间多了个\r\n,导致签名对不上。查了很久,最后用文本比对工具看了十六进制才发现。所以在对签名前,先把参与签名的原文在日志里原样打出来,和农行文档示例做对比,能省下大量排查时间。

3.4 同步响应与异步通知:两个都要解析

95接口和其他交易一样,调用后立即得到一个同步响应,但这个响应只在告诉你农行“收到你的请求了”,不代表交易成功。它的状态可能有两个常见值:受理成功、数据校验失败。拿到受理成功之后,真正的结果要靠异步通知获取。

异步通知是农行服务端主动往企业侧预留的回调URL发送POST请求,携带批次号、总笔数、成功笔数、失败笔数以及每笔明细的处理结果。企业侧必须对通知报文做两件事:验签、记录。如果验签不通过,宁可丢到异常表也不要落库,否则后续对账会对不上。

很多团队第一次做批量接口容易犯一个错:只重视请求端签名,忽略了回调验签。农行推送来的通知报文同样有签名,需要用农行公钥验。如果漏了这一步,理论上任何人都可以伪造一笔“成功”的批量结果,风险很大。这个环节必须像上送请求一样认真对待。

4. 联调阶段最容易翻车的五个细节

联调是银行接口对接中最耗精力的阶段。农行联调环境一般给到夜间或限时段开放,而且一次报错不一定立刻能定位到原因。下面几个问题几乎每个项目都会遇到,提前知道能少走很多弯路。

4.1 时间戳漂移:网关时钟校验比想象中严格

农行报文中的reqTime参与签名,同时网关服务端会校验这个时间和本机时间差。我们遇到过请求时间写的服务器本地时间,但两台机器时钟差了五六分钟,结果网关直接拒绝,返回“请求时间无效”。后来在代码里做了时钟同步方案:对接前用农行联调环境返回的时间戳校准本机时间偏移量,在生成reqTime时动态加上偏移值。

更简单一点的方案是直接对接NTP服务器做系统级时间同步。但要注意,云服务器内部时钟可能因为没有配置NTP或者被安全策略屏蔽而偏差很大,开发机尤其严重。在生成报文前先打印一行当前时间和UTC时间,确认无误再请求,比盯着看不懂的错误码瞎猜靠谱很多。

4.2 金额精度:传输用分、计算用Long、显示用元

这算不上新鲜事,但95接口金额字段更多,总共涉及总金额、单笔金额、可能还有手续费字段,任何一个地方用浮点数处理,都会埋雷。比如10000分乘以0.2作为手续费,浮点数算出1999.9999,传给农行就报金额异常。

我的习惯是全链路用Long表示“分”,只在展示层转成“元”。JSON序列化时,金额字段是数字类型,但如果是Java BigDecimal更好;如果文档里金额字段被定义成字符串,那就一律用字符串,不参与任何数字运算。关键是所有金额字段在一开始定义DTO时就统一成同一类型,不要请求体用Long、回调体里用BigDecimal,然后互相转换,很容易出现精度丢位。

4.3 NULL字段不能省略:空串和缺字段含义不同

互联网接口设计时,缺字段往往代表不传。农行接口在部分报文里有完全相反的语义:必填字段如果没值,要传空字符串,但不能不传;非必填字段如果不参与签名,可以不传。混用会导致签名串和网关解析后的字段集合不匹配。

我们联调时遇到一个报错,叫“报文格式错误:缺少[remark]字段”。文档里remark描述是“备注,可空”,按理解不传应该没问题。但农行网关对请求体的JSON模板要求字段必须完整,哪怕是空字符串也要占位。后来在DTO上加了@JsonInclude(Include.ALWAYS)注解,让空值也参与序列化,问题解决。每个接口这种隐性要求不一样,联调前最好先通读一遍JSON样例,把样例里的字段当最小集合来对待。

4.4 返回码要和状态字段一起看

95接口的通知报文里可能同时存在两套状态:一套是整体批次状态(如成功、部分成功、失败),另一套是每笔明细状态。只看批次状态就以为全部成功,会让对账逻辑出现漏洞。比如批次状态是“部分成功”,你必须再遍历detailList,找出那些失败明细,记录失败原因码。

同步响应里也有类似情况,外层可能返回“0000”表示受理成功,但业务字段里又有个resultCode表示数据校验是否通过。两个码一个都不能少看。我在日志工具里专门把外层响应码、内部业务码、错误信息三个字段拼成一行,这样过滤日志时一眼就能看出问题归属。

4.5 证书链更新:根证书过期比私钥失效更隐蔽

证书问题很烦,因为它不常发生,但一旦发生就让人摸不着头脑。我们联调到第二周,突然所有请求都报“证书链校验失败”,代码没改、私钥没换。排查了很久,发现是本机JDK的信任库没问题,但农行那边更新了服务端证书链,我们的请求客户端没有加载新的CA根证书,导致服务端无法建立可信链路。

后来我在代码里把农行提供的根证书文件单独加载到SSLContext中,不依赖JDK默认信任库,并在配置里留好根证书的路径。这样一来,农行更新证书链时,只需要替换文件、重启应用,不用重新部署整个服务。证书过期这种事,建议在监控系统里配置有效期告警,提前30天提醒,别等生产请求大面积失败再到处问。

5. 生产切流前要处理的事:白名单、幂等、重试和监控

联调环境跑通只是第一步,真正上线前还有一堆运营侧工作。银行接口的生产权限和网络策略都比较重,提前规划能避免“代码写完了却上不了线”的尴尬。

5.1 IP白名单与生产参数变更工单

农行生产环境通常会对企业侧出口IP做白名单限制。如果你用的是专线,那就是专线出口IP;如果走互联网,其实就是公网出口IP。最怕的是公司出口有多个,或者后续更换了网络出口,导致生产某天突然全挂。

建议在工单里一次性把正式环境的所有出口IP都提交完整,并且说明未来可能的变更窗口。另外,生产环境的证书、渠道号、商户号等配置,很多银行要求通过线下申请特定格式的变更单,即使已经拿到下线文档,也可能有个审批周期。不要等联调结束才去提,项目启动时就应该把申请流程跑起来,否则等待时间比写代码还长。

5.2 异步通知的幂等表:宁可重复,不能丢失

95接口的异步通知有一个特性:农行侧为了保证送达,会根据配置重发通知。企业侧如果处理逻辑不幂等,同一笔付款结果被处理两遍,可能造成业务重复入账或重复通知。我在库表里建了一张notify_record表,唯一键就用“批次号+明细序号+通知流水号”,收到通知先做insert,如果主键冲突说明已经处理过,直接忽略。

幂等表还有一个好处:方便排查。农行重发通知时,你可以通过查表看到第一次处理时间、处理结果、是否异常,不用靠日志猜。如果第一次处理时程序报错了,也可以额外设计一个状态字段,把失败的通知标记出来,人工或后台任务重新消费。

5.3 重试策略和超时阈值的取舍

请求农行接口有同步等待时间。连接超时设太短,网络抖动时容易误判失败;读超时设太长,应用线程又被占住,拖垮整体性能。我通常的经验是:

  • 连接超时:3000ms到5000ms
  • 读取超时:10000ms到15000ms
  • 业务重试次数:日间交易重试不超过3次,且必须有退避间隔
  • 重试前提:仅当请求没有成功拿到同步响应(比如超时、连接中断)时才重试;如果同步响应明确返回业务码错误,不重试

异步通知场景下,后台任务消费失败的消息,同样要做退避重试,比如第一次延迟30秒,第二次延迟2分钟,第三次延迟10分钟,超过三次进人工处理队列。

5.4 日志要留全,但绝不能出现完整卡号

银行接口涉及资金交易,日志记录水平直接影响排障效率。我的习惯是把请求流水号、批次号、业务码、错误信息、请求耗时、签名结果打全;但敏感字段如卡号、户名必须脱敏,只保留前四位后四位。这里并不是单纯为了合规,而是防止日志文件泄露后被恶意利用。

日志格式建议统一采用“traceId + 业务流水号 + 阶段描述 + 关键参数”的模式,把一次95交易从发起、上送、受理、回调的几个阶段全部串起来。排障时用traceId一查,整条链路状态全出来了,比一个个日志文件翻找快得多。

6. 对接完成之后,我复盘出的几点建议

整个95农业银行接口对接的项目收尾后,我复盘出了一些可以提前落地的方法,虽然不涉及具体代码,但作用不亚于修任何一个Bug。

第一,先写Mock服务,不要死等农行联调环境。银行联调环境往往要排队申请,与其干等,不如先把农行返回的JSON样例存成Mock文件,用WireMock或者普通的本地Controller模拟同步响应和异步通知,把整套流程先跑通。等到联调环境下来,重点只验证真实签名、真实证书、真实网关行为,效率会高很多。

第二,把字段映射表提前交给业务确认。95接口里很多字段并不是纯技术字段,比如用途、摘要、会计科目,这些字段的值会直接打到客户的银行回单上。如果技术组自己拍脑袋填了一个用途,客户拿到回单后肯定会不满意,后续又要改。提前让业务确认好每个字段的取值规则,做好字段映射表,能省掉上线后返工。

第三,留一个手动触发重发的管理入口。生产环境总有极少数通知因为网络或程序问题没有送达,或者送达了没处理成功。你在后台管理页面里做一个“按批次号重发查询/重新消费”的按钮,让运维人员可以手动触发,比每次临时改数据库字段要安全可靠得多。

这次对接下来,我觉得银行接口并没有想象中那么“神秘”,它更像是一套规则极度严谨的API:证书、签名、报文、回调,每一步都有固定套路。只要你把文档吃到足够细,把链路图搞明白,剩下的就是耐心联调。尤其像95这种批量交易接口,字段多、依赖异步通知,只要提前把幂等、日志、监控、重试这些生产必备项想清楚,上线后其实比很多互联网接口还稳定。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询