财务核算系统设计与实现
前言
财务核算系统是企业信息化建设的核心系统之一,承担着记录、分类、汇总企业经济业务的重要职责。一个完善的财务核算系统需要满足会计准则要求,支持多维度核算,保证数据准确性和一致性。本文将从系统架构、核心模块、代码实现等方面,详细讲解如何设计一个企业级财务核算系统。
一、财务核算基础概念
1.1 会计恒等式
+------------------------------------------------------------------+ | 会计恒等式 | +------------------------------------------------------------------+ | | | 资产 = 负债 + 所有者权益 | | | | Assets = Liabilities + Owner's Equity | | | +------------------------------------------------------------------+ | | | 借方(Debit) | 贷方(Credit) | | 资产增加 | 资产减少 | | 负债减少 | 负债增加 | | 费用增加 | 收入增加 | | 所有者权益减少 | 所有者权益增加 | | | +------------------------------------------------------------------+
1.2 核算体系结构
+------------------+ +------------------+ +------------------+ | 科目体系 | | 辅助核算 | | 期间管理 | +------------------+ +------------------+ +------------------+ | 一级科目 | | 部门核算 | | 会计年度 | | 二级科目 | | 项目核算 | | 会计期间 | | 三级科目 | | 往来核算 | | 期间状态 | | 末级科目 | | 现金流核算 | | 结账/反结账 | +------------------+ +------------------+ +------------------+ | | | +-------------------+---+-------------------+---+ | +--------v--------+ | 凭证管理 | +-----------------+ | 凭证录入 | | 凭证审核 | | 凭证过账 | | 凭证冲销 | +-----------------+ | +--------v--------+ | 账簿管理 | +-----------------+ | 总账 | | 明细账 | | 多栏账 | | 序时账 | +-----------------+ | +--------v--------+ | 报表管理 | +-----------------+ | 资产负债表 | | 利润表 | | 现金流量表 | +-----------------+
1.3 核心业务指标
| 指标 | 说明 | 要求 |
|---|---|---|
| 借贷平衡 | 每笔凭证借贷必须相等 | 100%准确 |
| 科目余额 | 期初+本期发生=期末 | 100%准确 |
| 期间完整 | 期间连续不断 | 100%完整 |
| 审计追踪 | 所有操作可追溯 | 完整日志 |
二、系统架构设计
2.1 整体架构
+------------------+ | 前端应用 | | Vue/React/H5 | +---------+--------+ | +---------v--------+ | API Gateway | | 认证/限流/路由 | +---------+--------+ | +------------------------------+------------------------------+ | | | +--------v--------+ +---------v--------+ +---------v--------+ | 核算服务 | | 报表服务 | | 基础服务 | | Accounting-Svc | | Report-Service | | Base-Service | +--------+--------+ +---------+--------+ +---------+--------+ | | | +------------------------------+------------------------------+ | +------------------------------+------------------------------+ | | | +--------v--------+ +---------v--------+ +---------v--------+ | Redis缓存 | | 消息队列 | | 数据库 | | 科目/期间缓存 | | 异步处理/通知 | | 核算数据 | +-----------------+ +------------------+ +------------------+
2.2 核算服务核心模块
+------------------------------------------------------------------+ | 核算服务 | +------------------------------------------------------------------+ | +-------------+ +-------------+ +-------------+ +-----------+ | | | 科目管理 | | 凭证管理 | | 期间管理 | | 辅助核算 | | | | Account | | Voucher | | Period | | Auxiliary | | | +-------------+ +-------------+ +-------------+ +-----------+ | | | | +-------------+ +-------------+ +-------------+ +-----------+ | | | 账簿查询 | | 结账处理 | | 期末结转 | | 对账服务 | | | | Ledger | | Closing | | Carryover | | Reconcile | | | +-------------+ +-------------+ +-------------+ +-----------+ | +------------------------------------------------------------------+
三、数据库设计
3.1 核心表结构
-- 会计科目表 CREATE TABLE `acc_subject` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '科目ID', `subject_code` VARCHAR(32) NOT NULL COMMENT '科目编码', `subject_name` VARCHAR(64) NOT NULL COMMENT '科目名称', `parent_code` VARCHAR(32) DEFAULT NULL COMMENT '上级科目编码', `subject_level` INT(11) NOT NULL DEFAULT 1 COMMENT '科目级次', `subject_type` TINYINT(1) NOT NULL COMMENT '科目类型:1-资产,2-负债,3-权益,4-成本,5-损益', `balance_direction` TINYINT(1) NOT NULL COMMENT '余额方向:1-借方,2-贷方', `is_leaf` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '是否末级:0-否,1-是', `is_cash_subject` TINYINT(1) DEFAULT 0 COMMENT '是否现金科目', `is_bank_subject` TINYINT(1) DEFAULT 0 COMMENT '是否银行科目', `auxiliary_types` VARCHAR(128) DEFAULT NULL COMMENT '辅助核算类型(JSON数组)', `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-停用,1-启用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_subject_code` (`subject_code`), KEY `idx_parent_code` (`parent_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会计科目表'; -- 会计期间表 CREATE TABLE `acc_period` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '期间ID', `period_year` INT(11) NOT NULL COMMENT '会计年度', `period_month` INT(11) NOT NULL COMMENT '会计月份', `period_code` VARCHAR(10) NOT NULL COMMENT '期间编码(如:202401)', `start_date` DATE NOT NULL COMMENT '开始日期', `end_date` DATE NOT NULL COMMENT '结束日期', `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:0-未启用,1-已启用,2-已结账', `close_time` DATETIME DEFAULT NULL COMMENT '结账时间', `close_user` VARCHAR(32) DEFAULT NULL COMMENT '结账人', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_period_code` (`period_code`), KEY `idx_year_month` (`period_year`, `period_month`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会计期间表'; -- 会计凭证表(凭证头) CREATE TABLE `acc_voucher` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '凭证ID', `voucher_no` VARCHAR(32) NOT NULL COMMENT '凭证号', `voucher_type` VARCHAR(10) NOT NULL DEFAULT '记' COMMENT '凭证字(记/收/付/转)', `voucher_date` DATE NOT NULL COMMENT '凭证日期', `period_code` VARCHAR(10) NOT NULL COMMENT '所属期间', `attachment_count` INT(11) DEFAULT 0 COMMENT '附件数', `total_debit` DECIMAL(18,2) NOT NULL COMMENT '借方合计', `total_credit` DECIMAL(18,2) NOT NULL COMMENT '贷方合计', `summary` VARCHAR(256) DEFAULT NULL COMMENT '凭证摘要', `status` TINYINT(1) NOT NULL DEFAULT 1 COMMENT '状态:1-未审核,2-已审核,3-已过账,4-已作废', `source_type` VARCHAR(32) DEFAULT NULL COMMENT '来源类型(手工/导入/自动生成)', `source_no` VARCHAR(64) DEFAULT NULL COMMENT '来源单据号', `create_user` VARCHAR(32) NOT NULL COMMENT '制单人', `audit_user` VARCHAR(32) DEFAULT NULL COMMENT '审核人', `audit_time` DATETIME DEFAULT NULL COMMENT '审核时间', `post_user` VARCHAR(32) DEFAULT NULL COMMENT '过账人', `post_time` DATETIME DEFAULT NULL COMMENT '过账时间', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_voucher_no` (`voucher_no`), KEY `idx_period_code` (`period_code`), KEY `idx_voucher_date` (`voucher_date`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会计凭证表'; -- 凭证分录表(凭证明细行) CREATE TABLE `acc_voucher_entry` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '分录ID', `voucher_id` BIGINT(20) NOT NULL COMMENT '凭证ID', `voucher_no` VARCHAR(32) NOT NULL COMMENT '凭证号', `line_no` INT(11) NOT NULL COMMENT '行号', `subject_code` VARCHAR(32) NOT NULL COMMENT '科目编码', `subject_name` VARCHAR(64) NOT NULL COMMENT '科目名称', `summary` VARCHAR(256) NOT NULL COMMENT '摘要', `debit_amount` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '借方金额', `credit_amount` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '贷方金额', `currency_code` VARCHAR(10) DEFAULT 'CNY' COMMENT '币种', `exchange_rate` DECIMAL(10,6) DEFAULT 1 COMMENT '汇率', `original_amount` DECIMAL(18,2) DEFAULT NULL COMMENT '原币金额', `auxiliary_data` TEXT DEFAULT NULL COMMENT '辅助核算数据(JSON)', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_voucher_id` (`voucher_id`), KEY `idx_subject_code` (`subject_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='凭证分录表'; -- 科目余额表 CREATE TABLE `acc_subject_balance` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `period_code` VARCHAR(10) NOT NULL COMMENT '期间编码', `subject_code` VARCHAR(32) NOT NULL COMMENT '科目编码', `begin_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期初借方', `begin_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期初贷方', `period_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '本期借方', `period_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '本期贷方', `year_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '本年累计借方', `year_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '本年累计贷方', `end_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期末借方', `end_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期末贷方', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_period_subject` (`period_code`, `subject_code`), KEY `idx_subject_code` (`subject_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科目余额表'; -- 辅助核算明细表 CREATE TABLE `acc_auxiliary_balance` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT 'ID', `period_code` VARCHAR(10) NOT NULL COMMENT '期间编码', `subject_code` VARCHAR(32) NOT NULL COMMENT '科目编码', `auxiliary_type` VARCHAR(32) NOT NULL COMMENT '辅助核算类型', `auxiliary_id` BIGINT(20) NOT NULL COMMENT '辅助核算ID', `auxiliary_code` VARCHAR(64) DEFAULT NULL COMMENT '辅助核算编码', `auxiliary_name` VARCHAR(128) DEFAULT NULL COMMENT '辅助核算名称', `begin_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期初借方', `begin_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期初贷方', `period_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '本期借方', `period_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '本期贷方', `end_debit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期末借方', `end_credit` DECIMAL(18,2) NOT NULL DEFAULT 0 COMMENT '期末贷方', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_balance` (`period_code`, `subject_code`, `auxiliary_type`, `auxiliary_id`), KEY `idx_auxiliary` (`auxiliary_type`, `auxiliary_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='辅助核算余额表'; -- 操作日志表(审计追踪) CREATE TABLE `acc_operation_log` ( `id` BIGINT(20) NOT NULL AUTO_INCREMENT, `operation_type` VARCHAR(32) NOT NULL COMMENT '操作类型', `business_type` VARCHAR(32) NOT NULL COMMENT '业务类型(凭证/科目/期间等)', `business_id` VARCHAR(64) NOT NULL COMMENT '业务ID', `business_no` VARCHAR(64) DEFAULT NULL COMMENT '业务单号', `operation_content` TEXT DEFAULT NULL COMMENT '操作内容', `before_data` TEXT DEFAULT NULL COMMENT '操作前数据', `after_data` TEXT DEFAULT NULL COMMENT '操作后数据', `operator` VARCHAR(32) NOT NULL COMMENT '操作人', `operate_time` DATETIME NOT NULL COMMENT '操作时间', `ip_address` VARCHAR(64) DEFAULT NULL COMMENT 'IP地址', PRIMARY KEY (`id`), KEY `idx_business` (`business_type`, `business_id`), KEY `idx_operate_time` (`operate_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
3.2 ER关系图
+------------------+ +------------------+ +------------------+ | acc_subject | | acc_voucher | | acc_voucher_entry| +------------------+ +------------------+ +------------------+ | subject_code(PK) |<------| subject_code(FK) | | id (PK) | | subject_name | | id (PK) |<------| voucher_id (FK) | | parent_code | | voucher_no (UK) | | subject_code | | subject_type | | period_code | | debit_amount | | balance_direction| | total_debit | | credit_amount | +------------------+ | total_credit | | auxiliary_data | | status | +------------------+ +------------------+ | +-------v--------+ | acc_period | +----------------+ | period_code(PK)| | period_year | | period_month | | status | +----------------+ +------------------+ +------------------------+ |acc_subject_balance| | acc_auxiliary_balance | +------------------+ +------------------------+ | period_code | | period_code | | subject_code | | subject_code | | begin_debit | | auxiliary_type | | period_debit | | auxiliary_id | | end_debit | | begin_debit | +------------------+ | period_debit | +------------------------+
四、核心实体类
4.1 科目实体
package com.accounting.service.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; import java.util.List; /** * 会计科目实体 */ @Data @TableName("acc_subject") public class AccSubject { @TableId(type = IdType.AUTO) private Long id; /** * 科目编码 */ private String subjectCode; /** * 科目名称 */ private String subjectName; /** * 上级科目编码 */ private String parentCode; /** * 科目级次 */ private Integer subjectLevel; /** * 科目类型:1-资产,2-负债,3-权益,4-成本,5-损益 */ private Integer subjectType; /** * 余额方向:1-借方,2-贷方 */ private Integer balanceDirection; /** * 是否末级科目 */ private Integer isLeaf; /** * 是否现金科目 */ private Integer isCashSubject; /** * 是否银行科目 */ private Integer isBankSubject; /** * 辅助核算类型(JSON数组) */ private String auxiliaryTypes; /** * 状态:0-停用,1-启用 */ private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; /** * 子科目列表(非数据库字段) */ @TableField(exist = false) private List<AccSubject> children; }
4.2 凭证实体
package com.accounting.service.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.util.List; /** * 会计凭证实体 */ @Data @TableName("acc_voucher") public class AccVoucher { @TableId(type = IdType.AUTO) private Long id; /** * 凭证号 */ private String voucherNo; /** * 凭证字(记/收/付/转) */ private String voucherType; /** * 凭证日期 */ private LocalDate voucherDate; /** * 所属期间 */ private String periodCode; /** * 附件数 */ private Integer attachmentCount; /** * 借方合计 */ private BigDecimal totalDebit; /** * 贷方合计 */ private BigDecimal totalCredit; /** * 凭证摘要 */ private String summary; /** * 状态:1-未审核,2-已审核,3-已过账,4-已作废 */ private Integer status; /** * 来源类型 */ private String sourceType; /** * 来源单据号 */ private String sourceNo; private String createUser; private String auditUser; private LocalDateTime auditTime; private String postUser; private LocalDateTime postTime; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; /** * 凭证分录列表(非数据库字段) */ @TableField(exist = false) private List<AccVoucherEntry> entries; }
package com.accounting.service.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; /** * 凭证分录实体 */ @Data @TableName("acc_voucher_entry") public class AccVoucherEntry { @TableId(type = IdType.AUTO) private Long id; private Long voucherId; private String voucherNo; /** * 行号 */ private Integer lineNo; private String subjectCode; private String subjectName; /** * 摘要 */ private String summary; /** * 借方金额 */ private BigDecimal debitAmount; /** * 贷方金额 */ private BigDecimal creditAmount; /** * 币种 */ private String currencyCode; /** * 汇率 */ private BigDecimal exchangeRate; /** * 原币金额 */ private BigDecimal originalAmount; /** * 辅助核算数据(JSON) */ private String auxiliaryData; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; }
4.3 枚举定义
package com.accounting.service.enums; import lombok.Getter; /** * 科目类型枚举 */ @Getter public enum SubjectTypeEnum { ASSET(1, "资产"), LIABILITY(2, "负债"), EQUITY(3, "所有者权益"), COST(4, "成本"), PROFIT_LOSS(5, "损益"); private final Integer code; private final String desc; SubjectTypeEnum(Integer code, String desc) { this.code = code; this.desc = desc; } public static SubjectTypeEnum of(Integer code) { for (SubjectTypeEnum value : values()) { if (value.getCode().equals(code)) { return value; } } return null; } }
package com.accounting.service.enums; import lombok.Getter; /** * 凭证状态枚举 */ @Getter public enum VoucherStatusEnum { DRAFT(1, "未审核"), AUDITED(2, "已审核"), POSTED(3, "已过账"), VOID(4, "已作废"); private final Integer code; private final String desc; VoucherStatusEnum(Integer code, String desc) { this.code = code; this.desc = desc; } }
package com.accounting.service.enums; import lombok.Getter; /** * 余额方向枚举 */ @Getter public enum BalanceDirectionEnum { DEBIT(1, "借方"), CREDIT(2, "贷方"); private final Integer code; private final String desc; BalanceDirectionEnum(Integer code, String desc) { this.code = code; this.desc = desc; } }
4.4 DTO定义
package com.accounting.service.dto; import lombok.Data; import javax.validation.constraints.*; import java.math.BigDecimal; import java.time.LocalDate; import java.util.List; /** * 创建凭证请求 */ @Data public class CreateVoucherRequest { /** * 凭证字 */ @NotBlank(message = "凭证字不能为空") private String voucherType; /** * 凭证日期 */ @NotNull(message = "凭证日期不能为空") private LocalDate voucherDate; /** * 附件数 */ private Integer attachmentCount; /** * 凭证摘要 */ private String summary; /** * 来源类型 */ private String sourceType; /** * 来源单据号 */ private String sourceNo; /** * 凭证分录 */ @NotEmpty(message = "凭证分录不能为空") @Size(min = 2, message = "凭证分录至少2行") private List<VoucherEntryDTO> entries; }
package com.accounting.service.dto; import lombok.Data; import javax.validation.constraints.*; import java.math.BigDecimal; /** * 凭证分录DTO */ @Data public class VoucherEntryDTO { /** * 科目编码 */ @NotBlank(message = "科目编码不能为空") private String subjectCode; /** * 摘要 */ @NotBlank(message = "摘要不能为空") private String summary; /** * 借方金额 */ private BigDecimal debitAmount; /** * 贷方金额 */ private BigDecimal creditAmount; /** * 币种 */ private String currencyCode; /** * 汇率 */ private BigDecimal exchangeRate; /** * 原币金额 */ private BigDecimal originalAmount; /** * 辅助核算数据 */ private AuxiliaryDataDTO auxiliaryData; }
package com.accounting.service.dto; import lombok.Data; import java.util.Map; /** * 辅助核算数据DTO */ @Data public class AuxiliaryDataDTO { /** * 部门 */ private Long departmentId; private String departmentName; /** * 项目 */ private Long projectId; private String projectName; /** * 往来单位 */ private Long partnerId; private String partnerName; /** * 员工 */ private Long employeeId; private String employeeName; /** * 现金流项目 */ private Long cashFlowItemId; private String cashFlowItemName; /** * 扩展字段 */ private Map<String, Object> extFields; }
五、科目管理服务
5.1 科目树结构
1001 库存现金 1002 银行存款 1002.01 工商银行 1002.02 建设银行 1122 应收账款 1122.01 客户A 1122.02 客户B 1403 原材料 1403.01 主要材料 1403.01.01 钢材 1403.01.02 铝材 1403.02 辅助材料 1601 固定资产 1601.01 房屋建筑物 1601.02 机器设备
5.2 科目管理服务
package com.accounting.service.service; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.accounting.common.exception.AccountingException; import com.accounting.common.result.ResultCode; import com.accounting.service.dto.CreateSubjectRequest; import com.accounting.service.entity.AccSubject; import com.accounting.service.mapper.AccSubjectMapper; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.*; import java.util.stream.Collectors; /** * 科目管理服务 */ @Slf4j @Service @RequiredArgsConstructor public class SubjectService { private final AccSubjectMapper subjectMapper; /** * 获取科目树 */ @Cacheable(value = "subject", key = "'tree'") public List<AccSubject> getSubjectTree() { List<AccSubject> allSubjects = subjectMapper.selectList( new LambdaQueryWrapper<AccSubject>() .eq(AccSubject::getStatus, 1) .orderByAsc(AccSubject::getSubjectCode) ); return buildTree(allSubjects); } /** * 构建科目树 */ private List<AccSubject> buildTree(List<AccSubject> subjects) { Map<String, List<AccSubject>> parentMap = subjects.stream() .filter(s -> s.getParentCode() != null) .collect(Collectors.groupingBy(AccSubject::getParentCode)); List<AccSubject> roots = subjects.stream() .filter(s -> s.getParentCode() == null || s.getParentCode().isEmpty()) .collect(Collectors.toList()); for (AccSubject root : roots) { buildChildren(root, parentMap); } return roots; } private void buildChildren(AccSubject parent, Map<String, List<AccSubject>> parentMap) { List<AccSubject> children = parentMap.get(parent.getSubjectCode()); if (children != null && !children.isEmpty()) { parent.setChildren(children); for (AccSubject child : children) { buildChildren(child, parentMap); } } } /** * 根据编码获取科目 */ @Cacheable(value = "subject", key = "#subjectCode") public AccSubject getByCode(String subjectCode) { return subjectMapper.selectOne( new LambdaQueryWrapper<AccSubject>() .eq(AccSubject::getSubjectCode, subjectCode) ); } /** * 获取末级科目列表 */ public List<AccSubject> getLeafSubjects() { return subjectMapper.selectList( new LambdaQueryWrapper<AccSubject>() .eq(AccSubject::getIsLeaf, 1) .eq(AccSubject::getStatus, 1) .orderByAsc(AccSubject::getSubjectCode) ); } /** * 创建科目 */ @Transactional(rollbackFor = Exception.class) @CacheEvict(value = "subject", allEntries = true) public AccSubject createSubject(CreateSubjectRequest request) { // 1. 校验科目编码唯一性 AccSubject existSubject = getByCode(request.getSubjectCode()); if (existSubject != null) { throw new AccountingException(ResultCode.SUBJECT_CODE_EXISTS); } // 2. 校验上级科目 AccSubject parent = null; if (request.getParentCode() != null && !request.getParentCode().isEmpty()) { parent = getByCode(request.getParentCode()); if (parent == null) { throw new AccountingException(ResultCode.PARENT_SUBJECT_NOT_EXIST); } // 上级科目必须是非末级 if (parent.getIsLeaf() == 1) { // 检查上级科目是否已有发生额,有则不能添加下级 if (hasBalance(parent.getSubjectCode())) { throw new AccountingException(ResultCode.PARENT_HAS_BALANCE); } // 更新上级为非末级 parent.setIsLeaf(0); subjectMapper.updateById(parent); } } // 3. 创建科目 AccSubject subject = new AccSubject(); subject.setSubjectCode(request.getSubjectCode()); subject.setSubjectName(request.getSubjectName()); subject.setParentCode(request.getParentCode()); subject.setSubjectLevel(parent != null ? parent.getSubjectLevel() + 1 : 1); subject.setSubjectType(parent != null ? parent.getSubjectType() : request.getSubjectType()); subject.setBalanceDirection(parent != null ? parent.getBalanceDirection() : request.getBalanceDirection()); subject.setIsLeaf(1); subject.setIsCashSubject(request.getIsCashSubject() != null ? request.getIsCashSubject() : 0); subject.setIsBankSubject(request.getIsBankSubject() != null ? request.getIsBankSubject() : 0); subject.setAuxiliaryTypes(request.getAuxiliaryTypes()); subject.setStatus(1); subjectMapper.insert(subject); log.info("创建科目成功: code={}, name={}", subject.getSubjectCode(), subject.getSubjectName()); return subject; } /** * 检查科目是否有余额 */ private boolean hasBalance(String subjectCode) { // TODO: 查询科目余额表判断 return false; } /** * 校验科目是否可用于记账 */ public void validateForVoucher(String subjectCode) { AccSubject subject = getByCode(subjectCode); if (subject == null) { throw new AccountingException(ResultCode.SUBJECT_NOT_EXIST); } if (subject.getStatus() != 1) { throw new AccountingException(ResultCode.SUBJECT_DISABLED); } if (subject.getIsLeaf() != 1) { throw new AccountingException(ResultCode.SUBJECT_NOT_LEAF); } } }
六、凭证管理服务
6.1 凭证处理流程
+----------+ +----------+ +----------+ +----------+ +----------+ | 录入 | --> | 保存 | --> | 审核 | --> | 过账 | --> | 完成 | +----------+ +----------+ +----------+ +----------+ +----------+ | | | | | v v v v v 填写分录 校验借贷平衡 审核人确认 更新科目余额 凭证状态完成 选择科目 生成凭证号 检查合规性 记录明细账 生成账簿 辅助核算 保存数据库 更新状态 更新辅助余额
2761

被折叠的 条评论
为什么被折叠?



