本文目录导读:

- 理解核心概念
- 数据结构设计(MySQL/PostgreSQL)
- PHP货币处理类(推荐使用bcmath)
- 数据库操作示例
- 汇率与转换逻辑(当eKGS与KGS不同价时)
- 前端显示最佳实践
- 测试示例(PHPUnit)
- 安全与性能建议
- 常见问题场景处理
针对PHP项目中处理“数字吉尔吉斯斯坦索姆”(KGS)与“eKGS”(可能指电子索姆或特定系统内的代币/积分)的需求,以下是个完整的开发指南,涵盖数据结构、货币格式化、汇率处理及安全最佳实践。
理解核心概念
- KGS:吉尔吉斯斯坦索姆(ISO 4217: KGS),传统法币,1 KGS = 100 tyiyn(提因)。
- eKGS:可能指:
- 电子索姆(数字货币版本)
- 系统内虚拟积分(如1 eKGS = 1 KGS,但不可提现)
- 第三方支付系统代币(如Elcard、Megapay等电子钱包)
关键假设:本文假设eKGS是系统内部与KGS1:1锚定的电子形式,但在计算、显示和交易中需要明确区分。
数据结构设计(MySQL/PostgreSQL)
1 用户余额表
CREATE TABLE user_balances (
id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL UNIQUE,
eKGS_balance DECIMAL(16,2) NOT NULL DEFAULT 0.00, -- 电子索姆
KGS_balance DECIMAL(16,2) NOT NULL DEFAULT 0.00, -- 真实索姆
tyiyn_balance INT NOT NULL DEFAULT 0, -- 提因(分币)
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
2 交易日志表
CREATE TABLE transaction_log (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id INT NOT NULL,
amount DECIMAL(16,2) NOT NULL,
currency_type ENUM('KGS', 'eKGS', 'tyiyn') NOT NULL,
transaction_type ENUM('deposit', 'withdrawal', 'transfer', 'conversion', 'payment'),
balance_before DECIMAL(16,2),
balance_after DECIMAL(16,2),
exchange_rate DECIMAL(10,6) DEFAULT NULL, -- 当涉及KGS/eKGS互转时
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_currency (user_id, currency_type)
);
PHP货币处理类(推荐使用bcmath)
1 核心货币类 KyrgyzstaniSom.php
<?php
class KyrgyzstaniSom {
private const DECIMAL_PLACES = 2;
private const TYIYN_PER_SOM = 100;
private string $amount; // 使用字符串精确存储
/**
* @param string|int|float $amount 索姆金额
* @throws InvalidArgumentException
*/
public function __construct($amount = '0.00') {
$this->setAmount($amount);
}
public function setAmount($amount): void {
if (is_numeric($amount)) {
$amount = number_format((float)$amount, self::DECIMAL_PLACES, '.', '');
}
if (!preg_match('/^-?\d+\.\d{2}$/', $amount)) {
throw new InvalidArgumentException("Invalid KGS amount: $amount");
}
$this->amount = $amount;
}
// 加法(保留精度)
public function add(KyrgyzstaniSom $other): KyrgyzstaniSom {
$result = bcadd($this->amount, $other->amount, self::DECIMAL_PLACES);
return new self($result);
}
// 减法
public function subtract(KyrgyzstaniSom $other): KyrgyzstaniSom {
$result = bcsub($this->amount, $other->amount, self::DECIMAL_PLACES);
return new self($result);
}
// 转换为tyiyn(整数)
public function toTyiyn(): int {
return (int)bcmul($this->amount, (string)self::TYIYN_PER_SOM, 0);
}
// 格式化显示
public function format(string $locale = 'ky_KG'): string {
$formatter = new NumberFormatter($locale, NumberFormatter::DECIMAL);
$formatter->setAttribute(NumberFormatter::MIN_FRACTION_DIGITS, 2);
$formatter->setAttribute(NumberFormatter::MAX_FRACTION_DIGITS, 2);
$formatter->setTextAttribute(NumberFormatter::CURRENCY_CODE, 'KGS');
return $formatter->formatCurrency((float)$this->amount, 'KGS');
}
// JSON序列化
public function __toString(): string {
return $this->amount;
}
}
2 eKGS专用处理扩展
class EKGS extends KyrgyzstaniSom {
private string $type = 'eKGS';
// 覆写格式化,添加前缀
public function format(string $locale = 'ky_KG'): string {
return 'e' . parent::format($locale);
}
// 转换到KGS(1:1,但可在此加入费率逻辑)
public function toKGS(float $exchangeRate = 1.0): KyrgyzstaniSom {
$kgsAmount = bcmul($this->amount, (string)$exchangeRate, 2);
return new KyrgyzstaniSom($kgsAmount);
}
}
数据库操作示例
1 查询用户余额
class BalanceService {
private PDO $pdo;
public function getUserBalances(int $userId): array {
$stmt = $this->pdo->prepare(
"SELECT eKGS_balance, KGS_balance FROM user_balances WHERE user_id = ?"
);
$stmt->execute([$userId]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return [
'eKGS' => new EKGS($row['eKGS_balance']),
'KGS' => new KyrgyzstaniSom($row['KGS_balance']),
];
}
// 转账逻辑(带原子操作)
public function transfer(int $fromUserId, int $toUserId, KyrgyzstaniSom $amount, string $currency): bool {
try {
$this->pdo->beginTransaction();
// 1. 检查发送方余额(使用数据库级锁)
$stmt = $this->pdo->prepare(
"SELECT {$currency}_balance FROM user_balances WHERE user_id = ? FOR UPDATE"
);
$stmt->execute([$fromUserId]);
$fromBalance = $stmt->fetchColumn();
if ($fromBalance < $amount->__toString()) {
throw new \RuntimeException('余额不足');
}
// 2. 更新余额
$this->pdo->prepare(
"UPDATE user_balances SET {$currency}_balance = {$currency}_balance - ? WHERE user_id = ?"
)->execute([$amount->__toString(), $fromUserId]);
$this->pdo->prepare(
"UPDATE user_balances SET {$currency}_balance = {$currency}_balance + ? WHERE user_id = ?"
)->execute([$amount->__toString(), $toUserId]);
// 3. 记录交易日志
$this->logTransaction($fromUserId, $amount, $currency, 'transfer');
$this->pdo->commit();
return true;
} catch (\Exception $e) {
$this->pdo->rollBack();
throw $e;
}
}
}
汇率与转换逻辑(当eKGS与KGS不同价时)
1 汇率表结构
CREATE TABLE exchange_rates (
id INT PRIMARY KEY AUTO_INCREMENT,
from_currency ENUM('KGS','eKGS','USD','RUB') NOT NULL,
to_currency ENUM('KGS','eKGS','USD','RUB') NOT NULL,
rate DECIMAL(12,6) NOT NULL,
valid_from TIMESTAMP NOT NULL,
valid_until TIMESTAMP NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_currency_pair (from_currency, to_currency, valid_from)
);
2 汇率转换服务
class CurrencyConverter {
private PDO $pdo;
public function convert(string $from, string $to, KyrgyzstaniSom $amount): KyrgyzstaniSom {
if ($from === $to) return $amount;
// 获取实时汇率
$rate = $this->getCurrentRate($from, $to);
$converted = bcmul($amount->__toString(), (string)$rate, 2);
return new KyrgyzstaniSom($converted);
}
private function getCurrentRate(string $from, string $to): float {
$stmt = $this->pdo->prepare(
"SELECT rate FROM exchange_rates
WHERE from_currency = ? AND to_currency = ?
AND valid_from <= NOW()
AND (valid_until IS NULL OR valid_until > NOW())
ORDER BY valid_from DESC LIMIT 1"
);
$stmt->execute([$from, $to]);
return (float)$stmt->fetchColumn();
}
}
前端显示最佳实践
1 JavaScript辅助格式化
// 格式化KGS金额
const formatKGS = (amount) => {
return new Intl.NumberFormat('ky-KG', {
style: 'currency',
currency: 'KGS',
minimumFractionDigits: 2,
maximumFractionDigits: 2
}).format(amount);
};
// 格式化eKGS(添加电子标识)
const formatEKGS = (amount) => {
return `e${formatKGS(amount)}`;
};
// 使用示例
console.log(formatKGS(1250.50)); // 1 250,50 сом
console.log(formatEKGS(500)); // e500,00 сом
2 国际化(ICU规则)
- 吉尔吉斯斯坦语(ky_KG):
- 分组分隔符:空格 (如
1 234,56 сом) - 小数分隔符:逗号
- 货币符号后置:
сом(西里尔)或KGS(拉丁)
- 分组分隔符:空格 (如
测试示例(PHPUnit)
class KyrgyzstaniSomTest extends \PHPUnit\Framework\TestCase {
public function testAddition(): void {
$a = new KyrgyzstaniSom('100.50');
$b = new KyrgyzstaniSom('200.75');
$result = $a->add($b);
$this->assertEquals('301.25', (string)$result);
}
public function testTyiynConversion(): void {
$kgs = new KyrgyzstaniSom('150.25');
$this->assertEquals(15025, $kgs->toTyiyn());
}
public function testEKGSFormat(): void {
$eKGS = new EKGS('500.00');
$this->assertStringContainsString('e', $eKGS->format());
$this->assertStringContainsString('500', $eKGS->format());
}
}
安全与性能建议
- 始终使用bcmath:避免浮点精度问题
- 数据库使用DECIMAL:
DECIMAL(16,2)支持最大10亿索姆 - 交易加锁:使用
SELECT ... FOR UPDATE防止并发问题 - 审计日志:所有货币操作记录完整的before/after状态
- 汇率更新:通过cron定期更新,或使用实时API(如NBKR央行数据)
常见问题场景处理
- 充值eKGS:生成唯一交易ID,调用支付网关回调
- eKGS兑KGS:设置固定汇率或浮动汇率,限制每日兑换限额
- 展示历史余额:快照表保存每日余额,避免长事务
如果你有更具体的业务场景(如eKGS是否支持提现、是否与特定支付系统对接),欢迎补充,我可以进一步优化方案。