
做过一个基于 RuoYi Office 的整合项目把 OA、CRM、HRM、ERP 四大业务塞进一套 Spring Boot 后端同时撑起 PC 浏览器、移动 H5、小程序/App 三端访问。这个项目做完之后最直观的感受是解决的不是“少买几台服务器”的问题而是把企业里原本散落的审批流、客户关系、人事数据和进销存彻底打通了。比如销售在 CRM 里把合同提交上去OA 会自动生成一份审批单审批通过后 ERP 的财务模块直接看到应收款HRM 那边也能同步到销售业绩用于提成核算。这篇文章更像一份项目复盘适合正在选型企业级管理系统、想基于开源框架做二次开发的团队也适合刚接触前后端分离项目并对“一套后端多端复用”感兴趣的同学。我会把架构拆分思路、四大业务域的数据建模、三端联动的落地细节以及实操中踩过的坑都展开讲清楚。内容偏实战能让你在动手之前先知道哪些设计必须提前想明白哪些问题会在上线之后集中爆发。1. 为什么坚持“一套后端”架构博弈与模块拆分的底层逻辑1.1 一套后端解决的是“系统孤岛”不是“省两台服务器”很多团队考虑上 OA、CRM、ERP 的时候第一反应是分别买三套软件各跑各的美其名曰“专业产品做专业事”。但等你真正把三套系统摆在一起就会发现最痛苦的从来不是某个系统本身好不好用而是用户要记三套账号、数据要人工搬运、报表要手动合并。一套后端的核心价值是让四个业务域共享同一套用户体系、同一套权限模型、同一套组织架构和同一套流程引擎。员工 ID 和用户 ID 是打通的主键部门权限在 RuoYi 的部门表里统一维护一条审批流可以从 OA 的请假单延伸到 HRM 的考勤记录再从 HRM 的薪资计算延伸到 ERP 的人力成本分摊。我举个例子员工在小程序端发起报销填了 2000 元差旅费提交后走 Flowable 流程。部门主管在 PC 后台审批财务在移动 H5 上复核流程结束后系统自动把数据写入 ERP 的财务凭证表同时更新项目成本。这种跨模块的联动如果用四套独立系统来做光接口对接和字段映射就要写成千上万行代码而且任何一个环节改了需求联调成本都会成倍增长。所以“一套后端”不是偷懒是数据关系真正被建模之后的自然结果。1.2 四大模块怎么切边界才不会变成大泥球后端只有一个服务不意味着所有代码都应该堆在一起。我在项目拆分上采用了一个很朴素的原则按业务域划分包结构表结构用前缀隔离跨模块数据不许直接查询表必须走服务方法。具体落地的时候工程被分成这样的模块边界ruoyi-office-oa处理请假、报销、用章、出差等审批类业务ruoyi-office-crm管理客户、联系人、商机、合同、回款计划ruoyi-office-hrm维护员工档案、部门调动、考勤、薪资和绩效ruoyi-office-erp覆盖采购、库存、销售出库、财务凭证ruoyi-office-system系统基础能力用户、角色、菜单、字典、文件数据库表也按前缀区分oa_leave、crm_customer、hr_employee、erp_stock、sys_user一眼就能看出归属。RuoYi 自带的sys_user和sys_dept是整个体系的基石所有业务表都通过user_id或dept_id与它们关联。边界确认之后模块间的协作统一做成服务调用。比如 CRM 提交合同需要自动生成 OA 审批单我不会让 CRM 模块去操作oa_process表而是在 CRM 服务里调用 OA 模块提供的OaProcessService.startProcess(...)由 OA 模块自己负责流程实例创建、审批节点初始化和消息通知。这样做的直接好处是后续你想把 OA 模块替换成别的流程引擎只需要改 OA 模块内部实现CRM 模块一行代码都不用动。2. 四大业务域的数据建模与联动设计2.1 OA流程为主表单为辅审批状态贯穿全局OA 域的核心是“流程”和“表单”的关系。我采用的方案是把流程实例和业务表单分开建模。流程实例表负责记录审批到了哪个节点、谁处理过、状态是待审还是已通过业务表单表负责存储真正的业务字段比如请假类型、起止时间、事由说明。以请假单为例oa_leave表存业务数据字段包括leave_type、start_time、end_time、days、reason、status。oa_process_instance表存流程数据包含process_key、business_key、current_node、approve_status。两条数据通过business_key关联相当于把“流程跑在哪里”和“业务长什么样”彻底分开。这样做最大的好处是改流程不改表单、改表单不动流程逻辑。产品经理今天说请假超过 3 天要加一级审批我只用改 BPMN 流程定义明天说请假单要增加一个“紧急程度”字段我只需要在业务表加一列并在移动端表单里补一个控件。Flowable 的回调监听器会在流程结束之后自动把oa_leave.status从待审改成已通过如果还需要写回考勤再额外调用 HRM 模块的服务即可。2.2 CRM客户认领、商机漏斗、跟单记录怎样做到不抢单不丢单CRM 是四个模块里数据权限最敏感的销售之间抢客户是永恒的矛盾。我在crm_customer表里设计了owner_user_id、owner_dept_id、follow_status、next_follow_time等字段客户归属权用 RuoYi 的数据权限注解来控制。具体实现是这样的销售登录后查询客户列表DataScope(deptAlias c, userAlias c)会按照 RuoYi 角色配置的数据权限自动追加 SQL 条件。配置了“仅本人数据”的角色SQL 会被拼上c.owner_user_id 当前用户ID配置了“本部门及以下”的角色则拼上c.owner_dept_id IN (当前部门及子部门)。这样做从数据库层就拦截了越权访问比前端隐藏按钮那种方式安全得多。商机漏斗建模同样围绕阶段展开。crm_business表里有money、stages、expect_deal_date阶段值对应 RuoYi 字典项从“初步接洽”到“方案报价”再到“成交签约”。移动端的跟进场景很典型业务员见完客户在地铁上掏出手机点开客户详情录入跟进记录顺手下拉刷新商机列表看到成交概率变化。后台的图表组件则直接从crm_business表聚合出各阶段的金额和数量不需要单独建统计表。2.3 HRM从员工档案到考勤薪资如何和 OA 审批互相引用HRM 模块我把它定位成员工主数据的源头。hr_employee表与sys_user表通过user_id关联每个能登录系统的员工必然有一条 HRM 档案反过来有档案的人不一定有系统账号比如新入职还没开通权限的员工也提前录入。这种关联关系带来的直接效益是OA 请假审批通过后流程回调 HRM 的考勤服务自动扣减年假余额报销审批通过后薪资计算里能拿到这个月的报销补贴金额离职审批通过后HRM 自动冻结账号权限ERP 里对应的采购审批权限、销售订单操作权限一并失效。你想想如果 HRM 和 OA 是两套系统这种联动要做到什么程度才能不靠人工答案是要靠持续加班写脚本而且总有漏同步的时候。2.4 ERP进销存与财务的核对链路ERP 域的核心是采购、库存、销售和财务之间的单据闭环。采购流程是采购申请单在 OA 里审批通过ERP 生成采购订单收货后生成入库单系统自动生成应付账款销售流程类似订单发货出库后生成应收账款。每一笔单据流转都有唯一的单号通过单号可以一路追溯到最初的审批实例。这里有个容易忽略的细节单据状态必须和业务操作绑定而不是靠定时任务去猜。比如库存扣减采用乐观锁更新库存时检查version字段防止并发操作导致超卖。在“销售订单出库”这个动作上我用 Redis 分布式锁包住了“查库存-扣库存-生成出库单”三步操作保证多个销售同时操作同一个物料时只会有一个人成功。这个场景在我们上线后的第一个月就真的遇到了两个销售在同一秒下了同一款物料的订单如果没有锁一定会出现库存负数。3. 三端联动的技术实现与落地细节3.1 同一套接口不同的端只是不同的“穿着”“三端联动”在这套系统里的具体含义是PC 浏览器管理端、移动 H5 办公端、微信小程序/App 端共享同一套后端接口。前端只是载体不同后端不需要为某一端单独造一套接口而是按业务能力提供原子接口由前端组合呈现。身份认证是联动的基础。RuoYi 登录成功后签发一个 token后续所有请求在 Header 里带Authorization: Bearer token。后端通过 Spring 的拦截器把 token 解析成登录用户信息并放入 SecurityUtils 上下文业务代码只需要调用SecurityUtils.getLoginUser()就能拿到当前操作人。三端在这一点上完全没有区别唯一的差异是各端对 token 的存储方式不同Web 端存在 Cookies 或 localStorage移动 H5 存在 sessionStorage小程序端存在uni.setStorageSync。移动端请求封装是我比较看重的部分。我在 uni-app 里封装了一个request.js统一做四件事注入 token、处理 401 跳转登录、错误 toast 提示、超时重试。基础代码如下// request.js 核心逻辑 let baseUrl http://192.168.1.50:8080/api; function request(path, method, data) { return new Promise((resolve, reject) { uni.request({ url: baseUrl path, method: method || GET, data: data || {}, header: { Authorization: Bearer uni.getStorageSync(token), Content-Type: application/json }, timeout: 15000, success: (res) { if (res.statusCode 401) { uni.removeStorageSync(token); uni.redirectTo({ url: /pages/login/login }); return; } if (res.data.code 200) { resolve(res.data); } else { uni.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { if (err.errMsg err.errMsg.indexOf(timeout) -1) { uni.showToast({ title: 请求超时请重试, icon: none }); } reject(err); } }); }); }这样一个封装之后三端各自的业务代码只需要关心页面渲染和数据展示不需要重复写 token 处理和状态码判断。实测下来整个移动端接口开发周期至少缩短了三分之一。3.2 Web 管理端重配置、重报表、重操作PC 端的使用场景是管理员和职能岗位的日常工作台所以要给足操作空间。RuoYi 的代码生成器在这里起了很大作用我只需要把设计好的数据库表导入代码生成器就能自动产出前端 Vue 页面和后端 CRUD 接口。生成之后做的第一件事是替换掉默认的列表查询和保存逻辑把业务校验、审批状态流转、模块间服务调用补进去。比如 ERP 的采购订单页面生成器默认只有一个订单主表和一个订单明细表前端自动生成主表和明细的编辑组件。但真实的采购业务里订单提交前要校验预算、订单审批中不允许修改、审批通过后要更新供应商往来余额。这些逻辑代码生成器写不了需要在生成的ErpPurchaseOrderServiceImpl里针对状态机做业务规则控制。我给这套系统的实际操作经验是代码生成器只解决“80% 的重复 CRUD”剩下 20% 的业务判断永远需要自己写不要指望任何低代码工具能完全替代人工逻辑。3.3 移动端重审批、重提醒、轻查询移动端的用户习惯和 PC 端完全不同。员工在小程序里希望只做一件事把审批给过了。所以移动端首页我设计成一个“待办工作台”展示的是当前用户的待办审批、已办流程、消息通知三个入口。每个审批卡片只展示关键信息例如请假单显示“张三 请假3天 07-12 到 07-14”点击进去才是详情和“通过/驳回”按钮。移动端接口和 PC 端接口虽然是同一套服务但移动端列表接口我做了字段瘦身。oa_leave表 30 个字段小程序列表只需要 8 个字段申请人姓名、头像、请假类型、开始时间、结束时间、天数、状态、流程实例 ID。我单独提供了一个AppController专门组装移动端需要的轻量结构而不是直接返回实体类理由很简单手机屏幕小、弱网环境普遍、移动端流量成本高返回一堆用不到的字段纯属浪费。润色上一个问题这里补充说明一下“三端”中任一端应该怎么定义——在这套项目里它的理解方式是前端载体可以动态替换后端边界不用变。后续如果你要加一个钉钉工作台入口或者企业微信端只需要再写一套前端应用调同一批后端接口就能快速上线。3.4 消息与任务联动流程事件驱动整条业务链三端联动最深的一层是消息与任务的联动。系统里每一件重要事情都会产生一条任务而任务会推送到正确的端上。实现思路是流程事件产生消息消息生成待办待办通过 WebSocket 推送在线端离线用户则在下次登录时通过待办清单拉取。Flowable 的流程事件可以挂监听器比如在流程结束时触发一个ProcessCompletedListener这个 Listener 里完成两件事更新业务单据状态、调用消息服务生成站内信。站内信记录写入sys_notice_record表然后通过 RuoYi 集成的 WebSocket 服务实时推送给申请人。PC 端的右上角消息铃铛会闪红点移动端的首页待办数会自增。这种体验比单纯发短信要舒服得多用户不用离开平台就能感知到流程进展。对于真正需要强提醒的场景比如大额合同审批到了总经理环节我还对接了钉钉/企微的应用消息推送。管理员在后台配置消息模板系统在审批节点进入指定用户时自动调用外部 API把审批链接以卡片形式推送到对应的 IM 应用里。这是“三端联动”向外部工作平台的延伸但后端逻辑依然只在 RuoYi Office 这一套系统里维护外部客户端只是另一个展示壳。4. 从零搭建的实操过程初始化、代码生成、权限配置4.1 环境准备和项目初始化我使用的技术栈是 RuoYi-Vue3 版本JDK 17、Spring Boot 3.2、Vue 3、Element Plus、MySQL 8。这套组合的好处是生态比较新Spring Boot 3 的 AOT 和 Spring Security 的集成更顺畅Vue 3 的 Composition API 写业务代码也更顺手。初始化过程分为三步。第一步从 RuoYi 的仓库拉取前后端代码把ry_*.sql数据库脚本导进本地 MySQL。脚本会创建sys_user、sys_dept、sys_role、menu、dict、quartz等基础表。第二步把后端application-druid.yml里的数据源地址改成本地库Redis 地址改成自己的启动后端服务确认登录接口通。第三步启动前端工程用 admin/admin123 登录后台确认菜单加载、用户管理、字典管理这几个基础页面能正常访问。这里给新手一个建议初始化阶段不要急着动代码先花时间把 RuoYi 的菜单表结构、部门表结构、字典表结构看明白。很多后续的“模块联动”本质上就是把业务表通过user_id、dept_id、dict_type挂到基础架构上基础表理解透了后面做什么都能事半功倍。4.2 用代码生成器落地一个 OA 请假单模块我以“请假单”为例完整走一遍代码生成流程。首先设计数据库表CREATE TABLE oa_leave ( id BIGINT AUTO_INCREMENT PRIMARY KEY, leave_no VARCHAR(32) COMMENT 请假单号, user_id BIGINT COMMENT 申请人ID, dept_id BIGINT COMMENT 申请人部门ID, leave_type VARCHAR(2) COMMENT 请假类型 1年假 2事假 3病假 4调休, start_time DATETIME COMMENT 开始时间, end_time DATETIME COMMENT 结束时间, days DECIMAL(4,1) COMMENT 请假天数, reason VARCHAR(500) COMMENT 请假事由, status CHAR(1) COMMENT 状态 0草稿 1审批中 2已通过 3已驳回, process_id VARCHAR(64) COMMENT 流程实例ID, create_time DATETIME, update_time DATETIME, del_flag CHAR(1) DEFAULT 0, PRIMARY KEY (id) );表建好后在 RuoYi 后台的“代码生成”页面导入这张表填写生成配置模块名填oa业务名填leave类名填OaLeave生成后自动得到后端 Controller、Service、Mapper 和前端 Vue 页面。生成的代码里列表请求走/oa/leave/list保存走/oa/leave/add修改走/oa/leave/edit删除走/oa/leave/removeRuoYi 的路由和权限注解也都自动配好了。但生成完还不能直接用。请假业务的实际逻辑是用户发起请假系统先保存草稿再提交时创建 Flowable 流程实例流程结束回写状态。所以我把生成的保存方法拆成了两个接口——/oa/leave/saveDraft和/oa/leave/submit。submit里调用流程服务启动实例并把process_id回填到业务表。这一步相当于把 RuoYi 的“通用业务”升级成了“真实业务”也是二开最有价值的部分。4.3 权限接入角色—菜单—按钮—数据范围四级递进RuoYi 的权限模型是我见过的开源框架里设计得比较完整的。系统登录用户后权限信息被打包进 LoginUser 对象每次请求通过PreAuthorize注解做功能级校验通过DataScope做数据级过滤。功能级权限的例子是PreAuthorize(ss.hasPermi(oa:leave:list)) GetMapping(/list) public TableDataInfo list(OaLeave oaLeave) { startPage(); ListOaLeave list oaLeaveService.selectOaLeaveList(oaLeave); return getDataTable(list); } PreAuthorize(ss.hasPermi(oa:leave:remove)) DeleteMapping(/{ids}) public AjaxResult remove(PathVariable Long[] ids) { return toAjax(oaLeaveService.deleteOaLeaveByIds(ids)); }前端按钮用v-ifcheckPermi([oa:leave:edit])控制显隐。这样一来整个操作链就是管理员在后台给角色分配菜单和按钮权限前端控制展示后端控制执行双重保险。数据级权限更容易出问题也更值得花时间调。RuoYi 的DataScope会自动识别当前用户角色动态拼接 SQL 条件。规则是全部数据权限不加条件、自定义数据权限按sys_role_dept关联、本部门及以下数据权限按dept_id in (当前部门及所有子部门)、仅本人数据权限按user_id 当前用户。我用在 CRM 客户列表上的效果是销售角色看到的是owner_user_id 自己的客户销售总监看到的是本部门及以下所有客户后台管理员看到全部。同一个接口、同一份代码因为登录者的角色不同而返回不同范围的数据这就是多端联动时权限模型最需要保证的一点移动端和 PC 端看到的必须是一致的。4.4 移动端联调的三件事跨域、接口前缀、模式适配移动端联调阶段碰到的问题基本就三类。第一是跨域。PC 端因为前端工程自带 Vite 代理很少遇到跨域但 uni-app 编译到小程序后请求是直接发到后端接口的没有代理层兜底。解决办法是在后端统一允许跨域RuOyi 里有CorsFilter配置类把允许的来源设置成*允许的方法设置成GET, POST, PUT, DELETE。上线后更严格的做法是配置成具体域名避免任意站点都能调用接口不过在联调阶段先放开问题最小。第二是接口前缀。RuoYi 后端默认接口路径带/prod-api前缀Nginx 会把/prod-api反代到后端服务。移动端请求的baseUrl需要区分环境开发环境用内网 IP 加端口测试环境用测试域名生产环境用正式域名。我习惯在 uni-app 的config.js里维护不同环境的配置对象切环境时只改一个全局变量。第三是模式适配。H5 端、微信小程序、App 在部分 API 上存在差异选择图片、扫码、分享这些能力需要判断端类型。我在移动端工具类里封装了isApp()、isH5()、isMiniProgram()三个函数需要调用平台特有 API 时先判断环境再走不同分支。这个细节虽然小但几乎每个移动端项目都会遇到提前封装能省掉后面大量if判断的脏代码。5. 排坑实录这五个坑百分之八十的团队都会遇到5.1 大表列表查询慢问题出在“什么都要查”系统上线跑了一个月客户列表和采购订单列表明显变卡。原因是生成器生成的查询逻辑默认select *字段全返回加上 CRM 客户表每天新增大量跟进记录单表数据涨到几十万行之后页面加载明显变慢。我排查后发现两个主要瓶颈一个是列表接口返回了全部列包括follow_records这种文本字段光网络传输就浪费很多时间另一个是没有为查询条件里的owner_user_id、next_follow_time建索引。解决办法是先用EXPLAIN分析了慢 SQL然后把列表接口改成只返回表格需要的 10 个字段最后为高频查询字段补齐联合索引。优化完列表接口耗时从 2.3 秒降到了 300 毫秒以内这种体验提升是用户能直接感受到的。这里有一个很值得借鉴的思路查询和详情接口的字段集合应该分开设计。列表只返回展示字段和行操作需要的 ID详情接口才返回完整字段。RuoYi 生成器默认把这两个接口合并了二开时不要嫌麻烦结构设计成两个接口是大数据量列表的必走之路。5.2 Token 过期导致的移动端“闪退”移动端使用过程中最容易被人骂的问题就是 token 过期。RuoYi 默认的 token 有效期是 30 分钟员工在审批详情页停留久了再点提交接口返回 401直接被踢回登录页刚填的内容全部丢失。这个问题的标准解法是“静默续期”。我在移动端请求封装里增加了统一处理逻辑请求遇到 401 时尝试调用后端/auth/refresh接口刷新 token刷新成功则重新发送原请求刷新失败才跳转登录页。后端配合做了一次改造登录接口签发 accessToken 时同时返回 refreshTokenaccessToken 过期后允许用 refreshToken 换取新 token。这样用户在无感的情况下就能续期不会再出现填了一半表单被踢出去的尴尬。实测上线后关于“掉线”的吐槽几乎清零。5.3 并发场景下的重复提交与库存超卖报销单重复提交通常发生在移动端弱网环境。用户点了一次“提交”按钮没反应以为没点上又点了一次后端就收到两条流程实例。解决办法我做了两层防护前端把提交按钮在发起请求后立即置灰并展示 loading 状态后端在提交接口里用 Redis 做幂等把业务类型用户ID业务单号作为分布式锁的 key锁不存在则放行锁存在则直接提示“请勿重复提交”。库存超卖则出在 ERP 模块。两个销售同时下单同一件物料都查到库存还有 3 件都扣减成功了库存变成了负数。后来我在物料表的库存扣减 SQL 里加入了版本号乐观锁UPDATE erp_material_stock SET stock stock - #{quantity}, version version 1 WHERE material_id #{materialId} AND version #{oldVersion};执行后判断影响行数为 0 则说明并发冲突提示用户“库存已变更请刷新后重试”。这套逻辑虽然简单但在单体应用里已经足够可靠不需要为了这个场景引入分布式事务。5.4 工作流动态表单的字段存储方案OA 审批流里最头疼的是表单动态字段。同一个流程不同部门有不同字段有的部门建了 5 个字段有的部门建了 10 个字段如果每个流程都建一张表表数量会失控。我采用方案是“固定字段 扩展字段 JSON”双轨存储。固定字段如title、apply_user_id、apply_time、department_id做成独立列方便查询和统计扩展字段统一存进process_form_data_json字段用 JSON 格式存储键值对。查询时固定字段走 SQL 过滤扩展字段需要查明细时再解析 JSON。这个方案在报表统计和流程回显两个场景里都够用省的是一对多表单关系的复杂建模代价是无法用 SQL 直接对扩展字段做聚合。如果你需要按扩展字段做统计分析建议单独加一张宽表用消息或者流水线把 JSON 里的字段展开到真正的列上。这也是这套架构后续演进我会做的方向。5.5 部门调整后历史数据归属变混乱HRM 里有员工调岗CRM 里有客户负责人调整但历史单据上的dept_id往往还是旧部门导致部门维度报表里的数据对不上。这个问题在开发阶段几乎不会被发现但上线三个月后管理层开始看报表时就会爆发。我的处理方案是“业务单据快照”。所有涉及部门和人员归属的关键字段在单据创建时就保留一份快照而不是实时去查部门表。比如采购订单创建时就把purchase_dept_name、purchase_owner_name存进单据。这样做的好处是历史报表始终和当时业务一致不会有“张三从销售部调到市场部结果他过去半年的业绩全记到市场部”这种尴尬事。坏处是代码里多了一点冗余字段但从财务审计和业务追溯的角度看这点冗余非常值得。6. 最后聊几句实际体会这个项目从头到尾做下来我个人最大的体会是一套后端、三端联动这件事难点从来不在技术而在业务建模阶段的边界取舍。模块划分得太清晰跨模块联动时服务调用会变多模块划分得太随意后续维护时改动一个字段可能牵扯七八个服务。RuoYi 提供的代码生成器和权限模型帮我把重复劳动压到了最低让我把精力集中在了真正值得思考的业务规则、状态机和数据一致性上。最后再分享一个小技巧如果你计划在同一套后端上支撑 Web 和移动端建议从第一天就把接口的返回结构设计成“字段可选按端裁剪”。不一定像我一样单独写一个 AppController但至少在返回前留好扩展点。因为移动端的性能优化、弱网适配、离线缓存这些需求都会在你上线后的第一个月集中砸过来提前留好结构性空间真的能救你一命。如果你是正准备做企业数字化整合的团队我建议你先拿 OA 审批和 CRM 客户资料这两个模块做试点跑通“一套后端、三端联动”的完整链路再逐步把 HRM 和 ERP 接进来。步子小一点踩坑的成本就低很多。这套架构后续想往微服务演进也顺理成章按业务域拆分是眼下最主流的方向。