工商银行 B2C 聚合支付对接全指南(Python / PHP / Java)
·
1. 工商银行 B2C 聚合支付简介

工商银行 B2C 聚合支付是工行为商户提供的一站式收款解决方案,整合了网银支付、快捷支付、微信支付、支付宝等多种主流支付通道。商户只需对接一套接口,即可同时支持用户在 PC 端、H5 端通过各家钱包完成付款,极大降低多通道接入和维护成本。
核心特点:
- 通道聚合:一个商户号覆盖国内主流支付方式,无需分别签约。
- 统一接口:下单、支付通知、退款等关键流程标准统一。
- 安全合规:银行级风控与资金清算,满足监管要求。
- 全场景覆盖:PC 电脑端、手机 H5、公众号/小程序均可接入。
2. 业务流程说明
一次典型的 B2C 聚合支付交互流程如下:
participant 用户
participant 商户前端
participant 商户后端
participant 工行聚合网关
用户->>商户前端:选择商品,点击“去支付”
商户前端->>商户后端:提交订单信息(金额、订单号等)
商户后端->>工行聚合网关:调用统一下单接口(带签名)
工行聚合网关-->>商户后端:返回预支付交易单号与支付跳转地址
商户后端-->>商户前端:将跳转地址或收银台信息返回页面
商户前端->>工行聚合网关:跳转到工行收银台(或打开 H5 支付页)
用户->>工行聚合网关:在收银台选择支付方式并完成支付
工行聚合网关-->>商户后端:异步通知支付结果(携带订单号与签名)
商户后端-->>工行聚合网关:返回“SUCCESS”确认收到通知
商户后端-->>商户前端:展示支付成功页面
步骤要点:
-
统一下单
商户后端组装订单号、金额、通知地址等参数,按工行提供的签名算法生成签名,调用聚合下单 API 获取prepay_id和收银台跳转地址。 -
唤起收银台
PC 场景下使用工行提供的 PC 收银台页面链接;移动端则按 SDK 或 H5 参数打开支付页面。用户选择支付方式完成付款。 -
支付结果通知
工行网关以 POST 方式向商户配置的notify_url发送支付结果,包含订单号、交易流水、支付状态等字段。商户必须验签成功后再处理业务,最后返回大写SUCCESS字符串。 -
退款与查询
商户可通过退款接口发起全额或部分退款,并通过订单查询接口主动确认支付状态。
3. 技术对接准备
- 商户号与密钥:向工行申请聚合支付商户号,获取
mch_id、api_key(或证书)。 - 签名算法:通常采用 MD5 或 RSA 签名,按参数名 ASCII 排序后拼接 key 生成。
- 接口地址:测试环境和生产环境分别提供独立的域名(生产环境为
https://api.icbc.com.cn,具体以银行最新文档为准)。 - 回调处理:
notify_url必须为外网可访问的绝对路径,且需正确处理重复通知。
下面的代码示例使用简化的模拟接口地址和签名方式,实际开发请严格参照工行官方文档替换。
4. 代码实现(Python / PHP / Java)
以下提供三种后端语言的聚合支付下单、回调验签实现示例。
4.1 Python 示例
import hashlib
import requests
import json
import time
import xml.etree.ElementTree as ET
# 商户配置
MCH_ID = "商户号"
API_KEY = "api密钥"
NOTIFY_URL = "https://your.domain.com/notify"
ORDER_URL = "https://icbc-api.example.com/gateway" # 示例地址
def generate_sign(params: dict, key: str) -> str:
"""按参数名 ASCII 排序并拼接 key 后 MD5 生成签名"""
sorted_params = sorted(params.items(), key=lambda x: x[0])
sign_str = "&".join(f"{k}={v}" for k, v in sorted_params if v not in (None, ""))
sign_str += f"&key={key}"
return hashlib.md5(sign_str.encode("utf-8")).hexdigest().upper()
def unified_order(out_trade_no: str, total_fee: int, body: str, attach: str = ""):
"""统一下单接口"""
params = {
"mch_id": MCH_ID,
"out_trade_no": out_trade_no,
"total_fee": str(total_fee), # 单位:分
"body": body,
"notify_url": NOTIFY_URL,
"attach": attach,
"nonce_str": str(int(time.time() * 1000))[-12:],
}
params["sign"] = generate_sign(params, API_KEY)
# 示例使用 XML 格式提交,部分工行接口要求 application/xml
xml_body = "<xml>\n"
for k, v in params.items():
xml_body += f" <{k}>{v}</{k}>\n"
xml_body += "</xml>"
resp = requests.post(ORDER_URL, data=xml_body.encode("utf-8"),
headers={"Content-Type": "application/xml"})
if resp.status_code == 200:
root = ET.fromstring(resp.text)
result = {child.tag: child.text for child in root}
if result.get("return_code") == "SUCCESS":
return result.get("prepay_id"), result.get("pay_url")
else:
raise Exception(f"下单失败: {result.get('return_msg')}")
else:
raise Exception(f"HTTP 错误: {resp.status_code}")
def verify_notify_sign(data: str, sign_param: str) -> bool:
"""验签:将 XML 数据转为 dict 并验证签名"""
root = ET.fromstring(data)
params = {child.tag: child.text for child in root if child.tag != "sign"}
calc_sign = generate_sign(params, API_KEY)
return calc_sign == sign_param
# Flask 示例回调处理
from flask import Flask, request
app = Flask(__name__)
@app.route("/notify", methods=["POST"])
def notify():
raw_data = request.data.decode("utf-8")
# 假设 sign 已包含在 XML 中或通过单独参数传递
root = ET.fromstring(raw_data)
sign_node = root.find("sign")
sign = sign_node.text if sign_node is not None else ""
if verify_notify_sign(raw_data, sign):
# 处理订单逻辑
return "SUCCESS"
return "FAIL"
if __name__ == "__main__":
app.run(port=8080)
4.2 PHP 示例
<?php
// 商户配置
define('MCH_ID', '商户号');
define('API_KEY', 'api密钥');
define('NOTIFY_URL', 'https://your.domain.com/notify');
define('ORDER_URL', 'https://icbc-api.example.com/gateway');
function generateSign(array $params, string $key): string {
ksort($params);
$signStr = '';
foreach ($params as $k => $v) {
if ($v !== null && $v !== '') {
$signStr .= "{$k}={$v}&";
}
}
$signStr .= "key={$key}";
return strtoupper(md5($signStr));
}
function unifiedOrder(string $outTradeNo, int $totalFee, string $body, string $attach = '') {
$params = [
'mch_id' => MCH_ID,
'out_trade_no' => $outTradeNo,
'total_fee' => $totalFee,
'body' => $body,
'notify_url' => NOTIFY_URL,
'attach' => $attach,
'nonce_str' => substr(strval(time() . mt_rand()), 0, 12),
];
$params['sign'] = generateSign($params, API_KEY);
$xml = '<xml>';
foreach ($params as $key => $val) {
$xml .= "<{$key}>{$val}</{$key}>";
}
$xml .= '</xml>';
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, ORDER_URL);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/xml']);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
$xmlObj = simplexml_load_string($response);
if ($xmlObj === false) throw new Exception('XML 解析失败');
$result = json_decode(json_encode($xmlObj), true);
if ($result['return_code'] === 'SUCCESS') {
return [$result['prepay_id'], $result['pay_url']];
}
throw new Exception('下单失败: ' . ($result['return_msg'] ?? '未知错误'));
}
function verifyNotifySign(string $xmlStr, string $signParam): bool {
$xmlObj = simplexml_load_string($xmlStr);
$params = json_decode(json_encode($xmlObj), true);
unset($params['sign']);
$calc = generateSign($params, API_KEY);
return $calc === $signParam;
}
// 回调处理示例
$rawPost = file_get_contents('php://input');
$xml = simplexml_load_string($rawPost);
$sign = (string)$xml->sign;
if (verifyNotifySign($rawPost, $sign)) {
// 处理订单
echo 'SUCCESS';
} else {
echo 'FAIL';
}
4.3 Java 示例
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.*;
public class IcbcAggregatePay {
private static final String MCH_ID = "商户号";
private static final String API_KEY = "api密钥";
private static final String NOTIFY_URL = "https://your.domain.com/notify";
private static final String ORDER_URL = "https://icbc-api.example.com/gateway";
// 生成签名
public static String generateSign(Map<String, String> params, String key) {
List<String> keys = new ArrayList<>(params.keySet());
Collections.sort(keys);
StringBuilder sb = new StringBuilder();
for (String k : keys) {
String v = params.get(k);
if (v != null && !v.isEmpty()) {
sb.append(k).append("=").append(v).append("&");
}
}
sb.append("key=").append(key);
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] digest = md.digest(sb.toString().getBytes(StandardCharsets.UTF_8));
StringBuilder hex = new StringBuilder();
for (byte b : digest) {
String h = Integer.toHexString(0xFF & b);
if (h.length() == 1) hex.append("0");
hex.append(h);
}
return hex.toString().toUpperCase();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
// 统一下单
public static Map<String, String> unifiedOrder(String outTradeNo, int totalFee, String body) throws Exception {
Map<String, String> params = new LinkedHashMap<>();
params.put("mch_id", MCH_ID);
params.put("out_trade_no", outTradeNo);
params.put("total_fee", String.valueOf(totalFee));
params.put("body", body);
params.put("notify_url", NOTIFY_URL);
params.put("nonce_str", UUID.randomUUID().toString().replace("-", "").substring(0, 12));
String sign = generateSign(params, API_KEY);
params.put("sign", sign);
// 构造 XML
StringBuilder xml = new StringBuilder("<xml>\n");
for (Map.Entry<String, String> e : params.entrySet()) {
xml.append(" <").append(e.getKey()).append(">")
.append(e.getValue()).append("</").append(e.getKey()).append(">\n");
}
xml.append("</xml>");
URL url = new URL(ORDER_URL);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("POST");
conn.setDoOutput(true);
conn.setRequestProperty("Content-Type", "application/xml");
try (OutputStream os = conn.getOutputStream()) {
os.write(xml.toString().getBytes(StandardCharsets.UTF_8));
}
if (conn.getResponseCode() == 200) {
try (BufferedReader br = new BufferedReader(
new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
StringBuilder resp = new StringBuilder();
String line;
while ((line = br.readLine()) != null) resp.append(line);
Document doc = DocumentHelper.parseText(resp.toString());
Element root = doc.getRootElement();
Map<String, String> result = new HashMap<>();
for (Element child : root.elements()) {
result.put(child.getName(), child.getText());
}
return result;
}
}
throw new IOException("HTTP Error " + conn.getResponseCode());
}
// 验签
public static boolean verifyNotifySign(String xmlData, String signParam) throws Exception {
Document doc = DocumentHelper.parseText(xmlData);
Element root = doc.getRootElement();
Map<String, String> params = new HashMap<>();
for (Element child : root.elements()) {
if (!"sign".equals(child.getName())) {
params.put(child.getName(), child.getText());
}
}
String calc = generateSign(params, API_KEY);
return calc.equals(signParam);
}
// Servlet 回调示例
public void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
String raw = new String(request.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
Document doc = DocumentHelper.parseText(raw);
String sign = doc.getRootElement().elementText("sign");
if (verifyNotifySign(raw, sign)) {
response.getWriter().write("SUCCESS");
} else {
response.getWriter().write("FAIL");
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
5. 代码说明
- 三种语言示例都遵循同一签名算法:按键名排序、拼接、追加
key后 MD5 并转大写。 - 下单请求以 XML 格式发送,这是银行接口常见形式,实际可能还有 JSON 版本,以银行最新文档为准。
- 回调验签时,务必从原始通知体中去除
sign字段后重新计算签名,避免篡改。 - 生产环境建议将密钥存放在环境变量或配置中心,证书类材料需额外处理。
6. 总结
对接工商银行 B2C 聚合支付,本质上就是通过统一的统一下单 + 收银台唤起 + 异步通知流程,将不同支付方式抽象为一致的 API 调用。Python、PHP、Java 三种语言的实现思路完全一致,只是语言特性和依赖库略有差异:
- Python 简洁高效,适合快速原型和中小型项目;
- PHP 生态成熟,大量传统 Web 应用快速集成;
- Java 规范严谨,适合大型企业级生产系统。
建议在实际开发中,仔细阅读工行官方技术文档,重点注意签名算法、证书管理(RSA 场景)、重复通知处理和接口版本升级,确保支付链路安全稳定。
更多推荐

所有评论(0)