PHP 怎么复式记账

wen PHP项目 1

本文目录导读:

PHP 怎么复式记账

  1. 数据库设计
  2. PHP核心类实现
  3. 业务逻辑示例
  4. 使用示例
  5. 数据库初始化脚本
  6. 重要特性

我来详细介绍PHP中复式记账的实现方法,复式记账是现代会计的基础,每笔交易都需要记录借方和贷方。

数据库设计

基础表结构

-- 账户表
CREATE TABLE accounts (
    id INT PRIMARY KEY AUTO_INCREMENT,
    account_code VARCHAR(20) UNIQUE,  -- 科目编码
    account_name VARCHAR(100),        -- 科目名称
    account_type ENUM('asset','liability','equity','income','expense'),
    parent_id INT NULL,               -- 父科目
    status TINYINT DEFAULT 1,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_account_code (account_code)
);
-- 会计凭证表
CREATE TABLE vouchers (
    id INT PRIMARY KEY AUTO_INCREMENT,
    voucher_no VARCHAR(20) UNIQUE,    -- 凭证编号
    voucher_date DATE,
    description VARCHAR(500),
    status ENUM('draft','posted') DEFAULT 'pending',
    created_by INT,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    posted_at DATETIME NULL
);
-- 凭证明细表
CREATE TABLE voucher_entries (
    id INT PRIMARY KEY AUTO_INCREMENT,
    voucher_id INT,
    account_id INT,
    debit_amount DECIMAL(15,2) DEFAULT 0,  -- 借方金额
    credit_amount DECIMAL(15,2) DEFAULT 0, -- 贷方金额
    memo VARCHAR(200),
    FOREIGN KEY (voucher_id) REFERENCES vouchers(id),
    FOREIGN KEY (account_id) REFERENCES accounts(id)
);
-- 余额表
CREATE TABLE account_balances (
    account_id INT PRIMARY KEY,
    period_date DATE,
    opening_balance DECIMAL(15,2) DEFAULT 0,
    debit_total DECIMAL(15,2) DEFAULT 0,
    credit_total DECIMAL(15,2) DEFAULT 0,
    closing_balance DECIMAL(15,2) DEFAULT 0,
    UNIQUE KEY uk_account_period (account_id, period_date)
);

PHP核心类实现

复式记账核心类

