本文目录导读:

我来详细介绍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');
重要特性
- 借贷平衡校验:每笔凭证借贷必须相等
- 原子性操作:使用事务确保数据一致性
- 过账机制:先录入草稿,过账后才生效
- 余额更新:实时更新各账户余额
- 报表生成:支持试算平衡表和资产负债表
这个实现遵循了复式记账的基本原则:有借必有贷,借贷必相等,你可以根据实际业务需求进行扩展。