简介《建设银行【善付通】支付结算接入文档》面向需要接入建行善付通的企业技术团队、支付系统开发者与供应链平台实施人员围绕订单管理、收付款协同、对账及接口联调等场景梳理从申请接入到正式上线的技术路径。压缩包内为1个docx文件大小约1.09MB集中整理了对接流程、数据传输规范、报文格式、数据加解密及各接口定义便于按目录查阅支付、查询、退款、预付款、会员绑定、日终对账和订单协同等模块。文档对SFT系列接口的请求参数、响应格式与业务用途作了分节说明并覆盖手机号验证码支付、联机退款、物流协同等较完整的交易生命周期内容可作为企业设计接口调用、联调测试和排查字段问题的参考。目前已有2472人学习适合需要理解善付通支付结算接入规范、快速定位接口文档的中高级开发与实施人员。1. 从一次联调被打回说起善付通支付结算接入到底要准备什么第一次拿到善付通支付结算接入文档我盯的是接口定义——三十来个 SFT 交易码字段密密麻麻以为照抄就能通。真正把联调卡了两天的是文档前四页数据传输规范、报文格式、DES 加解密。密钥拿错一套、Data 多编了一次码、回调地址写成了内网 IP随便哪个都能让一笔在线支付在最后一步原地打转。这套文档的坑不在接口写得多细而在它是给已有系统的企业做对接用的你得先有能出公网的服务器、能接异步通知的公网地址、分得清测试密钥和生产密钥的两套配置。适合正在接支付结算的 Java 后端和运维也适合第一次做移动支付小程序对接、被回调通知折磨过的人。环境没列清接口看得再熟也白搭。2. 接入前的环境清单第三方系统 ID、密钥与回调地址怎么定接入善付通不是装一个包就完事它是把自己系统挂到别人平台上的过程。落地第一步永远是把东西配齐配不齐后面每一行代码都在猜。我一般会先单独拉一个文档把需要向对接方索取的凭证、需要自己准备的资源、需要报备的信息三列列出来全齐了再动代码比边写边问省事得多。2.1 三类必填凭证与两套环境的密钥差异善付通报文里有一个字段叫 ThirdSysID是委托单位平台 ID文档示例里写的是 1004。这个值不是自己编的是签约时分配给你的唯一标识所有签名都从它开始拼。跟它配套的还有两把密钥一把 DES 对称密钥用来加密 Data 字段一把 MD5 密钥用来生成 Auth 签名。测试环境的这两把钥匙文档写死了生产环境必须换成正式密钥。我习惯把两套环境做成一张对照表贴在内网 wiki 上谁改了配置都要同步配置项测试环境生产环境取自哪里ThirdSysID文档示例 1004正式分配值对接反馈表DES 密钥TestDesKey正式密钥对接反馈表MD5 密钥TestMD5Key正式密钥对接反馈表联机接口地址测试地址生产地址文档 6.1 接口地址回调接收地址自建测试域名自建公网域名文档 6.3 需第三方提供Hosts 记录可能需手工指定不写死文档 6.2 Hosts 配置注意文档里有两处容易读错。第一处是加解密章节的小标题写着公钥加密/解密实现方法但正文给的示例是 DES 对称密钥不是非对称那一套别被标题带偏。第二处是测试密钥只用于联调上线前如果忘了替换第一次真实扣款就会失败而且报错信息往往只告诉你验签不通过排查成本极高。拿到这几样东西之后先把它们落在项目的配置文件里不要写死在代码常量中。我见过不止一个项目把 TestMD5Key 直接写在 Java 类里后来换生产密钥要重新编译发版赶上发版窗口还拖了一天。2.2 服务器、域名与网络出口的准备清单善付通的接口分两类对网络的要求不一样。联机接口是你主动发起的服务器只要能出公网、能 POST 就行网关接口和异步通知反过来是平台往你这边发所以你必须准备一个外网能访问到的接收地址。这一点如果前期没规划最容易在上线前一周才发现回调地址在内网里被挡在防火墙外面。我一般按这几项过一遍一台能访问外网的业务服务器用于发起联机接口请求出口 IP 提前报备给对接方很多平台的频繁调用限额和 IP 白名单是绑在一起的一个公网可达的接收地址接收支付结果通知、退款结果通知、会员绑定结果通知这类异步报文必须是 POST 能打通的路径服务器时间同步装好 chrony 或 ntpdate 并配开机自启。流水号规则是 ThirdSysID yyyyMMddHHmmss 5 位顺序号时间一歪流水号就可能重复统一 UTF-8从操作系统 locale 到 JVM 的 file.encoding 到数据库连接串全链路对齐。文档数据传输规范第一条就是字符集统一用 UTF-8日志落盘请求报文、响应报文、通知报文各留一份原始串保留时间按财务要求来我一般至少留 180 天时间同步这条是被低估的。有一次测试环境的一台机器漂了七分钟跟另一台同时发起请求流水号撞在一起被平台判为重复请求直接拒绝现象看起来像接口偶发失败查了很久。2.3 参数落在配置文件里一份可直接抄的配置模板把凭证和地址从代码里剥出来落到配置文件是接入阶段最值钱的一次重构。下面这份是我常用的结构字段名对应文档里的参数分环境各写一份# application-sft-test.properties 联调用 sft.thirdSysId1004 sft.desKeyTestDesKey sft.md5KeyTestMD5Key sft.gatewayUrlhttps://sft-test.example.com/gateway.do sft.onlineUrlhttps://sft-test.example.com/online.do # 我们自己对外暴露的接收地址必须公网可达 sft.notifyUrlhttps://pay-test.example.com/sft/notify # 单笔订单金额上限分防止测试时误发真实大额 sft.orderAmountLimit100# application-sft-prod.properties 上线用 sft.thirdSysId${SFT_THIRDSYS_ID} sft.desKey${SFT_DES_KEY} sft.md5Key${SFT_MD5_KEY} sft.gatewayUrlhttps://sft.example.com/gateway.do sft.onlineUrlhttps://sft.example.com/online.do sft.notifyUrlhttps://pay.example.com/sft/notify sft.orderAmountLimit5000000两份文件的差别不只是值还有读取方式。生产环境我坚持从环境变量注入密钥不把明文密钥提交进仓库。Java 侧统一用一个配置类收口避免各处散落System.getPropertyComponent ConfigurationProperties(prefix sft) public class SftConfig { private String thirdSysId; private String desKey; private String md5Key; private String gatewayUrl; private String onlineUrl; private String notifyUrl; private long orderAmountLimit; // getter / setter 省略 }配置类本身没什么技术含量价值在于把密钥从哪来这件事固定成一条路径。之后再接对账、退款、预付款这些接口加字段只是加属性不会有人又去new一个常量类出来。启动时加一段校验密钥为空直接抛异常失败退出比跑到线上第一次支付才发现要好。3. 报文封装DES 加密与 MD5 签名的可执行实现报文是这套接入里最容易翻车的地方因为它一次性把编码、加密、签名三件事叠在一起任何一层错一位对方只会回你一个验签失败。文档第三章把报文拆成五部分第五章每个接口又只描述 Data 里的内容中间这层怎么把业务对象变成 Data文档不管得自己写。3.1 五个字段的报文结构拆解所有接口的传输数据总体分为五部分前四个是你发出去的后两个是平台出错时回来的字段名含义可空是否加密ThirdSysID对接系统的 ID签约时分配否否TxCode交易码如 SFT10001否否Data该接口所需的具体业务数据否是DES 加密AuthThirdSysID TxCode Data MD5Key 的 MD5否否errorCode错误码发送请求时不上送响应报错才有否errorMsg错误信息发送请求时不上送响应报错才有否看清这张表的关键是理解顺序先有明文业务数据再 DES 加密成 Data再用未加密的 ThirdSysID TxCode 加密后的 Data MD5Key拼串做 MD5 得到 Auth。也就是说签名签的是密文不是明文。很多人第一版把明文拿去签本地自测完全自洽一联调就全错。Data 内部的内容格式文档给了 dataset 的定义方式比如 SFT10001 里 OrderInfos 是个可传多行的数据集里面再嵌 Order_Products。落地上我一般用两级 Java Bean 映射序列化成平台约定的键值串而不是直接丢 JSON 过去——文档没有说 Data 明文是 JSON稳妥做法是按对端确认的格式拼别自己发明。3.2 DES 加解密密钥、填充与编码的三个坑文档给的示例代码调用了 DESTool.encrypt(desKey, data)示例密文长这样07B0FE28667D98092D5DC0730A56E784EF68EB3BAD2F5E8C。从这个密文能反推出两个关键信息输出是十六进制大写字符串不是 Base64长度是 48 个字符对应 24 字节说明分组模式是常见的 ECB。如果不按这两点来密文看起来也是一串乱码但对方解不出来。用 JDK 自带能力写一个等效实现重点在密钥处理和输出编码public final class SftDesUtil { private static final String ALGORITHM DES; // ECB PKCS5Padding与文档示例密文形态一致 private static final String TRANSFORMATION DES/ECB/PKCS5Padding; public static String encrypt(String key, String plain) throws Exception { Cipher cipher Cipher.getInstance(TRANSFORMATION); // DES 密钥固定 8 字节超长部分会被 JDK 拒绝这里显式截断 byte[] keyBytes Arrays.copyOf(key.getBytes(StandardCharsets.UTF_8), 8); cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(keyBytes, ALGORITHM)); byte[] out cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8)); // 输出必须是大写十六进制与示例 07B0FE... 对齐 return Hex.encodeHexString(out).toUpperCase(); } public static String decrypt(String key, String cipherText) throws Exception { Cipher cipher Cipher.getInstance(TRANSFORMATION); byte[] keyBytes Arrays.copyOf(key.getBytes(StandardCharsets.UTF_8), 8); cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, ALGORITHM)); byte[] out cipher.doFinal(Hex.decodeHex(cipherText.toCharArray())); return new String(out, StandardCharsets.UTF_8); } }TRANSFORMATION换成别的模式密文长度和内容都会变对端解不开。Arrays.copyOf那行是血泪经验DES 只认 8 字节密钥直接key.getBytes()传进去密钥超过 8 字节会抛InvalidKeyException: Wrong key size密钥不足 8 字节同样报错。文档示例里的密钥字符串长度并不规整说明对端工具类内部也做了截断或补齐跟它对齐最省事。第三个坑在编码。明文里有中文比如订单标题、收货方姓名一定要按 UTF-8 取字节再加密。用默认字符集在本地 Windows 上跑通、部署到 Linux 就乱码这类问题我踩过不止一次最后都是在getBytes里显式写死编码才彻底消停。3.3 MD5 签名的拼接顺序与 UTF-8签名规则文档写得很直白auth MD5.getMD5(thirdSysID txCode data MD5Key, UTF-8)。示例里 thirdSysID 是 1004txCode 是 SFT10001data 是上一步的密文MD5Key 是 TestMD5Key算出来 auth4764da69b2cd9497801d42394562dd3f32 位小写。public static String sign(String thirdSysId, String txCode, String data, String md5Key) throws Exception { // 拼接顺序不能变ID 交易码 密文 密钥 String raw thirdSysId txCode data md5Key; MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(raw.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { // 小写、补零得到 32 位字符串 sb.append(String.format(%02x, b)); } return sb.toString(); }三个细节值得单独说。第一拼接时不能加任何分隔符不要好心加|或第二参与签名的 Data 是加密后的密文原串不能是 URL 编码之后的样子第三输出是 32 位小写String.format(%02x)会自动补前导零如果用Integer.toHexString手写循环遇到小于 16 的字节会少一位签名长度变成 31 位验签必挂。还有一个隐蔽的坑网关接口是表单提交浏览器或 HttpClient 会对你传的 Data 做一次 URL 编码。如果服务端在验签前先做 URL 解码再拿原始串拼是对的如果先拼再解码就错位了。我的做法是接收侧统一从 request 取到参数后原样保留先验签验签通过再解码业务数据顺序不能反。文档给的接收示例也是这个思路先取request.getParameter(Data)再拼ThirdSysID TxCode Data MD5Key算临时签名跟传进来的 Auth 比对。3.4 一个能跑通 SFT10001 的最小请求把前面三块拼起来就是一次完整的订单支付请求。SFT10001 是网关接口发起后如果买家没在平台注册会跳转登录注册页注册完再跳支付页最终结果通过 SFT20001 异步通知回来。下面这段只构造请求不处理跳转用于验证加密签名链路是否打通public String buildPayRequest(SftConfig cfg, Order order) throws Exception { // 1. 组装业务数据字段名必须与文档大小写完全一致 MapString, String biz new LinkedHashMap(); biz.put(TxCode, SFT10001); biz.put(TransID, buildTransId(cfg.getThirdSysId())); biz.put(GoPayType, 2); // 1-PC 2-移动 3-小程序 biz.put(PayType, 01); // 01-直接支付 00-担保支付 biz.put(OrderIniter, 0); // 0-买方发起 1-卖方发起 biz.put(BuyerUserID_ThirdSys, order.getBuyerId()); biz.put(BuyerUserName_ThirdSys, order.getBuyerName()); biz.put(SellerUserID_ThirdSys, order.getSellerId()); biz.put(BuyerUserType_ThirdSys, 0); // 0-个人 1-企业 biz.put(BuyerTrueName_ThirdSys, order.getBuyerTrueName()); biz.put(BuyerCompany_ThirdSys, order.getBuyerCompany()); biz.put(BuyerPhoneNum_ThirdSys, order.getBuyerPhone()); biz.put(BuyerAddress_ThirdSys, order.getBuyerAddress()); biz.put(Pay_Interface, 00); // 00-收银台 01-个人网银 02-企业网银 04-聚合支付码 biz.put(OrderInfos, buildOrderInfos(order)); // dataset单笔订单 String plain toKvString(biz); String data SftDesUtil.encrypt(cfg.getDesKey(), plain); String auth SftSignUtil.sign(cfg.getThirdSysId(), SFT10001, data, cfg.getMd5Key()); // 四个参数按表单字段名提交注意 ThirdSysID 大小写 return ThirdSysID cfg.getThirdSysId() TxCodeSFT10001 Data URLEncoder.encode(data, UTF-8) Auth auth; }buildTransId按文档规则实现ThirdSysID yyyyMMddHHmmss 5 位顺序号总长不超过 30 位且一个流水号只能用一次发送和返回两次跳转都算一次重复请求无效。顺序号我一般用 Redis 自增别用随机数重复概率不为零。OrderInfos里必须带HaveProducts1以及Order_No、Order_Money、Order_TimeyyyyMMddHHmmss、Order_Title、Order_BuyerPhone和收货方四件套这些少一个都会被拒。单笔订单金额建议在配置里设个上限测试阶段把orderAmountLimit调小。文档明确说每个第三方系统每天允许调用接口的次数有限制具体数值协商确定超了当天该交易直接拒绝。联调阶段乱刷请求把当天额度打满是很常见的低级事故。4. 联机接口与网关接口两种调用方式的部署差异文档把接口分成联机接口和网关接口这个区分不是学术分类它直接决定你的服务器要开什么端口、代码要用什么客户端、要不要写页面。很多人把两者混着用用 HttpClient 去打网关接口或者拿表单去跳联机接口结果要么报错要么静默失败。4.1 联机接口HttpClient POST 与超时重试联机接口的特征是无页面跳转httpclient 发起 post 请求处理完原路返回。SFT10003 订单信息查询、SFT10005 绑定用户关系、SFT10008 日终对账、SFT10009 订单退款、SFT10013 联机退款都属于这一类。它们适合放在服务端做自动化比如定时对账、批量绑定、异步补偿。用 HttpClient 发的时候有三处参数必须显式设RequestConfig config RequestConfig.custom() .setConnectTimeout(3000) // 建连超时内网调可再小 .setSocketTimeout(15000) // 读超时查询类接口给足 .setConnectionRequestTimeout(1000) .build(); HttpPost post new HttpPost(cfg.getOnlineUrl()); // 表单编码必须是 UTF-8否则 Data 里的中文密文会被二次处理 post.setEntity(new UrlEncodedFormEntity(formParams, StandardCharsets.UTF_8)); post.setConfig(config);超时值不是随便填的。查询类接口返回的是订单列表SFT10003 一次最多查 10 个订单号超过 10 个直接被拒所以批量场景要自己切片。退款类接口一旦超时结果是不确定的——请求可能已经到了平台并执行成功只是响应没回来。这种接口不能简单重试我的做法是超时后立刻用 SFT10003 按订单号查一次真实状态查到已退款就不再重发把重试变成查询后补偿。返回格式文档也写清了查询类返回 Json 格式订单列表里面有Status1-成功 2-失败和Remark。注意这个 Status 表示这次查询请求本身成功与否不是订单支付成功与否订单的支付状态在OrderInfos里的PayStatus。0-付款中 1-付款成功 2-付款失败 3-订单取消 4-已支付未确认付款。我见过有人把外层 Status 当支付结果写进业务库结果所有订单都成了成功对账时才发现金额对不上。4.2 网关接口FORM 表单跳转与 GoPayType 的选择网关接口的要求相反必须 POST 报文 FORM 表单以页面跳转形式发起请求处理结果异步通知。SFT10001 订单支付、SFT10012 手机号验证码支付、SFT10020 预付款支付都是这类。它本质是把用户浏览器送到平台页面上完成支付所以不能由后端 HttpClient 发起。落地时后端只负责生成四个参数前端负责渲染一个自动提交的表单form idsftPayForm actionhttps://sft.example.com/gateway.do methodpost input typehidden nameThirdSysID value1004/ input typehidden nameTxCode valueSFT10001/ input typehidden nameData value07B0FE28.../ input typehidden nameAuth value4764da69b2cd9497801d42394562dd3f/ /form scriptdocument.getElementById(sftPayForm).submit();/scriptGoPayType是这里最关键的参数决定跳哪个版本的支付页传 1 跳 PC 版传 2 跳移动版传 3 表示走小程序跳支付。同一套后端代码要同时服务 PC、H5 和小程序时这个值必须由调用方传入而不是写死。小程序场景尤其要注意只是把 1 改成 3 不够小程序的跳转链路、支付目录、业务域名都要单独报备文档专门有一节小程序对接规范讲这件事。还有一个参数Pay_Interface决定支付接入方式00 收银台、01 个人网银、02 企业网银、04 聚合支付码。传 04 时平台会在响应里带一个Direct_Pay_Url你需要把这个地址渲染成二维码给用户扫。不传默认 00走收银台用户自己选。OrderIniter这个字段容易被忽略。0 表示作为买方发起1 表示作为卖方发起。供应链场景里买卖双方身份是相对的同一家企业既采购也销售发起方搞反了后续绑定关系、协同单据的方向全乱。我们项目里是在订单表上加了一列标记生成报文时按订单归属取不靠人工判断。4.3 异步通知接收端SFT20001 的幂等处理支付完成后平台会调用 SFT20001 通知你结果。文档说明每一笔订单支付完成后无论成功与否都会调用包括页面通知带报文的页面跳转和服务器通知POST 到指定服务器地址两种。这意味着你的接收接口会被重复调用——页面跳一次、服务器 POST 一次用户刷新页面可能再来一次。接收端必须做的第一件事是验签第二件事是幂等。PostMapping(/sft/notify) public String notify(RequestParam(ThirdSysID) String thirdSysId, RequestParam(TxCode) String txCode, RequestParam(Data) String data, RequestParam(Auth) String auth) throws Exception { // 1. 先验签用原始密文拼不要先解码 String expect SftSignUtil.sign(thirdSysId, txCode, data, cfg.getMd5Key()); if (!expect.equals(auth)) { log.warn(sft notify sign mismatch, thirdSysId{}, thirdSysId); return fail; // 验签失败不要抛异常避免对端无限重推 } // 2. 解密业务数据 String plain SftDesUtil.decrypt(cfg.getDesKey(), data); PayNotify notify parse(plain); // 3. 幂等以 Order_No PayStatus 落唯一索引重复的直接返回 success if (!notifyService.trySave(notify)) { log.info(duplicate notify ignored, orderNo{}, notify.getOrderNo()); } return success; }PayStatus有四个值1 付款成功、2 付款失败、4 已支付未确认付款。最后这个状态在担保交易场景下会出现意味着钱已经到了但还没确认收货不能当成功也不能当失败处理。很多系统只判断了 1 和 2遇到 4 就把订单卡在中间态用户投诉付了钱订单没变其实是状态机没覆盖全。Total_Money是累计支付金额Order_Money是本次支付金额。分账、部分支付的场景下这两个值不一样对账时要用Total_Money不能用本次金额。Pay_No_CCB是平台付款单号退款时要用到它务必落库。Pay_Card和PayNameByCard涉及付款人信息属于敏感数据落库前该脱敏的脱敏。5. 避坑与排查接入善付通支付结算最常见的五个翻车点这一章是我自己踩过、也帮别人排查过的记录按现象 → 原因 → 解决写遇上对得上的直接照方抓药。5.1 验签一直失败怎么看都一模一样现象本地用示例里的 1004 / SFT10001 / 07B0FE28… / TestMD5Key 算出来的 MD5 跟文档对不上或者线上联调对方永远回验签失败日志里两个签名肉眼看着一样长。原因绝大多数是拼接顺序或大小写问题。第一字段名ThirdSysID里 ID 是两个大写字母很多人写成ThirdSysId服务端取不到值拼出来的是空串第二MD5 输出是 32 位小写用Integer.toHexString手写循环会丢前导零长度变成 31 位第三网关场景下 Data 被 URL 编码过一次验签前如果先解码再拼拼的就不是原始密文。解决拿文档给的示例四件套1004 / SFT10001 / 07B0FE28667D98092D5DC0730A56E784EF68EB3BAD2F5E8C / TestMD5Key写一个单元测试断言结果等于 4764da69b2cd9497801d42394562dd3f。这个用例过不了别往下写业务先把签名链路焊死。5.2 中文订单标题变成问号对端说数据非法现象本地 Windows 上一切正常部署到 Linux 之后Order_Title里的中文在平台侧显示成乱码或者直接报数据校验失败。原因String.getBytes()不传字符集时用的是平台默认编码Windows 上可能是 GBKLinux 上是 UTF-8两边加密出来的密文完全不同。文档数据传输规范第一条就要求字符集统一用 UTF-8是全链路的统一。解决所有getBytes显式写StandardCharsets.UTF_8JVM 启动参数加-Dfile.encodingUTF-8容器镜像的 locale 设成C.UTF-8数据库连接串加characterEncodingutf8。改完用一条带中文的订单完整跑一遍加密、签名、发送、通知接收全链路验证。5.3 请求被拒提示流水号重复现象测试环境偶发失败隔一段时间重试就正常线上高并发时偶发率明显上升。原因流水号规则是 ThirdSysID yyyyMMddHHmmss 5 位顺序号一个流水号只能用一次发送和返回两次跳转都算。如果顺序号用的是new Random().nextInt(99999)两台机器同秒并发就有概率撞号如果服务器时间漂移两台机器的时间戳还可能倒挂。解决顺序号改用 Redis 自增key 带上日期做自然过期服务器统一配 NTP 并加监控漂移超过 1 秒告警。重试时不要复用原来的流水号重试必须重新生成同时用查询接口确认前一次是否已成功避免重复支付。5.4 异步通知一条都没收到现象用户在页面上付完款跳回商户页显示成功但后台订单状态一直不更新也没有任何通知日志。原因通知是你主动报备的地址平台从外网 POST 过来。最常见的是报备地址写成了内网 IP 或 localhost其次是防火墙只开了 443 没开 80或者路径被网关的鉴权拦截器挡在外面要求登录还有一种情况是接收接口返回了非约定的成功标识平台认为投递失败。解决先用 curl 从外网机器自测一下报备地址能不能 POST 通再检查 Nginx 和 Spring Security 的放行规则把通知路径加进白名单。接收接口处理成功后必须返回明确的成功响应处理失败也要正常返回而不是抛 500否则平台会持续重推把日志刷爆。5.5 对账文件拿不到或者拿到的不是当天的现象SFT10008 日终对账按日期查询返回的文本文件内容跟预期日期对不上或者查询直接报错。原因日期参数格式和对账时区没对齐。日终对账是按平台侧的日切时间出文件的你在凌晨两点查今天平台那边可能还没切日。另外对账接口每日调用次数同样有限制定时任务配得太密会被拒。解决把对账任务放到上午跑查前一天的日期文件解析用定长或按对端给的分隔符切不要用split(,)硬来解析结果跟本地支付流水做双向核对只比对总额不够要按订单号逐笔比对差异单单独出一张表人工跟。对账不平的单子用 SFT10003 带上Clr_Flag001去查清算状态Clr_Status返回 00 未清算、01 清算成功、05 清算成功后有退款能快速定位是没清算还是退过款。6. 装完怎么验证跑通从 SFT10003 查询到日终对账的闭环自检配置写完、代码提交、部署上线这时候最容易犯的错是页面能跳过去就算通了。真正的验证要走完一条闭环发起支付、收到通知、主动查询、日终对账四步都能对上才算这套接入真的立住了。我一般的自检顺序是这样第一步用测试环境密钥发一笔 SFT10001GoPayType1金额设成 0.01确认能正常跳转到支付页第二步支付完成后看接收端日志里有没有 SFT20001验签是否通过PayStatus是不是 1Order_No_CCB和Pay_No_CCB有没有落库第三步隔几分钟用 SFT10003 按订单号查一次比对查询结果和通知里的金额、状态是否一致第四步第二天上午跑 SFT10008 对账逐笔核。第三步里有个容易被忽略的参数Clr_Flag传 001 会带回清算状态和清算时间不传就只查支付状态。资金真正到账看的是清算不是支付成功财务口径和业务口径在这里分叉。我们项目里是把两个状态都落库业务侧看支付状态财务侧看清算状态避免订单显示已付款但钱没到的解释成本。验证阶段还有一张我必看的检查表检查项通过标准出问题先看哪加密链路示例四件套算出的签名等于 4764da69b2cd9497801d42394562dd3f密钥、拼接顺序、大小写网关跳转PC 和移动端分别能跳对页面GoPayType、Pay_Interface异步通知日志中有 SFT20001 且验签通过报备地址、防火墙、返回标识主动查询SFT10003 返回的 PayStatus 与通知一致是否把外层 Status 当支付状态幂等同一订单重复通知只处理一次唯一索引、状态机对账逐笔核对无差异差异单可解释日期、时区、清算标识这套流程跑通之后退款SFT10009 / SFT10013、预付款SFT10020 / SFT10021、会员绑定SFT10011基本是同一套报文骨架换字段把 Data 里的 dataset 换掉、TxCode 换掉验签和加解密复用边际成本很低。真正需要单独设计的是状态机支付、退款、预付款、协同单据各自的状态流转不一样尤其已支付未确认付款这个中间态一定要在状态机里留位置。从那以后我每次接这类支付结算平台不管文档写得多清楚都强制先跑一遍示例签名单元测试 0.01 元真实链路 次日对账这三步少一步都不敢上线。支付这东西没有后悔药验证阶段多花的两小时都是从线上事故里省出来的。希望帮到你。本文还有配套的精品资源点击获取