ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+SSM+Django双框架搭建OA系统:权限模型与审批流设计实战

Java+SSM+Django双框架搭建OA系统:权限模型与审批流设计实战 接手过办公系统类项目的朋友应该都有体会这类项目看起来是“无非就是增删改查”真做起来却牵扯到权限模型、审批流转、消息通知、附件管理、操作日志等一堆破事。尤其是当需求里同时出现 Java 系和 Python 系的技术栈时很多人第一反应是“为什么要这么折腾”。这篇就来聊聊我用 Java SSM 做核心业务、用 Django 做辅助模块整套网络办公系统OA 系统从零搭建的思路、踩坑和关键实现。如果你是刚接私活的学生、要交毕设的应届生或者是小团队里被指派去搞内部系统的后端开发这篇应该能帮你少走不少弯路。先说结论OA 系统拼的不是技术炫技拼的是对业务状态的理解和对权限控制的严谨程度。技术栈本身SSM 和 Django 都是成熟方案关键是让它们各司其职。1. 先把 OA 系统的模块边界画清楚1.1 一个典型办公系统到底包含哪些模块你不妨把 OA 系统理解成“把线下纸质办公流程搬到线上的容器”。我见过很多刚入行的开发拿到需求后就急着建表结果做到一半发现漏了这个流程、缺了那个状态。所以第一步一定是画模块边界。在我做完的这套系统里核心模块分为六块用户与组织管理用户、部门、岗位这是所有权限判断的地基。权限控制菜单权限、按钮权限、数据权限三层缺一不可。审批中心请假、报销、用章、采购申请等核心是状态流转。通知公告内部公告发布、已读未读跟踪。日程与会议会议创建、参会人确认、会议室占用。系统管理操作日志、字典管理、附件配置。这六个模块中真正有技术含量的是权限控制和审批中心。其余的模块本质上确实是增删改查但增删改查也有讲究——比如公告的已读未读如果用户量上来你不可能在公告表里存一个巨大的已读用户 ID 列表而是要拆成公告表和已读记录表用关联查询去统计。还有一个容易被忽略的点每个模块都要预留“操作日志”的写入点。审计需求一般不会写进第一版需求文档里但等系统上线后领导一定会来问“谁在什么时候改了这个审批单”。如果前期没留日志后期补几乎是灾难性的。1.2 功能清单背后的真实需求是什么大多数时候需求方说的“做个 OA”其实是模糊的。你需要通过沟通把模糊需求转化为功能列表。拿“审批”来说你要问清楚审批是单级还是多级审批人能不能转审、加签驳回后是流程终止还是回到上一级审批通过后需不需要自动通知发起人这些问题直接决定数据库怎么设计。以请假审批为例如果只需要单级审批那在请假表里加一个approver_id字段就够了。但如果要多级审批就必须要有独立的审批记录表并且每一条记录要保存当前节点、操作人、操作结果、操作时间、备注。我在实际设计时通常采用“主表 流程记录表”的方式主表存业务数据请假人、开始时间、结束时间、请假类型、当前状态。流程记录表存流转历史每条记录对应一次审批操作。这样设计的最大好处是任何时候都能完整还原这个审批单经历了什么而不是只留下一个最终状态。你可以把状态机当成一个“节点 动作”的模型每次动作都产生一条历史记录主表只需要冗余一个当前状态字段用于快速查询。2. 为什么选 Java SSM Django 这套组合2.1 SSM 在企业级系统里的地位SSM 是 Spring SpringMVC MyBatis 的组合。在 Java 后端开发里这套组合的稳定性和生态成熟度是经过大量企业项目验证的。尤其是 Spring 的 IoC 和 AOP几乎就是为这种“业务复杂、事务密集”的系统量身定做的。审批流程里事务控制特别重要发起一个审批要同时更新请假主表状态、插入审批记录、给审批人生成待办通知。这三步操作必须在一个事务里任何一个失败都要全部回滚。用 Spring 的Transactional注解一行代码就能搞定事务边界这在 Django 里也不是不行但 Java 这边的事务管理更细粒度尤其是涉及多数据源的时候。MyBatis 则给了你 SQL 的完全控制权。OA 系统里报表统计类的需求特别多比如“统计各部门本月的请假天数”这种场景用 MyBatis 写动态 SQL 非常顺手。if、where标签配合起来可以按部门、按时间、按审批状态灵活组合查询条件比 ORM 全自动生成 SQL 要可控得多。2.2 Django 在本项目里扮演的角色你可能会问既然 SSM 能搞定一切为什么还要引入 Django我在设计这套系统时Django 主要承担辅助模块数据统计与报表展示Django 的 ORM 写聚合查询很简洁配合模板渲染报表页面效率极高。定时任务比如每周自动汇总待办事项、发送提醒邮件。简单的内容管理公告发布、意见收集这类 CRUD 密集但逻辑不深的功能Django 自带 Admin 后台几分钟就能搭出一个可维护的后台。说白了这套组合的目的是让合适的工具做合适的事。Java 负责核心业务的稳定性和事务控制Django 负责快速迭代的管理类功能。两边通过 API 接口通信数据库共用同一个 MySQL 实例。2.3 双框架协作的整体架构整套系统的物理结构是这样的前端一个独立的 Vue 页面或者纯 HTML Ajax通过 HTTP 请求访问后端。Java 服务提供/api/xxx形式的 RESTful 接口处理用户认证、审批流、组织架构等核心业务端口 8080。Django 服务提供/report/xxx、/admin/等接口和页面处理统计报表、定时任务、公告管理端口 8000。MySQL两个服务连同一个库通过表名区分业务归属比如sys_前缀是 Java 端维护report_前缀是 Django 端维护。Redis存会话、缓存热点数据、做分布式锁。这里有一个关键设计用户登录认证统一走 Java 端。无论请求打到 Java 还是 Django都必须携带 Java 端签发的 Token。Django 端通过一个自定义中间件去调用 Java 端的验证接口或者用 JWT 的解密密钥直接本地验签。我在项目里选择了后者因为本地验签不用发 HTTP 请求性能更好而且只要两边共享同一个密钥就行。3. 数据库设计与权限模型落地3.1 用户、角色、菜单三张核心表怎么建权限模型我用了经典的 RBAC基于角色的访问控制。三张核心表加两张关联表sys_user用户表存用户名、密码BCrypt 加密、姓名、部门 ID、状态。sys_role角色表存角色名、角色编码、描述。sys_menu菜单表存菜单名称、父级 ID、路由地址、权限标识。sys_user_role用户角色关联表。sys_role_menu角色菜单关联表。权限标识的设计有个小技巧我习惯用字符串形式控制到按钮级别比如system:user:add、system:user:delete。前端拿到用户权限列表后用v-if判断是否渲染某个按钮。后端接口再用 Spring AOP 拦截检查当前用户是否有对应权限标识双重校验才能确保安全。3.2 动态菜单与接口权限的联动Java 端登录成功后会返回一个菜单树和一个权限标识列表。菜单树直接决定前端左侧导航栏渲染哪些菜单权限标识列表决定按钮显隐。这里有个容易踩坑的点菜单树一定要按用户角色动态生成不要在前端写死。因为系统上线后管理员肯定会在后台调整菜单权限如果前端写死了菜单后端再怎么控制都白搭。接口权限我用了 SpringMVC 的拦截器也可以直接用 Shiro 或 Spring Security。我在项目里用 Shiro 比较多配置相对轻量。核心配置就是// Shiro 配置片段 MapString, String filterChain new LinkedHashMap(); filterChain.put(/api/login, anon); filterChain.put(/api/**, jwt); filterChain.put(/admin/**, roles[admin]);这里有个顺序问题值得注意Filter 链的匹配顺序是从上到下的所以/api/login这种匿名接口必须放在最前面否则会被后面的jwt过滤器拦下来。我见过不少新手把顺序写反导致登录接口怎么都访问不了还以为是跨域问题。3.3 Django 端的模型与 Java 端如何共用数据Django 端不重新创建用户表而是用一个report_employee模型映射 Java 端的sys_user表。使用managed False让 Django 不要管理这张表的迁移只做读取# Django models.py class Employee(models.Model): id models.IntegerField(primary_keyTrue) username models.CharField(max_length50) real_name models.CharField(max_length50) dept_id models.IntegerField() class Meta: managed False db_table sys_user这样做的好处是报表查询可以直接关联员工部门信息不需要额外同步数据。但要注意Java 端改了表结构Django 这边读不到新字段就会报错。所以两边团队或者你自己需要约定好Java 端修改表结构时通知 Django 端同步更新模型。另外Django 的 Admin 后台登录用户本身就是 Django 的auth_user表跟 Java 端用户体系不一样。我的处理办法是在 Django Admin 后台创建单独的运维账号不跟 OA 前台用户混合。这样最省事也避免了两个用户体系互相干扰。4. 审批流这样设计才能撑住复杂业务4.1 状态机思维拒绝一锤子买卖的状态字段审批流最常见的错误设计是只用一个status字段0 表示待审批、1 表示通过、2 表示驳回、3 表示撤回。这样设计遇到多级审批就彻底失效了——因为一级审批通过了但二级还没审你没办法用一个字段同时表达“一级已过、二级待审”这个状态。正确做法是引入“当前节点” “流程定义”的概念。用字段current_node表示当前审批到哪一级流程定义表里存每个节点的执行人。这样无论审批有多少级状态都能表达清楚。以请假审批为例流程定义可能是这样的节点编码节点名称审批人来源node_1部门主管审批申请人的部门主管node_2人事复核人事部门指定角色node_3总经理审批总经理角色审批单只要记录current_node node_2就说明部门主管已经过了接下来等人事复核。4.2 Java 端审批核心代码结构审批的核心逻辑我放在 Service 层用Transactional保证一致性。以“提交请假 生成第一条审批任务”为例Transactional public void submitLeave(LeaveApplyDTO dto, Long userId) { // 1. 保存请假主表 Leave leave new Leave(); leave.setUserId(userId); leave.setStartDate(dto.getStartDate()); leave.setEndDate(dto.getEndDate()); leave.setType(dto.getType()); leave.setReason(dto.getReason()); leave.setCurrentNode(node_1); leave.setStatus(0); // 审批中 leaveMapper.insert(leave); // 2. 生成审批记录 LeaveFlow flow new LeaveFlow(); flow.setLeaveId(leave.getId()); flow.setNodeCode(node_1); flow.setOperatorId(getApproverByNode(node_1, userId)); flow.setAction(submit); flow.setComment(dto.getReason()); leaveFlowMapper.insert(flow); // 3. 给审批人生成待办通知 noticeService.createNotice(flow.getOperatorId(), 您有一条新的请假申请待审批, leave.getId()); }这段代码的核心就是三步插入主表、插入流程记录、生成待办。任何一个失败都会因为事务注解整体回滚不会出现请假单建了但审批人不知道的情况。4.3 驳回、撤回、加签这些特殊动作怎么处理审批流的复杂度全在异常动作上。我在系统里处理了三种情况驳回更新主表status 2流程记录表插入一条“驳回”记录审批结束。撤回仅当当前节点还是自己的直属上级、且无人操作时允许发起人撤回。撤回后status置为 3流程结束。加签在某个节点额外增加一个审批人此时当前节点状态不变但待办要发给多个人。这里有一个业务规则要先确认清楚多人审批时到底是一票通过还是一票否决不同公司策略不一样所以我在系统里用配置字段控制而不是写死在代码里。通过一个流程配置表存每个节点的审批策略any或all代码里只要读配置判断即可。4.4 Django 端如何展示审批动态Django 端做审批数据的统计报表时直接读取两张表oa_leave请假主表和oa_leave_flow审批记录表按部门、按月聚合from django.db.models import Count from report.models import Leave summary Leave.objects.filter( status1, start_date__year2024, start_date__month5 ).values(user__dept__name).annotate(totalCount(id))Django 的 ORM 用values().annotate()做分组统计差不多是最顺手的写法。报表页面套一个简单的 Django 模板或者返回 JSON 给前端页都可以。5. 实操过程中的踩坑记录与排查方法5.1 跨域问题为什么前端请求总是被浏览器拦截做前后端分离的时候跨域是绕不开的。前端跑在 8080 端口Java 接口也在 8080Django 在 8000表面上端口不同实际上只要前端页面的域名或端口跟后端不一致就必然产生跨域请求。Java 端解决跨域我倾向用 CORS 过滤器在 SpringMVC 里注册一个全局的CorsFilter注意要允许携带自定义 Header比如 TokenBean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:5173); // 前端地址 config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); config.addExposedHeader(Authorization); return new CorsFilter(source); }Django 端更简单用django-cors-headers中间件在settings.py里加INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... ] CORS_ALLOWED_ORIGINS [ http://localhost:5173, ]最容易踩的坑是跨域配置配了但还是报错。这种情况下多数是因为前端带了自定义 Header而后端allowedHeaders没有放行。另一个常见问题是预检请求OPTIONS没被正确处理SpringMVC 的拦截器会拦截所有请求包括OPTIONS导致预检失败。解决办法是在拦截器里放行所有OPTIONS请求。5.2 会话保持与 Token 过期处理我在这套系统里用的登录态方案是 JWT。用户登录成功后Java 端签发一个 JWT 字符串前端把它存在localStorage里每次请求放在 Header 的Authorization里。JWT 最大的缺点是“签发了就很难主动作废”。用户改密码或者被管理员踢下线时旧的 Token 在过期前依然有效。解决办法有两种一是维护一个 Token 黑名单Redis Set二是把 Token 的jti唯一 ID存到 Redis并设置 Token 的过期时间与 Redis 中的一致。我选了第二种简单直接登录成功SET token:{jti} userId EX 7200每次请求先判断 Redis 里有没有这个jti没有就拒绝退出登录DEL token:{jti}这套方案的代价是每次请求都要查一次 Redis但 OA 系统的并发量通常不大这个开销完全能接受。而且换来的是“随时可以主动控制会话失效”的能力非常值得。5.3 Java 端和 Django 端的数据一致性两个服务共用一个 MySQL最怕出现同时写同一张表的情况。我的处理原则是行级权限边界Java 只写oa_前缀的表Django 只写report_前缀的表。如果 Django 需要展示的数据来自 Java 端的表只读不写。但有一种情况需要小心Java 端删除用户时Django 端的报表历史数据不能跟着删。因为报表数据是历史事实用户被删了历史审批记录依然要保留。所以在设计表结构时Django 端的报表表不能建外键约束指向sys_user否则 Java 端一删用户报表数据也跟着遭殃。这个坑我踩过一次后来在报表表里只冗余user_name字段不建关联。5.4 接口性能优化列表查询避免 N1OA 系统里列表查询特别多待办列表、已办列表、审批历史。如果直接用 MyBatis 的懒加载做关联查询很容易出现 N1 问题——查了 100 条待办每条又去查一次申请人信息数据库压力一下子就上去了。我的解决办法是在 Mapper XML 里直接用关联查询一次性查出来禁止在循环里查库。比如待办列表select idselectTodoList resultTypecom.oa.vo.TodoVO SELECT f.id AS flowId, l.title AS leaveTitle, u.real_name AS applicantName, d.dept_name AS deptName, f.node_code AS currentNode FROM oa_leave_flow f INNER JOIN oa_leave l ON f.leave_id l.id INNER JOIN sys_user u ON l.user_id u.id LEFT JOIN sys_dept d ON u.dept_id d.id WHERE f.operator_id #{userId} AND f.action submit /select像这种查询JOIN 一次全部搞定避免逐个去查。经验之谈OA 系统的性能瓶颈基本不在数据库层面而在代码写法上只要不写循环查库、不上亿条数据MySQL 扛个几千人的公司完全没问题。6. 安全加固与系统运维要点6.1 密码存储与传输安全密码存储必须用 BCrypt 这类带盐的哈希算法不能直接用 MD5。MD5 撞库成本太低了随便一个在线彩虹表就能破解。Spring Security 内置了BCryptPasswordEncoder直接用就好// 加密 String encodedPwd new BCryptPasswordEncoder().encode(rawPassword); // 校验 boolean matches new BCryptPasswordEncoder().matches(rawPassword, encodedPwd);传输层面局域网内部系统可以用 HTTP但如果系统要部署到公网或者跨地域访问必须启用 HTTPS。最省事的办法是在 Nginx 层做 TLS 终止后端服务不用动。6.2 接口防刷与越权防护OA 系统虽然不是高并发 C 端但也要防两类问题一类是登录接口被爆破另一类是越权访问。登录爆破的防护可以用简单的计数器Redis 里存这个 IP 或账号在指定时间窗口内的失败次数超过 5 次就锁定 15 分钟。实现不复杂但能挡住 90% 的无脑扫描攻击。越权防护更关键。我在 Shiro 框架里除了做角色权限控制还会在 Service 层做一层数据归属校验。比如一个普通员工去查别人的请假详情接口首先判断当前登录用户的 ID 是否等于请假单发起人 ID不是就直接返回无权限。这个判断不能只依赖前端隐藏按钮因为接口是可以直接拿 Token 调的。6.3 定时任务Java 和 Django 的配合系统里有一个很典型的需求每天早晨九点给所有有未完成审批的用户推送提醒邮件。这个功能我放在了 Django 端因为 Django 的django-crontab或 Celery 配置起来更简单。用django-crontab只需要在settings.py里配置CRONJOBS [ (0 9 * * *, report.cron.send_todo_remind_email) ]然后python manage.py crontab add就能注册到系统 crontab。对应的函数读取 Java 端的待办表把未办结的审批单汇总成邮件发给审批人。这里要注意一个点两个服务的时间基准要一致。如果服务器时间不准定时任务会早发或者晚发用户投诉可不是闹着玩的。我的做法是在所有服务器上统一配置 NTP 时间同步。7. 从零到上线项目开发的心得清单如果让我重新做一遍这套系统我会在开工之前就把下面这些问题问清楚。列表中每一条都是实实在在踩过的教训审批流是单级还是多级多级是否可能动态变化审批人是谁定的固定角色还是由发起人指定驳回之后是否允许修改再提交文件附件存本地还是存云存储容量上限多少有没有软删除的需求历史数据保留多久管理员需不需要模拟用户视角查看流程宏观看来任何一个办公系统的难点都不在“写代码”而在“定义状态和权限”。你定义的边界越清晰后面的开发就越流畅。回看这次基于 Java SSM Django 搭建网络办公系统的过程我这边的体会是双框架并不可怕可怕的是心里没想清楚每个模块的边界。Java 的严谨和 Django 的轻快在同一个系统里像两个性格互补的同事——Java 守好核心数据Django 做好辅助输出。你只需要管好它的协作方式其余放心交给它们各自擅长的事情。把状态机画明白把权限边界定死把每一个环节的数据变更留痕这个系统就能安稳地陪着公司跑上很多年。
返回列表