ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

微信版请假管理系统实战:从数据模型到审批流避坑

微信版请假管理系统实战:从数据模型到审批流避坑

简介:这是一套基于ASP.NET与C#开发的请假管理系统(微信版),采用VS2010+SQL Server 2008环境,适合需要快速搭建请假考勤平台的学习者或开发人员。系统覆盖请假条管理、违纪人员信息管理和人员信息管理三大模块,支持请假条录入、修改、查询,违纪信息维护,以及人员信息批量导入、增删改查等操作,可用于学校或企业日常考勤请假场景。资源包共265个文件,压缩包约12.44MB,以C#源代码、ASP.NET页面和Excel表格为主,同时包含前端样式与交互脚本、图片素材,以及SQL Server数据库文件和相关配置文件,便于直接部署与二次开发。已有5801人学习下载。通过完整源码与页面结构,可掌握请假条流转、违纪记录管理及人员信息维护的实现思路,理解分层代码组织与数据库设计,并能根据实际需求修改页面及数据库,快速适配不同组织。

1. 请假管理系统(微信版):把审批从聊天记录里捞出来

“请假管理系统(微信版)”这个标题,说白了就是做一套跑在微信生态里的请假审批工具:员工在手机微信里提交请假申请,主管在微信里点一下通过或驳回,人事在后台能拉出月度考勤统计,全程不需要额外安装 App。很多中小团队现在还在用“群里发一句、表格记一笔”的方式管请假,月底对账时全靠人工翻聊天记录,漏记错记是常态。这套系统要解决的就是这个具体痛点:请假申请、审批流转、余额核算、统计导出,全链路搬进微信。

它适合两类人:一类是公司或团队的行政/人事负责人,被纸质假条和微信零散消息折磨过;另一类是接外包或做内部工具的开发者,需要一个需求明确、交付路径短、能快速上线的小型业务系统。接下来我按自己实际做这类项目的顺序来拆:先定架构和数据模型,再讲微信接入与前后端实现,最后把踩过的坑和上线前验证清单列出来,照着做基本能跑通。

2. 整体设计:微信载体怎么选,审批流和数据模型怎么定

2.1 先想清楚:公众号、企业微信、小程序,到底用哪个承载

做微信版请假系统,第一个要决策的不是写代码,而是选“微信的哪个入口”。我一般会先按团队规模和现有工具来判断,而不是一上来就定技术栈。

如果团队本身已经在用企业微信,那答案几乎不用想:直接基于企业微信自建应用。企业微信自带通讯录、审批接口和消息推送,员工身份不用额外绑定,JS-SDK 可以直接拉起微工作台,开发量能省掉一大截。

如果团队没有企业微信,平时主要用个人微信沟通,那常见的做法是二选一:微信公众号(服务号)里的 H5,或者微信小程序。服务号 H5 的好处是开发门槛低、调试方便,有一个网页就能跑;坏处是入口藏得深,员工得先关注公众号再从菜单点进去。小程序体验更接近原生,但需要走审核、维护版本,开发周期会略长。

还有个容易被忽略的点:无论选哪条路,最终“审批通知”都得靠微信的消息模板或企业微信的应用消息推给审批人。个人微信生态对模板消息有严格的用户互动时限要求,而企业微信没有这个限制。所以我的建议是:团队能上企业微信就优先企业微信;只有纯粹面向个人微信用户的场景(比如外包给某个没有企业微信的小公司)才退回服务号 H5 方案。

下面的对比表能帮你快速做选型判断:

承载方式员工身份获取成本审批消息推送开发难度适用场景
企业微信自建应用低,通讯录直接同步应用消息,无时限限制中已有企业微信的中小团队
公众号服务号 H5中,需用户授权 openid模板消息,有 48 小时互动时限低无企业微信、员工用个人微信
微信小程序中,需登录授权订阅消息,需用户主动订阅中高需要更好交互体验但愿意走审核

2.2 核心数据模型:请假单、审批记录、请假余额三张表打底

无论前端用哪种微信载体,后端数据模型几乎是通用的。核心就三张表:请假单主表(leave_order)、审批记录表(approval_record)、请假余额表(leave_balance)。请假单主表存每次申请的业务数据,审批记录表存每一步谁在什么时间做了什么操作,请假余额表存每个员工各类假别的剩余天数。

