一、表设计的核心原则
ERP系统的数据库设计,和普通业务系统不同。它需要支撑业务高频操作、确保数据一致性、支持复杂查询。以下是财务模块设计的几个关键点。
1. 凭证表的设计
CREATE TABLE fm_voucher ( id BIGINT PRIMARY KEY, voucher_no VARCHAR(30) NOT NULL, -- 凭证号 accounting_period VARCHAR(6) NOT NULL, -- 会计期间,格式:YYYYMM voucher_date DATE NOT NULL, attachment_count INT DEFAULT 0, -- 附件张数 poster_id BIGINT, poster_time DATETIME, poster_ip VARCHAR(50), make_id BIGINT, make_time DATETIME, source_module VARCHAR(20), -- 来源模块:GL/AR/AP/INV source_id BIGINT, -- 来源单据ID is_reversed BIT DEFAULT 0, -- 是否被冲销 reverse_id BIGINT, -- 冲销凭证ID approve_status VARCHAR(10), -- 审核状态 approve_id BIGINT, approve_time DATETIME, is_deleted BIT DEFAULT 0, ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );
注意几个字段:
source_module和source_id:业务系统生成的凭证必须记录来源,否则业务单据和凭证之间的联查就会断裂。is_reversed和reverse_id:冲销凭证和被冲销凭证之间要建立双向关联,否则查历史数据时容易遗漏。accounting_period用VARCHAR而非INT:这样可以支持跨年查询,SQL写法也更直观。
2. 分录表的设计
凭证表和分录表是一对多关系。
CREATE TABLE fm_voucher_entry ( id BIGINT PRIMARY KEY, voucher_id BIGINT NOT NULL, entry_no INT NOT NULL, -- 分录序号 account_id BIGINT NOT NULL, -- 科目ID account_code VARCHAR(20) NOT NULL, -- 科目编码,冗余存储加速查询 direction VARCHAR(4), -- 借贷方向:DR/CR amount DECIMAL(18,2) NOT NULL, -- 金额,精确到分 quantity DECIMAL(18,6), -- 数量,制造业需要 unit_price DECIMAL(18,6), -- 单价 currency_code VARCHAR(3), -- 币种,默认CNY exchange_rate DECIMAL(18,6) DEFAULT 1,-- 汇率 secondary_customer_id BIGINT, -- 辅助核算:客户 supplier_id BIGINT, -- 辅助核算:供应商 department_id BIGINT, -- 辅助核算:部门 person_id BIGINT, -- 辅助核算:个人 project_id BIGINT, -- 辅助核算:项目 cash_flow_id BIGINT, -- 现金流量项目 memo VARCHAR(200), -- 摘要 is_deleted BIT DEFAULT 0 );
金额字段用DECIMAL(18,2),适合绝大多数企业的最大金额范围(999万亿级别)。如果企业有特殊需求(如金融行业),可以改为DECIMAL(24,4)。
辅助核算字段的设计:财务系统必须支持多维辅助核算。常见的做法是把每个辅助核算方向做成独立字段,而不是用JSON或EAV模式。独立字段的优势是:查询性能好、索引支持好、报表工具兼容性好。
3. 会计期间的设计
CREATE TABLE bd_accounting_period ( org_id BIGINT, period_year INT NOT NULL, period_month INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, period_status VARCHAR(10), -- Open/Closed/Frozen is_current_period BIT DEFAULT 0, allow_post_before_date DATETIME, -- 允许在什么时候之前制单 PRIMARY KEY (org_id, period_year, period_month) );
会计期间的”锁定期”很关键。月末结账后,历史期间的凭证要锁定,禁止修改。allow_post_before_date字段可以精确控制制单的时间权限,比简单的”已结账/未结账”两态更灵活。
二、事务处理中的并发控制
1. 凭证编号的并发问题
多用户同时制单时,凭证号不能重复。
错误做法:
危险:可能产生重复编号 INSERT INTO fm_voucher (voucher_no, ...) VALUES ((SELECT MAX(voucher_no)+1 FROM fm_voucher), ...);
正确做法:使用数据库的序列或锁机制。
方法一:序列(MySQL 8.0+) voucher_no = CONCAT('JZ', NEXT VALUE FOR seq_voucher_no); 方法二:表锁 START TRANSACTION; SELECT MAX(voucher_no) FROM fm_voucher WHERE accounting_period = ? FOR UPDATE; 生成新编号 INSERT INTO fm_voucher ...; COMMIT;2. 余额表更新的原子性
科目余额表(account_balance)是财务系统最热的表,每次记账都要更新。如果并发控制不好,会导致余额数据不一致。
UPDATE ac_account_balance SET period_amount_dr = period_amount_dr + ?, period_amount_cr = period_amount_cr + ?, year_amount_dr = year_amount_dr + ?, year_amount_cr = year_amount_cr + ?, ts = NOW() WHERE account_id = ? AND period_year = ? AND period_month = ? AND direction = ?; -- 如果影响行数为0,说明该科目期间余额记录不存在,需要插入 INSERT INTO ac_account_balance (...) SELECT ... WHERE NOT EXISTS ( SELECT 1 FROM ac_account_balance WHERE ... );
这种方式比先查后改更高效,也能避免TOCTOU(检查-使用-检查)竞争条件。如果企业并发量特别大,可以考虑在应用层做余额队列:所有记账请求先入队,队列消费者顺序处理,避免数据库层面的锁竞争。
三、数据归档与性能维护
财务数据有一个特点:越老的数据访问频率越低,但必须保留。
按会计年度做数据归档是个好策略:
每年12月结账后,将历史年度数据迁移到归档库。
归档库和现行库分开,现行库保持在合理数据量内。
报表查询时,现行库和归档库分别查询后合并结果。
归档存储过程示例 CREATE PROCEDURE sp_archive_financial_data(IN p_year INT) BEGIN 1. 迁移凭证 INSERT INTO fm_voucher_archive SELECT * FROM fm_voucher WHERE LEFT(accounting_period, 4) = CAST(p_year AS CHAR); 2. 迁移分录 INSERT INTO fm_voucher_entry_archive SELECT e.* FROM fm_voucher_entry e JOIN fm_voucher_archive v ON e.voucher_id = v.id WHERE v.accounting_period LIKE CONCAT(p_year, '%'); 3. 迁移余额 INSERT INTO ac_account_balance_archive SELECT * FROM ac_account_balance WHERE period_year < p_year; 4. 删除现行库数据 DELETE FROM fm_voucher_entry WHERE voucher_id IN (SELECT id FROM fm_voucher WHERE LEFT(accounting_period, 4) = CAST(p_year AS CHAR)); DELETE FROM fm_voucher WHERE LEFT(accounting_period, 4) = CAST(p_year AS CHAR); DELETE FROM ac_account_balance WHERE period_year < p_year; END;
归档前务必检查:
当年数据是否已全部结账
归档后的查询接口是否需要修改
归档库的备份策略是否完善