PHP项目数字肯尼亚先令与eKES

wen PHP项目 1

本文目录导读:

PHP项目数字肯尼亚先令与eKES

  1. 核心问题:为什么需要区分?
  2. PHP 代码实现方案
  3. 关键注意事项

这是一个关于 PHP 项目中处理肯尼亚先令 (KES) 与 电子肯尼亚先令 (eKES) 的技术问题。

这是一个数字支付与本土货币管理的常见场景,eKES 并不是肯尼亚央行发行的独立数字货币,而是指代电子钱包、移动支付平台(如 M-Pesa、Airtel Money)或数字银行账户中的余额,在技术实现上,它们本质上是同一种货币单位(小额记账),但在业务逻辑、手续费和兑换处理上有区别。

以下是一个 PHP 项目如何设计和处理 KES (现金/物理货币) 与 eKES (电子货币) 的完整指南。


核心问题:为什么需要区分?

  1. 手续费不同:M-Pesa 提现到银行(Cash Out)或存入(Cash In)通常有手续费,而电子转账(M-Pesa to M-Pesa)可能免费或极其便宜。
  2. 兑换比率:有些场景下(例如点对点兑换平台、黑市或特定商家促销),1 KES 现金可能不等于 1 eKES 现金。
  3. 业务限制:某些支付方式只支持电子货币(如 Okoa Jahazi 信用购买),某些只支持现金。
  4. 会计审计:财务上需要清晰区分“在途电子资金”与“已变现现金”。

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% 手续费,这时你可以更新这条记录。


关键注意事项

  1. 原子性:永远使用数据库事务(BEGIN TRANSACTION + COMMIT/ROLLBACK)来保证 eKES 和 KES 的更新不会丢失。
  2. 浮点数精度:使用 DECIMAL(如 DECIMAL(12, 2))而非 FLOAT,避免 0.1 + 0.2 这种精度问题。
  3. 支付网关集成:如果通过 M-Pesa API 充值是 eKES,那么用户收到的是电子货币;如果是银行转账,则可能是 KES(现金),需要系统内部进行区分处理。
  4. 审计日志:每次转换(eKES ↔ KES)都是一个独立的交易,需要记录汇率、手续费、操作详情。

在 PHP 项目中处理 KES 与 eKES,核心思路是:

  • 数据库层:用 ENUM('KES', 'eKES') 字段区分货币类型。
  • 业务逻辑层:封装 MoneyManager 类,严格分离内部转账、兑换和外部网关回调。
  • 展示层:向用户清晰展示两种余额的不同属性。

如果你能提供更具体的场景(是构建一个 B2B 兑换平台,还是一个简单的电商结算系统?),我可以给出更针对性的代码示例。

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