2. 从项目标题到落地我在这个OA系统里到底做了什么做过企业信息化的人都有体会OA系统是那种听起来谁都能做做起来谁都想骂人的项目。审批流、组织架构、权限模型、消息提醒、附件管理单拆每一个都不算难但合在一起就是一场持久战。这个项目标题里提到的组合——Spring Boot Java Vue、前后端分离、附带源码文档和调试支持——正好踩中了当前企业级应用中主流且务实的技术栈。这篇文章不聊虚的我会把这个OA系统从架构设计、核心模块拆解、权限模型设计、前后端联调到部署调试的完整链路讲透。适合看这篇文章的人有三类一是正在准备毕设或求职项目的同学需要一个能讲清楚、能演示、能扛住追问的完整系统二是公司里被要求快速搭一套内部OA的后端开发想知道哪些模块必须自研、哪些可以简化三是对前后端分离架构感兴趣想找一个真实业务场景来理解Vue和Spring Boot如何协作的开发者。无论你是哪一类这篇文章都会给出可复现的路径。我最初拿到这个项目需求时第一反应不是写代码而是先问了自己三个问题这套OA的管理到底管什么审批流要走到什么复杂程度权限模型是RBAC够用还是要更细的维度这三个问题的答案直接决定了后续所有表结构设计和接口划分。如果一开始就想先建几个表再说后面大概率会陷入改表、改接口、改前端的死循环。2. 需求拆解与模块边界不要把OA做成大杂烩很多OA项目失败不是因为技术不行而是因为需求边界没划清楚。客户说我要一个OA如果你真按字面理解把考勤、工资、客户管理、项目管理全塞进去那这个项目永远做不完。这个项目我给自己划定的边界是以审批流程为核心以组织架构和权限管理为基础以消息通知和附件管理为辅助。2.1 核心模块清单这六个模块构成了OA的最小可用闭环我最终确定的模块划分如下模块核心功能关键表用户认证登录、登出、Token刷新、密码重置sys_user, sys_token组织架构部门树、用户归属、部门负责人sys_dept, sys_user角色权限RBAC模型角色绑定菜单和按钮权限sys_role, sys_menu, sys_role_menu审批流程请假、报销、用章申请支持多级审批oa_leave, oa_expense, oa_approval_record消息中心待办提醒、审批结果通知、已读未读oa_message, oa_message_user附件管理文件上传、下载、关联业务单据oa_attachment系统监控登录日志、操作日志sys_log这个清单不是拍脑袋定的。你看审批流是核心那么它必须依赖组织架构审批人从哪来依赖权限模型谁能发起、谁能审批依赖消息中心审完了怎么通知依赖附件管理报销单要贴发票照片。七个模块之间形成了天然的依赖闭环这个闭环就是OA的最小可用产品。2.2 那些我故意砍掉的功能为什么不做考勤和日程项目过程中经常有人问我要不要加个考勤模块日历要不要会议室预约呢我的回答一律是先不做。原因很简单这些功能在OA体系里往往有更专业的替代品而且它们对审批流程的依赖度极低。强行加进来只会让系统变得臃肿还会拖慢核心审批链路的开发节奏。砍需求也是技术决策。我在项目文档里专门写了一段范围外说明把已砍功能和理由列清楚。这样做的好处是验收时不会因为为什么没有考勤打卡产生扯皮答辩时也能体现你的系统边界意识。这个习惯我觉得每个做项目的都值得养成。3. 技术选型的底层逻辑为什么是Spring Boot Vue以及如何分工前后端分离这个词已经被说烂了但很多人其实没搞明白到底分离的是什么。简单说前端Vue负责用户看到的页面、交互逻辑、路由跳转通过Ajax携带Token调用后端接口后端Spring Boot负责业务逻辑、数据持久化、权限校验返回JSON数据。两者只通过HTTP接口通信不共享任何代码和模板。3.1 后端技术栈四层架构让代码有了清晰的楼层Spring Boot版本我选了2.7.x这个版本既兼容JDK8又支持JDK11部署时不容易折腾JVM版本问题。项目结构严格遵循Controller-Service-Mapper四层com.oa ├── controller // 接收请求参数校验返回统一Result对象 ├── service // 业务逻辑事务控制流程编排 ├── mapper // MyBatis-Plus的Mapper接口数据访问层 └── entity // 数据库实体类和表结构一一对应很多新手写代码喜欢把业务逻辑写在Controller里图快。图快一时爽但后面加一个审批通过后要同时生成消息通知和操作日志的需求时你就得在Controller里连写三行调用如果以后这个逻辑在别处也要用就等着复制粘贴吧。放在Service里一个方法approve()内部按顺序调用消息服务和日志服务哪个调用来用都行。这就是分层的价值——它不是教条是给未来的自己留退路。MyBatis-Plus用得特别多我主要用了它的三个能力LambdaQueryWrapper做条件查询、IService内置的saveOrUpdate做批量保存、分页插件做列表分页。手写SQL只在需要多表关联统计时才用比如查询当前用户所有待审批的报销单数量这种。3.2 前端技术栈Vue 3 Element Plus Pinia前端用的Vue 3组合式API写法UI组件库选Element Plus表单和表格组件状态管理用Pinia。我在前端做了三个关键约定所有请求都走统一封装好的request.js内部基于axios实例请求拦截器自动附加Authorization: Bearer token响应拦截器统一处理401跳转登录页和500错误提示。路由守卫大范围使用router.beforeEach里判断如果有Token就放行没有就强制跳转/login。按钮级权限用自定义指令v-permission比如v-permissionleave:add控制发起请假按钮是否渲染。这个后面展开讲。Element Plus的表格确实好用el-table配el-pagination做分页几乎不用写多余代码。但有一点要注意表格里的操作列如果放太多按钮小屏幕上会挤成一排我一般用el-dropdown把次要操作收起来只留审批和详情两个主按钮。3.3 表结构设计几张核心表让数据血缘一目了然这里我把最重要的几张表结构贴出来。先看用户和部门CREATE TABLE sys_dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT DEFAULT 0 COMMENT 父部门ID0表示顶级, dept_name VARCHAR(100) NOT NULL, order_num INT DEFAULT 0, del_flag TINYINT DEFAULT 0 ); CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT BCrypt加密, real_name VARCHAR(50), dept_id BIGINT, email VARCHAR(100), phone VARCHAR(20), status TINYINT DEFAULT 1 COMMENT 1正常 0禁用 );审批流程的主表我设计成了一张通用流程实例表加一张业务详情表CREATE TABLE oa_process_instance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_type VARCHAR(50) COMMENT leave/expense/seal, title VARCHAR(200), applicant_id BIGINT, current_node INT DEFAULT 1, status TINYINT COMMENT 0草稿 1审批中 2通过 3驳回 4撤销, create_time DATETIME ); CREATE TABLE oa_approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, instance_id BIGINT, approver_id BIGINT, node_order INT, action TINYINT COMMENT 1通过 2驳回, comment VARCHAR(500), create_time DATETIME );这种流程实例 业务详情 审批记录三表结构是轻量级审批流的核心模式。流程实例管状态流转审批记录管历史轨迹业务详情表比如oa_leave管具体业务字段。以后就算加新的审批类型也只需要新增一张业务详情表流程引擎部分完全不用动。这也是这个OA系统可扩展性的关键设计。4. RBAC权限模型从用户到按钮的四张表到底怎么串权限模型我选的是经典RBAC基于角色的访问控制。四个核心实体用户、角色、菜单、按钮。用户和角色是多对多角色和菜单含按钮是多对多。4.1 权限数据是怎么流动的一次登录背后的三次查询用户登录成功后后端干了三件事根据用户名查出用户信息用BCrypt校验密码。查出该用户所有角色ID。根据角色ID查出所有菜单和按钮权限码缓存到RedisKey是login:user:permissions:{userId}Value是权限码集合。之后每次请求后端在拦截器里从Token中解析出userId从Redis取出权限码集合再看当前接口需要的权限码通过自定义注解RequirePermission(leave:add)是否在其中。在放行不在返回403。这套流程听起来简单但有几个细节必须注意不要每次请求都查数据库拿权限Redis缓存是必须的。缓存失效策略是登录时写入、改角色时主动删除。超级管理员不走权限拦截我用isAdmin字段判断管理员直接放行所有接口。前端按钮权限和后端接口权限必须保持一致。后端是安全底线前端只是体验优化即使前端忘了做按钮隐藏后端也必须能拦住非法请求。4.2 为什么我不推荐AOP做权限校验拦截器和注解各司其职网上很多教程喜欢用AOP切面做权限校验在自定义注解上写切面逻辑。我实际对比过两种方案的差异AOP的优点是注解和逻辑可以解耦但缺点是切点表达式容易失控尤其是你只想拦截Controller层的时候一个表达式写错可能切入到不该切入的方法上。我的做法是用Spring MVC的HandlerInterceptor做全局拦截检查Token有效性用自定义注解配合HandlerMethod判断当前处理器方法上的权限码。具体是public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 从Header取Token无效则返回401 // 2. 从Redis取用户权限集合 // 3. 如果handler是HandlerMethod读取RequirePermission注解 // 4. 权限码匹配失败返回403 return true; } }这样做的好处是拦截器统一处理有没有登录注解精细处理能不能干这件事两者职责清晰排查问题时思路也清楚。项目里我还在同一个拦截器里顺手做了操作日志记录把请求路径、参数、耗时、操作人全部写入sys_log表审计需求也一并满足了。4.3 数据权限为什么说能看到哪些数据是另一个维度RBAC解决的是能不能做的问题但OA里还有一类需求同是部门经理A部门经理只能看到自己部门的请假数据B部门经理只能看自己部门的。这种能看到哪些数据的需求RBAC本身管不了需要数据权限规则。我在这个项目里做了一个简化版部门的ancestors字段维护了从根到自己的全路径比如0,1,3。查询数据时如果当前用户角色配置了本部门及以下部门的数据权限SQL里就追加一个条件dept_id IN (SELECT id FROM sys_dept WHERE ancestors LIKE ...)。实际实现用MyBatis-Plus的InterceptorIgnore加一个自定义数据权限拦截器或者直接在Service层手动拼接条件。因为是简化版我选择了Service层拼接的写法逻辑更直白if (dataScope 1) { // 本部门 queryWrapper.eq(dept_id, user.getDeptId()); } else if (dataScope 2) { // 本部门及以下 queryWrapper.in(dept_id, deptService.getChildDeptIds(user.getDeptId())); }数据权限是面试官非常喜欢追问的点把它讲清楚含金量比单纯我用了RBAC高一大截。5. 审批流程设计用里程碑节点表替代死板的流程引擎5.1 为什么不用Activity重型流程引擎在OA场景里的杀鸡用牛刀市面上的OA项目很多直接上Activity或Flowable那是BPMN全套流程引擎支持网关、并行、会签、子流程。但如果你认真想想这个OA的核心场景——请假三天以内部门经理审批超过三天总经理审批报销5000以内经理审批超过5000还要财务复核——这些场景其实都是固定链路完全用不到BPMN的动态编排能力。引入Activity的代价是显著的要学BPMN建模要把流程图部署到引擎里要给前端配流程设计器排错时还要去理解引擎内部的事件机制。对于这个项目体量和时间安排来说得不偿失。所以我自己实现了一个轻量级审批模型流程模板表 节点顺序表 审批记录表。5.2 里程碑节点设计一张配置表搞定多级审批核心表是流程节点配置表CREATE TABLE oa_process_config ( id BIGINT PRIMARY KEY AUTO_INCREMENT, process_type VARCHAR(50), node_order INT, node_name VARCHAR(100), approver_role_id BIGINT, condition_expr VARCHAR(200) COMMENT SPEL表达式如 amount 5000 );配置好之后请假审批就是查这张表取出符合条件的节点列表逐级推进。审批人判断逻辑用了一个小组件public ListLong getNextApprovers(ProcessInstance instance) { // 查询该流程类型所有节点按node_order排序 // 找到第一个未审批的节点 // 根据节点配置的condition_expr用Spring的SpelExpressionParser // 结合业务数据判断该节点是否生效 // 返回该节点配置的角色下所有用户ID }这个设计的精妙之处在于新增一种审批类型只需要往配置表里插数据不需要改Java代码。我在项目里已经预置了请假、报销、用章申请三套配置想扩展时仿照即可。5.3 审批操作的幂等性防止重复点击把流程搞乱实际开发中很大一个坑是接口重复提交。用户在审批页面手一抖点了两次通过如果后端没做幂等处理审批记录会插入两条流程状态直接乱掉。我的方案是数据库层面的防重在oa_approval_record表上加了一个唯一索引uk_instance_approver_node(instance_id, approver_id, node_order)。第二次提交时唯一索引直接抛异常捕获后返回该节点已被处理的提示。这个方案的优点是用了数据库强制兜底即使前端漏了防重、即使并发请求同时到达也不会污染数据。前端同时增加一个提交中的loading状态双保险。6. 关键业务模块的实现细节请假、报销、消息中心逐个击破6.1 请假模块日期校验、时长计算、流程发起三件事请假模块是OA里最典型最简单的流程用它开刀练手最合适。发起请假时前端提交开始日期、结束日期、请假类型、理由。后端除了保存业务数据还要做三件关键校验日期合法性校验开始日期不能晚于结束日期不能选过去的时间。时长计算请假天数要排除周末我用LocalDate遍历日期区间过滤DayOfWeek是周一到周五的日期。重复请假检查同一时间内如果已经有一个审批中或通过的请假单直接拒绝新的申请。流程发起后流程实例的状态改为审批中同时根据节点配置创建第一个审批任务并给审批人发送消息通知。6.2 报销模块附件上传是先传后建还是先建后传报销单和请假的区别主要在两点金额、附件。附件处理这里有一个常见的方案选择问题是先上传文件拿到URL再提交表单时把URL带过来还是先创建报销单再关联上传附件我选的是前端先调用独立的上传接口拿到文件ID和URL存入表单提交时再统一保存报销单。原因有两点一是用户体验更好填表单时看到附件已上传会安心二是实现简单不需要处理报销单还没创建附件挂在谁名下的问题。上传接口是一个通用的/api/file/upload返回fileId业务表里存的是fileId而不是文件路径。这样附件可以被多个业务复用比如一个发票照片既用在报销单上也被引用到消息里文件迁移也不影响业务数据。后端上传实现用的是Spring的MultipartFile存储路径按年/月分目录文件名重新生成UUID防止重名和路径穿越攻击。这个细节对安全很重要因为直接用用户传来的文件名拼路径等于把一个../注入漏洞拱手送人。6.3 消息中心轮询还是WebSocket这个项目我选了轮询消息中心如果只做待办提醒和审批结果通知用轮询完全够。我做了个定时器前端每30秒调用一次/api/message/unread-count有未读数就在导航栏的铃铛上显示小红点点开列表看到具体消息。为什么不选WebSocket因为OA里消息触发频率低审批动作才产生消息用户在线时长相对有限长连接需要维护心跳服务端还要处理连接状态复杂度指数上升。轮询的缺点30秒延迟对OA场景完全可接受。真正需要实时推送的场景是聊天系统那是IM的活不该混在OA里。消息表的设计稍微特殊一点oa_message存消息主体标题、内容、类型、关联业务IDoa_message_user存接收人ID和已读状态。这样一条消息可以推给多个人比如部门经理和总经理同时收到提醒每个人有独立的已读状态删除消息时也只是删除自己在oa_message_user里的记录不影响别人。6.4 前端页面组织从登录到审批看板的路由设计前端路由按角色划分了三个区域/dashboard工作台显示待办数量、已办数量、公告列表。不同角色看到的内容略有不同。/leave、/expense、/seal业务模块包含发起页、列表页、详情/审批页。/system系统管理只有管理员角色能看到包含用户管理、角色管理、菜单管理、日志管理。页面间的跳转逻辑是登录成功后根据用户角色动态生成路由表因为普通用户不应该能访问/system/user页面前端菜单直接不渲染。动态路由的实现方式是登录时后端返回该用户的菜单树前端用router.addRoute逐个注册。有个经验我特别想分享菜单树不要前端硬编码要从后端返回来。否则你每次调整菜单都要改代码重新部署前端而且后端接口权限改了前端菜单不同步很容易出现菜单能点但接口报403的尴尬场景。7. 联调、部署与调试真正让项目能跑起来的最后一公里代码写完只是第一步前后端能顺畅协作、能在一台干净服务器上部署起来、遇到问题能快速定位这才算真正交付了这个项目。7.1 前后端联调最容易翻车的三个地方跨域、Token丢失、时间格式跨域后端配置了全局CORS允许localhost:5173Vite开发服务器端口访问。千万要注意CORS配置里的allowedOrigins不能写成*否则携带Cookie的请求会被浏览器拦截。Token丢失axios拦截器只会在实例创建时注入一次Token如果用户登录后刷新页面Token还在但内存中的request.js已经重新执行了所以要确保拦截器里是从localStorage或pinia里实时读取Token而不是缓存成模块级变量。时间格式Java后端默认序列化LocalDateTime会输出2025-01-01T10:00:00前端Element Plus日期组件默认要求YYYY-MM-DD格式。统一在Jackson配置里加LocalDateTimeSerializer和LocalDateSerializer格式分别设为yyyy-MM-dd HH:mm:ss和yyyy-MM-dd避免前后端各调各的导致一堆解析报错。7.2 部署方案Nginx Jar包的传统姿势其实最稳部署这件事我强烈建议别在一开始就引入Docker Compose编排三四个容器。先把传统的前端构建产物放Nginx后端打成Jar包用java -jar跑这套跑通再考虑容器化。原因很朴素前者只有两个组件出问题排查起来路径短后者一旦容器编排写错你连是nginx问题还是网络问题都分不清。部署生产环境的路径是前端npm run build生成dist目录拷到服务器Nginx的/usr/share/nginx/html下。后端Maven打包成oa-system.jar用systemd服务管理配置EnvironmentJAVA_OPTS设置Aftermysql.service依赖。数据库MySQL 8.0项目启动时用Flyway自动执行初始化SQL脚本建库建表插入初始化数据一步到位。反向代理Nginx配置/api路径转发到后端的8080端口前端页面走静态文件。7.3 让人炸毛的调试场景大列表页慢八成是N1查询OA系统里最容易被业务方吐槽的就是列表页转圈圈。第一次实测时500条报销记录要等4秒多打开慢查询日志一查发现原因在列表接口的MyBatis-Plus分页插件正常工作但oa_expense列表里每条记录都要查一次用户名、部门名这是典型N1问题。解决方法是把关联字段冗余查询改成一次性关联。我的处理是列表接口里先查出当前页的报销单ID集合再用in查询一次性取出所有相关用户和部门映射最后在内存里组装。改造后接口响应时间从4秒降到200毫秒以内。8. 项目文档、调试与二次开发源码之外真正值钱的东西8.1 一份合格的项目文档应该包含什么这个项目标题强调源码文档调试我来说说文档这块我的理解。一份能被别人甚至三个月后的自己高效使用的文档至少要包含这些内容环境要求与版本清单JDK版本、Maven版本、Node版本、MySQL版本缺一个都不能跑起来。初始化步骤从建库、导入SQL、改配置文件、启动后端、安装前端依赖、启动前端一步步写清。账号说明初始管理员账号、测试用户账号、各自的角色和权限范围。接口文档每个核心模块的请求URL、参数、返回值示例用Apifox导出的Markdown格式即可。常见问题FAQ端口占用、跨域、Redis连不上、Token过期、前端依赖安装失败等高频坑。其中账号说明最重要但常常被忽略。没有初始账号别人拿到源码后第一步就卡住后面全部免谈。我在项目里预置了一个管理员账号用户名admin初始密码admin123BCrypt加密存储和三个测试账号部门经理、普通员工、财务角色每个账号的权限范围都写清楚。8.2 调试这套系统的高效三板斧日志、接口测试、断点说到调试我的工作习惯里有三条铁律对任何Java项目都适用接口报错先看日志日志要看Exception堆栈第一行。Spring Boot自带sensible error page方便定位。我额外配置了全局异常处理器把未捕获异常统一打点并返回Result.error()重点是把堆栈输出到日志文件而不是只返回给前端。用ApiFox或Postman这样一类工具做接口测试不要靠前端页面点点点。先测通接口再联调页面可以把前端问题和后端问题清晰地切分开。断点调试要放在Service层不放在Controller层。Controller层只是参数透传真正业务流程全在Service。在approve()方法里打断点能看到流程状态如何一步步变化调试效率比在Controller里强十倍。8.3 如果你要二次开发三个最容易扩展的方向这个系统我会预留三个明确的扩展点拿来做毕设加分项非常合适新增审批类型仿照oa_process_config插入新配置新建一张业务详情表前端加一个页面即可完成一种全新的审批流。接入企业微信/钉钉通知在消息中心Service接口里加一个工厂方法轮询改成WebHook推送到办公软件群整个OA的实时性会提升一大截。数据权限从部门维度扩展为自定义用户组加一张用户组表和用户组数据权限关联表即可实现更细粒度的数据隔离。这三个扩展方向之所以好做是因为我在设计表结构时没有把逻辑写死在代码里而是尽量做成配置驱动。表结构设计初期多花的一小时会在后续开发和扩展中成倍赚回来。9. 复盘这个OA系统我从头到尾踩过的坑和最后的体会项目最后收尾时我习惯性地做了一次复盘。几个印象深刻的坑值得拿出来分享第一个是关于JWT的密钥问题。刚开始我图省事把密钥写死在application.yml里后来觉得不对——一旦代码泄露到Git仓库所有人拿到密钥都能伪造Token。后来把密钥挪到环境变量里本地开发用application-dev.yml覆盖生产环境用System.getenv读取这是底线安全习惯。第二个是BCrypt加密的坑。初始数据里直接插明文密码登录时怎么都对不上。后来才意识到BCrypt是不可逆加密初始数据必须用BCryptPasswordEncoder.encode生成后的值入库不能直接写明文。这个问题浪费了我整整一个下午。第三个是多环境配置文件的组织。用spring.profiles.activedev/prod切换环境数据库地址、Redis地址、日志级别各自分离。这个习惯越早养成越好不要等部署到生产环境时才手忙脚乱地改配置文件。最后说点个人体会。做完这个OA项目我最深的感受是系统的复杂度不在单个功能而在功能之间的协作。请假、报销单拎出来任何一个都是CRUD但当它们共享同一套权限模型、同一套审批链路、同一套消息机制时真正的设计功力才体现出来。这也是面试官喜欢问你项目里最难的点是什么的原因——他们想听的不是一个CRUD接口花了多少小时而是你如何在多个模块之间平衡约束、如何为未来的变更留好扩展位。这套OA项目正好给了你一个完整、可说、能深挖的载体。如果你正在做类似的系统我的建议是前期的需求边界和表结构设计值得花比编码多一倍的时间去打磨。这部分工作看不见摸不着但最终决定项目是跑完交差还是能持续演化。