请假单主表往下拆,关键字段其实就那么几个:申请人、假别(年假/事假/病假/调休)、开始时间、结束时间、时长(按半天或整天折算)、请假事由、当前状态、审批人。这里要特别注意“时长计算”不要用简单的日期相减,因为请假是按工作日算的,周末和法定节假日得单独处理。我在设计表时会把 duration_days 做成 decimal(5,1),允许 0.5 天这种粒度,和很多公司的考勤规则对齐。

审批记录表建议做成 append-only,也就是只插入不更新。每次审批动作都新插一条记录,而不是在请假单上改一个状态字段了事。这样审批链条天然留痕,往后出问题排查时能看到完整时间线,不会出现“谁批的、什么时候批的”说不清的情况。请假余额表则要跟请假单主表联动:提交申请时预占额度,驳回时释放额度,销假时也释放。

2.3 状态机:一个请假单从提交到归档要过哪些状态

请假审批流看着简单,但状态设计不谨慎,后面统计报表会乱。我常用的状态枚举是:草稿、待审批、审批中、已通过、已驳回、已撤销、已归档。其中“审批中”通常用于多级审批场景,比如先组长批、再人事批;如果只有一级审批,草稿提交后直接进“待审批”即可。

状态流转有两个容易出错的边界场景。第一个是“驳回后重新提交”:通常做法是驳回后生成一个新版本的请假单,而不是在原单上改完再让审批人重新看,否则审批人无法区分自己看的是第几版。第二个是“销假”:已经通过的请假单,如果员工提前回来上班,不能直接改结束时间,应该走撤销或销假流程,生成一条变更记录,释放被预占的余额。

整个状态机可以用一个简单表格统一维护:

当前状态触发动作下一状态说明
草稿员工提交申请待审批提交后不可自行修改
待审批审批人通过已通过通过时预占余额转实扣
待审批审批人驳回已驳回驳回释放预占余额
待审批员工撤销已撤销仅限审批人未操作前
已通过管理员销假已归档提前回归,释放剩余额度

这套状态机设计能在后续写后端接口时省很多心。所有状态变更都走同一个入口,用枚举值做白名单校验,非法跳转直接拒绝,避免出现“已驳回还能继续审批”这种匪夷所思的线上事故。

3. 把审批流在微信里跑通:接入、菜单、建表与后端实现

3.1 微信接入:AppID、Secret、回调 URL 的配置顺序

先讲接入。以服务号 H5 方案为例,第一步是去微信公众平台拿到 AppID 和 AppSecret。AppID 是公开的,会出现在前端请求里;AppSecret 必须保存在后端,一旦泄露别人就能拿着它换取用户凭证,后果严重。

第二步是配置授权回调域名。在公众平台“设置与开发 - 接口权限 - 网页授权”里,把 H5 页面所在的域名填进“网页授权域名”。这个域名必须是你备案过的域名,且不能带协议头和路径。常见配置错误是填成了https://oa.example.com/h5/这种带路径的形式,微信会直接提示配置失败,正确写法是oa.example.com。

第三步是配置菜单。服务号的底部菜单在“自定义菜单”里配置,把“请假申请”菜单项的跳转链接指向 H5 页面的 URL,链接要加上appid和redirect_uri参数,让用户点击后自动进入 OAuth 授权流程。一个最小可用的授权跳转 URL 长这样:

https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect

这里的REDIRECT_URI必须先经过urlencode编码再拼接。我见过有人直接把中文域名或带参 URL 拼上去,结果微信跳转时报 400 错误,排查半天才发现是编码问题。scope参数选snsapi_base静默授权就够了,因为请假系统里只需要拿到用户 openid 去关联员工工号,不需要获取昵称头像等资料,用snsapi_userinfo反而会多弹一次授权确认页,体验变差。

3.2 数据库 DDL:直接能用的三张表建表语句

把表结构落成 SQL。下面这三张表我按实际项目里的字段精简过,去掉了审计日志等外围内容,保留核心业务字段。