<?php
class DoubleEntryBookkeeping {
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
    }
    /**
     * 创建会计凭证
     */
    public function createVoucher(array $data) {
        // 校验借贷平衡
        $totalDebit = 0;
        $totalCredit = 0;
        foreach ($data['entries'] as $entry) {
            $totalDebit += $entry['debit_amount'];
            $totalCredit += $entry['credit_amount'];
        }
        // 借贷必须相等
        if ($totalDebit !== $totalCredit) {
            throw new Exception("借贷不平衡: 借方={$totalDebit}, 贷方={$totalCredit}");
        }
        try {
            $this->pdo->beginTransaction();
            // 生成凭证号
            $voucherNo = $this->generateVoucherNo($data['voucher_date']);
            // 插入主表
            $sql = "INSERT INTO vouchers (voucher_no, voucher_date, description, status, created_by) 
                    VALUES (?, ?, ?, 'pending', ?)";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([
                $voucherNo, 
                $data['voucher_date'],
                $data['description'],
                $_SESSION['user_id'] ?? 1
            ]);
            $voucherId = $this->pdo->lastInsertId();
            // 插入明细
            foreach ($data['entries'] as $entry) {
                $sql = "INSERT INTO voucher_entries 
                        (voucher_id, account_id, debit_amount, credit_amount, memo) 
                        VALUES (?, ?, ?, ?, ?)";
                $stmt = $this->pdo->prepare($sql);
                $stmt->execute([
                    $voucherId,
                    $entry['account_id'],
                    $entry['debit_amount'],
                    $entry['credit_amount'],
                    $entry['memo'] ?? ''
                ]);
            }
            $this->pdo->commit();
            return $voucherId;
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }
    /**
     * 过账(正式记账)
     */
    public function postVoucher($voucherId) {
        try {
            $this->pdo->beginTransaction();
            // 检查凭证是否存在且未过账
            $sql = "SELECT * FROM vouchers WHERE id = ? AND status = 'pending'";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([$voucherId]);
            if (!$stmt->fetch()) {
                throw new Exception("凭证不存在或已过账");
            }
            // 获取凭证明细
            $sql = "SELECT * FROM voucher_entries WHERE voucher_id = ?";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([$voucherId]);
            $entries = $stmt->fetchAll();
            // 更新各账户余额
            foreach ($entries as $entry) {
                $this->updateAccountBalance(
                    $entry['account_id'],
                    $entry['debit_amount'],
                    $entry['credit_amount']
                );
            }
            // 更新凭证状态
            $sql = "UPDATE vouchers SET status = 'posted', posted_at = NOW() WHERE id = ?";
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute([$voucherId]);
            $this->pdo->commit();
        } catch (Exception $e) {
            $this->pdo->rollBack();
            throw $e;
        }
    }
    /**
     * 更新账户余额
     */
    private function updateAccountBalance($accountId, $debit, $credit) {
        // 获取账户类型
        $sql = "SELECT account_type FROM accounts WHERE id = ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$accountId]);
        $account = $stmt->fetch();
        // 根据账户类型计算方向
        $balanceChange = 0;
        switch ($account['account_type']) {
            case 'asset':
            case 'expense':
                $balanceChange = $debit - $credit;
                break;
            case 'liability':
            case 'equity':
            case 'income':
                $balanceChange = $credit - $debit;
                break;
        }
        // 更新当前余额
        $sql = "UPDATE accounts 
                SET current_balance = COALESCE(current_balance, 0) + ? 
                WHERE id = ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$balanceChange, $accountId]);
    }
    /**
     * 生成凭证编号
     */
    private function generateVoucherNo($date) {
        $prefix = date('Ymd', strtotime($date));
        $sql = "SELECT COUNT(*) FROM vouchers WHERE voucher_date = ?";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$date]);
        $count = $stmt->fetchColumn();
        return $prefix . '-' . str_pad($count + 1, 3, '0', STR_PAD_LEFT);
    }
    /**
     * 验证账户是否有效
     */
    public function validateAccount($accountId) {
        $sql = "SELECT * FROM accounts WHERE id = ? AND status = 1";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$accountId]);
        return $stmt->fetch() !== false;
    }
}

业务逻辑示例

账务服务类

