本文目录导读:

- 核心架构原则
- 数据模型设计(MySQL/PostgreSQL 示例)
- PHP 金额处理核心类
- 核心业务逻辑:DKK 与 eDKK 之间的兑换与支付
- 与外部 eDKK 区块链/中心化网络的集成
- 需要注意的安全与合规问题
- 简化的文件结构建议
这是一个非常有意思且专业的问题,在PHP项目中处理 数字格林兰克朗(DKK) 与 eDKK(电子克朗/数字货币克朗)时,核心挑战不在于PHP本身,而在于金融精度、货币表示以及法定货币与数字货币的交互模型。
由于当前(2025年)格林兰尚未推出独立的法定数字货币(eDKK仍是丹麦央行的试点或在格林兰的本地化方案),以下方案假设你正在构建一个支持 DKK(传统克朗) 与 eDKK(本地电子克朗) 并行支付、结算与记账的系统。
核心架构原则
- 绝对避免浮点数:使用
string或int(最小单位:欧尔)表示金额。 - 统一存储层:在数据库中,将DKK与eDKK视为同一货币的两种“发行形式”,但账户余额分开记录。
- 汇率模型:理论上1 eDKK = 1 DKK(固定汇率1:1),但需处理兑换手续费、每日限额或机构间结算汇率差。
数据模型设计(MySQL/PostgreSQL 示例)
-- 账户表:支持多币种(DKK/eDKK)余额
CREATE TABLE accounts (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
currency_type ENUM('DKK', 'eDKK') NOT NULL, -- 明确区分
balance DECIMAL(19, 4) NOT NULL DEFAULT 0.0000, -- 精度到0.0001克朗(1/10欧尔)
version INT UNSIGNED DEFAULT 1, -- 乐观锁防并发
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_currency (user_id, currency_type)
);
-- 交易流水表:记录所有资金变动
CREATE TABLE transactions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
account_id BIGINT UNSIGNED NOT NULL,
transaction_type ENUM('DEPOSIT', 'WITHDRAWAL', 'TRANSFER_IN', 'TRANSFER_OUT', 'EXCHANGE'),
currency_type ENUM('DKK', 'eDKK') NOT NULL,
amount DECIMAL(19, 4) NOT NULL, -- 正数表示增加,负数表示减少
fee DECIMAL(19, 4) NOT NULL DEFAULT 0.0000,
balance_after DECIMAL(19, 4) NOT NULL, -- 交易后余额
reference_id VARCHAR(64) NULL, -- eDKK交易哈希或DKK银行流水号
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
PHP 金额处理核心类
<?php
declare(strict_types=1);
class DanishKrone
{
private string $amount; // 始终以“欧尔(ører)”为单位存储整数(1 DKK = 100 øre)
private string $currency; // 'DKK' 或 'eDKK'
public function __construct(string $amount, string $currency = 'DKK')
{
// 输入格式:支持 "123.45" 或 "12345"(若仅整数)
// 内部转为最小整数单位(øre)
$this->amount = $this->normalizeToOre($amount);
$this->currency = strtoupper($currency);
if (!in_array($this->currency, ['DKK', 'EDKK'])) {
throw new InvalidArgumentException("不支持的货币类型: {$currency}");
}
}
// 将用户输入的金额转为欧尔(整数)
private function normalizeToOre(string $input): string
{
// 处理小数点,最多保留2位小数(欧尔无更小单位)
if (str_contains($input, '.')) {
$parts = explode('.', $input);
$integer = $parts[0];
$decimal = str_pad(substr($parts[1], 0, 2), 2, '0');
return bcmul("{$integer}.{$decimal}", '100', 0); // 乘以100
}
return bcmul($input, '100', 0);
}
// 显示为克朗(输出如 "123.45 DKK")
public function display(): string
{
$krone = bcdiv($this->amount, '100', 2); // 除以100保留2位
return $krone . ' ' . $this->currency;
}
// 加法(务必使用bcmath扩展)
public function add(self $other): self
{
if ($this->currency !== $other->currency) {
throw new RuntimeException("不能直接相加不同货币类型!请先兑换。");
}
$newOre = bcadd($this->amount, $other->amount, 0);
$new = clone $this;
$new->amount = $newOre;
return $new;
}
// 获取当前余额(用于数据库存储,将欧尔换算为DECIMAL(19,4)格式)
public function getDecimalForDB(): string
{
return bcdiv($this->amount, '100', 4); // 精确到4位小数存入DB
}
// 静态工厂:从DB读取的余额(DECIMAL)创建对象
public static function fromDB(string $decimalAmount, string $currency): self
{
$ore = bcmul($decimalAmount, '100', 0);
$instance = new self('0', $currency);
$instance->amount = $ore;
return $instance;
}
}
核心业务逻辑:DKK 与 eDKK 之间的兑换与支付
A. 用户内部兑换(1:1固定汇率 + 手续费)
<?php
class CurrencyExchangeService
{
private float $exchangeFeeRate = 0.005; // 0.5% 手续费
public function exchange(
int $userId,
string $fromCurrency, // 'DKK' 或 'eDKK'
string $toCurrency,
string $amountKrone // 输入的是克朗数,如 "100.00"
): array {
// 1. 起止金额验证
$amount = new DanishKrone($amountKrone, $fromCurrency);
$fee = new DanishKrone(bcmul($amountKrone, (string)$this->exchangeFeeRate, 4), $fromCurrency);
$netAmount = $amount->subtract($fee); // 实际转出金额
// 2. 数据库事务中执行(伪代码)
DB::beginTransaction();
try {
// 扣除源账户
$fromAccount = Account::lockForUpdate($userId, $fromCurrency);
if ($fromAccount->balance < $amount->getDecimalForDB()) {
throw new InsufficientBalanceException();
}
$fromAccount->decrement('balance', $amount->getDecimalForDB());
// 增加目标账户(按净额)
$toAccount = Account::lockForUpdate($userId, $toCurrency);
$toAccount->increment('balance', $netAmount->getDecimalForDB());
// 记录交易
Transaction::create([
'user_id' => $userId,
'type' => 'EXCHANGE',
'from_currency' => $fromCurrency,
'to_currency' => $toCurrency,
'gross_amount' => $amount->getDecimalForDB(),
'fee' => $fee->getDecimalForDB(),
'net_amount' => $netAmount->getDecimalForDB(),
]);
DB::commit();
return ['status' => 'success', 'net_received' => $netAmount->display()];
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
}
B. 支付场景:用户选择用 eDKK 付款
<?php
class PaymentProcessor
{
public function payWithEDKK(int $userId, string $totalAmountDKK): bool
{
$amount = new DanishKrone($totalAmountDKK, 'EDKK');
// 1. 检查 eDKK 余额
$eDKKAccount = Account::where(['user_id' => $userId, 'currency_type' => 'eDKK'])->first();
if (!$eDKKAccount || $eDKKAccount->balance < $amount->getDecimalForDB()) {
// 可选:自动从DKK兑换差额
$this->autoTopUpFromDKK($userId, $amount);
return false;
}
// 2. 扣除 eDKK 余额
DB::transaction(function() use ($eDKKAccount, $amount, $userId) {
$eDKKAccount->decrement('balance', $amount->getDecimalForDB());
// 3. 向商户结算(假设商户只收DKK)
// 此处需要将 eDKK 兑换为 DKK 再结算,或系统内部统一记账
$merchantAccount = Account::where(['user_id' => $merchantId, 'currency_type' => 'DKK'])->first();
$merchantAccount->increment('balance', $amount->getDecimalForDB()); // 即时兑换
// 记录内部清算流水
});
return true;
}
}
与外部 eDKK 区块链/中心化网络的集成
若 eDKK 运行在区块链或独立结算网络上,需在 PHP 中调用 SDK:
<?php
// 假设有一个 eDKK 钱包节点 API
class EDKKGateway
{
private string $apiEndpoint = 'https://edkk.greenland.gl';
private string $apiKey;
// 发起链上转账
public function sendTransaction(string $fromAddress, string $toAddress, string $amountOre): string
{
$response = Http::withHeaders([
'X-API-Key' => $this->apiKey
])->post("{$this->apiEndpoint}/api/v1/transactions", [
'from' => $fromAddress,
'to' => $toAddress,
'value' => $amountOre, // 最小单位(欧尔)
]);
return $response->json('tx_hash'); // 返回交易哈希
}
// 查询余额
public function getBalance(string $address): string
{
$response = Http::get("{$this->apiEndpoint}/api/v1/addresses/{$address}/balance");
return $response->json('balance'); // 返回欧尔数
}
}
需要注意的安全与合规问题
| 问题领域 | PHP 实现建议 |
|---|---|
| 精度 | 强制使用 bcmath 扩展,所有计算保留4位小数以上。 |
| 并发 | 账户余额更新使用 UPDATE ... WHERE version = ? 乐观锁 或 FOR UPDATE 悲观锁。 |
| 审计 | 所有余额变动必须产生不可篡改的流水记录。 |
| 合规 | eDKK 可能受《数字货币法》监管,需记录用户KYC、交易限额、可疑交易上报接口。 |
| 测试 | 使用 PHPUnit + 模拟 bcmath 进行单元测试,特别注意边界值(0、负数、超大金额)。 |
简化的文件结构建议
project/
├── src/
│ ├── Money/
│ │ ├── DanishKrone.php # 金额值对象
│ │ ├── CurrencyConverter.php # DKK/eDKK兑换器
│ │ └── FeeCalculator.php # 手续费计算
│ ├── Models/
│ │ ├── Account.php # Eloquent/Doctrine模型
│ │ └── Transaction.php # 流水模型
│ ├── Services/
│ │ ├── PaymentService.php # 支付流程编排
│ │ └── EDKKBlockchain.php # 外部eDKK集成
├── migrations/
│ └── create_accounts_table.php # 迁移文件
└── tests/
└── Money/
└── DanishKroneTest.php # 严格的单元测试
- 数字格林兰克朗(DKK) 与 eDKK 在PHP中主要是会计分类问题,而非技术问题。
- 使用
DECIMAL(19,4)存储,bcmath计算,事务保证一致性。 - eDKK 是真正的区块链货币,增加一个中间层隔离内部账户与链上状态。
- 当前(2025年)格林兰大概率仍在使用丹麦克朗(DKK),eDKK 若为当地数字货币,需关注是否与DKK完全等价(1:1锚定),否则需引入汇率浮动模型。
如果你能提供更具体的信息(eDKK 是完全去中心化的、还是央行中心化的、是否支持离链转账),我可以进一步优化代码结构。