PAYATHON 2026

如何在 PHP 中创建银行 APP 可识别的 Scan&Pay QR code

支付小周

结论

可以生成,但没有一种支付二维码能靠随意拼接字段,就让所有银行 APP 都识别。TCPDF 只负责把字符串编码成 QR Code。银行 APP 是否接受,要看二维码内容是否符合它支持的支付规范。

常见格式包括:

  • 欧元 SEPA 转账:使用 EPC QR Code,也称 SEPA Credit Transfer QR。
  • 捷克银行转账:通常使用 QR Platba 的 SPD 格式,variable symbol 对应 X-VS。
  • 斯洛伐克银行转账:通常使用 PAY by square。它不是简单的明文拼接格式,需要使用符合规范的编码库或服务。
  • 商户收单、POS 或钱包支付:可能使用 EMVCo Merchant Presented QR,但这类格式通常需要收单机构或支付服务商分配标识,不能只根据 IBAN 自行构造。

因此,第一步是确认目标银行 APP 支持哪种规范。普通扫码器能够读出二维码内容,不等于银行 APP 会将其识别为支付指令。

为什么当前字符串无效

下面的内容只是应用自行定义的一段文本:

$codeContents = "TXN:".$variableSymbol
    ."-AMT:".$amount
    ."-MCC:".$merchantCode
    ."-CUR:".$transactionCurrency;

TXN、AMT、MCC、CUR 组成的格式不属于 EPC、SPD 或标准 EMVCo 支付载荷。银行 APP 无法判断这些字段的含义,所以会提示数据无效。

这段内容还有几个问题:

  • "1 EUR" 通常不是合法的金额字段。规范一般要求使用类似 EUR1.00 的写法,或者单独写成 AM:1.00。
  • 978 是 EUR 的 ISO 4217 数字代码,而 EPC QR 使用 EUR。
  • MCC 是 Merchant Category Code,通常是四位行业分类代码,不是收款账号或商户编号。
  • EPC 银行转账使用 IBAN,不需要 MCC。
  • 支付二维码通常不需要对载荷做哈希。真正需要严格遵守的是格式、字段顺序、字符集和换行规则。

方案一:生成 EPC/SEPA 支付二维码

如果需要向 IBAN 发起 EUR SEPA Credit Transfer,可以构造 EPC QR Code 数据。

典型载荷如下:

BCD
002
1
SCT

ACME s.r.o.
SK3112000000198742637541
EUR1.00

1111

其中的空行也是字段的一部分,不能随意删除。字段顺序如下:

  1. BCD:EPC 服务标识。
  2. 002:格式版本。
  3. 1:UTF-8 字符集标识。
  4. SCT:SEPA Credit Transfer。
  5. BIC:在适用版本中可以留空,但必须保留字段位置。
  6. 收款人名称。
  7. 收款人 IBAN。
  8. 金额,例如 EUR1.00。
  9. Purpose,可以留空。
  10. Structured Remittance Reference,可以留空。
  11. Unstructured Remittance Information,可用于填写付款说明。
  12. Beneficiary-to-originator Information,可选。

Structured Remittance Reference 和 Unstructured Remittance Information 不能同时填写。

EPC 规范没有专门的 variable symbol 字段。可以把 1111 写入非结构化付款说明,但银行 APP 不一定会将它显示在本地的 variable symbol 输入框中。

PHP 实现

<?php

function sanitizeEpcText(string $value): string
{
    // EPC 字段以换行分隔,因此不允许业务数据自身包含换行。
    return trim(str_replace(["\r", "\n"], ' ', $value));
}

function buildEpcQrPayload(
    string $beneficiaryName,
    string $iban,
    string $amount,
    string $remittanceInfo,
    string $bic = ''
): string {
    $beneficiaryName = sanitizeEpcText($beneficiaryName);
    $bic = strtoupper(preg_replace('/\s+/', '', $bic));
    $iban = strtoupper(preg_replace('/\s+/', '', $iban));
    $remittanceInfo = sanitizeEpcText($remittanceInfo);

    if (!preg_match('/^[A-Z]{2}[0-9A-Z]{13,32}$/', $iban)) {
        throw new InvalidArgumentException('IBAN format is invalid.');
    }

    if (!preg_match('/^\d{1,8}\.\d{2}$/', $amount)) {
        throw new InvalidArgumentException(
            'Amount must use a decimal point and two decimal places, for example 1.00.'
        );
    }

    $fields = [
        'BCD',
        '002',
        '1',
        'SCT',
        $bic,
        $beneficiaryName,
        $iban,
        'EUR' . $amount,
        '',                  // Purpose
        '',                  // Structured Remittance Reference
        $remittanceInfo,     // Unstructured Remittance Information
        '',                  // Optional information
    ];

    // 删除末尾无意义的可选空字段,不改动中间的空字段。
    while (end($fields) === '') {
        array_pop($fields);
    }

    return implode("\n", $fields);
}

$codeContents = buildEpcQrPayload(
    'ACME s.r.o.',
    'SK3112000000198742637541',
    '1.00',
    'VS 1111'
);

$pdf->write2DBarcode(
    $codeContents,
    'QRCODE,M',
    20,
    20,
    50,
    50,
    $style,
    'N'
);

示例 IBAN 只用于演示代码结构。正式使用前必须替换,并通过可靠的 IBAN 校验程序进行验证。

