简介本资源是建设银行官方发布的《善付通支付结算接入文档》面向供应链企业技术负责人、支付系统开发工程师及金融IT集成人员旨在解决传统订单管理与收付款环节割裂导致的对账繁琐、错误率高、协同效率低等痛点。文档全面覆盖善付通对接全流程包括协议签订、系统开发、联调测试及上线规范并详细定义25类核心接口如SFT10001订单支付、SFT10009退款、SFT10008日终对账等配套数据加解密机制、报文格式与传输安全要求支撑企业快速完成与善融商务平台的支付结算系统集成。资源为1个1.09MB的Word文档.docx结构清晰含7大章节与细分接口说明便于开发者按需查阅与落地实施。目前已有2474人学习下载是企业接入建行善付通服务不可或缺的技术蓝本与实操依据。1. 善付通不是“又一个支付 SDK”而是供应链结算的协议级嵌入它把订单、资金、对账三件事压进一个 HTTP POST 里你手头这份《建设银行【善付通】支付结算接入文档》不是那种“下载个 jar 包、填个 appid、调个 pay() 就完事”的轻量级支付接入包。它是一份面向中大型企业供应链系统的协议级集成说明书——核心目标不是“让用户付钱”而是让采购方、销售方、建行三方在订单生命周期创建→支付→发货→签收→退款→对账中所有关键动作都具备可追溯、可协同、可轧差的金融级确定性。我去年帮某高校后勤集团对接过类似系统他们原以为只是加个“微信支付”按钮结果发现光是“SFT10003 订单信息查询”接口返回的PayStatus4已支付未确认付款这个状态就卡了三天——因为财务侧要等建行日终对账文件SFT10008落地后才敢做账而业务侧看到“已支付”就发了货中间存在天然时间差。这种“状态语义不一致”才是善付通真正要解决的痛点。它适合谁不是个体商户或 SaaS 小工具开发者而是✅ 已有自建 ERP/SCM 系统、需与银行直连完成资金闭环的企业✅ 作为核心企业主导供应链协同需向上游预付、向下游收款并统一管理应收应付账款的集团✅ 对资金安全、审计合规、T0 轧差有硬性要求不能接受“异步回调丢失”“状态不可回溯”的财务部门。关键词“在线支付”“移动支付”在这里是表象“支付结算”才是内核——它把支付行为锚定在订单实体上把结算动作绑定在银行清算周期里把对账逻辑下沉到每个接口字段中。你不是在接一个支付通道是在签署一份跨系统、跨组织、跨账期的数字契约。下面我们就从最常翻车的第一步开始怎么把那串看似简单的ThirdSysIDTxCodeDataMD5Key真正算对。2. 报文签名与加解密DESMD5 不是摆设是校验链第一道铁闸善付通的数据安全机制非常“古典但有效”DES 对称加密 MD5 拼接签名。它不追求前沿密码学而是用银行级工程思维确保两点① 数据传输中不被篡改② 接口调用方身份可信。很多团队栽在第一步——签名值算不对直接被拒收连错误码都收不到。2.1 DES 加密必须严格遵循“字节对齐”和“补位规则”文档里只写了DESTool.encrypt(desKey,data)但没说清楚底层细节。实际开发中你必须确认所用 DES 实现是否满足以下三点否则加密结果必然与建行服务端不一致密钥长度必须为 8 字节测试环境密钥TestDesKey是 9 个字符但实际参与运算的是前 8 字节TestDesK末尾e被截断。生产环境密钥同理务必按字节截取而非字符串长度。明文必须 PKCS#5 补位data字符串需先转 UTF-8 字节数组再按 PKCS#5 规则补位补N字节值全为N。例如abc3 字节补位后为0x61,0x62,0x63,0x05,0x05,0x05,0x05,0x058 字节。加密模式为 ECB无 IV这是最易踩坑点。很多现代加密库默认 CBC 模式并要求 IV若未显式指定ECB且禁用 IV加密结果必错。from Crypto.Cipher import DES from Crypto.Util.Padding import pad import binascii def des_encrypt(key: str, data: str) - str: # 关键1密钥强制截取前8字节bytes des_key key.encode(utf-8)[:8] # 关键2明文转UTF-8 bytes PKCS#5补位块长8 data_bytes data.encode(utf-8) padded pad(data_bytes, DES.block_size, stylepkcs5) # 关键3ECB模式无IV cipher DES.new(des_key, DES.MODE_ECB) encrypted cipher.encrypt(padded) # 返回大写十六进制字符串文档示例为大写 return binascii.hexlify(encrypted).upper().decode(utf-8) # 示例加密 HelloWorld print(des_encrypt(TestDesKey, HelloWorld)) # 输出应为类似A1B2C3D4E5F678901234567890ABCDEF...提示Crypto.Cipher.DES是 Python 的标准实现但务必验证其pad函数是否符合 PKCS#5部分旧版库用 PKCS#7虽兼容但建行明确要求 PKCS#5。若不确定手动补位更稳妥。2.2 MD5 签名拼接顺序与编码必须零误差签名字段Auth的计算公式是MD5(ThirdSysID TxCode Data MD5Key)表面简单实则三处致命陷阱陷阱点现象原因解决方案字符串拼接未转字节签名值与建行不一致MD5()函数若直接对 Unicode 字符串哈希不同语言处理方式不同如 Java 默认 UTF-16所有拼接项必须先.encode(utf-8)再拼接最后对 bytes 哈希Data 用明文还是密文签名失败返回errorCode999文档明确写Data是“经过 DES 加密后的密文”签名时必须用加密后的 hex 字符串而非原始 JSON在签名前先执行des_encrypt()得到data_cipher再参与拼接MD5Key 大小写敏感测试环境能过生产环境失败生产密钥由建行提供可能含大小写字母TestMD5Key是示例实际密钥区分大小写密钥变量名建议用MD5_KEY_PROD显式区分避免硬编码import hashlib def calculate_auth(third_sys_id: str, tx_code: str, data_cipher: str, md5_key: str) - str: # 关键全部转 bytes 后拼接 payload ( third_sys_id.encode(utf-8) tx_code.encode(utf-8) data_cipher.encode(utf-8) # 注意这里是加密后的 hex 字符串 md5_key.encode(utf-8) ) return hashlib.md5(payload).hexdigest() # 示例假设加密后 data_cipher A1B2C3D4... auth calculate_auth(1004, SFT10001, A1B2C3D4..., TestMD5Key) print(auth) # 输出 32 位小写 hex 字符串注意建行服务端返回的Auth是小写 MD5你发送的Auth也必须是小写。虽然文档示例4764da69...是小写但部分开发者用hexdigest().upper()导致校验失败。2.3 加解密工具类必须双向验证自己加密自己解密再比对别信“库没问题”必须写一个闭环验证函数。这是血泪经验某次上线前我们用pycryptodome加密建行返回Auth校验失败排查 6 小时才发现本地DES.new()初始化时传了iv参数虽 ECB 模式忽略 IV但某些版本会静默报错。最终靠这个验证脚本定位def verify_des_roundtrip(key: str, plain: str): # 正向加密 cipher_hex des_encrypt(key, plain) # 反向解密需实现 des_decrypt逻辑对称 decrypted des_decrypt(key, cipher_hex) if decrypted plain: print(f✅ DES 加解密闭环验证通过{plain} → {cipher_hex} → {decrypted}) return True else: print(f❌ 验证失败原始{plain}解密{decrypted}) return False # 必须跑通 verify_des_roundtrip(TestDesKey, {Order_No:ORD123,Amount:100.00})这个函数要放在单元测试里每次密钥变更、库升级都必须重跑。它比任何文档都可靠。3. 接口调用模式网关型 vs 联机型决定你的系统架构生死线善付通接口分两类网关接口Gateway和联机接口Online。这不是技术选型偏好而是业务语义的硬性划分——选错类型轻则功能不可用重则资金状态错乱。很多团队在SFT10001订单支付和SFT10003订单查询上反复调试却没意识到前者必须走页面跳转后者必须走 HttpClient POST。这是协议设计的底层逻辑。3.1 网关接口必须用 FORM 表单 POST 页面跳转禁止 AJAXSFT10001订单支付、SFT10012手机号验证码支付等网关接口本质是“委托建行渲染支付页”。你的系统只需生成一个 HTML 表单action指向建行网关地址methodPOSTinput字段包含ThirdSysID、TxCode、Data、Auth然后submit()。绝对禁止用fetch或axios异步提交——浏览器同源策略和建行网关的 CSRF 防护会直接拦截。!-- 正确纯前端跳转 -- form idpayForm actionhttps://test.sft.ccb.com/gateway methodPOST input typehidden nameThirdSysID value1004 input typehidden nameTxCode valueSFT10001 input typehidden nameData valueA1B2C3D4E5F6... input typehidden nameAuth value4764da69b2cd... /form script // 页面加载后自动提交 document.getElementById(payForm).submit(); /script提示SFT10001的GoPayType字段决定跳转目标——1跳 PC 支付页2跳 H5 支付页3跳小程序需额外配置。不要试图用window.open()模拟建行网关会校验Referer和User-Agent。3.2 联机接口必须用服务端 HttpClient 同步调用超时设置是命门SFT10003订单查询、SFT10005用户绑定、SFT10008日终对账等联机接口是你的服务器直连建行 API。关键参数不是 URL而是超时时间。建行生产环境平均响应 300~800ms但网络抖动时可达 3s。若你设connectTimeout1000、readTimeout1000大概率超时失败而建行不会重试——你的订单状态就卡在“未知”。// Java 示例OkHttpClient 必须设合理超时 OkHttpClient client new OkHttpClient.Builder() .connectTimeout(5, TimeUnit.SECONDS) // 连接超时 ≥5s .readTimeout(10, TimeUnit.SECONDS) // 读取超时 ≥10s对账文件较大 .writeTimeout(5, TimeUnit.SECONDS) // 写入超时 ≥5s .build(); // 构造请求体JSON 格式非 FORM RequestBody body RequestBody.create( MediaType.parse(application/json; charsetutf-8), {\TransID\:\1004202405201030123456789\,\OrderInfos\:[{\Order_No\:\ORD123\}]}); Request request new Request.Builder() .url(https://api.sft.ccb.com/SFT10003) // 注意联机接口是 RESTful URL .post(body) .addHeader(Content-Type, application/json; charsetutf-8) .build();注意联机接口返回Content-Type: application/json而网关接口返回text/html。用错解析方式会导致JSON parse error。3.3 结果通知异步回调的幂等性不是可选项是法律条款SFT20001支付结果通知、SFT20003退款结果通知等回调接口建行会多次推送网络重试、对账补推。文档没写但建行 SLA 要求同一TransID的通知你的服务必须能承受 5 次以内重复推送而不产生重复记账。实现幂等的核心是以TransID为唯一键写入数据库前先SELECT FOR UPDATE或用 RedisSETNX锁住。伪代码如下def handle_payment_notify(request): trans_id request.json.get(TransID) # 1. 尝试获取分布式锁Redis lock_key fsft:notify:lock:{trans_id} if not redis.set(lock_key, 1, ex30, nxTrue): # 30秒锁过期 return OK # 重复请求直接返回成功 try: # 2. 查询数据库是否已处理 if db.query(SELECT 1 FROM notify_log WHERE trans_id ?, trans_id): return OK # 3. 处理业务逻辑更新订单状态、发消息等 process_payment_result(request.json) # 4. 记录已处理 db.execute(INSERT INTO notify_log (trans_id, status) VALUES (?, success), trans_id) finally: redis.delete(lock_key) # 释放锁 return OK提示建行回调地址是你提供的公网 URL必须支持 HTTPS 且证书有效。HTTP 地址会被拒绝。同时你的服务必须在 3 秒内返回200 OK超时即视为失败建行将重试。4. 避坑五个让开发团队集体沉默的高频问题与根治方案对接善付通最痛苦的不是写代码而是那些文档里没写、报错信息模糊、建行客服也说不清的“玄学问题”。以下是我在三个项目中踩出的血坑每一条都附带可立即验证的解决方案。4.1 现象SFT10001跳转后显示“系统繁忙请稍后再试”无任何错误码原因Data字段加密前的 JSON 字符串含中文但 DES 加密时未用 UTF-8 编码导致字节流错乱建行解密失败。解决强制json.dumps(..., ensure_asciiFalse).encode(utf-8)。ensure_asciiFalse是关键否则中文变\u4f60\u597dDES 加密后长度剧增超出建行字段限制。4.2 现象SFT10003查询返回Status2失败Remark参数校验失败但所有字段都按文档填了原因Order_No字段含空格或不可见字符如\u200b零宽空格建行校验时trim()不彻底。解决在组装请求前对所有字符串字段执行str.strip().replace(\u200b, ).replace(\uFEFF, )并用len()校验长度是否超限如Order_No≤30。4.3 现象SFT10008日终对账接口返回 404但 URL 和 Hosts 都配置正确原因建行对账文件生成有严格时间窗——仅在每日02:00-04:00之间可用其他时间调用返回 404。文档未说明但这是生产事实。解决在定时任务中只在02:10-03:50之间调用该接口并捕获 404 后 sleep 300 秒再重试一次。4.4 现象SFT10005用户绑定成功但后续SFT10001支付时仍跳注册页原因BuyerUserID_ThirdSys在绑定接口和支付接口中大小写不一致如绑定用user123支付用USER123建行按精确字符串匹配。解决建立统一的用户 ID 标准化函数在所有接口调用前强制转小写或大写并存入数据库时也标准化。4.5 现象SFT10020预付款支付成功但SFT10021查询返回PayStatus0付款中数小时不更新原因预付款单号PrePayNo在SFT10020请求中未传或传了空字符串建行生成内部单号但SFT10021查询时用了你系统生成的单号无法匹配。解决SFT10020请求中必须传PrePayNo字段建议用UUID生成并在SFT10021中用同一值查询。建行文档未强调此字段必填但它是预付款场景的唯一索引。5. 日终对账与资金轧差用SFT10008文件解析打通财务最后一公里对账不是“查查余额”而是把你的系统订单、建行支付流水、银行清算结果三者逐笔勾稽。SFT10008日终对账接口返回的不是 JSON而是一个固定格式的文本文件.txt这是善付通区别于其他支付平台的关键设计——它把对账逻辑交还给企业自身确保财务人员能用 Excel 直接打开、筛选、打印、归档。5.1 对账文件结构字段位置即协议错一位就全盘失效建行返回的对账文件是定长字段 固定分隔符非 CSV每行代表一笔交易字段按文档定义严格对齐。例如20240520|SFT10001|ORD123|1004|100.00|1|20240520103012|SUCCESS|... 20240520|SFT10009|REF123|1004|20.00|2|20240520103545|FAILED |...关键点第一列Date是对账日期非交易日期20240520表示 2024-05-20 日终对账包含该日所有已清算交易第二列TxCode标识交易类型SFT10001支付SFT10009退款SFT10020预付款第三列Order_No是你系统的订单号与SFT10003查询结果中的Order_No完全一致第六列PayStatus是清算状态1成功2失败4已支付未确认付款需人工介入第七列PayTime是建行清算时间格式yyyyMMddHHmmss用于与你系统时间比对。5.2 解析脚本用 Python 按行切分拒绝正则拥抱split(|)别用正则匹配建行文件偶尔会在备注字段含|正则会崩。必须用line.split(|)并严格校验字段数def parse_recon_file(file_path: str) - list: records [] with open(file_path, r, encodingutf-8) as f: for line_num, line in enumerate(f, 1): line line.strip() if not line: continue fields line.split(|) # 建行文档定义共 12 个字段少于 12 则丢弃脏数据 if len(fields) 12: print(f⚠️ 第 {line_num} 行字段不足12个跳过{line}) continue record { date: fields[0], # 对账日期 tx_code: fields[1], # 交易码 order_no: fields[2], # 你系统的订单号 third_sys_id: fields[3], # 对接系统ID amount: float(fields[4]), # 金额 pay_status: fields[5], # 清算状态 pay_time: fields[6], # 清算时间 result: fields[7], # 结果描述 # ... 其他字段 } records.append(record) return records # 使用 recon_data parse_recon_file(/path/to/recon_20240520.txt)5.3 轧差逻辑三张表比对找出“幽灵订单”和“黑洞退款”真正的对账价值在于发现差异。你需要三张表你系统的订单表含order_no,amount,status,created_at你系统的支付流水表含order_no,pay_no_ccb,pay_status,pay_time建行对账文件解析结果含order_no,amount,pay_status,pay_time。差异类型及处理差异类型你系统状态建行对账状态处理动作幽灵订单有订单无支付流水无记录检查SFT10001是否调用成功或建行网关跳转失败黑洞退款有退款记录statussuccess无退款记录立即调用SFT10009重试建行允许同TransID重试状态漂移pay_status1成功pay_status2失败以建行对账为准回滚你系统订单状态并通知财务核查金额偏差amount100.00amount99.99建行手续费扣除需在你系统中增加“手续费”子订单提示建行对账文件每天凌晨生成但SFT10008接口返回的是文件下载链接URL需用requests.get(url)下载。该 URL 有效期仅 2 小时必须及时下载并解析。6. 从“能跑通”到“可运维”我把日志、监控、回滚做成 checklist现在每次上线都像呼吸一样自然对接善付通的终点不是curl -X POST返回{Status:1}而是当财务在凌晨 2 点打电话问“今天对账差 1 分钱是不是你们系统错了”你能 30 秒内打开 Grafana 看板定位到SFT10008解析脚本里float(fields[4])的精度丢失然后ssh进服务器用sed一行修复再发个钉钉通知“已修正差额已补平”。这才是生产级落地。下面是我沉淀的 checklist每一条都是真金白银换来的教训。6.1 日志必须带上下文TransID是你的生命线所有善付通相关日志必须在开头打上TransID。没有它等于没有日志。我见过太多团队的日志只写INFO: Payment success结果建行反馈某笔TransID1004202405201030123456789失败你得翻遍所有日志 grep耗时 2 小时。import logging logger logging.getLogger(__name__) def log_sft_call(trans_id: str, interface: str, direction: str, payload: dict None, response: dict None): # 方向request 或 response msg f[SFT-{interface}] {direction} | TransID{trans_id} if payload: msg f | PayloadKeys{list(payload.keys())} if response: msg f | Status{response.get(Status, N/A)} | Remark{response.get(Remark, N/A)} logger.info(msg) # 调用示例 log_sft_call(1004202405201030123456789, SFT10001, request, {Order_No: ORD123})6.2 监控必须覆盖“三态”调用态、结果态、对账态建行接口监控不能只看 HTTP 状态码。必须建三个指标调用态sft_api_calls_total{interfaceSFT10001,status200}—— 每分钟调用量结果态sft_notify_received_total{tx_codeSFT20001,pay_status1}—— 每分钟成功支付通知数对账态sft_recon_mismatch_total{typeamount_diff}—— 每日金额差异笔数。用 Prometheus Grafana阈值设为sft_api_calls_total5 分钟内status!200占比 5% → 触发告警sft_notify_received_total连续 15 分钟无pay_status1→ 触发告警支付通道中断sft_recon_mismatch_total0 → 每日早 9 点邮件发送差异明细表。6.3 回滚必须原子化用数据库事务包裹“支付-记账-通知”SFT10001支付成功后你的系统要干三件事① 更新订单状态为“已支付”② 插入支付流水③ 发送站内信。这三步必须在一个数据库事务里。我曾遇到过步骤①②成功步骤③ MQ 发送失败导致用户没收到通知反复点击支付按钮触发重复TransID建行拒收。-- 正确所有操作在事务内 BEGIN TRANSACTION; UPDATE orders SET status paid, paid_at NOW() WHERE order_no ORD123; INSERT INTO payment_logs (order_no, amount, trans_id, created_at) VALUES (ORD123, 100.00, 1004..., NOW()); -- 此处插入 MQ 消息记录表非发 MQ由独立消费者发消息 INSERT INTO mq_outbox (topic, payload, status) VALUES (payment.success, {order_no:ORD123}, pending); COMMIT;6.4 最后一道防线每天凌晨自动执行“对账健康检查”写一个脚本每天 03:00 自动运行做三件事调用SFT10008下载昨日对账文件解析文件统计PayStatus1笔数、总金额查询你数据库统计昨日statuspaid订单笔数、总金额若两者差额 0.01 元或笔数差 1则发邮件给技术负责人 财务负责人。#!/bin/bash # recon_health_check.sh DATE$(date -d yesterday %Y%m%d) python3 /opt/sft/recon_checker.py --date $DATE if [ $? -ne 0 ]; then echo 对账异常$DATE | mail -s 善付通对账告警 techcompany.com financecompany.com fi从那以后我每次上线新接口都强制走一遍这个 checklist日志打标、监控埋点、事务包裹、健康检查。它不再是一次性任务而是呼吸般的习惯。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?