简介:建设银行善付通支付结算接入文档是一份面向企业开发与财务人员的详细技术指南,适用于需要将善付通采购订单、销售订单、收付款及结算对账流程嵌入自有系统的场景。文档完整覆盖对接流程、数据传输规范、报文格式与数据加解密要求,并逐一说明SFT10001订单支付、SFT10008日终对账、SFT10009订单退款、SFT10013联机退款、SFT10020预付款支付等二十余个核心接口的请求参数与响应格式,基本涵盖从订单创建、支付、退款到对账的整个交易生命周期。资源包共1个docx文件,大小仅1.09MB,便于下载后直接查阅与内部传阅。目前已有2471人学习下载,可作为企业接入善付通或同类银行支付结算系统时的可靠参考。借助文档中的接口定义与协同流程说明,开发人员可快速完成联调测试,减少对接过程中的沟通与试错成本。
1. 善付通支付结算接入:不是一笔支付,而是一套供应链对账闭环
第一次拿到善付通的接入文档时,我以为它又是一份普通的支付网关对接说明,读完之后才发现完全不是那么回事。善付通是某大型商业银行面向供应链企业提供的支付结算专区,它解决的不是“用户怎么付款”这个单点问题,而是把订单、付款、退款、对账、上下游绑定串成一条完整的业务闭环。接入方要做的不只是调通一个下单接口,而是要在自己系统里同步维护一张“订单状态机”,处理异步通知、清算状态、分账参数这些比普通网银支付多出来的东西。这份文档适合正在做供应链平台、电商ERP或对账系统,并且需要对接善付通的企业开发人员。它的价值不在于某个函数怎么写,而在于把“支付结果”和“账务事实”对齐的思路。
2. 接入前的技术底座:报文四要素、流水号规则与三类接口形态
善付通的技术规范不复杂,但限制很死。我见过不少团队一上来就写下单接口,结果连签名都过不了,回头才发现是报文拼装顺序错了。接入前先把下面三件事吃透,后面联调会顺得多。
2.1 报文四要素与编码:四个参数名就是全部入口
所有接口统一走 HTTP POST,参数名固定为ThirdSysID、TxCode、Data、Auth四个,再加上响应时返回的errorCode和errorMsg。字符集强制 UTF-8,这一点文档写得很明确,但实际联调时仍然经常有人在取值后忘了指定编码,导致中文乱码。
| 参数名 | 含义 | 是否可空 | 是否加密 |
|---|---|---|---|
| ThirdSysID | 对接系统 ID,由善付通分配 | 否 | 否 |
| TxCode | 交易码,标识具体接口 | 否 | 否 |
| Data | 接口业务数据 | 否 | 是(DES 加密) |
| Auth | MD5 签名,防篡改 | 否 | 否 |
| errorCode / errorMsg | 错误信息,仅响应时返回 | 是 | 否 |
注意这四个参数名区分大小写,传输时连字段名带值一起原样拼。Data里放的是 DES 加密后的密文,Auth是明文的 MD5 校验值,后面第 3 章单独展开。之所以把报文格式放在接入第一步讲,是因为后面所有接口都复用这套结构,先把它跑通,等于所有接口的门都打开了。
2.2 流水号与幂等:一次一用,重复即拒绝
流水号规则是ThirdSysID + yyyyMMddHHmmss + 5位顺序号,总长度不超过 30 位,由发送方生成。关键限制是“一个流水号只能使用一次”,包括发送和返回的两次跳转在内,重复请求会被直接拒绝。
这个限制在实际业务里影响很大。网关类接口是页面跳转发起支付,用户支付完成后再跳回商户页面,这时候如果用户按了浏览器后退或者刷新,同一个TransID可能被再次提交,善付通会判定为重复请求。我一般会在本地把TransID和订单号的关系落库,重试时生成新的流水号,而不是复用旧的。联机接口虽然不涉及页面跳转,但重试逻辑同理,TransID生成后不能缓存复用。
2.3 网关接口与联机接口:处理模式完全不同的两类接口
文档把接口分成两类,这是接入时最容易搞混的地方。
联机接口用 httpclient 直接 POST 请求,善付通处理完原路返回结果,调用方可以同步拿到成功或失败。这类接口适合查询、绑定、对账,比如SFT10003订单信息查询、SFT10005绑定用户关系、SFT10008日终对账。
网关接口必须用 POST 表单以页面跳转形式发起请求,处理结果不原路返回,而是异步通知到第三方系统指定的地址。SFT10001订单支付就是典型的网关接口,提交订单后浏览器会跳转到善付通的支付页面,支付完成后由善付通主动 POST 通知结果。这意味着接入方必须单独实现一个接收通知的接口,否则订单状态永远停在“待支付”。
这两类接口的联调方式差别很大:联机接口可以在本地用 httpclient 模拟请求直接看响应,网关接口则需要真实页面跳转,联调环境必须能访问到善付通的测试网关。
3. DES 加密与 MD5 签名:密钥管理、Auth 生成与防篡改校验
善付通的数据保护分两层:业务数据用 DES 对称加密,整体报文用 MD5 做签名校验。这两层通常是接入中报错最多的地方,尤其是 Auth 拼接顺序,堪称新手翻车重灾区。
3.1 DES 加密为什么够用:Data 字段的加解密实现
平台之间的交互采用 DES 对称加密,加密对象是Data字段的业务数据。测试环境密钥固定为TestDesKey,生产环境换成正式密钥。虽然 DES 在今天看不算强加密,但在支付接口这种单次会话、短时效的场景里,配合 MD5 签名校验,已经能满足防窃听和防篡改的基本要求。
实现上我一般直接用 JDK 自带的 DES 算法封装,或者复用项目里已有的加密工具类。文档示例里的DESTool是封装好的工具,底层逻辑和下面这段等价:
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class DesUtil { public static String encrypt(String desKey, String data) throws Exception { // DES 密钥固定 8 字节 SecretKeySpec key = new SecretKeySpec(desKey.getBytes("UTF-8"), "DES"); // 一般按 ECB + PKCS5Padding 适配,具体以平台工具类为准 Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, key); byte[] raw = cipher.doFinal(data.getBytes("UTF-8")); return bytesToHex(raw); } public static String decrypt(String desKey, String hexData) throws Exception { SecretKeySpec key = new SecretKeySpec(desKey.getBytes("UTF-8"), "DES"); Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); cipher.init(Cipher.DECRYPT_MODE, key); byte[] raw = cipher.doFinal(hexToBytes(hexData)); return new String(raw, "UTF-8"); } private static String bytesToHex(byte[] bytes) { StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02X", b)); } return sb.toString(); } private static byte[] hexToBytes(String hex) { int len = hex.length(); byte[] out = new byte[len / 2]; for (int i = 0; i < len; i += 2) { out[i / 2] = (byte) Integer.parseInt(hex.substring(i, i + 2), 16); } return out; } }代码说明:SecretKeySpec要求密钥正好 8 字节,TestDesKey长度满足;DES/ECB/PKCS5Padding是常用的模式组合,如果善付通的工具类用了别的模式,联调时加密结果对不上,需要换成平台指定的模式。另外注意加密后的密文打印出来是大写十六进制,不要转成小写再参与后续步骤。
3.2 MD5 签名:拼接顺序与防篡改校验代码
Auth的生成规则是MD5(ThirdSysID + TxCode + Data + MD5Key),其中Data是 DES 加密后的密文,不是明文。MD5Key 测试环境是TestMD5Key。签名结果是 32 位小写十六进制字符串。
import java.security.MessageDigest; public class Md5Util { public static String sign(String raw, String charset) throws Exception { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(raw.getBytes(charset)); StringBuilder sb = new StringBuilder(); for (byte b : digest) { // %02x 生成小写十六进制 sb.append(String.format("%02x", b)); } return sb.toString(); } }调用方式就是按文档顺序拼接:
String thirdSysID = "1004"; String txCode = "SFT10001"; String data = "07B0FE28667D98092D5DC0730A56E784EF68EB3BAD2F5E8C"; // DES 密文 String md5Key = "TestMD5Key"; String auth = Md5Util.sign(thirdSysID + txCode + data + md5Key, "UTF-8"); // 结果为 32 位小写字符串逻辑说明:Data是密文,所以拼接前不能先解密,直接拿密文拼。MD5Key 属于接入方的“口令”,测试环境所有接入方共用同一套,生产环境每个系统不同,泄露出去了签名就形同虚设。
接收善付通通知时,校验逻辑一样:
String thirdSysID = request.getParameter("ThirdSysID"); String txCode = request.getParameter("TxCode"); String data = request.getParameter("Data"); String auth = request.getParameter("Auth"); // 注意两个地址都做校验,且参数名区分大小写 String expectAuth = Md5Util.sign(thirdSysID + txCode + data + "TestMD5Key", "UTF-8"); if (!expectAuth.equalsIgnoreCase(auth)) { // 被篡改或签名生成不一致,记录日志并拒绝处理 throw new RuntimeException("Auth verify failed"); }代码说明:校验时同样用密文Data拼串。我一般会在校验失败时把接收到的第三方参数原样打日志,方便排查是编码问题还是密钥问题。
3.3 密钥切换:测试环境到生产环境的替换点
测试环境只有一个固定密钥,生产环境按“对接善付通外联支付数据反馈表”分配正式密钥。切换时的坑在于:代码里不要硬编码密钥,而是放到配置中心或环境变量里。我见过有团队把TestDesKey留在生产代码里,上线后所有报文加解密错乱,排查了半天才发现密钥没换。
其次,DES 密钥和 MD5 密钥是两套,分别对应加密和签名,不要混用。联调阶段用一个系统 ID 跑通全部接口后,再向善付通申请正式参数,申请周期一般要预留出来,别等到上线前一周才提。
4. 核心支付链路拆解:SFT10001 下单支付与 SFT20001 异步通知
把加解密跑通之后,真正核心的支付链路就一条:SFT10001 提交订单 → 用户到善付通收银台支付 → SFT20001 异步通知结果 → SFT10003 查询兜底。这条链路吃透了,其他接口都是围绕订单状态做增补。
4.1 SFT10001 网关接口的业务定位与跳转逻辑
SFT10001订单支付接口是网关接口,第三方系统把订单信息提交至善付通。如果买家没在善付通注册,系统会跳转登录或注册页面,引导用户输入用户名密码后再跳转支付页面。这意味着商户端不需要自建支付密码体系,登录和支付都在善付通侧完成。
请求报文的GoPayType字段控制支付发起方式:
| GoPayType | 含义 | 触发行为 |
|---|---|---|
| 1 | 发起 PC 版支付 | 保存订单后自动跳转 PC 版支付页 |
| 2 | 发起移动版支付 | 保存订单后自动跳转移动版支付页 |
| 3 | 小程序支付 | 通过小程序拉起支付 |
PayType则决定是否担保支付:01是直接支付,00是担保支付,不传默认直接支付。担保支付场景下,资金不会立刻结算给卖家,而是等买家确认付款后才完成,对应的订单状态是“已支付未确认付款”。这两个字段组合决定了支付流程的走向,设计业务时先想清楚自己平台是即时到账还是担保交易。
4.2 请求参数的结构:订单头、子订单与分账参数
SFT10001的Data字段是整套报文里最复杂的结构。顶层参数里,BuyerUserID_ThirdSys、SellerUserID_ThirdSys分别标识买卖双方在第三方系统中的 ID,OrderInfos是订单明细的 dataset。虽然结构上支持多行,但文档明确备注“只支持单笔订单”,传多行实际不生效。
核心参数如下:
| 参数名 | 类型/长度 | 必填 | 说明 |
|---|---|---|---|
| TxCode | Char(8) | 是 | 固定 SFT10001 |
| TransID | Char(30) | 是 | 流水号,一次一用 |
| GoPayType | Char(2) | 是 | 1-PC、2-移动、3-小程序 |
| PayType | Char(2) | 否 | 01 直接支付,00 担保支付 |
| OrderIniter | Char(2) | 是 | 0-买方发起,1-卖方发起 |
| BuyerUserID_ThirdSys | Char(20) | 是 | 买方在第三方系统的 ID |
| SellerUserID_ThirdSys | Char(20) | 是 | 卖方在第三方系统的 ID |
| BuyerUserType_ThirdSys | Char(20) | 是 | 0-个人会员,1-企业会员 |
| OrderInfos | dataset | 是 | 子订单详情,只支持单笔 |
OrderInfos内部又分成订单头和商品详情两层,Order_No是第三方系统的订单号,Order_Money是支付金额,Order_Time遵循yyyyMMddHHmmss格式。商品明细是独立的 dataset,每个商品包含ProductID、ProductTitle、ProductPrice、ProductAmount等字段。拼接逻辑是“一个订单头挂一个商品明细数组”,中间用 dataset 层级串起来。
分账参数也要提前留意。Rlacc_Num指定分账账户个数,Cmsn_Inracc_No是分账账户列表,多个账户用竖线|分割,Rcvb_Hdcg_Val是每个账户对应的固定金额,同样用竖线分割。这两个竖线分割字段常见的坑是前后多了空格,或者数量与分账账户个数不一致导致报错。分账总金额必须小于等于订单金额,手续费和固定金额之和也不能超过支付金额。
请求发出时,整包数据用 DES 加密后放在Data字段里,浏览器通过表单 POST 跳转:
curl -X POST 'https://sandbox-sft.example.com/gateway' \ --data-urlencode 'ThirdSysID=1004' \ --data-urlencode 'TxCode=SFT10001' \ --data-urlencode 'Data=07B0FE28667D98092D5DC0730A56E784EF68EB3BAD2F5E8C' \ --data-urlencode 'Auth=4764da69b2cd9497801d42394562dd3f'注意网关接口是页面跳转,实际请求由浏览器发出,这里用 curl 只是示意报文构成。Data字段里的内容在联调时可以先固定一段密文来测签名是否通过,等签名通了再换真实订单数据。
4.3 SFT20001 异步通知与 SFT10003 查询核对:状态机是接入的核心
支付完成后,善付通会调用SFT20001通知第三方系统。文档明确说“每一笔订单支付完成后,无论支付成功与否都会调用”,而且同时包含页面通知和服务器 POST 通知两种形式。
通知里比较关键的字段是Order_No_CCB(善付通订单号)和Pay_No_CCB(善付通付款单号),这是第三方对接时用来对账的凭据。PayStatus取值要重点理解:1付款成功、2付款失败、4已支付未确认付款。4对应担保支付场景,买家已付款但尚未确认,业务上不能把这笔订单直接记为完成。
我接这类支付平台有个习惯:异步通知只做“登记”,不做“结算”。收到PayStatus=1时先更新订单状态为“已支付”,等清算完成后(对应清算状态01)再更新为“已结算”。因为支付成功和资金清算之间存在时间差,如果收到通知就把钱算进当日营收,日终对账时很容易对不上。
SFT10003订单查询接口是联机接口,按第三方订单号查询,每次最多 10 个订单号。返回字段比通知更完整,多了ExistInCCB(善付通是否存在该订单)、UpdateTime(最后更新时间)、Clr_Status(清算状态)和Clr_Time(清算时间)。这接口的价值就是兜底——异步通知丢了或者没收到,可以用它主动查询补齐状态。我在生产环境里会写一个定时任务,对“已支付但未清算”的订单每隔 15 分钟查一次Clr_Status,直到变成01。
5. 联机退款与日终对账:状态机设计与常见避坑
支付链路通了以后,真正体现接入水平的是退款和对账。善付通在这些环节提供了多套接口,选错接口会导致处理流程割裂,这也是我踩过最多坑的地方。
5.1 退款链路的三种接口选择
文档里退款相关接口有三个:SFT10009订单退款接口、SFT10013联机退款接口、SFT10018退款协同接口。从接口定义看,它们定位不同。
| 接口 | 类型 | 结果返回方式 | 适用场景 |
|---|---|---|---|
| SFT10009 | 退款接口 | 异步通知(SFT20003) | 常规订单退款 |
| SFT10013 | 联机退款接口 | 联机原路返回 | 需要同步获取退款结果 |
| SFT10018 | 退款协同接口 | 协同确认 | 多级供应链之间的退款确认 |
SFT10009提交后,退款结果通过SFT20003异步通知,设计上要处理通知幂等。SFT10013则适合管理端主动发起退款、需要马上知道结果的场景。我一般在用户端的“申请退款”用SFT10009,让售后人员提交后异步跟踪;管理后台的强制退款用SFT10013,界面直接展示结果。
退款完成后,SFT10003查询返回里会多一个thirdsysPaybackIds字段,已成功退款的第三方退款 ID 用逗号分隔。这是核对退款状态下发是否落库的依据。
另外,担保支付的下游还有一个SFT10023确认付款接口,买家确认收货后发起。它对应的通知是SFT20023。如果业务里用了PayType=00,这条链路必须一起实现,否则钱一直挂在中间态。
5.2 日终对账:SFT10008 的对账思路
SFT10008日终对账接口按日期查询,返回对账单文本文件。这里的“日终”概念要注意:不是自然日 0 点,而是银行当日清算批次结束后的时点。对账文件拉下来之后,我在本地会用订单号做映射,拿本地的支付订单金额、退款金额和善付通的对账明细逐笔核对。
一个容易忽略的细节是金额单位。文档里金额字段统一是 Char(20) 的字符串,值形如20.00,单位是元。但有些支付平台的对账单会给“分”,我每次写对账代码都会先确认单位,再决定要不要除以 100。宁可多写一个断言,也不要让线上金额差一分钱。
5.3 接入常见问题与排查:现象、原因、对策
1. Auth 签名一直通不过现象:请求发送后返回签名错误,或者异步通知校验失败。 原因:最常见的是拿明文Data参与签名,或者把密文转成大写在拼串;其次是拼接顺序写错,把MD5Key放在了前面。 解决:严格按ThirdSysID + TxCode + Data + MD5Key顺序拼串,Data用 DES 加密后的密文,签名结果用小写比较。
2. 用户支付完成后订单状态没变现象:用户在善付通完成了支付,商户后台仍显示“待支付”。 原因:只实现了页面通知接收,没实现服务器 POST 通知地址,或者通知地址配置错误。 解决:在配置里同时维护页面跳转地址和服务器通知地址,两者指向不同的处理逻辑。服务器通知地址必须以TxCode为分支,SFT20001分到支付结果处理,SFT20003分到退款处理。
3. 中文乱码和字段长度超限现象:买家姓名、公司名乱码,或报“字段超长”。 原因:HTTP 参数读写没有强制 UTF-8;或者字段长度没按文档限制,比如手机号字段是 Char(13),传了带区号的 14 位。 解决:所有参数读取和响应编码统一指定 UTF-8,发送前写一个统一的字段长度校验工具,超长直接拦截。
4. 同一笔订单收到两次成功通知现象:订单被重复记账或重复发货。 原因:页面通知和服务器 POST 通知各来一次,或者善付通重发报文,而本地没有做幂等。 解决:以Order_No加PayStatus为唯一键,处理前先查库;已经处理过的通知直接忽略,状态只允许从“待支付”单向流转到“已支付”。
5. 日终对账不平现象:对账单金额和本地订单金额有差异。 原因:可能涉及担保支付未确认、退款中间态、清算状态未知这三类跨日订单。 解决:先按Clr_Status分类,00未清算是正常的,01已清算才对账,05清算成功后有退款需要单独核销退款金额。
6. 上线前必做的自检:从模拟联调到生产切换的验证技巧
6.1 联调阶段先跑通三笔订单
我自己的习惯是联调阶段不做全量回归,而是先跑三笔核心订单:一笔直接支付成功、一笔支付后立刻查询核对状态、一笔退款后查清算状态。这三笔跑通,支付链路、查询链路、退款链路就都有了闭环。
| 自检项 | 操作 | 预期结果 |
|---|---|---|
| 支付闭环 | 调用 SFT10001 走完支付 | 收到 SFT20001,PayStatus=1 |
| 查询兜底 | 调用 SFT10003 查订单 | 返回 PayStatus、Clr_Status 与本地记录一致 |
| 退款闭环 | 调用 SFT10013 退款 | 返回成功,SFT10003 显示退款 ID |
6.2 生产切换前的检查项
上线前我会强制自己过一遍配置清单:生产 DES 密钥和 MD5 密钥是否已替换、服务器通知地址是否公网可达、Hosts 配置是否指向生产网关、退款通知和支付通知的TxCode是否已区分处理。这套流程看起来繁琐,但每次都能帮我挡掉至少一个问题。
善付通的接口数量多,但归类到最后就是支付、查询、退款、对账四条线。从那以后我接任何供应链支付平台,都会先画一张订单状态迁移图,标注清楚每个状态由哪个接口触发、哪个通知更新、哪个字段对账,再谈具体开发。这个习惯让我少踩了很多坑,希望帮到你。
本文还有配套的精品资源,点击获取