如果有 ISO 11649 creditor reference,例如 RF18539007547034,应将其放入 Structured Remittance Reference 字段,而不是付款说明字段:

$fields = [
    'BCD',
    '002',
    '1',
    'SCT',
    $bic,
    $beneficiaryName,
    $iban,
    'EUR' . $amount,
    '',
    'RF18539007547034',
    '',
];

方案二:捷克 QR Platba 的 SPD 格式

如果目标银行主要在捷克,而且必须填写 variable symbol,SPD 通常比 EPC 更合适。它的明文载荷通常类似:

SPD*1.0*ACC:CZ6508000000192000145399*AM:1.00*CC:EUR*X-VS:1111*RN:ACME s.r.o.*MSG:Invoice 20260001

PHP 可以这样构造:

<?php

function sanitizeSpdValue(string $value): string
{
    // "*" 是字段分隔符,不能直接出现在字段值中。
    return trim(str_replace(
        ["*", "\r", "\n"],
        [" ", " ", " "],
        $value
    ));
}

function buildSpdQrPayload(
    string $iban,
    string $amount,
    string $currency,
    string $variableSymbol,
    string $recipientName,
    string $message
): string {
    $iban = strtoupper(preg_replace('/\s+/', '', $iban));
    $currency = strtoupper($currency);
    $variableSymbol = preg_replace('/\D+/', '', $variableSymbol);

    if (!preg_match('/^[A-Z]{2}[0-9A-Z]{13,32}$/', $iban)) {
        throw new InvalidArgumentException('IBAN format is invalid.');
    }

    if (!preg_match('/^\d{1,8}\.\d{2}$/', $amount)) {
        throw new InvalidArgumentException('Invalid amount format.');
    }

    if (!preg_match('/^[A-Z]{3}$/', $currency)) {
        throw new InvalidArgumentException('Currency must be an ISO 4217 code.');
    }

    $fields = [
        'SPD',
        '1.0',
        'ACC:' . $iban,
        'AM:' . $amount,
        'CC:' . $currency,
        'X-VS:' . $variableSymbol,
        'RN:' . sanitizeSpdValue($recipientName),
        'MSG:' . sanitizeSpdValue($message),
    ];

    return implode('*', $fields);
}

$codeContents = buildSpdQrPayload(
    'CZ6508000000192000145399',
    '1.00',
    'EUR',
    '1111',
    'ACME s.r.o.',
    'Invoice 20260001'
);

$pdf->write2DBarcode(
    $codeContents,
    'QRCODE,M',
    20,
    20,
    50,
    50,
    $style,
    'N'
);

SPD 支持哪些字段,以及字段长度和字符限制,应以目标银行采用的 QR Platba 版本为准。某一家银行能够识别,并不代表所有欧洲银行都支持。

为什么不能直接套用 EMVCo

EMVCo Merchant Presented QR 按照 ID-Length-Value 形式组织字段,并在末尾加入 CRC。它适用于商户收单体系,通常还需要:

  • 支付网络或收单机构分配的标识;
  • Merchant Account Information;
  • Merchant Category Code;
  • 交易币种和国家代码;
  • 商户名称及城市;
  • CRC-16/CCITT-FALSE 校验值;
  • 当地支付方案要求的附加字段。

只填写一个所谓的 $merchantCode,无法生成可用的 EMVCo 支付码。即使载荷在语法上符合 EMVCo,银行仍可能因为无法识别其中的支付网络或账户标识而拒绝处理。

如果收单机构没有明确要求使用 EMVCo,而需求只是向 IBAN 转账,就不应采用这种格式。

TCPDF 方面的注意事项

TCPDF 不负责决定支付格式。只要 $codeContents 正确,它就能生成相应的二维码。输出时还需要注意以下事项:

  • 使用 UTF-8,并避免隐藏的 BOM。
  • EPC 字段必须用 \n 分隔,不能改成 HTML 的 <br>。
  • 金额使用小数点,不带千位分隔符,例如 1234.50。
  • IBAN 中不能包含空格。
  • 二维码四周要保留足够的空白区。
  • 二维码不能太小,尤其要考虑 PDF 打印后的尺寸。
  • EPC 通常使用中等级别的纠错即可,例如 TCPDF 的 QRCODE,M。
  • 收款人名称、付款说明等字段必须符合所选规范的长度和字符限制。
  • 生成二维码后,不要对图片进行非等比缩放或有损压缩。

应该选择哪一种格式

如果客户来自多个 SEPA 国家,只需要预填 IBAN、收款人、EUR 金额和付款说明,优先使用 EPC QR。

如果客户主要使用捷克银行,而且 variable symbol 必须写入银行的专用字段,应使用 SPD/QR Platba,并确认目标银行支持这种格式。

如果目标是斯洛伐克银行常用的 PAY by square,不能用上述 SPD 字符串代替。PAY by square 有特定的数据序列化、压缩和校验流程,应使用经过验证的实现。

如果业务涉及银行卡或钱包的商户收款,应先向支付服务商取得 EMVCo 实现要求、商户标识和测试工具,再生成二维码。

最终至少要使用几家实际目标银行的 APP 进行测试。支付二维码能否兼容,取决于银行支持的规范。普通二维码阅读器能够读出原始文本,并不足以证明银行 APP 可以处理这笔支付。

备注:内容仅供参考。