本文目录导读:

这是一个关于 PHP 项目中处理肯尼亚先令 (KES) 与 电子肯尼亚先令 (eKES) 的技术问题。
这是一个数字支付与本土货币管理的常见场景,eKES 并不是肯尼亚央行发行的独立数字货币,而是指代电子钱包、移动支付平台(如 M-Pesa、Airtel Money)或数字银行账户中的余额,在技术实现上,它们本质上是同一种货币单位(小额记账),但在业务逻辑、手续费和兑换处理上有区别。
以下是一个 PHP 项目如何设计和处理 KES (现金/物理货币) 与 eKES (电子货币) 的完整指南。
核心问题:为什么需要区分?
- 手续费不同:M-Pesa 提现到银行(Cash Out)或存入(Cash In)通常有手续费,而电子转账(M-Pesa to M-Pesa)可能免费或极其便宜。
- 兑换比率:有些场景下(例如点对点兑换平台、黑市或特定商家促销),1 KES 现金可能不等于 1 eKES 现金。
- 业务限制:某些支付方式只支持电子货币(如 Okoa Jahazi 信用购买),某些只支持现金。
- 会计审计:财务上需要清晰区分“在途电子资金”与“已变现现金”。
PHP 代码实现方案
数据库 Schema 设计(核心)
你需要一个货币类型字段来区分。
-- 用户钱包表
CREATE TABLE user_wallets (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
currency ENUM('KES', 'eKES') NOT NULL DEFAULT 'eKES', -- 关键:区分物理与电子
balance DECIMAL(12, 2) DEFAULT 0.00,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY (user_id, currency) -- 每个用户两种余额
);
-- 交易记录表
CREATE TABLE transactions (
id INT AUTO_INCREMENT PRIMARY KEY,
from_user_id INT,
to_user_id INT,
amount DECIMAL(12, 2) NOT NULL,
currency_type ENUM('KES', 'eKES') NOT NULL, -- 这笔交易属于哪种货币
payment_method ENUM('M-PESA', 'AIRTEL', 'BANK_TRANSFER', 'CASH', 'SYSTEM') NOT NULL,
fee DECIMAL(12, 2) DEFAULT 0.00,
status ENUM('PENDING', 'COMPLETED', 'FAILED') DEFAULT 'PENDING',
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
PHP 业务逻辑封装(关键类)
<?php
class MoneyManager {
private $db;
public function __construct(PDO $db) {
$this->db = $db;
}
/**
* 获取用户特定类型的余额
*/
public function getBalance(int $userId, string $type = 'eKES'): float {
$stmt = $this->db->prepare("SELECT balance FROM user_wallets
WHERE user_id = ? AND currency = ?");
$stmt->execute([$userId, $type]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
return $row ? (float)$row['balance'] : 0.0;
}
/**
* 转账(仅在 eKES 内部或 KES 内部)
*/
public function internalTransfer(int $fromUserId, int $toUserId,
float $amount, string $currency): bool {
if ($amount <= 0) throw new InvalidArgumentException("金额必须大于零");
$this->db->beginTransaction();
try {
// 检查发送方余额(使用行级锁避免重复消费)
$stmt = $this->db->prepare("SELECT balance FROM user_wallets
WHERE user_id = ? AND currency = ?
FOR UPDATE");
$stmt->execute([$fromUserId, $currency]);
$fromBalance = $stmt->fetchColumn();
if ($fromBalance < $amount) {
throw new Exception("余额不足");
}
// 发送方扣款
$this->db->prepare("UPDATE user_wallets SET balance = balance - ?
WHERE user_id = ? AND currency = ?")
->execute([$amount, $fromUserId, $currency]);
// 接收方加款
$this->db->prepare("UPDATE user_wallets SET balance = balance + ?
WHERE user_id = ? AND currency = ?")
->execute([$amount, $toUserId, $currency]);
// 记录交易日志
$this->db->prepare("INSERT INTO transactions
(from_user_id, to_user_id, amount, currency_type, payment_method, fee, status, description)
VALUES (?, ?, ?, ?, 'SYSTEM', 0, 'COMPLETED', '内部转账')")
->execute([$fromUserId, $toUserId, $amount, $currency]);
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 兑换:KES(现金) <-> eKES(电子)
* 假设手续费由发送方承担
*/
public function convertCurrency(int $userId, float $amount,
string $fromType, string $toType): bool {
if ($fromType === $toType) {
throw new InvalidArgumentException("不需要兑换相同货币");
}
// 费率逻辑(这里假设 1:1,但你可以根据外部API或数据库配置)
$exchangeRate = $this->getExchangeRate($fromType, $toType);
$feePercentage = 0.0; // M-Pesa 提现可能收 1%
$deductAmount = $amount;
$addAmount = $amount * $exchangeRate;
$fee = $deductAmount * $feePercentage;
$this->db->beginTransaction();
try {
// 扣减源货币
$this->db->prepare("UPDATE user_wallets SET balance = balance - ?
WHERE user_id = ? AND currency = ? AND balance >= ?")
->execute([$deductAmount, $userId, $fromType, $deductAmount]);
if ($this->db->rowCount() === 0) {
throw new Exception("源货币余额不足");
}
// 增加目标货币
$this->db->prepare("UPDATE user_wallets SET balance = balance + ?
WHERE user_id = ? AND currency = ?")
->execute([$addAmount - $fee, $userId, $toType]);
// 记录兑换交易
$this->db->prepare("INSERT INTO transactions
(from_user_id, to_user_id, amount, currency_type, payment_method, fee, status, description)
VALUES (?, NULL, ?, ?, 'CONVERSION', ?, 'COMPLETED', ?)")
->execute([
$userId,
$deductAmount,
$fromType,
$fee,
"从 {$fromType} 兑换到 {$toType} (汇率 {$exchangeRate})"
]);
$this->db->commit();
return true;
} catch (Exception $e) {
$this->db->rollBack();
throw $e;
}
}
/**
* 处理外部支付网关回调(如 M-Pesa API)
* 假设外部系统只操作 eKES 用户的电子钱包
*/
public function processExternalPayment(array $webhookData): void {
$phone = $webhookData['phone_number'];
$amount = $webhookData['amount'];
$transactionId = $webhookData['trans_id'];
// 1. 查找用户(用手机号)
$userId = $this->findUserIdByPhone($phone);
// 2. 给用户的 eKES 钱包加钱
$this->db->beginTransaction();
try {
$this->db->prepare("UPDATE user_wallets SET balance = balance + ?
WHERE user_id = ? AND currency = 'eKES'")
->execute([$amount, $userId]);
// 记录交易,标记为 M-PESA 来源
$this->db->prepare("INSERT INTO transactions (...) VALUES (..., 'M-PESA', ...)")
->execute([...]);
$this->db->commit();
} catch (Exception $e) {
$this->db->rollBack();
// 记录失败日志,可能需要人工介入
error_log("M-PESA 回调处理失败: " . $e->getMessage());
}
}
}
前端/API 展示示例
当用户查看余额时,你可能需要显示两者:
// 控制器
$user = auth()->user();
$eKESBalance = $moneyManager->getBalance($user->id, 'eKES');
$KESBalance = $moneyManager->getBalance($user->id, 'KES');
return view('wallet.index', compact('eKESBalance', 'KESBalance'));
前端显示:
<div class="balance-card">
<h3>电子钱包 (eKES)</h3>
<p class="amount">KES <?= number_format($eKESBalance, 2) ?></p>
<small>可用于线上支付、转账</small>
</div>
<div class="balance-card">
<h3>现金余额 (KES)</h3>
<p class="amount">KES <?= number_format($KESBalance, 2) ?></p>
<small>可提现到银行卡或M-Pesa</small>
</div>
费用与汇率管理(高级)
你可以创建一个专门配置表:
CREATE TABLE currency_rates (
id INT AUTO_INCREMENT PRIMARY KEY,
from_currency ENUM('KES', 'eKES'),
to_currency ENUM('KES', 'eKES'),
rate DECIMAL(10, 6) DEFAULT 1.000000, -- 1 from_currency = rate to_currency
fee_percent DECIMAL(5, 2) DEFAULT 0.00,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 插入默认数据 (1:1 且零手续费)
INSERT INTO currency_rates (from_currency, to_currency, rate, fee_percent) VALUES
('eKES', 'KES', 1.000000, 0.00),
('KES', 'eKES', 1.000000, 0.00);
在实际业务中,提现(eKES → KES)可能需要收取 1% 手续费,这时你可以更新这条记录。
关键注意事项
- 原子性:永远使用数据库事务(
BEGIN TRANSACTION+COMMIT/ROLLBACK)来保证 eKES 和 KES 的更新不会丢失。 - 浮点数精度:使用
DECIMAL(如DECIMAL(12, 2))而非FLOAT,避免 0.1 + 0.2 这种精度问题。 - 支付网关集成:如果通过 M-Pesa API 充值是 eKES,那么用户收到的是电子货币;如果是银行转账,则可能是 KES(现金),需要系统内部进行区分处理。
- 审计日志:每次转换(eKES ↔ KES)都是一个独立的交易,需要记录汇率、手续费、操作详情。
在 PHP 项目中处理 KES 与 eKES,核心思路是:
- 数据库层:用
ENUM('KES', 'eKES')字段区分货币类型。 - 业务逻辑层:封装
MoneyManager类,严格分离内部转账、兑换和外部网关回调。 - 展示层:向用户清晰展示两种余额的不同属性。
如果你能提供更具体的场景(是构建一个 B2B 兑换平台,还是一个简单的电商结算系统?),我可以给出更针对性的代码示例。