本文目录导读:

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