如何在 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
其中的空行也是字段的一部分,不能随意删除。字段顺序如下:
BCD:EPC 服务标识。002:格式版本。1:UTF-8 字符集标识。SCT:SEPA Credit Transfer。- BIC:在适用版本中可以留空,但必须保留字段位置。
- 收款人名称。
- 收款人 IBAN。
- 金额,例如
EUR1.00。 - Purpose,可以留空。
- Structured Remittance Reference,可以留空。
- Unstructured Remittance Information,可用于填写付款说明。
- 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 可以处理这笔支付。
备注:内容仅供参考。
