本文目录导读:

针对PHP项目中处理数字新加坡元(SGD) 与数字新加坡元(eSGD,即电子新加坡元) 的需求,以下是完整的实现方案、最佳实践与注意事项。
理解概念
- 新加坡元 (SGD):新加坡法定货币,最小单位是 1分 (cent),1 SGD = 100 cents。
- eSGD (Digital Singapore Dollar):新加坡金融管理局 (MAS) 测试中的央行数字货币 (CBDC),与SGD 1:1挂钩,数字形式流通,在业务处理中,通常需要与普通SGD相同的精度和格式,但可能需要标识来源或用途。
PHP中的货币处理核心原则
精度问题
PHP的float不适合精确货币计算,必须使用:
- 整数(以分为单位)
bcmath扩展(任意精度数学)moneyphp/money库(推荐专业方案)
数据库存储
- 存储为 整数(分),如 1000 表示 10.00 SGD
- 或使用
DECIMAL(10,2)但需确保框架/PHP端用字符串或整数处理
推荐实现方案
方案 A:使用 moneyphp/money 库(专业级)
composer require moneyphp/money
优势:
- 内置多位货币,SGD 作为 iso 代码支持
- 强制使用最小单位进行运算
- 支持格式化、分配、比较
use Money\Money;
use Money\Currency;
use Money\Formatter\DecimalMoneyFormatter;
use Money\Parser\DecimalMoneyParser;
// 创建 10.50 SGD => 1050 cents
$money = new Money(1050, new Currency('SGD'));
// 运算示例:加 5.20
$result = $money->add(new Money(520, new Currency('SGD')));
// 解析字符串
$parser = new DecimalMoneyParser(...);
$parsed = $parser->parse('15.75', 'SGD'); // 1575
// 格式化输出
$formatter = new DecimalMoneyFormatter(...);
echo $formatter->format($result); // "15.70"
扩展处理 eSGD:
可以定义 ESGD 作为虚拟货币代码,或使用同一个 SGD 货币,增加一个 type 字段区分。
class MoneyWithType {
public Money $amount;
public string $type; // 'sgd' 或 'esgd'
}
方案 B:使用整数 + bcmath(轻量级)
适合无框架约束的小型项目。
class SGDMoney {
private int $cents; // 始终以分为单位
public static function fromString(string $decimalString): self {
// 安全去除千分位,保留两位小数
$clean = str_replace(',', '', $decimalString);
$parts = explode('.', $clean);
$major = $parts[0];
$minor = $parts[1] ?? '00';
$minor = str_pad(substr($minor, 0, 2), 2, '0');
$amount = bcmul("$major.$minor", '100', 0);
return new self((int)$amount);
}
public function add(SGDMoney $other): self {
return new self($this->cents + $other->cents);
}
public function toDecimalString(): string {
$major = intdiv($this->cents, 100);
$minor = $this->cents % 100;
return sprintf('%d.%02d', $major, $minor);
}
}
// 使用
$price = SGDMoney::fromString('12.50');
$tax = SGDMoney::fromString('1.00');
$total = $price->add($tax);
echo $total->toDecimalString(); // 13.50
eSGD 特殊需求处理
标识区分
- 在数据库
transactions表中增加currency_type字段,可选值'SGD'或'ESGD' - 在前端显示时加注 eSGD 图标或
eS$前缀
兑换/转换
如果需要转换 eSGD <-> SGD(1:1),只需修改 currency_type 字段,金额不变,但注意审计要求:可能需要记录转换日志。
合规与精度
eSGD 与 SGD 精度一致(2位小数),因此货币处理代码完全复用。
最佳实践与注意事项
| 关注点 | 建议做法 |
|---|---|
| 输入验证 | 正则匹配 /^\d{1,10}(\.\d{1,2})?$/,防止 SQL 注入与精度丢失 |
| 输出格式化 | 使用 number_format($amount, 2, '.', ',') 或 Money 的格式化 |
| 前端显示 | 用 S$ 或 S$10.50;eSGD 可用 eS$10.50 或加蓝点标记 |
| 数据库索引 | 对 (currency_type, created_at) 建立复合索引 |
| 框架集成(Laravel) | 推荐 moneyphp/laravel-money 包 |
| 单元测试 | 覆盖边界值:0.00、0.01、999999.99、负值(如有退款) |
| 审计日志 | 所有金额变动记录 type、old_cents、new_cents、operator |
完整示例(Laravel + moneyphp)
// 控制器
use Money\Money;
use App\Models\Transaction;
public function store(Request $request) {
$request->validate([
'amount' => 'required|regex:/^\d+(\.\d{1,2})?$/',
'type' => 'required|in:sgd,esgd'
]);
$money = Money::SGD((int)(bcmul($request->amount, '100', 0))); // 转为分
Transaction::create([
'amount_cents' => $money->getAmount(),
'currency_type' => $request->type,
'formatted' => $money->format() // 字符串如 "S$10.50"
]);
}
// 模型访问器
public function getDisplayAttribute(): string {
$prefix = $this->currency_type === 'esgd' ? 'eS$' : 'S$';
$money = new Money($this->amount_cents, new Currency('SGD'));
return $prefix . $money->getAmount() / 100;
}
未来兼容性
- 若 eSGD 未来可能改变精度(如4位小数),则务必使用
Money类或bcmath,禁止使用float。 - 保留
currency_code字段为SGD和ESGD两个独立 ISO 代号,即使当前挂钩1:1。
| 项目类型 | 推荐方案 | 理由 |
|---|---|---|
| 企业级应用 | moneyphp/money + 类型字段 |
安全、测试充分、易扩展 |
| 快速原型 | 整数分 + bcmath | 无额外依赖,逻辑清晰 |
| 微服务/API | 同上,返回字符串金额 | 避免浮点传输 |
核心记住:在PHP中永远不要用 float 存储或计算任何货币,包括 eSGD。