<?php
class AccountingService {
    private $bookkeeping;
    private $pdo;
    public function __construct(PDO $pdo) {
        $this->pdo = $pdo;
        $this->bookkeeping = new DoubleEntryBookkeeping($pdo);
    }
    /**
     * 销售商品(假设销售价100元)
     */
    public function recordSale($customerName, $products) {
        // 计算总金额
        $totalAmount = 0;
        foreach ($products as $product) {
            $totalAmount += $product['price'] * $product['quantity'];
        }
        // 创建凭证
        $voucherData = [
            'voucher_date' => date('Y-m-d'),
            'description' => "销售商品给{$customerName}",
            'entries' => [
                [
                    'account_id' => 1,  // 银行存款
                    'debit_amount' => $totalAmount,
                    'credit_amount' => 0,
                    'memo' => '客户付款'
                ],
                [
                    'account_id' => 5,  // 销售收入
                    'debit_amount' => 0,
                    'credit_amount' => $totalAmount,
                    'memo' => '销售收入'
                ]
            ]
        ];
        return $this->bookkeeping->createVoucher($voucherData);
    }
    /**
     * 记录费用支出(如房租3000元)
     */
    public function recordExpense($expenseType, $amount, $payee) {
        $voucherData = [
            'voucher_date' => date('Y-m-d'),
            'description' => "支付{$expenseType}给{$payee}",
            'entries' => [
                [
                    'account_id' => 6,  // 房租费用
                    'debit_amount' => $amount,
                    'credit_amount' => 0,
                    'memo' => '费用支出'
                ],
                [
                    'account_id' => 1,  // 银行存款
                    'debit_amount' => 0,
                    'credit_amount' => $amount,
                    'memo' => '银行付款'
                ]
            ]
        ];
        return $this->bookkeeping->createVoucher($voucherData);
    }
    /**
     * 生成试算平衡表
     */
    public function trialBalance($periodStart, $periodEnd) {
        $sql = "
            SELECT 
                a.account_code,
                a.account_name,
                a.account_type,
                SUM(ve.debit_amount) as total_debit,
                SUM(ve.credit_amount) as total_credit
            FROM voucher_entries ve
            INNER JOIN vouchers v ON ve.voucher_id = v.id
            INNER JOIN accounts a ON ve.account_id = a.id
            WHERE v.voucher_date BETWEEN ? AND ?
            AND v.status = 'posted'
            GROUP BY a.id, a.account_code, a.account_name, a.account_type
            ORDER BY a.account_code
        ";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$periodStart, $periodEnd]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
    /**
     * 生成资产负债表
     */
    public function balanceSheet($date) {
        $sql = "
            SELECT 
                a.account_code,
                a.account_name,
                a.account_type,
                COALESCE(
                    (SELECT closing_balance 
                     FROM account_balances ab 
                     WHERE ab.account_id = a.id 
                     AND ab.period_date <= ?
                     ORDER BY ab.period_date DESC 
                     LIMIT 1),
                    0
                ) as balance
            FROM accounts a
            WHERE a.status = 1
            AND a.account_type IN ('asset', 'liability', 'equity')
            ORDER BY a.account_code
        ";
        $stmt = $this->pdo->prepare($sql);
        $stmt->execute([$date]);
        return $stmt->fetchAll(PDO::FETCH_ASSOC);
    }
}

使用示例

<?php
// 初始化PDO连接
$pdo = new PDO('mysql:host=localhost;dbname=accounting', 'user', 'pass');
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
// 创建服务
$service = new AccountingService($pdo);
try {
    // 记录一笔销售
    $saleVoucherId = $service->recordSale('张三', [
        ['name' => '产品A', 'price' => 80, 'quantity' => 2],
        ['name' => '产品B', 'price' => 20, 'quantity' => 3]
    ]);
    echo "销售凭证已创建: #{$saleVoucherId}\n";
    // 过账
    $bookkeeping = new DoubleEntryBookkeeping($pdo);
    $bookkeeping->postVoucher($saleVoucherId);
    // 生成试算平衡表
    $trialBalance = $service->trialBalance('2024-01-01', '2024-01-31');
    echo json_encode($trialBalance, JSON_PRETTY_PRINT);
} catch (Exception $e) {
    echo "错误: " . $e->getMessage();
}

数据库初始化脚本

-- 插入初始科目
INSERT INTO accounts (account_code, account_name, account_type) VALUES
('1001', '银行存款', 'asset'),
('1002', '应收账款', 'asset'),
('2001', '应付账款', 'liability'),
('3001', '实收资本', 'equity'),
('4001', '销售收入', 'income'),
('5001', '房屋租金', 'expense'),
('5002', '工资薪金', 'expense');

重要特性

  1. 借贷平衡校验:每笔凭证借贷必须相等
  2. 原子性操作:使用事务确保数据一致性
  3. 过账机制:先录入草稿,过账后才生效
  4. 余额更新:实时更新各账户余额
  5. 报表生成:支持试算平衡表和资产负债表

这个实现遵循了复式记账的基本原则:有借必有贷,借贷必相等,你可以根据实际业务需求进行扩展。

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