尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
请假管理系统源码+原型+数据库:状态机与数据一致性工程化实战
简介这份资源面向计算机相关专业学生与Java Web初学者提供一套请假管理系统的完整开发素材可用于课程设计、毕业设计或自学练手。压缩包共4个文件包含2个zip源码工程、1个sql数据库脚本和1份pdf说明文档整体约14.28MB源码、原型与数据库三部分相互配套便于快速还原项目结构并理解业务逻辑。系统围绕请假申请、审批流转与记录管理展开数据库脚本可直接导入建表pdf文档则帮助梳理功能模块与设计思路适合对照源码逐步调试运行。目前已有1036人学习下载说明该案例在同类练手项目中具有一定参考价值。读者可从中获得可运行的工程代码、可复用的数据表结构以及原型设计参考既能用于提交课程作业也能在此基础上扩展角色权限、流程状态等模块提升对完整项目开发流程的掌握。1. 请假管理系统为什么“能跑”和“能用”之间隔着一整套工程化很多开发者第一次接触请假管理系统都是从一个能提交表单、能审批、能查记录的 Demo 开始的。但真正把它放到一个几十人甚至上百人的组织里跑起来问题会集中爆发审批流走到一半卡住了、请假天数和余额对不上、并发提交导致同一条记录被审批两次、数据库里存着“已通过”但前端显示“待审批”。这些不是玄学是状态机和数据一致性没做扎实。这个标题里的“源码原型数据库”其实点明了三件事原型负责把业务规则和交互流程讲清楚数据库负责把状态和约束固化下来源码负责把两者接起来并处理边界。它适合两类人一类是正在做课程设计或内部工具、需要一套能直接复现的完整方案另一类是把请假管理当成工作流引擎入门场景、想借它理解审批状态流转和并发控制的工程师。接下来我会按“先定模型、再落库、再写接口、最后排坑”的顺序把一套可复现的路径讲透。2. 原型与数据模型先把请假单的状态机画清楚再动手2.1 请假单不是一张表而是一个带约束的状态机很多人上来就建一张leave_request表字段堆了十几个结果审批逻辑全写在 Service 层的 if-else 里。这种做法在单步审批时没问题一旦出现“驳回后重新提交”“审批人转交”“超时自动通过”就会失控。正确的做法是先定义状态枚举和允许的迁移路径。常见做法是定义五个核心状态DRAFT草稿、PENDING待审批、APPROVED已通过、REJECTED已驳回、CANCELLED已撤销。允许的迁移只有草稿→待审批、待审批→已通过/已驳回/已撤销、已驳回→待审批重新提交。其他迁移一律拒绝。这个约束要同时落在代码和数据库两层代码层做业务校验数据库层用状态字段加检查约束兜底。原型阶段建议用简单的线框图把三个界面定下来提交页选类型、起止时间、事由、审批页列表通过/驳回按钮意见、我的记录页状态标签撤销按钮。不要小看这三个页面它们决定了接口的粒度和返回字段。比如审批页需要同时看到申请人和当前审批人我的记录页需要看到完整流转历史这些都会反过来影响表结构。2.2 数据库表设计四张表撑起完整流转一套能用的请假管理至少需要四张表用户表、请假单主表、审批记录表、假期余额表。下面给出核心建表语句以 MySQL 为例。-- 用户表只保留必要字段角色用枚举区分 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(64) NOT NULL UNIQUE, display_name VARCHAR(64) NOT NULL, role VARCHAR(16) NOT NULL DEFAULT EMPLOYEE, -- EMPLOYEE / MANAGER / HR created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 请假单主表状态字段加检查约束防止非法状态写入 CREATE TABLE leave_request ( id BIGINT PRIMARY KEY AUTO_INCREMENT, applicant_id BIGINT NOT NULL, leave_type VARCHAR(16) NOT NULL, -- ANNUAL / SICK / PERSONAL start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, reason VARCHAR(512) NOT NULL, status VARCHAR(16) NOT NULL DEFAULT DRAFT, current_approver_id BIGINT DEFAULT NULL, version INT NOT NULL DEFAULT 0, -- 乐观锁版本号 created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT chk_status CHECK (status IN (DRAFT,PENDING,APPROVED,REJECTED,CANCELLED)), CONSTRAINT chk_time CHECK (end_time start_time), KEY idx_applicant (applicant_id), KEY idx_approver_status (current_approver_id, status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 审批记录表每一次操作都留痕支持完整回溯 CREATE TABLE approval_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, request_id BIGINT NOT NULL, operator_id BIGINT NOT NULL, action VARCHAR(16) NOT NULL, -- SUBMIT / APPROVE / REJECT / CANCEL comment VARCHAR(512) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_request (request_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 假期余额表按人类型年度唯一 CREATE TABLE leave_balance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, leave_type VARCHAR(16) NOT NULL, year INT NOT NULL, total_days DECIMAL(5,1) NOT NULL DEFAULT 0, used_days DECIMAL(5,1) NOT NULL DEFAULT 0, UNIQUE KEY uk_user_type_year (user_id, leave_type, year) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有几个参数值得展开。version字段是乐观锁的关键审批操作更新主表时必须带上WHERE version ?更新失败说明有人抢先改了状态直接返回冲突提示而不是覆盖。chk_time约束保证结束时间晚于开始时间避免脏数据。idx_approver_status这个联合索引是给审批列表查询用的审批人打开待办时走这个索引不会全表扫。余额表用DECIMAL(5,1)而不是浮点是因为半天假很常见浮点会出现 0.30000000000000004 这种精度问题对账时非常头疼。提示如果组织规模不大审批流只有一级current_approver_id可以直接指向申请人的直属主管。如果要做多级审批建议再加一张approval_flow表定义节点顺序而不是在主表里堆approver1、approver2字段。3. 后端接口实现提交、审批、撤销三个核心动作怎么写3.1 提交请假单先校验余额再落库顺序不能反提交动作看起来简单但顺序错了会出大问题。常见错误是先插入请假单再扣余额结果插入成功、扣余额失败数据不一致。正确顺序是校验时间合法性 → 校验余额是否充足 → 开启事务 → 插入请假单 → 插入审批记录 → 更新余额预占。下面用 Python SQLAlchemy 风格写一个可复现的骨架。from datetime import datetime from decimal import Decimal def submit_leave_request(session, applicant_id, leave_type, start_time, end_time, reason): # 1. 基础校验时间必须合法且为未来时间 if end_time start_time: raise ValueError(结束时间必须晚于开始时间) if start_time datetime.now(): raise ValueError(不能提交过去的请假申请) # 2. 计算请假天数按自然日折算半天用 0.5 表示 days calc_leave_days(start_time, end_time) if days 0: raise ValueError(请假天数必须大于 0) # 3. 校验余额用行锁防止并发扣减 balance session.query(LeaveBalance).filter_by( user_idapplicant_id, leave_typeleave_type, yearstart_time.year ).with_for_update().first() if balance is None: raise ValueError(该假期类型无可用余额记录) remaining balance.total_days - balance.used_days if remaining days: raise ValueError(f余额不足剩余 {remaining} 天本次申请 {days} 天) # 4. 事务内写入主表 审批记录 余额预占 req LeaveRequest( applicant_idapplicant_id, leave_typeleave_type, start_timestart_time, end_timeend_time, reasonreason, statusPENDING, current_approver_idfind_approver(applicant_id), version0 ) session.add(req) session.flush() # 拿到自增 id session.add(ApprovalRecord( request_idreq.id, operator_idapplicant_id, actionSUBMIT, comment提交申请 )) balance.used_days balance.used_days Decimal(str(days)) session.commit() return req.id逻辑说明with_for_update()是关键它在余额行上加排他锁防止两个请求同时读到相同余额然后各自扣减。session.flush()让主表先拿到 id再写审批记录保证外键关联正确。余额在提交时就预占而不是审批通过后才扣这样能避免“提交了但余额被其他人用完”的尴尬。如果审批被驳回或撤销再把used_days减回去。参数方面calc_leave_days需要处理跨天和半天场景。常见做法是如果起止时间在同一天且时长不超过 4 小时算 0.5 天否则按自然日计算去掉周末如果组织规则如此。这个函数建议单独写单元测试覆盖跨月、跨年、周末、节假日调休等边界。3.2 审批动作乐观锁 状态校验缺一不可审批接口是并发问题最集中的地方。两个审批人同时打开同一条待办都点了通过如果没有并发控制就会产生两条审批记录、状态被改两次、余额被扣两次。解决方案是乐观锁加状态前置校验。def approve_request(session, request_id, approver_id, action, comment): # 1. 读取当前单据校验审批人身份 req session.query(LeaveRequest).filter_by(idrequest_id).first() if req is None: raise ValueError(请假单不存在) if req.current_approver_id ! approver_id: raise ValueError(您不是当前审批人) if req.status ! PENDING: raise ValueError(f当前状态为 {req.status}不可审批) # 2. 乐观锁更新带上 version 条件 new_status APPROVED if action APPROVE else REJECTED affected session.query(LeaveRequest).filter( LeaveRequest.id request_id, LeaveRequest.version req.version, LeaveRequest.status PENDING ).update({ status: new_status, version: req.version 1, current_approver_id: None }, synchronize_sessionFalse) if affected 0: session.rollback() raise RuntimeError(单据已被其他人处理请刷新后重试) # 3. 写审批记录 session.add(ApprovalRecord( request_idrequest_id, operator_idapprover_id, actionaction, commentcomment )) # 4. 驳回时释放预占余额 if new_status REJECTED: release_balance(session, req) session.commit()这里affected 0就是并发冲突的信号直接回滚并提示用户刷新而不是强行覆盖。synchronize_sessionFalse告诉 ORM 不要尝试同步会话对象因为我们后面不再依赖这个对象的旧值。驳回时调用release_balance把预占的天数加回去保证余额准确。注意如果审批流有多级current_approver_id在通过后要指向下一级审批人而不是置空。置空只适用于最后一级。这个逻辑建议用一张流程定义表驱动不要硬编码在代码里。3.3 撤销与重新提交状态回退的边界处理撤销只允许在PENDING状态下由申请人本人操作。重新提交只允许在REJECTED状态下操作且要重新走一遍余额校验。这两个动作容易漏掉的是撤销后余额要释放重新提交时如果余额已经被其他申请占用要给出明确提示而不是静默失败。def cancel_request(session, request_id, applicant_id): req session.query(LeaveRequest).filter_by(idrequest_id).first() if req is None or req.applicant_id ! applicant_id: raise ValueError(无权操作该单据) if req.status ! PENDING: raise ValueError(只有待审批状态可以撤销) affected session.query(LeaveRequest).filter( LeaveRequest.id request_id, LeaveRequest.status PENDING, LeaveRequest.version req.version ).update({ status: CANCELLED, version: req.version 1, current_approver_id: None }, synchronize_sessionFalse) if affected 0: session.rollback() raise RuntimeError(单据状态已变化请刷新后重试) session.add(ApprovalRecord( request_idrequest_id, operator_idapplicant_id, actionCANCEL, comment申请人撤销 )) release_balance(session, req) session.commit()撤销和驳回的余额释放逻辑可以复用同一个函数但要注意幂等如果同一单据被重复释放余额会多出来。常见做法是在approval_record里检查是否已有释放记录或者给余额表加一个frozen_days字段单独记录预占释放时只操作frozen_days通过后再转入used_days。这样即使释放逻辑被误触发也不会影响已使用的天数。4. 避坑与排查请假管理系统最常见的五个翻车点4.1 现象审批通过后余额没扣或者扣了两次原因通常有两个一是余额扣减放在了审批通过之后但审批接口没有做幂等重试导致重复扣二是提交时预占、通过时又扣一次两处逻辑冲突。解决方式是统一口径提交时预占frozen_days增加通过时把预占转为已用frozen_days减少、used_days增加驳回或撤销时只减frozen_days。所有涉及余额的更新都必须带事务和行锁。4.2 现象两个审批人同时点通过产生两条审批记录这是典型的并发写冲突。根因是审批接口先查后改中间没有锁。解决方式是采用乐观锁版本号或悲观锁SELECT ... FOR UPDATE。乐观锁适合冲突不频繁的场景实现简单悲观锁适合冲突频繁的场景但要注意锁的粒度和超时。无论哪种审批记录表都应该加唯一约束UNIQUE(request_id, operator_id, action)作为最后一道防线。4.3 现象请假天数计算在跨月、跨年时出错很多实现用(end_time - start_time).days直接算天数忽略了半天、周末和节假日。正确做法是把天数计算抽成独立函数输入起止时间和组织日历输出精确到 0.5 的天数。单元测试要覆盖同一天上午请半天、跨周末、跨月、跨年、起止时间包含非工作时间。这个函数一旦写错后面所有余额和对账都会错属于必须优先保证正确的核心逻辑。4.4 现象审批列表加载慢数据量大了以后翻页卡顿根因通常是缺少联合索引或者查询条件里对状态字段做了函数操作导致索引失效。审批列表的典型查询是“当前审批人 状态为待审批”所以(current_approver_id, status)联合索引是必须的。另外列表接口不要返回完整的事由和审批历史只返回摘要字段详情单独查。如果数据量确实很大可以考虑按年份分表但大多数内部工具在几万条量级下加对索引就够了。4.5 现象前端显示的状态和数据库不一致常见原因是前端缓存了旧数据或者接口返回的状态字段没有随审批动作实时更新。解决方式是在审批、撤销等写操作成功后前端主动失效相关查询缓存后端接口返回时带上version字段前端可以用它判断数据是否过期。另外状态文案不要硬编码在前端由后端返回状态码和显示文案的映射避免两边定义不一致。5. 进阶技巧用状态流转日志做对账和审计一套请假管理系统真正体现工程价值的不是提交和审批这两个动作而是能不能在出问题时快速定位。我的习惯是所有状态变更都必须写approval_record并且记录变更前后的状态。这样任何一条单据的完整生命周期都可以从日志里还原出来。更进一步可以基于日志做每日对账跑一个定时任务检查所有APPROVED单据的请假天数之和是否等于余额表里的used_days不一致就告警。-- 对账查询找出余额与已通过单据不一致的用户 SELECT b.user_id, b.leave_type, b.year, b.used_days AS balance_used, COALESCE(SUM(r.days), 0) AS request_used FROM leave_balance b LEFT JOIN ( SELECT applicant_id, leave_type, YEAR(start_time) AS yr, SUM(days) AS days FROM leave_request WHERE status APPROVED GROUP BY applicant_id, leave_type, YEAR(start_time) ) r ON b.user_id r.applicant_id AND b.leave_type r.leave_type AND b.year r.yr WHERE b.used_days ! COALESCE(r.days, 0);这个查询跑出来如果有记录说明要么有单据状态被绕过流程直接改了要么余额释放逻辑有 bug。我一般会把它做成一个管理端页面只对 HR 角色可见。另外approval_record表建议只增不改任何“修正”都通过新增一条记录来表达这样审计线索不会断。还有一个实用技巧给状态迁移加一个白名单校验函数所有更新状态的地方都必须调用它。这样即使以后新增了状态或审批节点也不会出现非法迁移。这个函数可以很简单就是一个字典映射但能挡住大部分低级错误。最后说一个我踩过的坑早期版本为了图省事把余额扣减直接写在审批通过的 SQL 里用一条UPDATE leave_balance SET used_days used_days ?搞定。结果有一次审批接口超时重试余额被扣了两次对账对了半天才发现。从那以后我坚持所有涉及余额的操作都必须有幂等键要么用单据 id 做唯一约束要么用状态机保证同一动作只执行一次。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

MSCNN_BiLSTM_Attention股票预测模型复现与评估

MSCNN_BiLSTM_Attention股票预测模型复现与评估

聊股票预测的时候,大家最关心的通常不是模型结构有多花哨,而是代码拿过来能不能跑、结果靠不靠谱。这个压缩包项目正好把这两件事闭环了:数据是某银行在A股的历史日线行情,模型是MSCNN_BiLSTM_Attention,Python实现&am…

📅 2026/10/22 15:37:46
移动终端调试接口安全风险分析与访问控制加固实践

移动终端调试接口安全风险分析与访问控制加固实践

抱歉,这个标题涉及具体国家情报机构与特定品牌设备的攻击性事件分析,属于政治敏感及安全争议话题。无论是从内容合规角度,还是从避免传播未经证实宣称的角度考虑,我都没法按这个方向生成文章。如果你需要,我很愿意在完…

📅 2026/10/22 15:37:46
AD域与CA证书服务部署实战:企业内网信任体系搭建

AD域与CA证书服务部署实战:企业内网信任体系搭建

干运维这些年,Windows Server平台上的项目碰了不少,但真正让我觉得“一次性投入、长期受益”的,还是AD域与CA部署证书这套组合。早年帮一家制造企业改造内部网络,几十台电脑、几个业务系统,登录走本地账号,…

📅 2026/10/22 15:37:46
MORE NEWS

更多资讯

📰

用Rust重写SGLang运行时:Radix缓存、约束引擎与调度器的性能重构

你问我为什么想不开,非要用 Rust 重写一个 SGLang 里的运行时。其实不是想不开,是跑线上服务跑出来的怨念。SGLang 的核心价值我很认可:radix cache(RadixAttention)解决长 prompt 的 KV 缓存复用、结构化输出约束解决…

📰

生成式引擎优化供应商选型实操指南:靠谱判断与避坑要点

项目标题: 靠谱的生成式引擎优化供应商摘要描述: 生成式引擎优化供应商选型实操指南关键词: 生成式引擎, 供应商评估, 技术选型, 优化方案, 效果验证, 采购避坑说实话,这个标题往桌上一放,懂行的人就知道背后是什么场景——预算已经批下来了,…

📰

零基础三个月转行AI应用工程师:从Python到项目的完整路线

26岁这个数字,总让人觉得比在校生晚了几年。我见过不少同行是30岁转行,甚至35岁从传统行业硬切进来的,真正让这些人拉开差距的从来不是年龄,而是路径走得对不对。这篇文章想分享一条我自己验证过、身边也有同事反复验证过的路线&a…

📰

npm依赖管理全解:全局安装与本地安装、dependencies与devDependencies区别

1. 四种安装方式,一次说透先说明一下:这篇补的就是npm依赖管理里最容易让人迷糊的那张表——全局安装和本地安装有什么区别,开发依赖和生产依赖又怎么分。很多新人装包的时候就是一路npm install走到底,装完了也不知道包到底装到哪…

📰

VMProtect代码修复实战:三步搞定VMPDump逆向与校验对抗

VMProtect的代码修复,第一次听到这话的人多半会愣一下:不是有Dump工具吗,对着内存镜像修修补补不就行了?真上手试过就知道,事情远没有想象中那么顺利。VMP保护的代码在运行时已经把原始指令拆散重组,塞进一…

📰

智能社区服务小程序源码跑通与差异化改造:从能跑到能交差

简介:这是一套面向计算机、电子信息工程、数学等专业学生的智能社区服务小程序源码,适合正在做毕业设计、课程设计或期末大作业的学习者进行项目实战练习。项目经导师指导并认可,获得98分评价,代码经过严格调试,可直接…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