简介点狮OA是一套面向集团级企业及SaaS服务商的多租户企业办公系统支持多公司入驻各租户可独立配置个性化流程、人员管理、用户与角色管理、菜单权限及各类单据申请信息。系统基于点狮后台管理平台开发可无缝集成点狮HRM、AM档案、CRM、IM即时通讯、ERP等模块并可作为OA-APP与小程序的后台服务。其扩展流程设计器支持手动指定办理人或岗位办理集成Flowable流程引擎实现并行、串行、会签、回退、取回等审批操作内置转办、委托、抄送等功能适合中高级Java开发者学习企业级多租户架构与工作流实现。资源包共2000个文件涵盖850个html页面、485个java源码、414个js脚本、107个css样式及docx、sql、json等配置文档压缩包约37.61MB目录结构完整。已有76人学习下载可帮助读者掌握多租户权限设计、流程引擎集成与前后端模块组织方式。1. 点狮OA多租户架构从集团管控到SaaS入驻的落地路径如果你正在找一套能同时扛住「集团内部多子公司」和「对外SaaS多租户入驻」两种模式的企业办公系统点狮OA值得花时间拆一遍。它的核心定位很明确一套代码库支撑多个租户每个租户拥有独立的流程、人员、角色、菜单权限和单据数据租户之间互不干扰。这解决的是传统OA「一套系统只能服务一家公司」的硬伤——要么给每个子公司单独部署一套运维成本翻倍要么所有公司挤在一个租户里权限和数据隔离全靠人工约束迟早出乱子。点狮OA基于点狮后台管理平台开发这意味着它不是孤立的OA孤岛而是可以无缝扩展集成点狮HRM、点狮AM档案、点狮CRM、点狮IM、点狮ERP等模块同时作为点狮OA-APP和小程序的后台服务。流程层面集成了Flowable引擎支持并行、串行、会签、回退、取回等操作内置转办、委托、抄送功能。适合谁用一是集团级企业需要统一管控多子公司二是想做SaaS服务对外输出、让多家公司入驻的团队。下面从架构原理到部署实操再到踩坑排查一步步拆开讲。2. 多租户数据隔离与Flowable流程引擎的协同设计2.1 租户隔离的三种模式与点狮OA的选型逻辑多租户系统的核心难题是数据隔离。业界常见三种模式独立数据库一个租户一个库、共享数据库独立Schema、共享数据库共享表加租户ID字段。点狮OA的定位是「集团级SaaS入驻」意味着租户数量可能从几个到几百个不等独立数据库模式在租户数量膨胀后运维成本会失控而共享表模式在数据量大时查询性能又容易出问题。常见做法是采用共享数据库租户ID字段的方案配合MyBatis-Plus的租户插件做SQL自动拼接。这种方案的优势在于新增租户只需插入一条租户记录不需要建库建表升级维护只操作一套表结构租户ID作为查询条件自动注入业务代码不需要手动处理隔离逻辑。具体到配置层面需要在MyBatis-Plus的拦截器链中加入TenantLineInnerInterceptor并配置租户ID的获取策略。点狮OA的租户ID一般从登录用户的Session或JWT Token中解析请求进入Controller之前由过滤器写入ThreadLocal拦截器再从ThreadLocal中取值拼接到SQL的WHERE条件里。// MyBatis-Plus 多租户拦截器配置 Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); // 多租户插件指定租户ID字段名 interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() { Override public Expression getTenantId() { // 从ThreadLocal中获取当前请求的租户ID Long tenantId TenantContextHolder.getTenantId(); return new LongValue(tenantId); } Override public String getTenantIdColumn() { return tenant_id; // 数据库表中统一的租户字段 } Override public boolean ignoreTable(String tableName) { // 系统级表如租户信息表本身不参与租户过滤 return Arrays.asList(sys_tenant, sys_dict, sys_config) .contains(tableName); } })); return interceptor; } }这段配置的关键参数有三个getTenantId()决定从哪取租户标识getTenantIdColumn()指定数据库中的租户字段名ignoreTable()列出不需要隔离的系统级表。注意ignoreTable这个白名单必须仔细维护——漏掉一张系统表会导致租户查询被错误过滤多写一张业务表则会造成数据越权。我一般会在项目启动时打印所有被忽略的表名人工核对一遍。2.2 Flowable流程引擎的租户绑定与任务分配点狮OA集成了Flowable流程引擎来实现审批流的并行、串行、会签、回退、取回等操作。在多租户场景下Flowable本身提供了tenantId字段可以在部署流程定义和启动流程实例时指定租户。但实际落地时流程定义通常是所有租户共享一套模板而流程实例和任务必须严格按租户隔离。流程定义共享、实例隔离的做法是部署时tenantId留空表示全局流程模板启动流程实例时传入当前租户ID。这样每个租户看到的流程模板一致但运行时数据互不可见。Flowable的runtimeService.startProcessInstanceByKeyAndTenantId()方法就是干这个的。// 启动流程实例时绑定租户 Service public class OaProcessService { Autowired private RuntimeService runtimeService; public String startProcess(String processKey, String businessKey, MapString, Object variables) { Long tenantId TenantContextHolder.getTenantId(); // 设置流程发起人 Authentication.setAuthenticatedUserId(SecurityUtils.getUsername()); // 按租户启动流程实例 ProcessInstance instance runtimeService.startProcessInstanceByKeyAndTenantId( processKey, businessKey, variables, String.valueOf(tenantId) // 租户ID作为Flowable的tenantId ); return instance.getProcessInstanceId(); } }参数说明processKey是流程定义的标识businessKey关联业务单据IDvariables是流程变量比如审批人、金额等最后一个参数将租户ID写入Flowable的租户字段。这样后续查询待办任务时通过taskService.createTaskQuery().taskTenantId(tenantId)就能只看到当前租户的任务。流程设计器扩展了指定办理人和指定岗位办理的能力。指定办理人是在UserTask上直接绑定用户ID指定岗位办理则是绑定岗位编码运行时由Flowable的动态任务分配监听器解析岗位下的具体人员。会签场景下需要设置nrOfCompletedInstances和nrOfInstances两个变量来控制通过比例回退则需要记录目标节点ID并通过runtimeService.createChangeActivityStateBuilder()实现。2.3 租户初始化与个性化配置的落地步骤新增一个租户不是简单插一条记录就完事。点狮OA的租户初始化涉及菜单权限分配、角色模板复制、流程模板关联、字典数据初始化等步骤。常见做法是写一个租户初始化服务在租户创建成功后异步执行。// 租户初始化核心步骤 Service public class TenantInitService { Transactional public void initTenant(Long tenantId, String tenantName) { // 1. 创建默认角色管理员、普通员工 Long adminRoleId roleService.createDefaultRole(tenantId, 租户管理员); Long staffRoleId roleService.createDefaultRole(tenantId, 普通员工); // 2. 复制默认菜单权限到该租户 ListLong menuIds menuService.getDefaultMenuIds(); roleMenuService.batchInsert(tenantId, adminRoleId, menuIds); // 3. 初始化数据字典审批状态、单据类型等 dictService.initTenantDict(tenantId); // 4. 关联可用的流程模板 processService.bindDefaultProcesses(tenantId); // 5. 创建租户管理员账号 userService.createTenantAdmin(tenantId, tenantName); } }每一步都对应一个业务域的初始化。菜单权限复制决定了租户能看到哪些功能入口字典初始化影响下拉框和状态显示流程模板关联决定该租户能发起哪些审批。这些步骤如果漏掉任何一步租户登录后就会出现菜单空白、流程无法发起、状态显示异常等问题。建议在初始化完成后加一个健康检查接口逐项验证初始化结果。3. 从零部署点狮OA环境准备、数据库配置与前端资源加载3.1 后端环境搭建与数据库初始化部署点狮OA的第一步是把后端跑起来。技术栈基于Spring Boot MyBatis-Plus Flowable数据库通常用MySQL 8.x缓存用Redis。JDK版本建议17以上Maven 3.8。# 1. 克隆项目以实际仓库地址为准 git clone 点狮OA后端仓库地址 cd dianlion-oa # 2. 创建数据库字符集用utf8mb4 mysql -u root -p -e CREATE DATABASE dianlion_oa DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 3. 导入初始化SQL包含表结构和基础数据 mysql -u root -p dianlion_oa sql/init.sql # 4. 修改application.yml中的数据库和Redis连接 # spring.datasource.url: jdbc:mysql://127.0.0.1:3306/dianlion_oa?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai # spring.datasource.username: root # spring.datasource.password: 你的密码 # spring.redis.host: 127.0.0.1 # spring.redis.port: 6379 # 5. 编译启动 mvn clean package -DskipTests java -jar target/dianlion-oa.jar --spring.profiles.activedev数据库初始化SQL里包含了两部分系统级表租户表、字典表、菜单表和业务级表用户、角色、流程、单据。导入时注意检查sys_tenant表是否有一条默认租户记录没有的话需要手动插入一条否则登录时租户解析会失败。Redis用来缓存Token和租户配置如果Redis连不上系统启动时不会报错但登录会一直转圈——这个坑后面会细说。3.2 前端资源加载与样式文件组织项目正文里列出了一串CSS文件weather-icons-wind.css、bootstrap.css、ionicons.css、flag-icon.css、font-awesome.css、style.css以及对应的min版本。这些是前端页面的样式依赖分别负责天气图标、Bootstrap基础样式、Ionic图标、国旗图标、FontAwesome图标和项目自定义样式。前端资源加载的顺序有讲究。Bootstrap必须在自定义style.css之前加载否则自定义样式会被Bootstrap覆盖。图标库之间一般不会冲突但如果页面加载慢浏览器会按顺序阻塞渲染。常见优化做法是把这些CSS合并打包或者至少把min版本用于生产环境。!-- 前端CSS加载顺序index.html中 -- link relstylesheet href/static/css/bootstrap.min.css link relstylesheet href/static/css/font-awesome.css link relstylesheet href/static/css/ionicons.min.css link relstylesheet href/static/css/flag-icon.css link relstylesheet href/static/css/weather-icons-wind.min.css link relstylesheet href/static/css/style.css注意style.css放在最后它是项目级自定义样式需要覆盖前面框架的默认值。如果部署后发现页面图标显示为方块大概率是字体文件路径不对——FontAwesome和Ionicons依赖对应的woff/ttf字体文件检查/static/fonts/目录是否存在且路径配置正确。3.3 租户入驻配置与菜单权限验证后端和前端都跑起来之后下一步是验证多租户入驻流程。用系统管理员账号登录创建一个新租户然后检查该租户的菜单权限、角色、流程模板是否正常初始化。-- 验证租户数据隔离查询租户ID为2的用户列表 SELECT * FROM sys_user WHERE tenant_id 2; -- 验证菜单权限查询租户2的管理员角色拥有的菜单 SELECT m.* FROM sys_menu m JOIN sys_role_menu rm ON m.id rm.menu_id JOIN sys_role r ON rm.role_id r.id WHERE r.tenant_id 2 AND r.role_name 租户管理员; -- 验证流程模板绑定 SELECT * FROM oa_process_bind WHERE tenant_id 2;这三条SQL分别验证了用户隔离、菜单权限和流程绑定。如果第一条查出来包含其他租户的用户说明租户拦截器配置有问题如果第二条菜单为空说明初始化时菜单复制失败如果第三条没有记录说明流程模板没有绑定到该租户。建议把这三条检查写成一个自动化测试用例每次新增租户后跑一遍。4. 多租户场景下的避坑与排查记录4.1 租户ID丢失导致查询全表数据现象某个接口返回了所有租户的数据而不是当前租户的数据。原因租户ID存在ThreadLocal中但异步线程或定时任务中ThreadLocal没有传递导致TenantContextHolder.getTenantId()返回nullMyBatis-Plus拦截器在租户ID为null时可能跳过拼接条件。解决在异步任务和定时任务入口手动设置租户上下文或者使用TransmittableThreadLocal替代普通ThreadLocal。另外在拦截器中加一层保护如果租户ID为null且当前不是系统级操作直接抛出异常而不是放行查询。4.2 Flowable流程定义跨租户可见现象租户A登录后能看到租户B部署的流程定义。原因流程定义部署时指定了租户ID但查询流程定义列表时没有加租户过滤条件。Flowable的repositoryService.createProcessDefinitionQuery()默认返回所有租户的定义。解决查询流程定义时统一加上.processDefinitionTenantId(tenantId)条件。如果流程模板是全局共享的则部署时不设租户ID查询时用.processDefinitionWithoutTenantId()单独查全局模板再合并展示。4.3 菜单权限初始化不完整现象新租户创建后管理员登录看不到任何菜单页面空白。原因租户初始化是异步执行的如果初始化过程中某一步失败比如字典数据插入异常后续步骤不会执行但租户记录已经创建导致租户处于「半初始化」状态。解决把租户初始化改成事务操作任何一步失败就整体回滚。同时增加一个「重新初始化」按钮允许管理员对初始化失败的租户手动触发。初始化日志要记录每一步的执行结果方便定位卡在哪一步。4.4 Redis连接失败导致登录超时现象系统启动正常但登录请求一直pending最终超时。原因登录时需要往Redis写入Token和租户配置缓存如果Redis连接不上写入操作会阻塞直到超时。Spring Boot默认不会在启动时校验Redis连接。解决在应用启动后加一个Redis健康检查连不上时直接拒绝启动并打印明确错误。或者在Redis客户端配置中把连接超时和读取超时设短一些比如2秒避免请求长时间挂起。4.5 前端静态资源缓存导致样式不更新现象更新了style.css后浏览器看到的还是旧样式。原因浏览器缓存了旧版本的CSS文件或者Nginx配置了强缓存。解决在CSS文件引用后加版本号参数如style.css?v20240101或者在Nginx中对HTML文件禁用缓存、对CSS/JS文件设置较短的缓存时间。开发阶段可以用浏览器的无痕模式或强制刷新CtrlShiftR绕过缓存。5. 流程审批的进阶技巧会签比例控制与回退取回实现会签是OA审批里最容易出问题的环节。点狮OA通过Flowable的多实例任务实现会签核心是控制两个变量nrOfInstances总实例数和nrOfCompletedInstances已完成数。通过比例由完成条件表达式控制比如${nrOfCompletedInstances/nrOfInstances 0.5}表示半数通过。// 会签完成条件配置在BPMN XML中 // multiInstanceLoopCharacteristics isSequentialfalse // flowable:collectionassigneeList flowable:elementVariableassignee // completionCondition${nrOfCompletedInstances/nrOfInstances 0.5}/completionCondition // /multiInstanceLoopCharacteristics // 回退到指定节点 public void rollbackTask(String taskId, String targetActivityId) { runtimeService.createChangeActivityStateBuilder() .moveActivityIdTo( taskService.createTaskQuery().taskId(taskId).singleResult().getTaskDefinitionKey(), targetActivityId) .changeState(); } // 取回已提交的任务需在下一节点未处理前 public void withdrawTask(String processInstanceId, String currentUserId) { // 查询当前用户刚提交的任务 HistoricTaskInstance task historyService.createHistoricTaskInstanceQuery() .processInstanceId(processInstanceId) .taskAssignee(currentUserId) .orderByHistoricTaskInstanceEndTime().desc() .list().get(0); // 将流程回退到该任务节点 runtimeService.createChangeActivityStateBuilder() .processInstanceId(processInstanceId) .moveActivityIdTo( runtimeService.getActiveActivityIds(processInstanceId).get(0), task.getTaskDefinitionKey()) .changeState(); }会签比例的参数需要根据业务场景调整财务审批可能要求100%通过部门会签可能半数即可。回退和取回的区别在于回退是审批人主动把任务退回给之前的节点取回是发起人在下一节点未处理前撤回自己的申请。取回操作有一个时间窗口限制——一旦下一节点已经处理就不能再取回只能走回退流程。转办和委托的区别也容易混淆。转办是把任务完全交给另一个人处理原办理人不再拥有该任务委托是原办理人保留任务所有权但由被委托人代为处理。点狮OA内置了这两种操作底层分别对应Flowable的setAssignee()和addCandidateUser()。从那以后我每次配置会签流程都会先用一个测试租户跑一遍全流程发起→会签→回退→取回→转办→委托确认每个环节的任务归属和历史记录都正确再正式上线。希望帮到你。本文还有配套的精品资源点击获取