-- 请假单主表 CREATE TABLE leave_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '请假单号,格式 L+yyMMddHHmmss+随机3位', employee_id BIGINT NOT NULL COMMENT '申请人ID,关联员工表', leave_type TINYINT NOT NULL COMMENT '假别:1年假 2事假 3病假 4调休', start_time DATETIME NOT NULL COMMENT '请假开始时间', end_time DATETIME NOT NULL COMMENT '请假结束时间', duration_days DECIMAL(5,1) NOT NULL COMMENT '请假时长,支持0.5', reason VARCHAR(500) NULL COMMENT '请假事由', status TINYINT NOT NULL DEFAULT 0 COMMENT '状态:0草稿 1待审批 2审批中 3已通过 4已驳回 5已撤销 6已归档', current_approver BIGINT NULL COMMENT '当前待审批人ID', create_time DATETIME NOT NULL, update_time DATETIME NOT NULL, KEY idx_employee_status (employee_id, status), KEY idx_order_no (order_no) ) COMMENT '请假单主表'; -- 审批记录表 CREATE TABLE approval_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL COMMENT '关联请假单ID', approver_id BIGINT NOT NULL COMMENT '审批人ID', action TINYINT NOT NULL COMMENT '动作:1通过 2驳回 3撤销 4销假', comment VARCHAR(500) NULL COMMENT '审批意见', create_time DATETIME NOT NULL, KEY idx_order_id (order_id) ) COMMENT '审批记录表,只追加不修改'; -- 请假余额表 CREATE TABLE leave_balance ( id BIGINT AUTO_INCREMENT PRIMARY KEY, employee_id BIGINT NOT NULL COMMENT '员工ID', leave_type TINYINT NOT NULL COMMENT '假别,同上', total_days DECIMAL(5,1) NOT NULL COMMENT '年度总配额', used_days DECIMAL(5,1) NOT NULL DEFAULT 0 COMMENT '已用天数', remaining_days DECIMAL(5,1) NOT NULL COMMENT '剩余天数', year SMALLINT NOT NULL COMMENT '所属年度', UNIQUE KEY uk_emp_type_year (employee_id, leave_type, year) ) COMMENT '请假余额表';

建表时有几个点值得展开。order_no不用数据库自增ID做单号,而是L + 时间戳 + 随机数的格式,原因有二:一是避免请假单号被直接遍历猜出总数,二是后续对接其他系统时字符串单号更通用。duration_days用DECIMAL(5,1)而不是INT,是因为很多公司允许半天请假,用 INT 就得把半天拆成 0.5 的变体,反而麻烦。审批记录表不设status字段,每个动作天然通过action表达,查询时可按时间排序回溯整个审批链路。

3.3 后端核心接口:提交申请、审批通过、消息推送的代码骨架

后端我习惯用 Spring Boot 或类似框架实现,核心接口只有三个:提交申请、审批动作、消息推送。先看提交申请接口。这里最关键的是“预占余额”必须在事务里完成,否则并发提交时两个请假单可能同时扣同一段余额。

