PHP项目数字新加坡元与eSGD

wen PHP项目 1

本文目录导读:

PHP项目数字新加坡元与eSGD

  1. 理解概念
  2. PHP中的货币处理核心原则
  3. 推荐实现方案
  4. eSGD 特殊需求处理
  5. 最佳实践与注意事项
  6. 完整示例(Laravel + moneyphp)
  7. 未来兼容性

针对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、负值(如有退款)
审计日志 所有金额变动记录 typeold_centsnew_centsoperator

完整示例(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 字段为 SGDESGD 两个独立 ISO 代号,即使当前挂钩1:1。

项目类型 推荐方案 理由
企业级应用 moneyphp/money + 类型字段 安全、测试充分、易扩展
快速原型 整数分 + bcmath 无额外依赖,逻辑清晰
微服务/API 同上,返回字符串金额 避免浮点传输

核心记住:在PHP中永远不要用 float 存储或计算任何货币,包括 eSGD。

抱歉,评论功能暂时关闭!