十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

基于Spring Boot+Vue的协同办公系统设计与实现:请假销假核心流程解析

基于Spring Boot+Vue的协同办公系统设计与实现:请假销假核心流程解析 在我做过的那么多后台管理系统里springbootvue这套组合确实是中小型企业内部工具的首选。今天想跟你聊的是一个很典型的选题——基于Web的协同办公系统核心业务是企业员工的请假销假。这个题目看着不复杂但它把权限、流程状态、日期计算、前后端交互这些基本功全串起来了非常适合用来梳理完整的全栈开发思路也适合作为毕业设计或入职练手项目。这个系统能做什么一句话说透员工在线提交请假申请主管在线审批员工回来后提交销假系统自动算清请假时长并留痕备查。跟传统纸质请假条相比它最大的优势不是“快”而是“留痕”和“可统计”。下面我从需求拆解到技术实现把整个项目的关键环节捋一遍。这篇文章没有任何教学大纲味就是我实际在做这类系统时的完整思路和踩坑记录。1. 这个项目到底在解决什么痛点需求拆解与流程建模做企业系统最难的不是写代码是把业务流程翻译成状态机。你去看网上很多请假系统的Demo翻来覆去就是“增删改查”但实际企业里的请假流程远比这个复杂。拿到这个标题后我首先做的是把业务场景画清楚。1.1 请假与销假的完整业务链路企业员工请假不是“填个表打个勾”就完了。我接触过的多数中小企业流程是这样的员工填写请假单选择请假类型事假、病假、年假、调休选择起止日期和时长填写事由。系统先做自动校验假期余额够不够起止时间合不合法有没有跟已审批的请假单重叠校验通过后单子进入审批流推送给直属主管。部分企业还有多级审批主管 - 部门经理 - 人事。审批通过后考勤数据生效。员工休假结束后需要发起销假确认实际休假时长因为可能存在提前销假或延后销假的情况。销假确认后系统回写考勤数据更新假期余额流程闭环。所以这个系统的核心状态至少应该有这几个草稿、待审批、审批通过、审批驳回、已销假、已作废。每个状态背后都存在对应的操作权限和页面表现。比如待审批状态员工不能修改内容审批通过之后员工才能看到“销假”按钮销假之后请假单变灰不能再做任何操作。1.2 权限模型的确定不能只有“管理员”和“员工”很多初学者把这系统做成“管理员一键审批”这在实际企业场景里肯定是行不通的。真正的协同办公系统权限至少要有三层员工只能看自己的请假单只能发起请假和销假不能审批任何人的单子。部门主管能看到部门内所有员工的请假单能审批自己部门下属的单子。管理员人事/系统管理员能看到全公司所有请假单能审批所有单子能维护员工假期额度、部门信息、请假类型字典。这个权限模型落到Spring Boot后端就是Spring SecurityJWT的那一套登录成功后返回带角色信息的Token后端接口上加PreAuthorize(hasRole(ADMIN))这样的注解前端根据角色动态渲染按钮和菜单。我到后面会详细说这块的实现细节。1.3 节假日和请假时长的计算逻辑这里藏着一个最常见的坑请假时长不能只看日期差。比如周五请到周一中间有双休日这种怎么算国家法定节假日调休怎么算最简单的处理方案是引入一个工作日历表。这张表里存两个关键字段日期、是否工作日。也就是说每年年初把全年的工作日、休息日、节假日都维护进数据库里。计算请假时长时SQL或者后端逻辑只统计is_work_day true的天数。对于毕业设计级别的项目可以不用做得这么重。我当时采用的方式是判断起止日期之间包含的所有日期如果是周末就跳过法定节假日不单独维护作为优化扩展点写在文档里。但如果要做成真正能上线的系统工作日历表是必不可少的。这个点写进论文或者项目答辩里是一个非常加分的思考深度。1.4 角色之外的扩展空间通用审批流请假只是协同办公里最典型的一种流程类似的还有加班申请、出差申请、用章申请。所以设计表结构时不要把所有字段物理“焊死”在请假单上。我在做这类系统时会单独建一张通用审批表把业务ID、业务类型、当前节点、审批人ID、审批意见、审批时间都放进去。这样一来请假是这张表出差也是这张表以后新增流程类型只需要加一个业务类型枚举不用重新建表。这就是从“做功能”到“做系统”的差别。2. Spring Boot后端把请假单拆成“谁、何时、审什么”后端是整个系统的地基。我在设计这个项目的后端时走了完整的工程化结构不是那种把几百行代码塞在一个Controller的写法。下面把这个项目的后端核心模块拆开讲。2.1 项目结构设计我用了经典的分层结构清晰到哪怕三个月后回来看代码也能秒定位问题com.example.oa ├── controller // 控制层只负责参数接收和结果包装 ├── service // 业务层核心逻辑都在这里 │ └── impl ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 前端传参的对象模型 ├── vo // 返回前端展示的对象模型 ├── config // 配置类Security、跨域、拦截器 ├── utils // 工具类JWT工具、日期处理 ├── exception // 统一异常处理 └── common // 公共返回结构、常量、枚举这套结构是Spring Boot项目的标准姿势。多花两分钟建目录后期维护成本能降一半。2.2 请假单的核心实体与状态流转请假单的主表我设计成leave_form关键字段如下字段名类型说明idbigint主键user_idbigint申请人IDdept_idbigint所属部门ID冗余字段为了查询方便leave_typevarchar请假类型事假/病假/年假/调休start_datedate开始日期end_datedate结束日期daysdecimal(4,1)请假时长按工作日计算reasonvarchar请假事由statusint状态0草稿 1待审批 2通过 3驳回 4已销假 5已作废create_timedatetime创建时间update_timedatetime更新时间状态流转我拍了硬性的控制逻辑关键点在于每一步都要做合法性校验防止跳过审批直接销假。具体在Service层是这样的// 待审批 - 通过 public void approve(Long formId, Long approverId, String opinion) { LeaveForm form getById(formId); // 校验1单据必须存在 Assert.notNull(form, 请假单不存在); // 校验2当前状态必须为待审批 Assert.isTrue(form.getStatus() 1, 该单据当前状态不可审批); // 校验3审批人必须是该员工的主管或有管理员权限 LeaveForm update new LeaveForm(); update.setId(formId); update.setStatus(2); // 更新审批人相关信息 updateById(update); // 写入审批记录表 approvalRecordService.record(formId, approverId, opinion, PASS); }2.3 JWT认证与权限控制的落地姿势这项系统的所有接口除了登录接口都必须带Token访问。Spring Security JWT是当前最主流的选择原因很简单无状态、好扩展、前后端完全分离。具体落地步骤是配置Security的过滤链放行/api/auth/login和静态资源。自定义一个JwtAuthenticationFilter继承OncePerRequestFilter从请求头的Authorization字段取出Token解析出用户信息和角色塞进SecurityContextHolder。在需要权限控制的接口上用PreAuthorize(hasRole(ADMIN))之类的注解做方法级权限控制。这里有个实际踩坑的地方Spring Boot 2.7以后Security的WebSecurityConfigurerAdapter已经被标记为过时网上一大堆老教程还在用旧的写法。新写法是直接注册一个SecurityFilterChain的Bean。很多人在集成最新版Spring Boot时被这里卡住后面会细说。2.4 销假逻辑的设计不只是“改个状态”销假这个动作在需求面上就俩字但实现时要考虑很多员工实际销假日期晚于申请日期说明他延后销假了超出部分是否扣减年假余额员工提前销假剩余天数是否要退回假期余额销假单是否需要主管二次确认在我的项目里销假走的是“待销假 - 销假申请 - 主管确认 - 完成”的二级流程。销假提交时员工可以修改实际返回日期系统会重新计算实际休假天数再跟原申请天数做对比实际天数 申请天数多出来的假期回归余额池。实际天数 申请天数超出部分直接扣事假额度。这个逻辑看起来不复杂但它需要在一个事务里完成更新请假单状态 更新假期余额表 写审批记录。任何一步失败整个操作回滚。Transactional注解在这里是硬性的漏一个就会出现“状态变了但余额没变”的脏数据。2.5 统一异常处理与返回格式前后端分离的项目最忌讳的就是后端直接报500大红页。我在项目里定义了一个统一返回体{ code: 200, message: success, data: {} }然后写一个全局异常处理器RestControllerAdvice把所有业务异常和未知异常都抓进来转成这个结构返回。前端只需要判断code是不是200就能决定是正常处理还是弹错误提示。这里有一个很多教程没细讲的点RestControllerAdvice并不是所有异常都能捕获的——比如Filter里抛出的异常、Security认证失败抛出的异常这些发生在进入Controller之前所以需要在Security配置里单独配置认证失败/权限不足的处理器。等到联调时前端报“网络错误”而不是“业务错误”往往就是这个问题。3. Vue前端审批流程的“可视化体验”是协同系统的灵魂协同办公系统的前端承载着大量表单和状态展示。Vue在这个场景下的优势非常明显组件化开发让请假表单、审批记录、列表页都能独立维护Element UI或者Element Plus自带的表格、日期选择器、步骤条组件几乎是为这类后台量身定做的。3.1 前端项目初始化和路由设计我用Vue 3 Vite Element Plus搭建的前端路由设计分两块公共路由登录页。业务路由根据角色动态加载。具体在路由这部分不推荐把所有路由硬编码在router/index.js里。我采用的一种更工程化的做法前端定义一个路由表的“骨架”比如员工对应的/employee/dashboard、/employee/leave/list主管对应的/approver/pending管理员对应的/admin/user登录后根据用户角色用router.addRoute动态追加。这样前端代码里不会出现“我是员工却能看到审批按钮”之类的问题。核心路由示例const routes [ { path: /login, component: Login }, { path: /dashboard, component: Layout, meta: { roles: [STAFF, ADMIN, MANAGER] }, children: [ { path: leave/create, component: LeaveCreate }, { path: leave/my-list, component: LeaveMyList }, { path: leave/approve, component: LeaveApprove, meta: { roles: [MANAGER, ADMIN] } }, ]} ]然后在路由守卫里做角色判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (!token to.path ! /login) { next(/login) } else { const roles userStore.roles if (to.meta.roles !roles.some(r to.meta.roles.includes(r))) { next(/403) } else { next() } } })3.2 请假表单的“友好校验”怎么做前端表单校验这事儿看似简单但大部分项目都做得特别糙。日期选择器一拉时间一填就提交了。实际场景里下面的校验一个都不能少结束日期必须大于等于开始日期。请假时长不能超过剩余假期额度这个需要从后端拉取当前用户的额度信息前端做联动提示。假期重叠检测用户在同一个时间段内不能有两种不同类型的已通过请假单。实现第二点时我是在用户选择日期后立即调用后端接口/api/leave/check返回剩余额度、重叠假期、工作日天数等数据直接渲染在表单下方。员工还没提交就知道能不能请体验上比提交后被打回好太多。3.3 审批列表的状态色彩与操作限制审批列表是主管每天打开频次最高的页面。这里的表格用Element Plus的el-table状态列用el-tag来做颜色区分待审批橙黄色。已通过绿色。已驳回红色。已销假灰色。操作列也不能一直显示所有按钮。要通过v-if判断当前行的状态和当前用户的角色决定显示“通过”“驳回”“销假”还是“详情”。不然的话前端就会出现对一个已驳回的单子依然显示“销假”按钮那体验就彻底崩了。3.4 时间数据和后端时区的约定这个坑我几乎每次做项目都会遇到必须单独拎出来讲前端和后端传输时间统一用字符串格式定为yyyy-MM-dd HH:mm:ss。为什么因为Date对象在JSON序列化时默认输出的是时间戳或者ISO字符串中间牵扯到UTC和本地时区的自动转换极易出现“前端显示的时间比实际时间早了8小时”这种诡异问题。解决方案是后端在application.yml里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8前端axios不做默认转换后端返回什么就显示什么。日期选择器组件提交时也先格式化为这个格式再提交。别看这点小约定我不知道见过多少项目在这上面查了一整个下午的时区Bug。4. 数据库设计假期余额和审批流是两套不同逻辑的数据这个项目涉及的数据表最少也要有六张用户表、部门表、请假单表、审批记录表、假期余额表、工作日历表。这六张表的设计思路各不相同下面我拆细点说。4.1 核心表结构及关联关系用户表sys_userid、工号、姓名、密码BCrypt加密、角色ID、部门ID、入职日期。部门表sys_deptid、部门名、主管ID。请假单表leave_form见前面。审批记录表approval_recordid、表单ID、审批人ID、审批动作PASS/REJECT、审批意见、审批时间。假期余额表leave_balanceid、用户ID、年度、假期类型、总天数、剩余天数。工作日历表work_calendar日期、是否工作日、备注如“国庆节”。多角色用户和权限我建议用简单的做法用户表里一个role字段标识角色。虽然RBAC的标准做法是用户-角色-菜单三张表但对于这个规模的项目单角色字段完全够用理解起来也直接。4.2 假期余额和请假扣减的并发问题假设员工和主管同时操作员工提交销假主管在同一个瞬间也把这个单子驳回。这种情况下余额是加还是不加这种边界情况不处理好年终结算时一定会出现莫名其妙的负数余额。解决思路是请假单的状态更新和假期余额更新必须放在同一个数据库事务中并且对余额操作要加行锁。Transactional(rollbackFor Exception.class) public void completeLeave(LeaveForm form, BigDecimal actualDays) { // 1. 加锁查询余额防止并发扣减 LeaveBalance balance leaveBalanceMapper .selectForUpdate(form.getUserId(), form.getLeaveType()); // 2. 重新计算剩余天数 BigDecimal newRemaining balance.getRemainingDays() .add(form.getDays()) .subtract(actualDays); // 3. 更新余额 balance.setRemainingDays(newRemaining); // 4. 更新请假单状态 // 5. 写审批记录 }select ... for update在MySQL Innodb下会把对应行锁住直到事务提交或回滚。这样并发场景下就不会出现重复扣减的问题了。这个点面试时拿出来说非常加分因为大多数人不会想到这里。4.3 数据库索引设计的优先级这种系统的数据量不会太大但索引还是要建到位。我的经验是至少建三个leave_form(user_id, status)查“我的请假列表”时走这个联合索引。leave_form(dept_id, status)主管查部门待审批列表时走这个索引。approval_record(form_id)按表单查审批历史。联合索引要注意一个顺序原则等值字段放前面范围字段放后面。user_id是等值查询status虽然也是等值但区分度低放在前面仍然能帮MySQL快速缩小扫描范围。如果建反了索引效率会大打折扣。5. 前后端联调与部署最容易翻车的几个地方代码写完真正的工程挑战是把它跑起来。我见过太多项目单体看着没问题一联调就各种翻车。下面总结几个最容易出问题的地方。5.1 跨域配置为什么前端请求老是403前后端分离项目前端跑在http://localhost:5173后端跑在http://localhost:8080这天然就是跨域。Vite开发服务器配置代理是最省事的做法// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/leave/listVite会自动转发到http://localhost:8080/api/leave/list浏览器端不直接发起跨域请求也就没有跨域问题。开发环境用代理生产环境用Nginx反向代理这个方案最稳。我自己第一次做这个项目时图省事直接在后端加了CrossOrigin注解结果那个方法跨域通了但登录接口因为走了Security过滤器链跨域配置没生效依然报跨域错误排查了好久才发现是过滤器链的优先级问题。所以老老实实走代理能省掉90%的跨域烦恼。5.2 Security放行路径和JWT过滤器的顺序这是Spring Boot 2.7之后集成Security遇到的典型问题。新版的写法是Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable() .cors().and() .authorizeHttpRequests() .antMatchers(/api/auth/login).permitAll() .antMatchers(/api/leave/**).authenticated() .anyRequest().permitAll() .and() .exceptionHandling() .authenticationEntryPoint(new JsonAuthenticationEntryPoint()) // 未登录返回JSON .and() .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }重点在于jwtAuthenticationFilter必须添加在UsernamePasswordAuthenticationFilter之前。这样请求先经过JWT解析把用户信息放进SecurityContext后续的权限判断才能生效。这里我还踩过一个坑把.csrf().disable()漏掉了结果所有POST请求都被Security拦截报403。因为Spring Security默认开启CSRF防护POST、PUT、DELETE都会被拒。前后端分离的项目用Token认证CSRF防护本来就是多余的直接关掉即可。5.3 前端打包后刷新404前端用Vue Router的history模式打包之后部署到Nginx如果配置不到位点击路由跳转没问题但刷新当前页会报404。原因很简单Nginx找不到/leave/create这个路径对应的物理文件。Nginx配置要加上try_fileslocation / { root /usr/share/nginx/html; index index.html index.htm; try_files $uri $uri/ /index.html; }这行配置的意思是请求的路径匹配不到实际文件时就回退到index.html让Vue Router自己接管路由解析。这是history模式的标配一定要记住。5.4 打包部署的整体流程我用Docker Compose做了一键部署把前端Nginx、后端Java、MySQL三个服务编排在一起version: 3 services: mysql: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD: root123456 MYSQL_DATABASE: oa_system ports: - 3306:3306 volumes: - ./sql:/docker-entrypoint-initdb.d backend: build: ./backend depends_on: - mysql ports: - 8080:8080 frontend: build: ./frontend depends_on: - backend ports: - 80:80前端容器构建时多阶段构建是必须的第一个阶段用node:16镜像执行npm run build第二个阶段用nginx:alpine镜像把构建产物复制到/usr/share/nginx/html。这样最终镜像体积能压缩到几十MB部署速度飞快。6. 从“能跑”到“好用”协同办公系统的几个加分细节一个系统功能都能跑和真正能交付给企业用之间还差着一大截。下面这几个细节是我在实际开发中踩过坑之后总结出来的经验做在这个项目里质感会完全不一样。6.1 审批不是“一个人点一下”就完事很多Demo系统的审批就是一道工序主管点一下通过就结束了。但真实企业主管不在、主管出差、主管自己也请假了这些都是常态。所以审批流至少要支持“转交”和“委托”转交当前审批人把单子转给其他有权限的人审批。委托主管出差前设置一个代理人出差期间所有待审批单子自动跑到代理人那里。这个逻辑实现在后端的审批服务里本质是在审批表里增加一个delegate_to字段。虽然做起来要多几张表、多几个接口但这是从“练习项目”迈向“生产可用”的关键一步。6.2 站内消息通知不能只靠主动刷新审批完成后员工怎么知道结果最low的方案是刷新列表看到状态变了。好一点的方案是加一个站内信模块审批通过或驳回时后端给申请人插入一条站内通知前端顶部导航栏放一个小铃铛打开能看到未读消息列表。实现上不需要引入消息队列直接建一张message表审批事件发生时事务里顺便插入一条记录即可。前端用el-badge组件显示未读数量每次新开页面时拉取一下未读消息数。6.3 假期余额的年度重置策略这是一个非常容易被忽视的问题。员工的年假额度通常是按年度发放的比如入职满一年每年10天年假1月1日一次性发放。如果你在数据库里用“当前剩余天数”字段存余额跨年时会有一个定时任务去重置数据。但更稳妥的做法是余额表里存total_days年度总额和used_days已用天数剩余天数实时计算为total_days - used_days。这样查询某年历史数据时依然能还原当时的余额快照比直接存剩余天数更灵活。6.4 管理员的报表视图最后一个建议给管理员加一个简单的统计页用图表展示每个部门的月度请假天数、各类假期的占比。Spring Boot后端只需要提供一个聚合查询接口SELECT dept_name, leave_type, SUM(days) AS total_days FROM leave_form WHERE status IN (2, 4) AND create_time BETWEEN #{start} AND #{end} GROUP BY dept_name, leave_type前端用ECharts画柱状图和饼图大概半天时间就能完工。但就是这么一个简单的报表会让整个系统的“管理”属性瞬间落地。人事不用再手动导Excel去统计了。6.5 导出Excel协同系统的隐形刚需还有一个我强烈建议做的功能请假记录导出Excel。人事月底对账、财务核对考勤都要求“给我导个表”。后端用EasyExcel定义一个实体类加几个导出注解几十行代码就搞定ExcelProperty(工号) private String userId; ExcelProperty(姓名) private String userName; ExcelProperty(请假类型) private String leaveType; ExcelProperty(开始时间) private Date startDate; ExcelProperty(结束时间) private Date endDate; ExcelProperty(请假时长) private BigDecimal days; ExcelProperty(状态) private String status;导出的接口加上权限控制只允许管理员和部门主管调用并且导出前要做数据量限制防止一次导出几百万条把内存撑爆。回到这个项目本身springbootvue的选型在我看来依然是最适合中小团队做内部系统的组合Spring Boot负责把业务逻辑和权限控制做扎实Vue负责把表单和状态流转做得顺手。请假销假只是这套架构的一个入口把它吃透后面再做报销、用章、用车申请其实就是复制一套状态机的事。如果你正打算拿这个题目做毕业设计或者学习项目我的建议是别追求炫技把审批状态机、假期余额计算、权限控制这三块做扎实就已经超过80%的同类项目了。把这些逻辑落到代码里面试时只要面试官问到“你遇到过什么复杂业务”你把这套流程抛出去非常能打。
返回列表