
1. 项目起因为什么我用一套后端撑起了四套业务系统先说结论RuoYi Office 并不是某个商业产品的名字而是我在接手公司信息化改造时基于 RuoYi若依框架自己攒出来的一套多业务域统一平台。当时老板给我提了个需求——OA、CRM、HRM、ERP 四个系统要能一起登录、数据互通还要 Web 端和手机端都能用。我第一反应是这不得上四个独立项目后来冷静下来一想拆成四套系统就意味着四套账号体系、四套权限模型、四套数据库光打通单点登录和主数据同步就够喝一壶的更别提后续的维护成本。反复权衡之后我决定走“一套后端、三端联动”的路线。所谓一套后端就是把 OA、CRM、HRM、ERP 全部作为模块挂载在同一个 Spring Boot 应用里共用一套底层基础设施三端则指 PC 管理端、移动 App 端用 UniApp 一套代码同时出 H5 和原生包、以及后续这半年被业务部门反复要求的第三方集成端。说白了就是把四个系统从“四个项目”变成“一个项目里的四个业务域”。这个方案最实在的好处有三条一是账号和权限天然统一员工不需要记住四套密码二是公共主数据只需要维护一份比如员工信息OA 里请假要用HRM 里算工资要用CRM 里建客户归属也要用一份数据源头能避免大量对账麻烦三是从研发角度看一次部署就能覆盖全部业务发版、升级、运维都集中在一处。当然代价也很明确——单个应用的复杂度上来了后面我会专门讲如何拆模块、控依赖、避免“大泥球”。2. 整体架构设计模块怎么拆才能既独立又不打架2.1 底座选型为什么是 RuoYi 而不是从零搭很多朋友问为什么不用 Spring Boot 脚手架自己搭。我承认从零布局自由度更高但那是大团队、有充裕时间的玩法。像我们这种业务驱动型的团队最要紧的是快速把 OA、CRM、HRM、ERP 的核心流程跑起来而不是把时间花在写部门树、用户管理、操作日志这些“通用轮子”上。RuoYi 的价值就在于它已经把企业后台最常见的公共能力做成了标准件用户管理、角色权限、部门机构、菜单管理、字典管理、操作日志、在线用户、定时任务这些都是开箱即用的。RuoYi Office 这个词在社区里的共识就是“基于若依扩展出办公与业务一体化能力”的工程实践而不是某个官方产品。我实际用的是 RuoYi 的 Spring Boot 3 JDK 17 分支数据库 MySQL 8.0缓存用 Redis 存 Token 和热点配置Web 端用 Vue 3 Element Plus。选这套组合没有太高深的原因——社区活跃、Bug 修复快、文档多出了问题随便一搜就有答案。2.2 后端模块划分一个工程下的生意也要门户分明Maven 多模块是 RuoYi 默认支持的工程结构我在此基础上做了二次切分。核心模块如下表模块名职责范围主要实体ruoyi-system用户、角色、部门、岗位、菜单、字典SysUser、SysRole、SysDeptruoyi-oa审批、公文、公告、会议、用车、物资申领OaLeave、OaApproval、OaNoticeruoyi-crm线索、客户、联系人、商机、合同、回款CrmCustomer、CrmLead、CrmContractruoyi-hrm员工档案、招聘、考勤、薪资、绩效HrmEmployee、HrmAttendance、HrmSalaryruoyi-erp商品、库存、采购单、销售单、盘点ErpProduct、ErpStock、ErpPurchaseruoyi-admin应用入口、路由转发、统一鉴权无实体仅 Spring Boot 启动类这里有个很关键的设计原则模块之间禁止直接调用 Service。比如 OA 模块的审批单需要读取 CRM 的客户名称时不许把 CrmCustomerService 直接注入到 OaApprovalController 里。我采用的是“跨模块场景走 API 包 本地事务编排”的方式在每个业务模块里抽一个 api 子包专门放对外暴露的接口和 DTO需要跨模块业务时由 admin 层或者单独的场景服务来编排。这样做的原因是防止 maven 依赖图变成一团乱麻——如果 OA 依赖 CRM、CRM 又依赖 ERP、ERP 再依赖 OA编译器不报错但改代码时谁都提心吊胆。2.3 数据库设计一库多 Schema还是单库加前缀业务域多了之后第一个纠结的就是表怎么放。放到同一个数据库里、用前缀区分这是我最推荐的方案也就是 oa_leave、crm_customer、hrm_employee、erp_product 这样命名。理由很直接后续做跨模块查询时一条 SQL 就能把审批单和客户联表查出来不用搞跨库 JOIN——MySQL 跨库 JOIN 不是不行但性能和维护体验都比较难受。而且公共表如 sys_user、sys_dept、sys_role 本身就用默认前缀所有模块都能直接引用。同时要对连接池和事务范围做明确的纪律约定业务模块的 Service 方法只允许在自己的 Mapper 范围里操作数据如果某个流程要同时写两个模块的表那就在场景服务里开启一个事务多个 Mapper 按顺序执行。这个场景服务实际放在 admin 模块下命名如 CrmContractApprovalService专门负责“创建合同时同步发起 OA 审批”这类跨域操作。这样每个模块仍保留自己的事务边界跨域场景才有全局一致性可言。3. 三端联动的核心机制Web、App、集成端如何共享一套后端3.1 统一 API 设计一个接口同时喂饱 Web 和移动端三端联动的第一步是让后端 API 不区分客户端。我用的是典型 RESTful 风格统一前缀/api后端根据请求头里的 Authorization 拿到 Token再解析出登录用户和角色。Web 端是 Vue3 Axios移动端是 UniApp 内置的 uni.request两者发出的 HTTP 请求在格式上完全一致所以后端控制器不需要做任何 UA 判断。为了让移动端少一点“桌面端思维”我在接口设计上定了几个约定。第一个约定是列表接口统一支持分页参数pageNum、pageSize、orderByColumn、isAsc 这四件套是标配不管是 Web 端表格还是移动端下拉加载更多用的全是同一套参数。第二个约定是日期时间统一返回时间戳不直接返回格式化字符串。原因是移动端不同机型对 “yyyy-MM-dd HH:mm:ss” 的 ios 兼容性有坑后面排查篇会细说时间戳传递到前端后再用 dayjs 本地格式化一劳永逸。第三个约定是新增和编辑接口统一用 POST JSON body避免表单编码和 JSON 乱串uni.request 里也是用 header 声明 content-type 为 application/json。3.2 移动端的技术选型和页面复用策略移动端我用的是 UniApp一个很现实的理由是一个代码仓库同时打包 H5、微信小程序和安卓/iOS App。RuoYi 官方社区有不少人把移动端单独做成 App 壳 H5 内嵌的方案但那种模式在弱网环境里体验不太行而且很多原生能力如推送、扫码、定位还要额外搭桥。UniApp 至少有现成的插件市场蓝牙打印、地图选点、扫码枪对接都有现成的原生插件签个字也比纯 H5 顺畅。页面层面的复用逻辑是管理类页面留在 Web 端操作类页面重点做移动端。比如 CRM 的客户列表在 Web 端做大表格带高级筛选但在移动端就做卡片流、支持“最近跟进”“未跟进客户”这种快捷分组OA 审批在 Web 端是复杂表单 流程监控大屏移动端则聚焦“通过/驳回/转交”三个核心动作。这背后的产品逻辑是 PC 适合处理信息密度高的管理工作手机适合处理高频率、碎片化的执行动作。重复代码肯定有但只在 DTO 和 API 层复用页面组件各自适配不要强行追求 100% 的代码复用率。3.3 认证与授信Web、App、第三方怎么统一登录态认证方案我直接沿用了 RuoYi 的 JWT Redis 模式但针对三端做了两个加强。第一个是 Token 里塞了 clientType 字段取值 web、app、wechat后端在 Redis 里存 Token 时把这个字段带上。这样做的直接收益是权限控制可以做到“同一角色在不同端拥有不同操作权限”——比如 ERP 的盘点审核功能只允许 Web 端操作App 端只能查看用 Spring Security 的自定义鉴权注解就能实现。第二个加强是移动端单独做了 token 刷新机制。Web 端登录后 Token 是两小时过期但移动端的用户可能一整天都在外面跑中途 token 过期就强制重新登录实在太伤体验。我的做法是移动端登录时传一个 rememberMe 标记后端把 Token 有效期延长到 7 天并且在 Redis 里保存一个 refreshToken当 accessToken 快过期时客户端调用/auth/refresh接口换新。安全上的妥协是 refreshToken 只允许在移动端使用而且刷新时校验 IP 归属地。4. 三端业务联动落地OA 审批、CRM 客户、ERP 库存怎么串成一条线4.1 打通 OA 与 CRM从“合同审批”看跨模块流程设计一体化平台最常被管理层夸赞的功能就是“合同审批自动联动客户信息”。以前用独立 CRM 时销售录入合同后还要跑到 OA 系统再填一遍基本信息不仅重复劳动而且经常填错客户名称。拆成一套后端之后这个流程被重构成一个完整事件链销售在 CRM 模块创建合同 → 系统自动把合同编号、客户名称、金额填入 OA 审批单的明细 → 审批通过后自动回写合同状态为“已生效”同时往 ERP 模块生成一张待出库的销售单。实现这个链路的关键是“事件触发而非人工催办”。我实现的方式是用 Spring 的事件监听机制在 CRM 模块的合同 Service 里发布一个 ContractCreatedEvent由一个位于 admin 层的监听器接收这个监听器会协调调用 OaApprovalApi 创建审批单、调用 ErpSalesApi 创建销售单。这套设计的好处是 CRM 模块不需要直接依赖 OA 和 ERP 的 Service业务边界依然清晰出了问题也能从监听器日志里快速定位究竟是哪一段链路失败了。4.2 移动端的核心操作流程我每天演示最多的一套路径移动端有几个功能点是业务方真实高频使用的我建议做同类系统时优先保障它们。第一个是移动审批。列表页卡片需要展示四要素申请人头像姓名、审批类型请假/报销/用款/合同、关键金额或日期、当前节点。点击进入详情页底部按钮区固定显示“通过/驳回/转交”通过时需要填写意见并有可能要求上传附件。这个看似简单的流程业务上其实有潜规则不同审批类型驳回时需要的字段不一样比如报销驳回必须填原因而请假驳回允许只写“不同意”三个字。我的做法是在后端返回的审批详情里附带一个 approvable 配置对象由后端根据流程定义动态生成移动端只要渲染这个对象就行。第二个是外勤签到。员工到客户现场后打开 App 定位签到系统把经纬度传给后端后端计算出与客户地址的距离并判断是否在打卡范围内。RuoYi 自带的数据字典里我用它维护了每个客户的预设坐标避免在代码里写死门店地址。注意这个功能的技术难点不在定位本身而在于偏移纠正——国内手机原生定位拿到的是 GCJ-02 坐标国测局加密坐标如果客户地址是逆地理编码得到的必须统一坐标系否则会出现“人站在客户门口却显示距离 300 米”的尴尬情况。4.3 ERP 模块与移动端库存盘点把桌面的重活搬到现场ERP 模块和移动端的结合点我重点做了库存盘点。传统方式是打印盘点表仓库人员手工记录再回办公室让文员录入系统每一步都可能出错。现在的流程是仓库人员打开 App 的盘点功能选择仓库后系统列出该仓库所有商品的理论库存逐项或扫码输入实盘数量提交后生成差异单由主管在 Web 端审核确认。这个功能在技术上的坑在于“扫码枪兼容性”。UniApp 的扫码能力在 H5 端使用摄像头扫码但到了 App 端可以直接调用原生扫码插件两者在触发事件、返回数据格式上完全不一样。我的做法是做了一层扫码服务封装内部判断运行环境对外统一返回 isbn/qrCode 字符串这样盘点页面不用关心底层是原生还是摄像头识别。另外大批量盘点时必须支持“盘点过程中断点续传”——我在本地存储里缓存了已盘数据网络不好时先存在本地点击同步后再批量提交后端。5. 数据权限与多租户四套系统共用一个后台如何防止越权5.1 从 RBAC 到数据权限角色能看哪些单部门边界怎么控制用户、角色、菜单这套标准 RBAC 我就不赘述了RuoYi 自带且文档很多。我更想强调的是数据权限。因为 OA、CRM、HRM、ERP 天然带有“部门隔离”的诉求——比如一个销售只能看自己名下的客户销售经理可以看本部门的客户销售总监能看全公司的客户。RuoYi 的数据权限注解 DataScope 给了我很大的便利它通过 AOP 在查询 SQL 前动态拼一个 dept_id 和 user_id 的过滤条件关键配置是三层第一层是角色数据权限范围定义有全部、自定义、本部门、本部门及以下、仅本人五个档位。第二层是角色与部门的关联关系做“自定义”时需要在界面上勾选这个角色能管哪些部门。第三层是每张业务表要预留两个字段user_id创建人和 dept_id归属部门而且要仔细处理数据迁移时旧数据的字段回填。这块我踩过的坑是 CRM 客户表最初设计时没有 dept_id后来销售离职交接客户时只能人工改归属非常痛苦。设计新表时哪怕业务上暂时用不到也要把这两个字段先建上没有坏处。5.2 移动端的数据权限接口层面只能过滤不能靠前端移动端做数据权限有个常见误区有些同学为了省事在后端接口把全量数据返回然后在移动端用 JS 在前端过滤。这种方案在纯内勤员工场景下或许能跑但只要用户量过百就会出现两类问题一是手机内存本来就有限一次拉一万条客户数据卡顿不说还费流量二是前端过滤只是视觉隐藏接口返回的 JSON 里其他部门的信息依然可以通过抓包看到这在企业应用里是合规硬伤。我的做法很粗暴所有业务列表接口都强制走数据权限拦截器。也就是 Uniapp 调/crm/customer/list时后端在用 DataScope 注解的 Mapper 查询前首先会判断当前用户的 clientType 和角色拼接的 SQL 过滤条件里附带“仅本人名下且状态为跟进中”的逻辑。移动端和 Web 端唯一差别是可以传不同参数比如移动端支持 status4 表示“今日需跟进”但无论如何后端不会返回超出权限范围的数据。把安全边界放在后端这是三端联动的底线。6. 常见问题与排查技巧实录6.1 移动端 Token 过期频繁为什么 App 老是被踢下线我遇到最多的生产问题是“用户手机端的登录状态不稳定”。排查下来发现根因有两个。原因一是移动端和 Web 端共用了同一套登录逻辑但 Web 端的 Token 有效期只有两小时移动端没做续期用户中午午休回来 token 就过期了。解决方式参考 3.3 里说到的 rememberMe 参数现在移动端 Token 有效期 7 天但前提是 Redis 里配置了合理的过期刷新策略。原因二容易忽略App 在切换 wifi 和 4G 网络时用户的出口 IP 会变。如果后端的登录校验里写了“登录 IP 与当前一致”的规则用户在外跑一天IP 换好几次就会出现“明明登录着却突然提示会话过期”。我的建议是缓存里的登录 IP 只作为展示和风控参考不要做严格变更判定除非你的业务是金融级别的强安全场景。6.2 跨模块依赖循环Maven 编译永远差一个模块刚把四个业务模块放进一个工程那阵我几乎每天都要改一次 Maven 依赖。今天 OA 要用 CRM 的客户下拉明天 CRM 要用 OA 的审批记录后天 HRM 又要用 ERP 的产品信息选型。如果图省事直接把 A 模块依赖 B 模块、B 模块又依赖 A 模块Maven 在执行 install 时大概率报循环依赖错误The projects in the reactor contain a cyclic reference。解决方案就是我前面说的 API 拆分包思路。具体落地时我建了一个独立的 maven 模块叫 ruoyi-api这个模块只放通用 DTO、接口定义、枚举常量不包含任何业务实现。OA、CRM、HRM、ERP 都只依赖 ruoyi-api而 ruoyi-api 不依赖任何业务模块。这样核心依赖图就变成了一张星型结构永远不会出现循环。切换时把原业务模块里的 DTO 类和接口签名迁移到 ruoyi-api各模块实现类改用 import 方式调用就一步到位了。6.3 审批流并发处理两个审批人同时点“通过”怎么收场最后一个要命的坑来自 OA 审批流。流程是部门经理和财务会签当两个审批人几乎同时点击“通过”时控制台的日志里出现了两次更新操作。表面看没有异常但实际把会签节点渲染成了两次通过记录导致后续节点收到双倍的通知。排查后发现是更新审批意见的 SQL 写的条件太宽UPDATE oa_approval SET node_status approved WHERE approval_id #{id}没有校验当前节点是否已经被处理过。写这种更新条件时必须带上“当前状态”作为乐观锁条件UPDATE oa_approval SET node_status approved WHERE approval_id #{id} AND node_status pending如果更新的影响行数为 0说明这个节点已被其他人处理当前请求直接返回“该节点已处理请勿重复操作”。一句话总结凡是涉及“领任务、改状态、扣库存”这类操作的 SQL必须有带条件的 update不能想当然地 update where id。7. 如果你也要做同类项目我最希望你记住的三句话最后说点实在的。做完这个 RuoYi Office 项目我最大的感触是“一体化系统”的关键不在技术多炫而在于怎么把多业务域的公共底座做得足够稳。稳定让 OA、CRM、HRM、ERP 各跑各的同时又能在底层共享同一套用户、同一套权限、同一套数据字典今天新接入一个模块的成本也就只剩写业务代码了。第二句话是“移动端不是 Web 端的缩水版它是另一套交互逻辑”。如果你只是把 Web 端的表格搬到手机上那注定体验不会好用户只会继续抱着电脑找领导签字。一定要从真实办公场景出发把高频操作做到三步以内能完成移动端才有存在价值。第三句话是“跨模块的数据一致性永远不要指望代码自觉一定要靠接口契约和日志追踪”。我这次把典型跨域流程全部记录到了统一的链路日志表里每次跑批都能看到完整事件链排查问题从几小时缩短到十几分钟。这套机制虽然不是什么高深技术但它是整个项目之后能够持续迭代、不被历史债务拖垮的底气。如果你也在考虑用一套后端支撑多个业务系统我建议你先别急着选框架而是用一两天把业务模块边界和数据归属画清楚。边界定得清楚后面每一步都顺。