@Transactional public LeaveOrder submitLeave(LeaveSubmitRequest req) { // 1. 校验请假时间是否合法 validateTimeRange(req.getStartTime(), req.getEndTime()); // 2. 查余额并预占:用悲观锁锁住余额行,防止并发超扣 LeaveBalance balance = balanceMapper.selectForUpdate( req.getEmployeeId(), req.getLeaveType(), currentYear); if (balance.getRemainingDays() < req.getDurationDays()) { throw new BusinessException("剩余假期不足"); } // 3. 扣减余额(预占) balance.setRemainingDays(balance.getRemainingDays() - req.getDurationDays()); balanceMapper.updateById(balance); // 4. 创建请假单,状态置为待审批,指定审批人 LeaveOrder order = new LeaveOrder(); order.setOrderNo(generateOrderNo()); order.setEmployeeId(req.getEmployeeId()); order.setLeaveType(req.getLeaveType()); order.setStartTime(req.getStartTime()); order.setEndTime(req.getEndTime()); order.setDurationDays(req.getDurationDays()); order.setStatus(LeaveStatus.PENDING); order.setCurrentApprover(req.getApproverId()); orderMapper.insert(order); // 5. 异步推送微信通知给审批人 messageSender.sendApprovalNotification(order); return order; }

selectForUpdate是这笔事务里的关键点:MySQL 会对命中的余额行加行锁,直到事务提交才释放。这样两个员工同时操作同一个余额账户时,第二个请求必须等第一个事务结束才能读到最新值,从根上避免超扣。messageSender建议用@Async走异步线程池,不能让推送失败影响主流程,否则通知没发出去,审批人那边永远收不到提醒。

再来看审批通过接口。审批动作天然是写操作密集的,要同时改请假单状态、插入审批记录、释放或实扣余额。通过和驳回的核心区别在于:驳回要回补余额,通过则要把预占变成实扣,不用再动余额数值。

@Transactional public void approve(Long orderId, Long approverId, String comment) { LeaveOrder order = orderMapper.selectByIdForUpdate(orderId); if (order.getCurrentApprover().equals(approverId) == false) { throw new BusinessException("当前审批人不是您"); } if (order.getStatus() != LeaveStatus.PENDING) { throw new BusinessException("请假单状态已变更,请刷新后重试"); } // 插入审批记录 ApprovalRecord record = new ApprovalRecord(); record.setOrderId(orderId); record.setApproverId(approverId); record.setAction(Action.PASS); record.setComment(comment); approvalRecordMapper.insert(record); // 更新请假单状态 order.setStatus(LeaveStatus.APPROVED); orderMapper.updateById(order); // 推送微信模板消息给申请人 messageSender.sendApprovalResult(order, "通过"); }

注意这里加了乐观校验:当前审批人如果不是操作人,直接拒绝。这个校验既是权限控制,也是防止审批人 A 和 B 同时操作同一个单据时的竞态。comment参数在驳回时必填,通过时可空,前端需要据此决定是否渲染必填星号。

推送微信模板消息是我在这类项目里花时间最多的地方。先看一段消息推送的代码骨架:

// 基于 axios 的微信模板消息推送 async function sendTemplateMessage(openid, templateId, order, url) { const token = await getAccessToken(); // 内部带缓存与自动刷新 const payload = { touser: openid, template_id: templateId, page: url, data: { thing1: { value: order.orderNo }, time2: { value: formatDateTime(order.startTime) + ' 至 ' + formatDateTime(order.endTime) }, thing3: { value: order.reason.length > 20 ? order.reason.substring(0, 20) : order.reason } } }; const resp = await axios.post( `https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=${token}`, payload ); if (resp.data.errcode !== 0) { // 失败时记录日志并补偿重试,不要直接抛异常 logger.error('模板消息发送失败', resp.data); } }

模板消息的字段名thing1、time2不是随便起的,是你在微信公众平台申请模板时模板里预定义的关键词索引。申请模板时要做“类目匹配”,瞎选类目会被驳回。thing类型字段限制 20 个字符,超长会被截断或直接报错,所以代码里做了substring(0, 20)兜底。getAccessToken必须做缓存,微信对 access_token 的获取频率有限制(每日 2000 次),每次调用都重新获取会很快触发限流。

3.4 前端 H5:OAuth 授权、拉取身份、请假表单提交

前端部分用 Vue 或 React 写 H5 都行,核心流程是三步:打开页面时先走 OAuth 静默授权拿 code,拿 code 换 openid,再拿 openid 换员工身份。页面加载时自动完成,用户无感知。

// 页面加载时自动执行身份引导 async function initPage() { const code = new URLSearchParams(window.location.search).get('code'); if (!code) { // 没有 code,说明是第一次进入,跳转微信授权 const redirectUri = encodeURIComponent(window.location.href); const authUrl = `https://open.weixin.qq.com/connect/oauth2/authorize` + `?appid=${APP_ID}&redirect_uri=${redirectUri}` + `&response_type=code&scope=snsapi_base&state=leave#wechat_redirect`; window.location.href = authUrl; return; } // 有 code,调后端接口换 openid 和员工身份 const identity = await fetch('/api/wechat/identity?code=' + code).then(res => res.json()); if (identity.employeeId) { renderLeaveForm(identity); } else { showBindPage(); // 提示员工先绑定工号 } }

这里有个开发环境要注意的问题:window.location.href在整个授权流程中会被微信跳转两次,所以后端日志里看到的回调 URL 可能和前端配置的 redirect_uri 不完全一致。调试时直接用微信开发者工具看跳转链路的完整 URL,比看后端日志更直观。授权完成后用户身份就是openid + employeeId的绑定关系,后续所有请假接口用employeeId做业务关联,openid 只作为微信通信凭证,两者不要混用。

4. 避坑指南:微信版请假系统的五个高频翻车点

4.1 access_token 频繁过期导致消息推送大面积失败

现象:上线初期发现模板消息时不时推送失败,报errcode 40001或42001,有时候重启后端服务后短暂恢复,过一阵又挂。

原因:多个后端实例各自调用getAccessToken,每次调用都刷新 token,导致后获取的 token 把之前有效的 token 挤下线。微信的 access_token 机制是“刷新后旧 token 立即失效”,多实例同时刷新必然互相踩踏。

解决:把 access_token 的获取和刷新收敛到一个统一的服务里,用数据库或 Redis 做全局缓存,加分布式锁防并发刷新。我在项目里就是起了个定时任务,每 100 分钟主动刷新一次并写入 Redis,所有业务实例只从 Redis 读取,彻底解决。

4.2 回调域名配置正确却提示 redirect_uri 参数错误

现象:公众号后台回调域名明明填了oa.example.com,用户点菜单却报“redirect_uri 参数错误”。

原因:最常见的是拼接 URL 时用了未编码的redirect_uri。另一种是前端代码里window.location.href取到的是完整含参路径,把这个路径直接拼进来后,微信端做比对时发现和公众号后台配置的域名不一致。

解决:统一先encodeURIComponent编码 redirect_uri,再拼授权链接。另外建议在公众号后台把回调域名配置成根域名,而不是某个具体页面路径,这样授权后跳转的子页面更灵活。

4.3 审批通知发给了离职或禁用状态的审批人

现象:A 员工提交请假单后,审批人一直收不到微信通知,后台看记录却是“已发送”。

原因:审批人已经离职工号被停用,或取消关注了服务号,导致微信端无法触达。代码里只判断了“推送接口调用成功”,没判断用户是否可接收。

解决:在推送前查一下审批人的微信绑定状态和在职状态,离职或未绑定微信的用户直接跳过微信推送,降级为站内待办提醒。同时把“已发送但用户未读”的情况记录到日志,行政人员可以在后台补发邮件或口头提醒。

4.4 时区与跨天问题:凌晨提交的假条算错了日期

现象:员工晚上 23:50 提交次日上午的请假,系统显示“时长 0 天”,审批人看着不对。

原因:请假时长按“工作日天数”算,代码用了(endTime - startTime) / 86400000直接相减,没排除周末,也没按工作日历折算。而 23:50 提交次日 09:00 开始请假,endTime和startTime相差不到 1 天,被整数除法归零。

解决:写一个专门的工作日计算工具,传入开始和结束时间,依次排除周六日和国家法定节假日,再按公司规则折算半天。排班制团队还需支持自定义休息日,不能只依赖国家假日表。

4.5 多级审批流程里“审批中”状态永远没人收尾

现象:设置了“组长审批 → 人事审批”两级流程,组长通过后,单据停在“审批中”不再流转,人事那边看不到待办。

原因:后端只实现了“通过”动作,没有在通过后判断下一级审批人是谁并把current_approver更新过去。这个逻辑在代码评审时容易被漏掉,因为单级审批时根本走不到这分支。

解决:在审批通过接口里加一个nextApprover查询,查流程配置表得到下一节点审批人。如果存在,则更新current_approver并保持“审批中”;如果不存在,才把状态置为“已通过”。这个逻辑务必写在事务里,避免“状态改了但审批人没更新”的中间态。

5. 上线前验证与后续优化:从能跑到好用

5.1 上线前自测清单:照着点一遍再放量

微信生态的坑大多在上线后才会集中暴露,因为生产环境的网络、用户行为和开发环境差别太大。我每次上线前都会拿一张清单过一遍,大概这么几项:第一,用一个从未绑定过的微信号走完整流程,确认能正确跳到绑定页;第二,用已绑定员工提交请假,确认余额预占正确;第三,审批人通过后,确认申请人微信能收到“已通过”模板消息;第四,驳回后确认余额回补到剩余天数;第五,查一下服务端日志,确认 access_token 只在一个位置刷新。

5.2 日志审计:审批链路必须能追溯

请假审批可能涉及工资核算,日志不能只记info级别的一句话。我在项目里会为每个请假单维护一份完整的事件时间线,包括提交、预占余额变更、审批动作、余额回补/扣减、消息推送结果,全部落库。查问题时直接按order_no拉时间线,一行不缺。

5.3 往后的优化方向:从“能请假”到“能核算”

系统跑顺之后,可以往两个方向加深。第一个方向是跟考勤打通:请假审批通过后自动同步到考勤系统,月底核算出勤时不用人工比对。第二个方向是报表可视化:按部门、假别、时间维度假期消耗趋势,给人事决策用。这些扩展在数据模型稳定的前提下都是增量开发,不用推翻重来。

最后说个我自己的教训:第一次做这类系统时,我花了大量精力在后端权限控制上,结果上线后最先出问题的反而是微信回调配置和消息推送。给后来者的建议是,先把微信侧的通路打通,再回头优化业务代码。毕竟用户感知最直接的是“提交后审批人能不能马上看到”,系统响应速度反而没那么敏感。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进