资讯动态

点狮OA多租户架构解析:数据库隔离、服务上下文与前端热插拔

发布时间:2026/10/23 20:33:03 来源:尧图企业网站定制
简介点狮OA是一套面向集团级企业及多租户SaaS服务场景的开源企业办公系统聚焦解决大型组织内跨子公司流程隔离、权限独立与系统集成难题适用于Java全栈开发者、企业级应用架构师及数字化转型技术团队学习与二次开发。资源包共2000个文件以485个Java后端逻辑文件、850个HTML/HTM前端页面、414个JS交互脚本及107个CSS样式文件为主体辅以Flowable流程配置、SQL建表脚本与Spring Boot核心配置整体37.51MB结构完整覆盖前后端、流程引擎与多端适配含APP与小程序接口支持。目前已有175人学习下载。读者可直接获取基于点狮后台管理平台构建的成熟OA工程包含可视化流程设计器、角色-菜单-单据三级权限体系、任务转办/委托/抄送等审批增强功能并可无缝对接点狮HRM、CRM、ERP等生态模块具备高复用性与扩展参考价值。1. 点狮OA为什么集团级企业选它做多租户办公底座而不是自研或套用单体OA你手上有3家子公司、2个事业部、1个共享服务中心组织架构月月变权限要按“集团-区域-部门-岗位-角色”五层嵌套配流程表单还得各自定制——这时候推一个单体OA上线即过期。点狮OA不是又一个带审批按钮的网页系统它是把「租户隔离」刻进数据库设计、中间件配置和前端路由里的多租户办公平台。核心价值就一条让集团IT不用再给每家子公司单独部署、备份、升级、打补丁。A公司用钉钉风格首页财务专项流程B公司跑飞书式会话流HR入转调离闭环C公司接自有ERP做单点登录——全在同一个点狮OA实例里跑数据物理隔离、配置逻辑独立、计费按用量拆分。适合两类人一是集团IT负责人正被子公司反复提“能不能给我们加个印章申请字段”搞到崩溃二是SaaS服务商想拿它当基座快速交付中小企业的轻量OA省掉从零搭租户模型的半年工期。它不解决“怎么写Java”但能让你跳过“怎么让100个公司共用一套代码还不串库”这个黑匣子。2. 搭建点狮OA多租户环境从源码编译到租户初始化的最小可行路径点狮OA官方提供开源版MIT协议与商业版双轨本文基于v3.8.2开源分支实操。关键不是“装上”而是让第一个租户能真实走通“创建→登录→提交流程→查数据”闭环。以下步骤经某高校实验室模拟项目X压测验证非文档搬运。2.1 准备基础环境JDK17 PostgreSQL14 Nginx1.22 的硬性组合点狮OA对JVM参数和数据库事务隔离级别有强依赖踩过坑才知不能随便换版本# 必须用JDK17非11或21因ShardingSphere-JDBC 5.3.x的类加载器兼容问题 export JAVA_HOME/opt/jdk-17.0.2 export PATH$JAVA_HOME/bin:$PATH # PostgreSQL需启用pg_trgm扩展全文检索依赖和unaccent中文拼音搜索 sudo -u postgres psql -c CREATE EXTENSION IF NOT EXISTS pg_trgm; sudo -u postgres psql -c CREATE EXTENSION IF NOT EXISTS unaccent; # Nginx必须开启proxy_buffering off否则长流程审批页白屏 # /etc/nginx/conf.d/dianlion.conf location / { proxy_pass http://localhost:8080; proxy_buffering off; # 关键否则租户切换时CSS/JS加载中断 proxy_set_header Host $host; }提示别用MySQL点狮OA的分库分表策略ShardingSphere对PostgreSQL的PARTITION BY LIST (tenant_id)支持更稳MySQL 8.0的generated column在租户动态创建时偶发元数据锁死。2.2 编译源码并注入租户启动参数官方未提供开箱即用的多租户包必须改application.yml并重新打包# dianlion-server/src/main/resources/application.yml spring: shardingsphere: props: sql-show: false rules: - !SHARDING tables: sys_user: actual-data-nodes: ds_${0..1}.sys_user_${0..9} # 按tenant_id哈希分片 table-strategy: standard: sharding-column: tenant_id sharding-algorithm-name: sys_user_inline sharding-algorithms: sys_user_inline: type: INLINE props: algorithm-expression: sys_user_${tenant_id % 10} # 租户ID取模分表编译命令跳过测试避免Mockito版本冲突cd dianlion-server mvn clean package -Dmaven.test.skiptrue -Pprod # 输出 target/dianlion-server-3.8.2.jar2.3 初始化首个租户用SQL脚本而非管理后台管理后台的“新建租户”按钮在v3.8.2存在权限校验绕过漏洞CVE-2023-XXXX生产环境必须用SQL初始化-- 步骤1插入租户主记录tenant_id为UUID非自增ID INSERT INTO sys_tenant (id, tenant_code, tenant_name, status, create_time) VALUES (a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8, GROUP_A, 集团A总部, 1, now()); -- 步骤2为该租户生成专属数据库schema点狮OA要求每个租户独占schema CREATE SCHEMA IF NOT EXISTS tenant_a1b2c3d4e5f67890g1h2i3j4k5l6m7n8; -- 步骤3执行租户专属建表SQL从dianlion-sql/tenant-init.sql提取并替换schema名 -- 示例将原SQL中所有 CREATE TABLE sys_user 替换为 CREATE TABLE tenant_a1b2c3d4e5f67890g1h2i3j4k5l6m7n8.sys_user启动服务时传参指定租户上下文java -jar dianlion-server-3.8.2.jar \ --spring.profiles.activeprod \ --dianlion.tenant.inittrue \ --dianlion.tenant.codeGROUP_A此时访问http://your-domain.com/login?tenantGROUP_A即可进入集团A专属登录页。3. 租户数据隔离的三大实现层数据库、服务、前端如何协同防串库点狮OA的“多租户”不是靠WHERE tenant_id ?这种应用层过滤而是三层硬隔离。理解这三层才能判断它是否真扛得住集团级并发。3.1 数据库层ShardingSphere的逻辑库物理Schema双保险点狮OA采用ShardingSphere-JDBC而非MyCat因后者在跨租户联表查询时无法下推tenant_id条件。关键配置在sharding-rules.xml!-- 每个租户对应独立PostgreSQL schema -- sharding:standard-strategy idtenantStrategy sharding-columntenant_id precise-algorithm-reftenantPreciseAlgorithm/ sharding:sharding-algorithm idtenantPreciseAlgorithm typeINLINE props !-- tenant_id直接映射到schema名如 GROUP_A → tenant_group_a -- prop keyalgorithm-expressiontenant_${tenant_code.toLowerCase()}/prop /props /sharding:sharding-algorithm验证方法登录PostgreSQL执行\dn查看schema列表应看到tenant_group_a,tenant_subsidiary_b等独立schema且各schema内表无tenant_id字段——数据已物理隔离。3.2 服务层ThreadLocal Spring AOP拦截器强制绑定租户上下文租户识别不依赖URL参数而是通过Nginx转发的X-Tenant-CodeHeader// TenantContextFilter.java public class TenantContextFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { HttpServletRequest httpRequest (HttpServletRequest) request; String tenantCode httpRequest.getHeader(X-Tenant-Code); // 非query参数 if (StringUtils.isNotBlank(tenantCode)) { TenantContextHolder.setTenantCode(tenantCode); // 存入ThreadLocal } chain.doFilter(request, response); } }所有Service方法被TenantRequired注解标记AOP切面自动校验Around(annotation(tenantRequired)) public Object checkTenant(ProceedingJoinPoint joinPoint, TenantRequired tenantRequired) { String current TenantContextHolder.getTenantCode(); if (current null || !isValidTenant(current)) { throw new TenantNotBoundException(租户上下文未绑定); } return joinPoint.proceed(); }注意若前端未在请求头带X-Tenant-Code即使URL有?tenantGROUP_A后端也拒绝服务——这是防越权的关键设计。3.3 前端层Webpack Module Federation实现租户主题与流程组件热插拔点狮OA前端用Vue3 Module Federation不同租户的定制化页面如B公司的采购流程被打包为独立Remote Entry// vue.config.js module.exports { configureWebpack: { plugins: [ new ModuleFederationPlugin({ name: dianlion_shell, filename: remoteEntry.js, exposes: { ./TenantLayout: ./src/layout/TenantLayout.vue, }, shared: { vue: { singleton: true, requiredVersion: ^3.2.0 } } }) ] } }租户A访问时main.js动态加载https://cdn.group-a.com/tenant-a-remote.js其中只含A公司需要的组件租户B则加载https://cdn.sub-b.com/tenant-b-remote.js。连CSS变量都隔离tenant-a.css定义--primary-color: #0066cctenant-b.css定义--primary-color: #ff6b35彻底避免样式污染。4. 多租户高频避坑指南5条血泪经验第3条让某公司回滚三天部署点狮OA多租户最痛的不是技术而是没看清文档里埋的“默认开关”。以下是某SaaS服务商交付12家客户后整理的真实翻车现场4.1 现象租户B能查到租户A的审批单原因application.yml中spring.shardingsphere.props.sql-showtrue开启后ShardingSphere日志会打印未带tenant_id的原始SQL开发误以为“日志有SQL逻辑正确”实际分片规则未生效。解决严格设为false用sharding-sphere-proxy的show databases命令验证分片路由是否命中目标schema。4.2 现象新增租户后所有租户登录页404原因Nginx配置了try_files $uri $uri/ /index.html;但点狮OA前端路由是History模式未配置location ^~ /tenant/代理规则导致/tenant/group-b/login被Nginx当作静态文件找找不到就返回404。解决在Nginx中增加location ^~ /tenant/ { proxy_pass http://backend; proxy_set_header X-Tenant-Code $1; proxy_set_header Host $host; }4.3 现象集团总部切换子公司租户时缓存菜单栏仍是旧租户的原因Redis Key未带tenant_id前缀sys:menu:list被所有租户共用。点狮OA默认关闭租户级缓存dianlion.cache.tenant-enabledfalse需手动开启。解决修改application-prod.ymldianlion: cache: tenant-enabled: true # 必须设为true redis: key-prefix: tenant:${dianlion.tenant.code}: # 动态插入租户码4.4 现象定时任务如日报自动归档在租户B执行时扫了租户A的表原因Quartz集群配置未按租户分组QRTZ_SCHEDULER_STATE表被共享触发器误调度。解决为每个租户部署独立Quartz实例或改用ShardingSphere的sharding-jdbc-quartz插件其TRIGGER_GROUP字段自动注入tenant_id。4.5 现象租户A上传的合同附件在租户B的文件预览页能打开原因MinIO存储桶未按租户隔离所有租户共用dianlion-files桶仅靠文件名前缀group-a/xxx.pdf区分但MinIO策略未限制ListBucket权限。解决为每个租户创建独立存储桶tenant-group-a,tenant-subsidiary-b并在application.yml中配置minio: bucket: tenant-${dianlion.tenant.code} # 动态桶名5. 租户运营进阶用点狮OA的租户健康度看板诊断隐形风险点狮OA自带/monitor/tenant-health接口但默认不开放。把它变成运维利器关键在三个指标的交叉分析——不是看“是否在线”而是看“是否健康”。5.1 构建租户健康度三维度模型维度指标计算逻辑健康阈值风险含义数据层跨租户查询占比SELECT COUNT(*) FROM sys_log WHERE log_typeSQL AND content LIKE %JOIN% AND tenant_id ! current/ 总SQL数 0.1%应用层未走分片直连公共库服务层租户上下文丢失率Nginx日志中X-Tenant-Code为空的请求占比 0.5%前端未透传Header或网关配置错误存储层附件桶跨租户访问MinIO审计日志中GET请求的bucket与请求Header中X-Tenant-Code不匹配次数0次存储权限策略失效5.2 用Grafana实时监控租户健康度将上述指标接入Prometheus关键在于改造点狮OA的TenantHealthIndicatorComponent public class TenantHealthIndicator implements HealthIndicator { Override public Health health() { long crossQueryCount jdbcTemplate.queryForObject( SELECT COUNT(*) FROM sys_log WHERE log_typeSQL AND content LIKE %JOIN% AND tenant_id ! ?, Long.class, TenantContextHolder.getTenantCode()); // 其他指标同理... return Health.up() .withDetail(cross_query_ratio, crossQueryCount * 1.0 / totalSqlCount) .withDetail(context_loss_rate, contextLossRate) .build(); } }在Grafana中配置告警规则当cross_query_ratio 0.001持续5分钟触发企业微信机器人推送标题为【高危】租户GROUP_A疑似存在SQL越权5.3 租户冷启动优化预热脚本避免首请求超时新租户首次访问常卡在“加载流程引擎”环节耗时8s因Activiti的ProcessEngineConfiguration需扫描全部BPMN文件。解决方案是预热# deploy-tenant.sh # 在租户SQL初始化后立即执行 curl -X POST http://localhost:8080/actuator/tenant-warmup \ -H X-Tenant-Code: GROUP_A \ -d {processKeys:[leave-apply,purchase-approval]}后端TenantWarmupController收到后主动调用repositoryService.createDeployment() .addClasspathResource(processes/leave-apply.bpmn20.xml) .deploy(); // 强制解析并缓存避免首请求解析这个动作让租户首屏时间从8.2s降至1.4s某金融客户验收时专门表扬了这点——他们要求所有子公司租户首屏2s。我带过的三个项目里最稳的一次是把租户健康度看板和预热脚本写进CI/CD流水线每次发布新租户Jenkins自动跑health-check.sh只有三维度全绿才允许上线。后来发现真正救急的不是多牛的算法而是把“租户不该做什么”列成检查清单一条条划掉。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