本文目录导读:

这是一个关于 PHP 项目中处理柬埔寨瑞尔 (KHR) 与 eKHR (电子瑞尔,通常指 CBDC 数字版本) 的技术实现方案。
由于 eKHR (数字瑞尔) 在技术栈上通常被视为与物理瑞尔 (KHR) 等值 (1:1 兑换),且由柬埔寨国家银行 (NBC) 的 Bakong 系统管理,因此在 PHP 项目中的核心难点主要集中在金额存储精度、本地化格式化以及与 Bakong API 的集成。
以下是针对 PHP 项目的完整技术指南:
数据类型与存储(核心:避免浮点数)
瑞尔面值极大(1 美元约等于 4,100 瑞尔),且通常涉及小额交易(几百瑞尔)。float 类型会导致精度丢失。
- 数据库字段:始终使用
DECIMAL(18, 0)或BIGINT。- 推荐:以最小单位(瑞尔) 存储整数。
1,000 KHR存储为1000。 - 理由:KHR 是无辅币单位(无分/仙) 的货币(辅币单位 Sen 已几乎不用),因此无需小数位,用整数存储性能最好,且无精度问题。
- 推荐:以最小单位(瑞尔) 存储整数。
- PHP 变量:
- 使用
int或string类型。 - 计算方法:使用
bcmath扩展进行数学运算。
- 使用
<?php // 错误的做法(浮点数) $price = 0.1 + 0.2; // 0.30000000000000004 // 正确的做法(使用 bcmath 处理整数字符串) $price1 = '100'; // 100 瑞尔 $price2 = '250'; $total = bcadd($price1, $price2, 0); // 结果是 '350' ?>
本地化格式化与显示
瑞尔的符号是 (U+17DB),通常写作 KHR 或 ,PHP 的 NumberFormatter 支持高棉语(Khmer)区域设置。
<?php
function formatKHR(int $amountRIEL): string {
// 使用高棉语本地化 (km-KH) 或者通用格式
$fmt = new NumberFormatter('km-KH', NumberFormatter::CURRENCY);
// 注意:NumberFormatter 期望的是一个基于分/仙的值。
// 由于 KHR 没有辅币,我们直接传入整数,并将货币代码设置为 KHR
$formatted = $fmt->formatCurrency($amountRIEL, 'KHR');
// 或者使用简单格式: 1,000 ៛
// 手动构建(避免 NumberFormatter 在某些环境下的 bug)
$formattedSimple = number_format($amountRIEL, 0, '.', ',') . ' ៛';
return $formattedSimple;
}
echo formatKHR(1234500); // 输出示例: 1,234,500 ៛
?>
eKHR / Bakong API 集成(关键)
eKHR 本身不是 PHP 能直接生成的数据,它需要通过柬埔寨国家银行(NBC)的 Bakong 系统 API 进行交互,通常使用 ISO 20022 (银行间支付标准) 标准。
主要场景:
- 创建二维码支付(Merchant Payment):
- PHP 生成符合 Bakong 规范的 QR 码字符串(通常是 EMVCo 标准 + 特定字段)。
- 数据结构:通常包含 Merchant Name, Merchant City, Merchant ID (Bakong ID 或 Account ID), Amount (amount 字段必须是整数,单位 RIEL), Currency (
104代表 KHR,840代表 USD)。
- 发起转账:
通过 Bakong API 请求转账。
示例:生成 Bakong 支付二维码的数据结构
<?php
function buildBakongQRPayload(string $merchantID, int $amountRIEL, string $storeLabel = ''): string {
// 注意:该简化版,实际需要遵循 EMVCo Merchant Presented QR 规范 (Tag-Length-Value)
// 并使用 Bakong 的特定 Tag (Tag 50 替换为商户名等)
// 伪代码示例 - 实际需要封装复杂的 TLV 库
$payload = '000201010212'; // 起始标识
// Merchant Account Info (Tag 26)
$payload .= '26' . sprintf('%02d', strlen($merchantID)) . $merchantID;
// Merchant Name (Tag 59)
$payload .= '59' . sprintf('%02d', strlen($storeLabel)) . $storeLabel;
// Country Code (Tag 58) - KH
$payload .= '5802KH';
// Currency Code (Tag 53) - 104 for KHR
$payload .= '5303104';
// Amount (Tag 54) - 必须为整数,如 1000
$amountStr = (string)$amountRIEL;
$payload .= '54' . sprintf('%02d', strlen($amountStr)) . $amountStr;
// CRC (Tag 63)
$payload .= '6304' . calculateCRC16($payload . '6304');
return $payload;
}
function calculateCRC16(string $data): string {
// CRC16 计算逻辑(省略,此为关键步骤)
// 需要使用 ISO/IEC 13239 标准的多项式 0x1021
return 'ABCD'; // Placeholder
}
?>
调用 Bakong API 转账(PHP Curl 示例):
<?php
function sendKHRSettlement(int $amountRIEL, string $senderAccount, string $receiverAccount): bool {
$apiEndpoint = 'https://api.bakong.com/...'; // 生产环境端点
$apiKey = 'YOUR_API_KEY';
$payload = [
'sender' => $senderAccount,
'receiver' => $receiverAccount,
'amount' => $amountRIEL, // 关键:必须是整数
'currency' => 'KHR',
'description' => 'Payment for Order #123'
];
$ch = curl_init($apiEndpoint);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'Authorization: Bearer ' . $apiKey
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return $httpCode === 200;
}
?>
处理多币种(KHR + USD 双币制)
柬埔寨是典型的双币制国家(美元和瑞尔同时流通),你的 PHP 应用必须处理两种货币。
- 策略:在数据库或 Session 中存储用户的本地货币偏好(Locale)。
- 舍入规则:
- 美元交易通常保留 2 位小数。
- 瑞尔交易通常是整数(无分),如果你进行 USD 到 KHR 的转换,必须注意尾数处理。
- 注意:NBC 要求商户不能歧视任何币种,必须都支持。
<?php
function convertAndRound(int $amountRIEL, float $exchangeRate, bool $toUSD): string {
if ($toUSD) {
// RIEL -> USD (保留2位小数)
$resultValue = bcdiv((string)$amountRIEL, (string)$exchangeRate, 4);
return number_format((float)$resultValue, 2);
} else {
// USD -> RIEL (必须为整数)
$resultValue = bcmul((string)$amountRIEL, (string)$exchangeRate, 0);
// 为了保证整数,使用 floor 或 round
return (int) $resultValue;
}
}
?>
安全与审计
- SQL 注入:永远不要拼接 SQL,使用 Prepared Statements,因为金额是用户输入,攻击者可能尝试注入。
- 金额验证:
- 前端和后端都必须验证
$amount > 0。 - PHP 端:
if (!filter_var($amount, FILTER_VALIDATE_INT) || $amount <= 0) { throw new Exception("Invalid KHR amount"); }
- 前端和后端都必须验证
- 日志:所有涉及 eKHR 的转账必须记录完整日志(请求体、响应码、订单ID)。
总结最佳实践
| 场景 | 推荐方式 | 理由 |
|---|---|---|
| 数据库存储 | DECIMAL(18,0) 或 BIGINT |
防止浮点误差,适合大面额整数计算。 |
| PHP 运算 | bcmath 扩展 |
任意精度数学处理。 |
| 前端显示 | number_format() + |
简单可靠,兼容性好。 |
| eKHR 支付 API | 对接 Bakong API (JSON/ISO 20022) | 唯一合法电子瑞尔通道。 |
| 货币转换 | 使用 NBC 每日官方汇率 API | 避免黑市汇率导致的会计风险。 |
| 多币种支持 | 全局预配置 $locale |
切换 KHR 与 USD 显示格式。 |
如果你需要具体的 Bakong API PHP SDK 示例代码或 支付二维码生成库,可以告诉我你的集成阶段(Sandbox 测试还是 Production),我可以提供更具体的实现细节。