1. 工商银行 B2C 聚合支付简介

在这里插入图片描述

工商银行 B2C 聚合支付是工行为商户提供的一站式收款解决方案,整合了网银支付、快捷支付、微信支付、支付宝等多种主流支付通道。商户只需对接一套接口,即可同时支持用户在 PC 端、H5 端通过各家钱包完成付款,极大降低多通道接入和维护成本。

核心特点:

  • 通道聚合:一个商户号覆盖国内主流支付方式,无需分别签约。
  • 统一接口:下单、支付通知、退款等关键流程标准统一。
  • 安全合规:银行级风控与资金清算,满足监管要求。
  • 全场景覆盖:PC 电脑端、手机 H5、公众号/小程序均可接入。

2. 业务流程说明

一次典型的 B2C 聚合支付交互流程如下:

    participant 用户
    participant 商户前端
    participant 商户后端
    participant 工行聚合网关
    用户->>商户前端:选择商品,点击“去支付”
    商户前端->>商户后端:提交订单信息(金额、订单号等)
    商户后端->>工行聚合网关:调用统一下单接口(带签名)
    工行聚合网关-->>商户后端:返回预支付交易单号与支付跳转地址
    商户后端-->>商户前端:将跳转地址或收银台信息返回页面
    商户前端->>工行聚合网关:跳转到工行收银台(或打开 H5 支付页)
    用户->>工行聚合网关:在收银台选择支付方式并完成支付
    工行聚合网关-->>商户后端:异步通知支付结果(携带订单号与签名)
    商户后端-->>工行聚合网关:返回“SUCCESS”确认收到通知
    商户后端-->>商户前端:展示支付成功页面

步骤要点:

  1. 统一下单
    商户后端组装订单号、金额、通知地址等参数,按工行提供的签名算法生成签名,调用聚合下单 API 获取 prepay_id 和收银台跳转地址。

  2. 唤起收银台
    PC 场景下使用工行提供的 PC 收银台页面链接;移动端则按 SDK 或 H5 参数打开支付页面。用户选择支付方式完成付款。

  3. 支付结果通知
    工行网关以 POST 方式向商户配置的 notify_url 发送支付结果,包含订单号、交易流水、支付状态等字段。商户必须验签成功后再处理业务,最后返回大写 SUCCESS 字符串。

  4. 退款与查询
    商户可通过退款接口发起全额或部分退款,并通过订单查询接口主动确认支付状态。

3. 技术对接准备

  • 商户号与密钥:向工行申请聚合支付商户号,获取 mch_idapi_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 场景)、重复通知处理和接口版本升级,确保支付链路安全稳定。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