多公司进销存系统的账套模型与数据隔离架构设计
如果你的公司刚从一个做单门店进销存的小软件切换到“集团管控、多公司核算”这套体系你很快就会发现很多号称支持“多公司”的进销存软件其实只是在商品资料上加了一个“公司”字段。查询时它确实能按公司过滤但报表、库存、单据编号、月结年结全部混在一起账根本没法独立审计。这不是小事。财务上“多公司”与“多账套”是两套完全不同的需求前者可能只要求集团能看到所有数据后者要求每个公司拥有独立账本、独立库存、独立核算周期、独立期初数据。一套真正能用于集团企业的多公司进销存系统核心在于“账套”模型而不是一个简单的公司字段。这篇文章不会去拼一份软件选型功能清单而是从技术架构和数据库设计角度拆解“多家企业用一套进销存”这件事背后的账套体系、数据隔离方案、库存核算边界、权限模型和常见坑。如果你正准备做集团化进销存改造或者正在开发一套支持多公司的进销存/财务系统这篇文章会很有参考价值。1. 多公司进销存真正要解决的问题先看一个典型场景。某商贸集团旗下有三家独立法人公司A 公司做批发B 公司做零售C 公司做物流。三家公司共用同一套进销存系统但每家公司的商品、库存、采购订单、销售订单、应收应付必须严格分开。老板月底想看集团汇总报表但是审计时每家公司的账又要能独立拿出来。这个需求落到系统里至少要解决四件事第一数据隔离。A 公司的库存数据不能出现在 B 公司的单据里A 公司的采购订单不能被 C 公司的操作员选中并入库。第二基础资料独立。三家公司的商品资料可能部分相同但价格、库存单位、默认供应商不同如果共用一套商品档案很容易出现“改一个商品价格影响所有公司”的事故。第三单据编号独立。A 公司的采购单从 1 号开始编号B 公司也应有自己的独立编号序列。否则集团统一的连续编号会让分公司对账时非常痛苦。第四核算与结账独立。每个公司有自己的会计期间、自己的期初库存、自己的月结/年结流程。A 公司已经完成 12 月结账B 公司可能还在 11 月月底调整单据。系统不能让一个公司的结账操作把另一个公司的账一起锁住。这也是“多账套进销存”和普通“多公司进销存”的本质区别多账套意味着每个公司天然拥有一套独立的账目体系业务单据、库存余额、总账凭证、基础档案全部按账套边界隔离。而从实现路径看要想在进入生产环境后不返工第一关就是确定你采用的是“单库多账套”还是“多实例多数据库”模式。这个问题如果不在一开始决定后面改起来几乎等于重写系统。2. 账套、多账套、独立账套与共享账套2.1 什么是账套账套来源于财务软件最直白的解释是一套完整的账目体系。一个账套里包含凭证、科目、期初余额、辅助核算项、报表等财务数据它可以独立进行记账、结账、出报表。进销存软件发展到后期逐渐把“账套”概念引入业务层一个账套不仅包含财务账还包含库存账、往来账、单据流水。因此进销存里的“账套”可以理解为一套完整独立的业务财务数据集合。2.2 多账套的关键判断在集团库存管理软件中多账套需要满足三个原则业务数据按账套隔离查询、修改、审核、删除都只作用于当前账套。基础数据可选共享或独立商品、客户、供应商资料可以共享也可以按账套私有化。期末处理按账套独立结账、反结账、结转成本只影响当前账套。2.3 独立账套与共享账套独立账套是指每个公司拥有自己的基础资料和业务数据公司之间基本不互通。共享账套则是指多家公司共用一套基础资料但通过组织维度做权限和数据范围控制。在集团进销存场景里常见做法是“基础资料共享 业务数据隔离”集团统一维护商品、客户、供应商主数据单据数据按公司账套隔离。这种做法既能减少基础资料重复维护的成本又能保证各分公司独立考核。维度独立账套共享账套基础资料各公司维护各自的集团统一维护分公司引用业务单据完全隔离按公司/部门隔离报表汇总需要额外合并天然可透视集团汇总适用场景独立核算、独立审计的子公司集团管控、统一商品目录的商贸企业从实践看纯独立账套适合子公司业务差异较大的集团共享账套适合商品高度统一、只有组织和价格不同的商贸集团。绝大多数真正的多公司进销存系统落在两者之间只是程度不同。3. 多公司进销存系统的三种实现路径3.1 路径一独立部署每公司一套实例每家公司部署一套完整的进销存系统包括独立的数据库、应用服务。集团报表另做拉取汇总。优点系统相互隔离某一公司故障不影响其他公司。权限控制最简单登录哪套就是哪家公司。每套系统数据库独立备份恢复互不干扰。缺点运维成本高三家公司要维护三套环境。基础资料不一致集团汇总报表要处理编码映射。升级要逐套执行容易版本漂移。新增一家公司要重新部署一套系统。这种路径适合子公司数量很少且业务完全独立的集团公司。但对商贸企业来说业务上存在大量内部调拨和集中采购独立部署会让公司间协同变得很麻烦。3.2 路径二单实例多账套共享数据库这是当前中小型多公司进销存最主流的实现方式。一套系统、一个数据库通过账套字段隔离数据。应用启动时根据当前登录用户绑定的账套决定所有 SQL 查询的范围。优点部署简单一套系统支持所有公司。基础资料可以设置共享范围。集团公司内部调拨、结算可以在一个系统内完成。新增公司只要在系统里开一个新账套无需部署新环境。缺点数据库单点压力随公司数量增加而上升。账套隔离逻辑如果设计不严谨容易产生越权数据访问。表数据量是所有公司叠加的需要更认真的索引优化。这个路径是本文后续重点展开的方向因为它最符合“多家企业用一套进销存”这个标题语境。技术上更准确的说法是单实例多账套也叫共享应用、隔离数据。3.3 路径三原生多租户 SaaS 架构这是面向公共云服务的架构。一套系统服务成百上千家企业通过租户 ID 隔离数据。所有租户共享同一套代码和基础设施数据库层可以是独立数据库、共享库独立 Schema或共享表加租户字段。优点规模效应好边际成本低。租户自助开通、按量计费。适合软件厂商做标准化 SaaS 产品。缺点架构复杂度高租户数据隔离需要精细设计。单个租户的需求要收敛到标准化功能里很难做深度定制。对于集团型客户还要在租户内再划分公司/账套层级。3.4 三种路径的选型对比对比维度独立部署多实例单实例多账套原生多租户 SaaS部署成本高低低维护成本高中低数据隔离强度最强中取决于设计中集团报表汇总难较易较易定制化能力强中弱典型客户规模少量子公司集团/连锁企业大量中小商户对大多数打算上线多公司进销存的企业来说路径二是性价比最高的选择。选择路径二之后数据库层面怎么设计就成了决定系统成败的核心问题这也是下一节要重点讲的。4. 数据库设计多账套进销存的核心表结构单实例多账套的数据库设计核心原则是所有业务表显式携带账套字段所有查询强制带账套条件。不要指望“通过 session 里存一个 companyId系统自动过滤”。你需要在表结构、ORM 层、查询层三层同时加约束数据才不会串。4.1 账套主表账套不仅仅是公司名称还需要存储账套的启用状态、会计期间、业务模式等。-- 文件路径sql/acc_set.sql CREATE TABLE t_acc_set ( acc_set_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 账套ID, acc_set_code VARCHAR(32) NOT NULL COMMENT 账套编码全局唯一, acc_set_name VARCHAR(128) NOT NULL COMMENT 账套名称/公司名称, parent_set_id BIGINT DEFAULT 0 COMMENT 上级账套ID0为集团根, company_type TINYINT NOT NULL COMMENT 公司类型1批发 2零售 3物流 4生产, is_group TINYINT DEFAULT 0 COMMENT 是否集团汇总账套, status TINYINT DEFAULT 1 COMMENT 状态1启用 0停用, start_date DATE COMMENT 启用日期/建账日期, current_period VARCHAR(16) COMMENT 当前会计期间如2025-12, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (acc_set_id), UNIQUE KEY uk_acc_set_code (acc_set_code) ) ENGINEInnoDB COMMENT账套主表;这里的关键点是acc_set_code必须是全局唯一的。它不仅是数据库层面的标识也会出现在单据编号、报表筛选条件中。编码规则建议使用集团统一规则比如“GS001”“GS002”这种有业务含义的编码便于人工识别。4.2 业务表统一携带账套字段以采购入库单为例所有业务单据表都要带acc_set_id。不仅主表要带单据明细表也要带。-- 文件路径sql/purchase_in.sql CREATE TABLE t_purchase_in ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 单据ID, acc_set_id BIGINT NOT NULL COMMENT 账套ID, bill_no VARCHAR(32) NOT NULL COMMENT 单据编号账套内唯一, supplier_id BIGINT NOT NULL COMMENT 供应商ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, bill_date DATE NOT NULL COMMENT 单据日期, total_amount DECIMAL(18,2) DEFAULT 0 COMMENT 金额合计, status TINYINT DEFAULT 0 COMMENT 状态0草稿 1已审核 2已结账, created_by BIGINT NOT NULL COMMENT 创建人ID, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_acc_bill_no (acc_set_id, bill_no), KEY idx_acc_set_date (acc_set_id, bill_date) ) ENGINEInnoDB COMMENT采购入库单主表; CREATE TABLE t_purchase_in_item ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 明细ID, p_in_id BIGINT NOT NULL COMMENT 主表ID, acc_set_id BIGINT NOT NULL COMMENT 账套ID, item_id BIGINT NOT NULL COMMENT 商品ID, qty DECIMAL(18,3) NOT NULL COMMENT 数量, price DECIMAL(18,6) NOT NULL COMMENT 单价, amount DECIMAL(18,2) NOT NULL COMMENT 金额, PRIMARY KEY (id), KEY idx_acc_set (acc_set_id, p_in_id) ) ENGINEInnoDB COMMENT采购入库单明细表;看到这里你应该能理解为什么“加一个 company 字段”不等于多账套。账套隔离要求唯一键在账套内唯一而不是全局唯一。这里把uk_acc_bill_no设计成(acc_set_id, bill_no)就是为了让每个账套里的“单号-2025-001”互不冲突。4.3 库存表设计库存表是多公司进销存中最容易出问题的表。如果没有账套维度两个公司可能操作同一个商品ID和仓库ID库存数量直接互相覆盖。-- 文件路径sql/stock.sql CREATE TABLE t_stock ( id BIGINT NOT NULL AUTO_INCREMENT, acc_set_id BIGINT NOT NULL COMMENT 账套ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, item_id BIGINT NOT NULL COMMENT 商品ID, qty DECIMAL(18,3) DEFAULT 0 COMMENT 当前库存数量, batch_no VARCHAR(64) DEFAULT COMMENT 批次号, expire_date DATE COMMENT 到期日, PRIMARY KEY (id), UNIQUE KEY uk_acc_wh_item_batch (acc_set_id, warehouse_id, item_id, batch_no), KEY idx_acc_item (acc_set_id, item_id) ) ENGINEInnoDB COMMENT实时库存表;实际项目中库存表除了实时余额还应该配合“库存流水表”记录每次出入库明细。流水表同样需要acc_set_id因为月中要查某账套某商品的出入库明细。4.4 查询必须强制带账套条件设计完成后落地时最怕开发人员写 SQL 漏掉acc_set_id。在共享表模式下漏掉账套条件就是数据泄露。常见做法有三种ORM 全局过滤器在 MyBatis-Plus 中用拦截器自动追加acc_set_id条件。查询层强制校验所有 Service 方法的查询对象必须包含acc_set_id缺失直接抛异常。数据权限插件利用 Shiro/Spring Security 的数据权限表达式把当前账套 ID 注入 SQL。推荐至少使用“全局过滤器 查询层校验”双保险。只依赖开发人员自觉迟早要出事故。5. 基础资料共享与独立的边界设计多账套系统里基础资料的共享范围是最容易被业务挑战的设计点。你需要提前想清楚商品、供应商、客户、仓库到底每个账套一份还是集团共享一份。5.1 基础资料共享设计通常商贸集团希望商品、供应商是共享的同一批商品在所有公司统一编码同一家供应商给不同公司供货价格不一定相同。这就需要在共享主数据下再挂一套“按账套扩展”的私有属性。-- 文件路径sql/item_shared.sql -- 商品主数据集团共享 CREATE TABLE t_item ( item_id BIGINT NOT NULL AUTO_INCREMENT, item_code VARCHAR(32) NOT NULL, item_name VARCHAR(128) NOT NULL, spec VARCHAR(64) DEFAULT COMMENT 规格, unit VARCHAR(16) DEFAULT COMMENT 基本单位, status TINYINT DEFAULT 1, PRIMARY KEY (item_id), UNIQUE KEY uk_item_code (item_code) ) ENGINEInnoDB COMMENT商品主数据; -- 账套扩展属性每个账套私有的价格、税率、默认仓库等 CREATE TABLE t_item_acc_set ( id BIGINT NOT NULL AUTO_INCREMENT, acc_set_id BIGINT NOT NULL, item_id BIGINT NOT NULL, sale_price DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT 该账套销售价, purchase_price DECIMAL(18,6) NOT NULL DEFAULT 0 COMMENT 该账套采购价, tax_rate DECIMAL(5,4) DEFAULT 0 COMMENT 该账套默认税率, is_active TINYINT DEFAULT 1 COMMENT 该账套是否启用此商品, PRIMARY KEY (id), UNIQUE KEY uk_acc_item (acc_set_id, item_id) ) ENGINEInnoDB COMMENT账套商品扩展表;这个设计的好处是集团统一维护商品名称、编码、规格每个账套自定义价格、税率、可用状态。查询商品时先通过t_item找到主数据再通过t_item_acc_set取当前账套的扩展属性。如果你把价格直接写在商品主表里一个分公司改价另一个分公司就会跟着变这在业务上是不可接受的。5.2 组织与用户权限用户与账套是多对多关系。一个集团管理员可以操作所有账套一个分公司操作员只能进入自己所属账套。这个关系必须单独建表维护。-- 文件路径sql/user_acc_set.sql CREATE TABLE t_user_acc_set ( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, acc_set_id BIGINT NOT NULL COMMENT 可访问账套ID, role_id BIGINT NOT NULL COMMENT 角色ID在账套内生效, status TINYINT DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY uk_user_acc (user_id, acc_set_id) ) ENGINEInnoDB COMMENT用户账套权限表;登录后用户选择当前操作账套系统从t_user_acc_set校验该用户是否有权访问。查询账套列表时也要先查这张表而不是直接返回所有账套。5.3 账套内数据权限除账套隔离外大型集团还会在一家公司内部分仓库、部门、业务员。此时除了acc_set_id还需要增加数据范围维度。建议不要在设计初期就把权限维度铺得太复杂先保证账套隔离正确再逐层叠加组织权限。这里有一个容易犯的错误把“公司”和“仓库”绑定成同一个维度。在商贸集团场景里A 公司的仓库可能放在 B 公司的物流园区如果仓库维度直接归属公司后续库存调拨实现起来会很别扭。更稳妥的设计是公司维度管账、仓库维度管物理库存两者独立建模通过单据关联。6. 单据编号与库存核算最容易出错的三个细节单实例多账套系统开发到中后期真正让你痛苦的往往不是大框架而是三个细节单据编号、结账周期、库存调拨。这三个细节做不好系统很难真正投入使用。6.1 单据编号规则单据编号必须在账套内独立。推荐采用“账套编码 年月 流水号”的方式生成例如GS001-PUR-202512-0001其中GS001是账套编码PUR是单据类型202512是年月0001是当月流水号。这种编号规则的好处是从单号上直接能看出是哪家公司、什么单据、哪个期间的业务。业务人员在电话沟通“单号多少”时不需要先解释是哪家公司。编号生成必须依赖数据库的唯一索引保证不能只靠应用层计数器。高并发下应用层计数器容易产生重复号。推荐用一张编号生成表-- 文件路径sql/bill_no_serial.sql CREATE TABLE t_bill_no_serial ( id BIGINT NOT NULL AUTO_INCREMENT, acc_set_id BIGINT NOT NULL, bill_type VARCHAR(16) NOT NULL, period VARCHAR(16) NOT NULL COMMENT 期间如2025-12, last_no INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_acc_type_period (acc_set_id, bill_type, period) ) ENGINEInnoDB COMMENT单据编号流水表;生成编号时用一条带FOR UPDATE的事务更新last_no再拼接编号。虽然会对该账套、该单据类型的编号生成产生一定串行化但进销存单据并发量通常远达不到需要分布式发号器的程度这种方式简单、可靠、可追溯。6.2 期初库存和结账周期新账套开账时必须录入期初库存。注意不同账套的期初库存是独立的。哪怕 A、B 公司经营同样商品也要分别录入期初。期末处理同样要独立。以 12 月结账为例A 账套结账后库存数据锁定B 账套还在 12 月调整期不能因为 A 已经结账就禁止 B 操作。实现时结账状态应该记录在账套参数表里而不是全局配置里。t_acc_set.current_period 2025-12 表示该账套当前业务期间 t_acc_set.is_closed 0/1 表示该账套本月是否已结账当账套结账时系统校验该账套所有单据状态生成本账套的期末库存快照然后把current_period推进到下一期。集团合并报表模块单独按账套取数不受单个账套结账进度影响。6.3 公司间库存调拨与结算集团内 A 公司调货给 B 公司在业务上是“内部销售”在仓储上是“库存移库”。这需要两边账套各生成一张单据A 账套做“调出/销售出库”B 账套做“调入/采购入库”。两张单通过一个“调拨任务号”关联方便对账。如果系统直接把库存从一个账套扣减、加到另一个账套而不生成双方单据后续财务结算会无从谈起。这一点在需求评审时要专门确认跨账套调拨必须双单生成不允许直接改库存余额。7. 多公司进销存完整实操示例下面用一个最小示例演示一套单实例多账套进销存系统从账套创建、数据初始化的完整后端处理流程。这里用 Spring Boot MyBatis-Plus 作为示例框架重点是流程逻辑不依赖特定版本。7.1 账套创建接口// 文件路径src/main/java/com/example/accset/service/AccSetService.java Service public class AccSetService { Autowired private AccSetMapper accSetMapper; Autowired private ItemAccSetMapper itemAccSetMapper; Autowired private UserAccSetMapper userAccSetMapper; Transactional(rollbackFor Exception.class) public Long createAccSet(AccSetCreateCmd cmd) { // 1. 检查账套编码是否重复 Long count accSetMapper.selectCount( new LambdaQueryWrapperAccSet() .eq(AccSet::getAccSetCode, cmd.getAccSetCode())); if (count 0) { throw new BizException(账套编码已存在); } // 2. 创建账套主记录 AccSet accSet new AccSet(); accSet.setAccSetCode(cmd.getAccSetCode()); accSet.setAccSetName(cmd.getAccSetName()); accSet.setParentSetId(cmd.getParentSetId() null ? 0L : cmd.getParentSetId()); accSet.setCompanyType(cmd.getCompanyType()); accSet.setStatus(1); accSet.setCurrentPeriod(cmd.getStartPeriod()); accSetMapper.insert(accSet); // 3. 初始化账套商品扩展属性 // 从集团公共商品池复制启用的商品 ListItem allItems itemMapper.selectList( new LambdaQueryWrapperItem().eq(Item::getStatus, 1)); if (allItems ! null) { for (Item item : allItems) { ItemAccSet ext new ItemAccSet(); ext.setAccSetId(accSet.getId()); ext.setItemId(item.getItemId()); ext.setSalePrice(BigDecimal.ZERO); ext.setPurchasePrice(BigDecimal.ZERO); ext.setIsActive(1); itemAccSetMapper.insert(ext); } } // 4. 默认管理员分配该账套权限 UserAccSet uas new UserAccSet(); uas.setUserId(cmd.getAdminUserId()); uas.setAccSetId(accSet.getId()); uas.setRoleId(1L); // 默认账套管理员角色 userAccSetMapper.insert(uas); return accSet.getId(); } }这段代码演示了一个非常重要的逻辑开通新账套时系统自动从集团商品池复制一份商品扩展记录。新建账套后的商品依然与主数据共享编码但每个账套有独立的价格和启用状态。这就是第 5 节“共享主数据 私有扩展”的落地写法。7.2 账套内库存初始化新账套创建后需要录入期初库存。期初库存只影响当前账套不影响其他账套。// 文件路径src/main/java/com/example/accset/service/StockInitService.java Service public class StockInitService { Autowired private StockMapper stockMapper; Transactional(rollbackFor Exception.class) public void initStock(Long accSetId, ListStockInitItem items) { for (StockInitItem item : items) { // 校验当前账套是否启用了该商品 // ... Stock stock new Stock(); stock.setAccSetId(accSetId); stock.setWarehouseId(item.getWarehouseId()); stock.setItemId(item.getItemId()); stock.setQty(item.getQty()); stock.setBatchNo(item.getBatchNo()); stockMapper.insert(stock); } } }这里有一个很容易忽略的问题期初库存初始化时必须同时写入库存流水表形成一条“期初结存”类型的流水。否则期末对账时库存余额明细会缺少来源无法解释库存是怎么产生的。库存流水表的字段建议包含acc_set_id、stock_id、source_type期初/采购/销售/调拨/盘点、source_bill_no、change_qty、before_qty、after_qty。7.3 账套隔离查询示例在业务查询中强制使用acc_set_id作为第一个条件// 文件路径src/main/java/com/example/accset/service/PurchaseInService.java public PageResultPurchaseInVO queryPurchaseIn(Long accSetId, PurchaseInQuery query) { if (accSetId null) { throw new BizException(缺少账套参数); } PagePurchaseIn page new Page(query.getPageNo(), query.getPageSize()); LambdaQueryWrapperPurchaseIn wrapper new LambdaQueryWrapper(); wrapper.eq(PurchaseIn::getAccSetId, accSetId) // 账套条件 .eq(query.getBillNo() ! null, PurchaseIn::getBillNo, query.getBillNo()) .orderByDesc(PurchaseIn::getBillDate); PagePurchaseIn result purchaseInMapper.selectPage(page, wrapper); // 转换VO... return new PageResult(result.getTotal(), voList); }注意这里accSetId的来源必须是服务器端从当前登录用户的账套上下文获取不能从前端参数信任。前端只传accSetId后端校验该用户是否确实绑定了该账套再执行查询。否则一个操作员可以直接修改请求参数看到其他公司的单据。8. 常见问题与排查思路问题现象可能原因排查方式解决方案A 公司查询到 B 公司的库存SQL 漏写acc_set_id检查后端 SQL 日志看 WHERE 条件是否包含账套字段全局 SQL 拦截器兜底强制追加账套条件新建账套后商品价格混乱扩展表未初始化或直接修改了主表价格查看t_item_acc_set表是否有当前账套记录新账套启用时自动复制基础资料禁止业务直接改主表价格单据编号重复编号生成逻辑未按账套唯一约束检查t_bill_no_serial唯一索引生成编号前加FOR UPDATE行锁保证并发安全期初库存录入后余额对不上期初库存未写入库存流水表查看库存流水查找“期初结存”记录期初数据必须同步生成来源流水结账后还能改上期单据结账逻辑只校验状态未锁业务期间检查t_acc_set.current_period和单据期间比较单据创建、修改时校验单据期间小于当前期间则拒绝跨公司调拨导致库存丢失调拨只扣了一个账套的库存未生成对方单据检查调拨任务是否双单生成调拨必须产生调出库和调入单两边各自走审批集团报表汇总数据翻倍汇总逻辑重复汇总了共享商品、客户等主数据检查汇总报表的 SQL 是否 JOIN 了t_item汇总报表只统计业务表基础资料不参与数量汇总排查思路有一个顺序先看登录用户上下文账套是否正确再看 SQL 是否带账套条件再看账套参数表状态。大多数“串账”问题都是在这三步中某一环漏掉了。9. 最佳实践与工程建议9.1 账套编码规范账套编码是集团化系统的地基。建议采用“集团级编码规范”一旦确定不要随意变更。示例规则纯数字字母长度 4-8 位。层级关系不要编码进账套编码用parent_set_id表达层级。编码全局唯一即使账套停用编码也不允许复用。9.2 数据备份与账套恢复单实例多账套意味着所有公司的数据都在一个数据库里备份与恢复策略比独立部署更复杂。每天全量备份保留至少 30 天。备份恢复演练要定期执行尤其要验证“只恢复某个账套”的可行性。如果某个账套数据异常不能直接恢复整个库影响其他公司要有按账套导出/导入工具。9.3 账套内参数配置不同公司对进销存功能的要求可能存在差异比如A 公司需要批次管理B 公司不需要A 公司用移动加权平均法核算成本B 公司用先进先出法。这些参数应该放在账套参数表中以acc_set_id为维度存储而不是写死在代码或全局配置里。# 文件路径bootstrap.properties # 示例账套参数不建议写死在全局配置文件中 # 推荐在数据库中维护 t_acc_set_param 表-- 文件路径sql/acc_set_param.sql CREATE TABLE t_acc_set_param ( id BIGINT NOT NULL AUTO_INCREMENT, acc_set_id BIGINT NOT NULL, param_key VARCHAR(64) NOT NULL, param_value VARCHAR(255) DEFAULT , PRIMARY KEY (id), UNIQUE KEY uk_acc_param (acc_set_id, param_key) ) ENGINEInnoDB COMMENT账套参数表;9.4 审计日志多公司系统的审计比单公司系统更重要。因为一个集团管理员可能操作多家账套如果没有操作日志出了问题很难追责。建议至少记录用户登录时选择的账套上下文。所有单据的创建、修改、审核、反审核操作。账套参数变更。结账与反结账操作。跨账套调拨操作。9.5 版本升级与多账套兼容单实例多账套的版本升级要特别注意兼容性新增的字段如果是业务必填要给旧账套设置默认值。不要因为新功能增加“全局强制校验”影响尚未使用该功能的账套。迁移脚本要添加acc_set_id条件避免更新所有账套的数据。9.6 安全与最小权限在生产环境中数据库账号不要使用 root 或高权限账号应用账号只授予当前库必要权限。账套数据隔离在应用层实现不等于数据库层面可以放开访问。如果开发人员能从数据库客户端直接查询所有账套数据那多账套隔离就形同虚设。10. 结语选择多公司进销存前先想清楚账套模型多公司进销存软件在选型或开发时最值得花时间讨论的不是“功能列表”而是账套模型。架构上究竟是独立部署、单实例多账套还是原生多租户直接决定后续的业务边界、运维成本和扩展空间。对绝大多数集团商贸企业来说单实例多账套是性价比较高的选择但前提是数据库设计从一开始就严格贯彻“业务表携带账套 ID、查询强制账套条件、基础资料共享加私有扩展、期末处理按账套独立”这几条原则。从实现看多账套不是高不可攀的技术但它是一项“细心工程”。数据隔离、单据编号、期初库存、结账周期、跨账套调拨、审计日志每一个环节都必须有明确的边界和规则。如果你正在做相关系统建议先把本文的账套表结构、编号生成表、权限关系表和库存流水设计落到实际项目中再用一个最小账套案例走通全流程。做完这些再谈集团报表和跨公司结算地基就扎实了。如果你对集团合并报表、跨账套调拨结算、成本核算方式这些方向有兴趣建议先从“账套参数独立化”和“内部交易双单生成”这两个功能入手深入实践。它们是多公司进销存里最考验业务理解的部分也是出错后影响最大的部分。