尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多账套与多公司进销存系统设计:数据隔离与Spring Boot实现

多账套与多公司进销存系统设计:数据隔离与Spring Boot实现 当你所在的企业不止一家公司而是拥有多个分公司、多个独立核算主体时一套系统能不能既满足“每个公司独立做账、独立管库存”又方便集团层面统一查看数据这就是多公司进销存、多账套软件的典型场景。最近在项目里做多账套系统重构踩了不少数据串账和权限隔离的坑。本文围绕“多公司进销存软件”与“多账套”这两个核心点从概念拆解、架构方案、数据库设计到 Spring Boot 最小可运行示例完整梳理一套可落地的多账套实现思路。无论你是企业信息化负责人还是正在做 SaaS 进销存系统的开发者本文都值得收藏备用。1. 背景与核心概念先来解决一个基础问题多公司进销存软件和多账套到底是什么关系。1.1 什么是多账套“账套”一词来自财务软件领域指的是一套独立、完整的账务数据体系。传统单机版财务软件中一家公司通常建一个账套包括科目表、凭证、账簿、报表等。如果企业有多家法人公司就需要建多个账套每个账套单独核算互不干扰。当这个概念扩展到进销存和 ERP 场景后多账套就不再只是财务术语而是一种系统级的数据隔离能力每个公司拥有独立的基础资料比如商品档案、客户档案、供应商档案。每个公司的采购单、销售单、库存单据必须分开存储、分开统计。每个公司的财务凭证、科目余额、利润报表不能混在一起。集团总部可以按公司维度查询汇总数据但不能随便改动下属公司的账。所以多账套进销存软件的本质是“一套系统、多个独立业务空间”。它解决的不只是数据存储问题更是权限、流程、报表和运营管理上的多维隔离问题。1.2 多公司进销存软件解决什么问题在没有多账套系统之前很多集团公司是怎么管账的最常见的是两种土办法第一种给每个分公司买一套独立软件。数据是隔离了但版本升级、人员维护、服务器成本全部翻倍总部想汇总各公司数据只能靠导出 Excel 再手工合并效率低且容易出错。第二种所有分公司硬塞在一个账套里靠仓库名称或部门字段去区分。短期看省事但月底一看库存台账分不清哪个分公司的货财务科目对不上业务数据互相污染最后账目一团乱。多账套系统的价值就在这里既保留独立核算的清晰度又避免重复部署和多套维护。从技术角度看这意味着需要解决几个核心问题集团组织架构如何建模。数据表层面如何隔离各公司数据。登录用户如何感知并切换到对应账套。跨公司查询和汇总时如何保证数据准确。1.3 需要区分的几个概念我整理了几个容易混淆的术语先看清楚再往下走术语含义侧重点账套一套独立账务或业务数据集合财务/业务数据的完整性和独立性多账套同一系统内存在多个账套互相隔离数据隔离、权限独立多租户SaaS 架构中多个客户共用同一套应用共享资源、按租户隔离数据集团管控总部对下属公司进行统一控制与汇总组织层级、审批流、合并报表分公司独立账套每个分公司拥有自己的账套经营主体独立核算从实现角度多账套往往可以看成多租户架构在集团企业场景下的一种落地形式。大多数集团型进销存系统就是典型的“多租户 多组织”混合模型。2. 多账套系统的技术方案盘点聊完概念再看落地时的技术选型。多账套系统最关键的问题是数据怎么隔离。不同方案对应不同的复杂度、成本和扩展性。2.1 独立数据库方案每一个公司账套分配一个独立数据库。比如数据库db_company_001对应 A 公司db_company_002对应 B 公司。优点是数据隔离最彻底某一套数据损坏不影响其他公司。备份恢复方便按数据库粒度操作。适合大型集团、数据量和并发都较大的场景。缺点是数据库实例和连接池数量多运维成本高。所有表结构升级时要逐个数据库执行可能需要脚本批量处理。跨公司汇总统计需要跨库查询通常要通过汇总服务或数据仓库。代码中如何实现一般是在数据源层面做动态路由根据当前登录用户的账套编码切换 DataSource。Spring 中可以通过AbstractRoutingDataSource实现。// 文件路径src/main/java/com/example/multiacct/datasource/DynamicDataSource.java public class DynamicDataSource extends AbstractRoutingDataSource { Override protected Object determineCurrentLookupKey() { return AccountContextHolder.getCurrentAccount(); } }2.2 共享数据库、共享 Schema、租户 ID 隔离所有公司数据存放在同一个数据库、同一批表里每张业务表增加一个company_id或tenant_id字段。查询时强制带上该字段实现逻辑隔离。这种方案正是当前很多 SaaS 进销存系统采用的方案也是本文将重点演示的方案。优点是成本低、扩展方便、跨账套汇总相对容易缺点是如果开发者疏忽漏写company_id条件就会出现严重的数据串账问题。2.3 共享数据库、独立 Schema同一个数据库实例下每个账套一个 Schema。例如schema_company_001、schema_company_002。相比多数据库方案它共享了数据库实例减少了重复资源相比共享表方案隔离性又更好。但这种方案在 MySQL 中支持较差MySQL 的 Schema 就是 Database本质还是多库在 PostgreSQL、SQL Server 中比较常见。2.4 三种方案怎么选这里给一个比较实用的选择建议方案成本隔离性扩展性推荐场景独立数据库高最强一般大型集团、高安全要求共享 Schema中较强中中型企业、行业私有化部署共享表 租户 ID低逻辑隔离高SaaS 进销存、中小客户多如果你们公司是给多家企业提供进销存系统也就是做 SaaS 产品优先考虑共享表加company_id的隔离方案。开发效率高后续做数据汇总和平台运营都很方便。重点是把“数据隔离”作为架构红线所有数据库访问都必须经过统一入口。3. 多账套系统核心设计思路技术方案确定后接着考虑业务建模。多账套进销存系统的核心不只是“加一个公司字段”而是从组织、权限、库存、财务多个维度重新设计。3.1 组织与账套模型我建议在设计数据库时区分“组织”和“账套”两个概念而不是简单合并。组织Organization表示真实的企业结构比如集团、分公司、事业部、门店。账套Account Set表示业务核算单元是业务数据的归属维度。一个集团公司下面可能有两个分公司它们可以各建一套账也可能某个分公司刚开始业务小暂时跟总部共用一套账。让组织和账套解耦后续调整更灵活。可以设计成如下模型组织表sys_org - org_id组织ID - org_code组织编码 - org_name组织名称 - parent_id上级组织ID 账套表sys_account - account_id账套ID - account_code账套编码 - account_name账套名称 - owner_org_id所属组织ID - status状态启用/停用 组织与账套关系表sys_org_account - org_id组织ID - account_id账套ID - is_default是否默认账套在进销存界面中用户登录后首先要选择“当前账套”也就是切换业务身份。这个选择决定了下单、查库存、做凭证时的数据范围。3.2 数据隔离维度设计采用共享表加company_id的方案时并不是所有表都需要加这个字段。要区分基础档案表和业务单据表。商品档案如果各公司使用统一的商品目录可以不加company_id用“数据权限”控制可见范围。采购单、销售单、库存单据必须加company_id因为这些单据完全属于某个账套。财务凭证、科目余额表必须加company_id还要考虑会计科目是否按账套隔离。一个比较稳妥的规则是凡是跟钱、货、单据相关的流水表一律用company_id隔离凡是主数据表通过权限表处理可见性。以商品表为例CREATE TABLE product ( id BIGINT PRIMARY KEY COMMENT 主键, company_id BIGINT NOT NULL COMMENT 所属公司ID, product_code VARCHAR(50) NOT NULL COMMENT 商品编码, product_name VARCHAR(200) NOT NULL COMMENT 商品名称, spec VARCHAR(100) COMMENT 规格型号, unit VARCHAR(20) COMMENT 单位, sale_price DECIMAL(12, 2) COMMENT 销售价, cost_price DECIMAL(12, 2) COMMENT 成本价, create_time DATETIME, update_time DATETIME, KEY idx_company_code (company_id, product_code) ) COMMENT 商品档案表;注意这里的索引设计company_id和product_code联合索引查询时不仅过滤了公司还加快了商品编码检索速度。如果所有公司的商品编码各不相同也可以建立唯一索引但通常不建议全局唯一因为不同账套中编码规则可能不同。3.3 权限与角色设计多账套系统里登录用户的权限需要区分两层第一层功能权限。用户能操作哪些菜单比如采购管理、销售管理、库存管理、财务报表。第二层数据权限。用户能访问哪些账套的数据。数据权限又可以分为只能访问自己负责的账套。可以访问集团范围内所有账套。只能查看部分报表但不能新增单据。常见的做法是维护一张用户与账套的关联表CREATE TABLE sys_user_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 用户ID, account_id BIGINT NOT NULL COMMENT 账套ID, role_code VARCHAR(50) COMMENT 在该账套下的角色, status TINYINT DEFAULT 1 COMMENT 状态, UNIQUE KEY uk_user_account (user_id, account_id) ) COMMENT 用户账套权限表;不要直接把账套编码放在用户表里。一个用户可能属于多个账套正确的做法是维护多对多关系。登录后展示用户可以访问的账套列表选择某个账套后将账套编码写入当前会话上下文后续所有业务操作都从这里取值。3.4 库存与财务数据联动进销存系统最核心的两条数据链分别是“货物流”和“资金流”。货物流采购入库单变更库存数量销售出库单减少库存数量调拨单在仓库之间移动库存。资金流采购单生成应付凭证销售单生成应收凭证费用单生成费用凭证。多账套环境下要确保这些联动操作不越界。比如 A 公司的采购入库绝不能影响 B 公司的库存数量A 公司的销售凭证也不能写入 B 公司的凭证表。这里有一个经验库存流水表和财务凭证表必须显式记录company_id而且生成凭证的来源单据也要带source_company_id字段避免跨账套引用时出现错误。例如库存流水表CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL COMMENT 公司ID, warehouse_id BIGINT NOT NULL COMMENT 仓库ID, product_id BIGINT NOT NULL COMMENT 商品ID, change_type VARCHAR(20) NOT NULL COMMENT 变动类型IN-入库OUT-出库, quantity DECIMAL(12, 2) NOT NULL COMMENT 变动数量, before_quantity DECIMAL(12, 2) COMMENT 变动前库存, after_quantity DECIMAL(12, 2) COMMENT 变动后库存, source_bill_no VARCHAR(50) COMMENT 来源单据号, create_by BIGINT COMMENT 创建人, create_time DATETIME COMMENT 创建时间, KEY idx_company_product (company_id, product_id), KEY idx_company_time (company_id, create_time) ) COMMENT 库存流水表;3.5 分布式事务与跨账套查询如果只是单数据库内的共享表方案事务相对简单同一个数据库连接上完成了采购单、库存流水、凭证的写入用Transactional可以保证原子性。但如果选择了独立数据库方案A 公司的业务操作需要同时改 B 公司的数据就涉及跨库事务。这时不要硬用本地事务解决问题可以考虑尽量减少跨账套的实时事务操作。异步消息加对账补偿机制。引入分布式事务框架比如 Seata但复杂度和运维成本会明显上升。跨账套汇总查询也是一样。比如集团要查所有分公司的销售汇总直接在业务库里做跨库大查询通常性能很差。比较好的方式是在业务库之外构建一个汇总库或数仓通过定时任务或消息队列将各账套数据同步过去再按组织维度出报表。4. 完整实战案例Spring Boot 实现多账套数据隔离下面用一个精简但完整的小案例演示“共享库 company_id 隔离”的多账套进销存系统核心实现。技术栈选 Spring Boot MyBatis-Plus这套组合在企业级进销存系统中非常常见。4.1 创建项目结构先看一下项目结构multi-account-system/ ├── pom.xml ├── src/main/java/com/example/multiacct/ │ ├── MultiAccountApplication.java │ ├── config/ │ │ └── MybatisPlusConfig.java │ ├── context/ │ │ └── AccountContextHolder.java │ ├── handler/ │ │ └── AccountLineHandler.java │ ├── entity/ │ │ ├── Product.java │ │ └── StockFlow.java │ ├── mapper/ │ │ ├── ProductMapper.java │ │ └── StockFlowMapper.java │ ├── service/ │ │ ├── ProductService.java │ │ └── impl/ │ │ └── ProductServiceImpl.java │ └── controller/ │ └── ProductController.java └── src/main/resources/ └── application.yml示例代码并不复杂重点在于“自动在 SQL 上追加company_id条件”这个设计。4.2 Maven 依赖配置pom.xml中加入 Spring Boot Web、MyBatis-Plus、MySQL 驱动等依赖。版本以 3.5 版本为例实际使用时请按项目环境调整。!-- 文件路径pom.xml -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.1.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies如果你的项目还是 Spring Boot 2.x可以换成mybatis-plus-boot-starter用法基本一致。4.3 数据库表初始化为了演示本次案例包含两张表商品表和库存流水表。两张表都带company_id字段。-- 初始化 SQL CREATE DATABASE IF NOT EXISTS multi_acct_demo DEFAULT CHARACTER SET utf8mb4; USE multi_acct_demo; DROP TABLE IF EXISTS product; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, product_code VARCHAR(50) NOT NULL, product_name VARCHAR(200) NOT NULL, spec VARCHAR(100), unit VARCHAR(20), sale_price DECIMAL(12, 2), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_company_code (company_id, product_code) ); DROP TABLE IF EXISTS stock_flow; CREATE TABLE stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, company_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, product_id BIGINT NOT NULL, change_type VARCHAR(20) NOT NULL, quantity DECIMAL(12, 2) NOT NULL, before_quantity DECIMAL(12, 2), after_quantity DECIMAL(12, 2), source_bill_no VARCHAR(50), create_by BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_company_product (company_id, product_id) ); INSERT INTO product (company_id, product_code, product_name, spec, unit, sale_price) VALUES (1001, SKU-001, 无线鼠标, 黑色, 个, 89.90), (1001, SKU-002, 机械键盘, 87键, 个, 299.00), (1002, SKU-001, 无线鼠标, 白色, 个, 99.90);插入的数据中1001 公司有两个商品1002 公司有一个商品。两个公司允许存在相同的商品编码 SKU-001但实际上是不同公司的独立商品档案。4.4 配置 MyBatis-Plus 多租户插件MyBatis-Plus 提供了多租户 SQL 解析插件可以在执行 SQL 时自动追加租户条件非常契合多账套场景。我们只需要实现TenantLineHandler接口把“账套 ID”当成租户 ID。首先创建当前账套上下文// 文件路径src/main/java/com/example/multiacct/context/AccountContextHolder.java package com.example.multiacct.context; public class AccountContextHolder { private static final ThreadLocalLong ACCOUNT_HOLDER new ThreadLocal(); public static void setAccountId(Long accountId) { ACCOUNT_HOLDER.set(accountId); } public static Long getAccountId() { return ACCOUNT_HOLDER.get(); } public static void clear() { ACCOUNT_HOLDER.remove(); } }ThreadLocal保证每个请求线程的账套上下文互不影响。实际项目中可以在登录拦截器或 AOP 切面中调用setAccountId并在请求处理结束后调用clear防止线程池复用导致数据串账。接着实现多租户处理器// 文件路径src/main/java/com/example/multiacct/handler/AccountLineHandler.java package com.example.multiacct.handler; import com.baomidou.mybatisplus.extension.plugins.handler.TenantLineHandler; import com.example.multiacct.context.AccountContextHolder; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.LongValue; import org.springframework.stereotype.Component; Component public class AccountLineHandler implements TenantLineHandler { Override public Expression getTenantId() { Long accountId AccountContextHolder.getAccountId(); if (accountId null) { // 未设置账套时返回一个不可能存在的账套ID强制查不到数据 return new LongValue(-1L); } return new LongValue(accountId); } Override public boolean ignoreTable(String tableName) { // 系统表、字典表可以不用按账套隔离 return sys_user.equalsIgnoreCase(tableName) || sys_dict.equalsIgnoreCase(tableName); } }getTenantId()方法返回当前用户的账套 ID。ignoreTable()方法指定哪些表不需要自动追加账套条件。这里只演示了两个系统表业务表如product、stock_flow默认都会自动追加company_id。然后配置 MyBatis-Plus 拦截器// 文件路径src/main/java/com/example/multiacct/config/MybatisPlusConfig.java package com.example.multiacct.config; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor; import com.example.multiacct.handler.AccountLineHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor(AccountLineHandler accountLineHandler) { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(accountLineHandler)); return interceptor; } }这个配置是全局性的。从这一刻开始所有没有在ignoreTable()中忽略的表在执行 SELECT、UPDATE、DELETE 时都会自动追加company_id条件INSERT 时自动填充company_id字段。4.5 编写实体类与 Mapper商品实体Product// 文件路径src/main/java/com/example/multiacct/entity/Product.java package com.example.multiacct.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(product) public class Product { TableId(type IdType.AUTO) private Long id; private Long companyId; private String productCode; private String productName; private String spec; private String unit; private BigDecimal salePrice; private LocalDateTime createTime; private LocalDateTime updateTime; }Mapper 接口// 文件路径src/main/java/com/example/multiacct/mapper/ProductMapper.java package com.example.multiacct.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.multiacct.entity.Product; import org.apache.ibatis.annotations.Mapper; Mapper public interface ProductMapper extends BaseMapperProduct { }4.6 编写 Service 层Service 层定义两个方法一个新增商品一个按商品编码查询。// 文件路径src/main/java/com/example/multiacct/service/ProductService.java package com.example.multiacct.service; import com.example.multiacct.entity.Product; public interface ProductService { void addProduct(Product product); Product getByCode(String productCode); }// 文件路径src/main/java/com/example/multiacct/service/impl/ProductServiceImpl.java package com.example.multiacct.service.impl; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.example.multiacct.entity.Product; import com.example.multiacct.mapper.ProductMapper; import com.example.multiacct.service.ProductService; import org.springframework.stereotype.Service; Service public class ProductServiceImpl implements ProductService { private final ProductMapper productMapper; public ProductServiceImpl(ProductMapper productMapper) { this.productMapper productMapper; } Override public void addProduct(Product product) { productMapper.insert(product); } Override public Product getByCode(String productCode) { return productMapper.selectOne(new LambdaQueryWrapperProduct() .eq(Product::getProductCode, productCode)); } }这里getByCode没有手动写company_id条件但多租户插件会自动在 SQL 中追加AND company_id ?所以查询结果天然只属于当前账套。4.7 编写 Controller为了演示效果提供一个简单的 REST API通过请求参数切换账套。// 文件路径src/main/java/com/example/multiacct/controller/ProductController.java package com.example.multiacct.controller; import com.example.multiacct.context.AccountContextHolder; import com.example.multiacct.entity.Product; import com.example.multiacct.service.ProductService; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/product) public class ProductController { private final ProductService productService; public ProductController(ProductService productService) { this.productService productService; } PostMapping(/switch) public String switchAccount(RequestParam Long accountId) { AccountContextHolder.setAccountId(accountId); return 当前账套 accountId; } PostMapping(/add) public Product add(RequestBody Product product) { productService.addProduct(product); return product; } GetMapping(/list) public ListProduct list() { return productService.list(); } }其中list()方法调用了 MyBatis-Plus 自带的list()方法。如果直接继承IService可以简化很多样板代码。为了控制篇幅示例中把 Controller 与 Service 写清楚了实际项目里可以进一步封装。4.8 运行与验证启动应用后按下面顺序测试切换到账套 1001curl -X POST http://localhost:8080/product/switch?accountId1001查询商品列表curl http://localhost:8080/product/list预期结果只返回 1001 公司的两个商品。切换到账套 1002curl -X POST http://localhost:8080/product/switch?accountId1002再次查询商品列表curl http://localhost:8080/product/list预期结果只返回 1002 公司的一个商品。数据库层面实际执行的 SQL 类似SELECT id, company_id, product_code, product_name, spec, unit, sale_price, create_time, update_time FROM product WHERE company_id 1001;可以修改 MyBatis 配置打印 SQL观察多租户插件自动追加的条件。这也是排查数据串账问题最直接的手段。5. 多账套进销存系统的常见问题与排查多账套系统的很多问题不是功能没开发出来而是“数据隔离”没做到位。下面列出我实际开发和实施中遇到的高频问题。问题现象常见原因解决思路查询商品/单据时出现其他公司的数据查询 SQL 遗漏了 company_id 条件检查所有 Mapper XML 和 Wrapper 查询统一使用多租户插件用户切换账套后数据仍停留在上一个账套ThreadLocal 未在请求结束后 clear在拦截器 finally 块中调用 AccountContextHolder.clear()新增单据时 company_id 为空插入前未从上下文取值赋值使用多租户插件自动填充租户字段批量任务或定时任务数据串账异步线程没有账套上下文传入账套参数或重新设置 AccountContextHolder跨公司汇总报表数据不准各账套基础资料编码不一致建立集团统一编码映射或通过汇总服务标准化删除条件漏加 company_id误删其他公司数据手工 SQL 绕过插件或 ignoreTable 配置不当禁用无 company_id 的 DELETE/UPDATE增加 SQL 审核几个排查建议第一先把 MyBatis-Plus 的 SQL 日志打开查看实际执行的 SQL 是否都带上了company_id条件。# application.yml mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl第二不要在业务代码中到处手动设置company_id条件这会导致不一致。应该通过全局插件统一处理让开发人员在写 SQL 时无法绕过隔离条件。第三如果有些查询确实需要跨账套比如集团汇总不要直接在业务接口里写死忽略租户条件。更安全的做法是把汇总逻辑独立到专门的数据服务中通过明确标注和独立权限来控制。6. 多账套系统最佳实践与工程建议这部分内容不止于写代码更多是从项目落地的角度分享的一些工程经验。6.1 账套字段统一命名建议所有业务表的隔离字段统一为company_id而不是公司名称或简称。名称看起来直观但公司改名、合并时会连累历史数据。使用 ID 作为关联键性能更好也避免歧义。字段类型统一为BIGINT不要部分表用INT、部分表用VARCHAR。后续做批量导出、数据迁移时统一类型能减少很多麻烦。6.2 强制走多租户插件开发阶段要明确一条制度业务表不允许手写“裸 SQL”访问。所有访问数据库的操作要么走 MyBatis-Plus 的 BaseMapper要么在 XML 中显式声明company_id条件。如果是 MyBatis XML 文件也要注意多租户插件对 XML SQL 的解析能力。个别复杂 SQL 可能无法被拦截器正确改写这种情况要单独审查 SQL并在代码注释中标注“已手工处理账套条件”。6.3 分页和列表查询要带上账套索引多账套系统最常见的性能场景是页面筛选商品、单据时SQL 条件为company_id ? AND create_time BETWEEN ? AND ?。所以常用查询字段要建立联合索引。例如商品库存表KEY idx_company_time (company_id, create_time)不要把company_id放在索引后面。如果条件里没有company_id前导索引失效整个查询会变慢。6.4 备份与迁移要按账套维度设计进销存数据是企业的核心资产备份计划必须单独考虑。共享表方案下备份是整个库的粒度回滚也要按时间点处理。如果客户要求“只恢复 A 公司的数据”操作起来会非常困难。更稳妥的做法是定期将各账套核心数据导出到独立备份表或对象存储中至少保证单据、商品、往来单位这几类核心数据可按账套恢复。6.5 财务模块要符合独立核算原则多公司财务软件中每个账套的凭证号必须独立连续。比如 A 公司的 2025 年 1 月凭证号从 1 到 50B 公司同样有 1 到 50不能混排。这需要在凭证表设计时使用(company_id, period, voucher_no)做唯一约束。会计科目表则要考虑两种模式统一会计制度各账套科目结构一致报表合并简单。各公司可自定义科目灵活但汇总报表需要映射。建议中小集团先采用统一科目模板降低合并报表成本。6.6 日志与审计不可少多账套系统的操作审计尤其重要。谁在哪个账套做了采购单、谁修改了销售价格、谁切换了账套都要有记录。审计日志至少包含用户 ID、用户姓名操作的账套 ID操作模块和操作类型数据描述和变更前后快照操作时间和 IP不要把所有操作日志混在一个大表里可以按月分表或者按账套分表存储。7. 总结与下一步学习方向多公司进销存软件和多账套系统本质上是在一套软件里解决多个企业、多个核算主体之间的数据隔离与协同问题。从技术实现上看核心工作是选对数据隔离方案、建立清晰的账套模型、在数据库访问层强制隔离条件并把权限、日志、备份、财务核算规则一并考虑进来。如果你们正处在选型阶段可以先从这两个问题入手梳理需求总部需要汇总什么数据、各分公司需要独立到什么程度。需求不同技术方案差别很大。如果你们正在开发阶段建议优先把数据隔离红线确立下来再逐步扩展供应商、客户、库存、财务等模块。下一步可以参考的学习方向包括Spring Security 与多账套权限结合、MyBatis 多租户插件源码分析、集团合并报表的数据同步设计、海量库存单据的分表方案。多账套系统看起来是个业务概念真正深入后会发现它同时涉及架构、数据模型、权限体系和性能优化是一个很值得花时间研究的领域。希望这篇文章能帮你少走弯路。如果你在实际落地过程中遇到过其他数据串账或账套切换的问题欢迎在评论区一起交流。
返回列表