基于Spring Boot的企业OA管理系统设计与权限控制实践

发布时间:2026/9/16 7:47:13
基于Spring Boot的企业OA管理系统设计与权限控制实践 简介基于 Spring Boot 开发的企业 OA 管理系统是一份适合毕业设计、课程设计及期末大作业的完整 Java 项目。面向计算机、软件、人工智能、自动化等专业的学生、教师或从业者既能帮助初学者理解企业级后端开发流程也能让有基础者在此基础上扩展业务模块。项目曾作为个人毕业设计使用答辩评审得分较高源码经过调试可运行验证。压缩包内含 159 个文件大小约 1.44MB主要文件类型包括 48 个 Java 后端源码、27 个 JavaScript、20 个 HTML、10 个 CSS以及 SQL 数据库脚本、配置文件、说明文档等基本覆盖后端接口、前端页面、样式资源和数据初始化。通过项目目录和文档可以学习 Spring Boot 分层结构、业务逻辑组织方式以及 OA 系统中常见的审批、考勤、权限管理等模块设计思路。目前已有 156 人学习是一份值得反复拆解和二次开发的实践资源。1. 这套OA系统的“含金量”不在代码在选型拿到一套“Java毕业设计-基于springboot开发的企业oa管理系统源码数据库”最容易踩的坑就是直接mvn spring-boot:run启动失败后才发现自己连数据库都没建。这个标题实际上交付了两样东西一套Spring Boot工程的完整骨架和一套已经初始化好的MySQL库。前者决定代码能不能编过后者决定登录后有没有数据可看。对做毕业设计的人来说真正的价值不是重复造轮子而是把“组织架构 审批流 权限控制”这条主线拆开看明白对已经在工作的人来说这套东西最大的意义是能省掉搭基础管理后台的时间改一改就能接手内部项目。问题的关键从来不是Spring Boot本身而是数据模型怎么设计、鉴权链路怎么闭合、初始化数据怎么跟代码对齐。以下几章按我自己的实现习惯把这条链路完整走一遍。2. 先把要有什么模块想清楚OA数据模型里的关键表能叫“企业OA管理系统”的表集合可以很大考勤、会议、日程、周报、固定资产都能塞进来。但跟评审老师和跟甲方讲的重点完全不一样毕业设计看的是数据模型是否清晰、权限闭环是否完整、业务状态是否能自洽。我一般会把OA拆成三组表组织架构部门、用户、角色、业务单据请假、报销、审批记录、行政信息公告、日程。三组之间尽量不混用字段写Mapper的时候各组独立。2.1 OA系统的模块边界必做项与加分项先给一个务实的分类。必做项是“没有它整个系统立不住”的部分组织管理部门/用户/角色/菜单、登录认证、一项完整的审批业务。请假是审批流里最典型的案例因为它的状态少、字段少、审批链路短但状态机模型和报销/用章完全一致做透一个就能举一反三。加分项是公告、日程、周报这类单表CRUD每加一个都意味着多一套Controller、Service、Mapper和前端页面工作量递增但技术亮点不递增。不建议碰会议预定、固定资产这类牵扯到资源冲突检测和生命周期管理的功能难度曲线陡却很难在答辩时讲出彩。模块边界定的原则是让代码量集中在能体现设计能力的部分而不是堆砌接口数量。2.2 组织架构的三张核心表部门、用户、角色的DDL设计先看最核心的三张表建表语句这套结构基本是所有OA和后台管理系统的通用底座CREATE TABLE sys_dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 部门ID, parent_id BIGINT DEFAULT 0 COMMENT 上级部门ID0为根, name VARCHAR(50) NOT NULL COMMENT 部门名称, sort INT DEFAULT 0 COMMENT 同级排序号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_parent (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT部门表; CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 用户ID, dept_id BIGINT COMMENT 所属部门ID关联sys_dept.id, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录账号, password VARCHAR(100) NOT NULL COMMENT BCrypt密文固定60位, real_name VARCHAR(50) COMMENT 真实姓名, email VARCHAR(100) COMMENT 邮箱, phone VARCHAR(20) COMMENT 手机号, status TINYINT DEFAULT 1 COMMENT 状态1启用0禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 角色ID, role_code VARCHAR(30) NOT NULL UNIQUE COMMENT 角色编码如ADMIN/LEADER, role_name VARCHAR(50) NOT NULL COMMENT 角色名称, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT角色表;逻辑说明这三张表全部采用逻辑外键而不是物理外键dept_id用普通索引原因是物理外键在插入数据时要多一次约束校验数据量上来之后会影响写入性能而且一旦业务分层为微服务物理外键会变成拆分障碍。password字段设VARCHAR(100)而不是VARCHAR(255)因为BCrypt算法固定输出60位密文留出余量即可设太大反而浪费InnoDB的索引空间。用户与角色是多对多关系需要一张关联表CREATE TABLE sys_user_role ( user_id BIGINT NOT NULL COMMENT 用户ID, role_id BIGINT NOT NULL COMMENT 角色ID, PRIMARY KEY (user_id, role_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户角色关联表;角色与菜单的关联表结构完全一致只是把user_id换成role_id、role_id换成menu_id。菜单表sys_menu需要重点设计的是perms字段它是权限标识比如system:user:add、leave:approve。前端按钮的v-if判断和后端接口的PreAuthorize校验共用同一套perms编码两边用一套规则不会出现前端隐藏了、后端却还能直接调接口的情况。2.3 审批业务用两张表表达业务单据表加审批记录表请假、报销这类审批业务不需要真的引入工作流引擎。用一张业务表记录单据本身再用一张流水表记录每一次审批动作就能把状态机完整表达出来CREATE TABLE biz_leave ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 请假单ID, applicant_id BIGINT NOT NULL COMMENT 申请人ID, leave_type TINYINT COMMENT 请假类型1事假2病假3年假, start_time DATETIME COMMENT 开始时间, end_time DATETIME COMMENT 结束时间, reason VARCHAR(200) COMMENT 请假事由, status TINYINT DEFAULT 0 COMMENT 状态0草稿1审批中2已通过3已驳回, current_approver_id BIGINT COMMENT 当前待审批人ID空闲为NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_status (status), KEY idx_applicant (applicant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT请假业务表; CREATE TABLE biz_approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, business_type VARCHAR(20) NOT NULL COMMENT 业务类型leave/expense等, business_id BIGINT NOT NULL COMMENT 业务单据ID, approver_id BIGINT COMMENT 审批人ID, action TINYINT COMMENT 审批动作1通过2驳回, remark VARCHAR(200) COMMENT 审批意见, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 审批时间, KEY idx_business (business_type, business_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT审批记录流水表;逻辑说明status用TINYINT存数字而不是直接存中文是为了后期调整文案时不用更新历史数据前端或者枚举类统一做映射即可。biz_approval_record里的business_type business_id组合索引是为了让“查询某个单据的全部审批轨迹”这个高频操作走索引同一条请假单的通过记录、驳回记录按时间升序排列就是完整的审批链路图。2.4 为什么不直接引入Flowable工作流引擎常见疑问是“OA系统不都应该用Activiti或Flowable吗”。真实企业里几千甚至上万单的审批量引入引擎有它的价值但这套OA的定位是毕业设计和轻量内部管理系统引入Flowable意味着要理解它的几十张引擎表、流程定义表、运行实例表代码调试成本远远高于业务代码本身。用上面这种“状态字段 审批流水表”的模型审批逻辑完全由自己控制答辩时能对着表结构把状态流转讲清楚这就是最大的优势。3. Spring Boot里的权限链路从登录到接口鉴权的落地组织架构表建好之后接下来就是把Java后台的认证与授权串起来。这套OA的权限设计核心是Spring Security JWT登录成功后前端拿token后续请求在请求头携带token后端通过拦截器解析并校验。3.1 技术选型为什么是Spring Security JWT而不是ShiroSpring Security在Spring Boot 2.x之后配置量大减常见做法是继承WebSecurityConfigurerAdapter或者直接注入SecurityFilterChain配合PreAuthorize注解就能实现方法级权限控制。而Shiro虽然轻量但Spring生态内的注解支持和Security相比不够顺滑RBAC权限点写在注解上需要额外引入AOP封装。JWT负责的是“无状态认证”好处是后端不需要存sessiontoken本身携带用户标识和过期时间非常适合OA这类前后端分离的管理系统。缺点就是token在过期前无法强制失效所以登录接口必须配合一个“账号禁用校验”每次请求都检查sys_user.status字段。3.2 JWT工具类生成与解析token的完整代码Component public class JwtUtil { Value(${jwt.secret}) private String secret; /** 过期时间单位秒默认2小时 */ Value(${jwt.expire:7200}) private Long expire; public String generateToken(Long userId, String username) { Date now new Date(); Date expiryDate new Date(now.getTime() expire * 1000); return Jwts.builder() .setSubject(username) .claim(userId, userId) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } }逻辑说明setSubject放用户名claim里塞userId这样后续拦截器取到Claims就能拿到当前操作人的身份。signWith(SignatureAlgorithm.HS256, secret)中的secret不要写死在代码里放到application.yml配置并且长度至少32位。jjwt库的parseClaimsJws如果token被篡改或过期会抛出SignatureException和ExpiredJwtException在拦截器里统一捕获并转成401状态码即可。3.3 登录接口BCrypt密码校验与token发放Service public class AuthService { private final UserMapper userMapper; private final JwtUtil jwtUtil; public String login(LoginRequest request) { SysUser user userMapper.selectByUsername(request.getUsername()); if (user null || !BCrypt.checkpw(request.getPassword(), user.getPassword())) { throw new BusinessException(用户名或密码错误); } if (user.getStatus() 0) { throw new BusinessException(账号已被禁用请联系管理员); } return jwtUtil.generateToken(user.getId(), user.getUsername()); } }逻辑说明BCrypt.checkpw每次校验时随机加盐所以同一个密码每次生成的密文都不一样但checkpw都能正确比对。密码字段在数据库里存的必须是BCrypt密文不能在库里存明文或MD5。校验顺序上先判断用户是否存在再看密码最后看状态三个条件顺序固定既避免暴露“用户不存在”这种信息也保证禁用账号不会拿到token。3.4 注册拦截器让每个接口自动完成token解析Component public class JwtInterceptor implements HandlerInterceptor { private final JwtUtil jwtUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 放行非Controller方法比如静态资源映射 if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { throw new UnauthorizedException(未登录或token缺失); } try { Claims claims jwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); request.setAttribute(username, claims.getSubject()); return true; } catch (ExpiredJwtException e) { throw new UnauthorizedException(登录已过期请重新登录); } catch (Exception e) { throw new UnauthorizedException(无效的token); } } }以后端接口路径统一前缀/api/**为例注册方式如下Configuration public class WebMvcConfig implements WebMvcConfigurer { private final JwtInterceptor jwtInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/api/**) .excludePathPatterns(/api/auth/login); } }逻辑说明拦截器把解析出的userId和username放进了request的attribute里Controller方法中直接用RequestAttribute Long userId就能取到当前操作人。excludePathPatterns统一维护放行清单登录接口、验证码接口、Swagger文档这些不需要鉴权的路径都加在这里。能注册拦截器就不要用Filter因为拦截器能拿到HandlerMethod可以做方法级权限判断Filter只能在Servlet层面处理拿不到具体的处理方法。3.5 方法级权限PreAuthorize控制按钮级操作在登录链路闭合之后细粒度的权限控制交给Spring Security的方法注解。Controller的方法上加上权限标识例如PreAuthorize(hasAuthority(leave:approve)) PostMapping(/api/leave/{id}/approve) public Result approve(PathVariable Long id, RequestBody ApproveRequest req) { leaveService.approve(id, req); return Result.success(); }这一步配合角色的perms集合实现的效果就是用户A有leave:approve权限标识才能调用审批接口没有就返回403。前端页面里同一个按钮的v-ifhasPerm(leave:approve)和后端hasAuthority对应上权限控制的闭环就算完整了。4. 把“源码数据库”跑起来一次完整的启动验证这套交付物里常见的坑不是代码编译失败而是数据库没有初始化干净。我给一份从环境检查到登录成功的完整步骤照着走一遍就知道问题出在哪。4.1 数据库的交付形态SQL脚本与Dump文件的区别源码包里数据库文件一般有两种形态。一种是.sql脚本里面包含CREATE TABLE和INSERT INTO语句导入时会自动建表插数据另一种是mysqldump导出的dump文件头部可能带有CREATE DATABASE语句和USE指令。两者的处理方式完全不同。判断方法很简单用文本编辑器打开文件看到第一行是CREATE DATABASE就用source命令整体导入如果是直接CREATE TABLE需要先手动建库再导入。同时关注文件中是否包含INSERT INTO sys_user的语句这决定登录时能不能找到初始管理员账号。常见的初始账号密码一般是admin/admin123或admin/123456具体以脚本里INSERT的密文为准如果是明文就说明脚本没做加密处理需要检查初始化脚本的正确性。4.2 从源码到登录页的最小启动步骤以下操作命令在Windows和Linux通用项目根目录执行# 以root身份进入MySQL创建数据库 mysql -uroot -p CREATE DATABASE oa_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; exit; # 导入SQL文件注意文件路径 mysql -uroot -p oa_system /path/to/oa_system.sql # Maven打包跳过单元测试 mvn clean package -DskipTests # 启动Spring Boot应用 java -jar target/oa-system-0.0.1-SNAPSHOT.jar逻辑说明CREATE DATABASE必须显式指定utf8mb4字符集否则建表时会继承MySQL实例的默认字符集通常会是latin1或utf8存中文就可能出现乱码。-DskipTests跳过测试是为了避免测试类里的数据源配置和本地不一致导致打包失败。启动成功后访问http://localhost:8080能看到登录页说明前端资源打包正常用初始账号登录成功后跳到首页整套链路就验证通过了。4.3 基于Spring Boot的配置文件修改点application.yml里需要改的一般只有数据库连接和JWT配置spring: datasource: url: jdbc:mysql://localhost:3306/oa_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 换成你的数据库密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true jwt: secret: 换成自己的32位以上随机字符串 expire: 7200逻辑说明数据库连接串必须保留serverTimezoneAsia/ShanghaiMySQL 8.x的驱动默认使用UTC时区不指定的话会报“The server time zone value”错误。map-underscore-to-camel-case开启后数据库里的create_time字段能自动映射到Java实体里的createTime这个配置漏了就会导致查询结果里时间字段全是null。4.4 启动失败的常见症状与排查方向用一个表格把最常见的问题和排查方向列出来比逐个搜报错信息快得多报错现象可能原因处理方式Communications link failure连接串缺serverTimezone或数据库端口错误检查url参数和MySQL端口号Access denied for user用户名密码错误或权限不足核对application.yml里的账号权限Port 8080 was already in use端口被其他进程占用server.port改成8081或杀掉占用进程登录页中文乱码数据库字符集不是utf8mb4重建库并指定utf8mb4启动时找不到数据源没有导入SQL或库名不匹配检查是否存在oa_system库和表结构一个比较容易忽略的问题Spring Boot版本太高导致启动失败。如果源码是基于Spring Boot 2.x写的本地电脑装了JDK 17后又手动升级成3.x可能出现javax.servlet包名找不到的异常因为这系列源码里引用的部分依赖还是旧版本的javax命名空间需要检查pom.xml声明的版本号和JDK匹配情况。5. 从能跑到能答辩4个值得动手的打磨细节5.1 给审批状态机补一个“撤销”操作现在的biz_leave状态是从0到1再到2或3中间缺少“申请人主动撤回”的路径。补一个status4 已撤销并在Service层加对应方法只有当前状态是“审批中”且当前操作人是申请人本人时才能执行同时往biz_approval_record里插一条“系统撤销”的记录。这个细节能让审批流的状态图展示得更完整答辩时可以直接画状态机图。5.2 用MyBatis的MetaObjectHandler统一填充时间字段每张表都有create_time和update_time如果每个Service里都手动set一遍代码冗余且容易漏。常见做法是注入一个MetaObjectHandler实现类Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }逻辑说明在实体类的createTime和updateTime字段上加TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)注解插入和更新时自动填充新增表结构时不需要再写多余的赋值代码。5.3 登录接口的体验兜底验证码与登录日志加上一张sys_login_log表记录每次登录的账号、IP、时间、成功失败标识这是一个性价比极高的功能代码量不大但能让“系统管理”模块立刻多出一个可展示的页面。验证码端可以用Hutool的CaptchaUtil生成登录前先校验验证码校验通过再走BCrypt密码校验顺序千万别颠倒否则验证码错误的请求也会消耗数据库查询。5.4 一张报表慢查询的索引优化示范如果用户列表页要按部门加时间范围筛选SQL类似SELECT * FROM sys_user WHERE dept_id ? AND create_time BETWEEN ? AND ?那单独建立在dept_id上的索引就不够用最佳实践是建立联合索引(dept_id, create_time)。在线执行EXPLAIN SELECT ...看key_len和rows就能验证是否命中索引。把这套验证思路写进毕业论文的测试章节比单纯贴几张页面截图更有说服力。本文还有配套的精品资源点击获取