
这套系统我从需求梳理、技术选型到落地部署一共花了三周时间。起因是公司行政每个月底都要用Excel把打卡记录、请假单、绩效评分汇总到工资表里VLOOKUP串行、公式改坏、漏算迟到是家常便饭。后来我直接用 Python Vue3 做了一套企业员工考勤打卡薪酬绩效管理系统把员工、主管、HR、系统管理员四个角色的流程全部打通。这篇文章把这套系统的业务设计、数据库建模、前后端核心实现和踩过的真实坑完整写出来适合正在做毕业设计、企业内部系统或者想搞懂考勤薪酬业务逻辑的开发者复现和参考。1. 业务视角先想清楚四个角色到底在折腾什么很多人一上来就写代码这是做管理系统最忌讳的。考勤、薪酬、绩效这种系统业务规则比代码复杂得多角色边界一旦没划清楚后面每个页面都要返工。我先把四个角色在系统里的职责理了一遍再做数据库和接口设计。1.1 四个角色的权限边界这个系统我定义了四种用户身份普通员工、部门主管、HR管理员、系统管理员。别看角色少权限差异非常大我把功能权限和数据权限分开设计。功能权限是能不能点某个菜单数据权限是看得到哪些数据两者必须同时校验。功能模块普通员工部门主管HR管理员系统管理员上下班打卡本人打卡本人打卡全部考勤全部考勤考勤记录查询仅本人本部门成员全公司全公司补卡/请假申请发起审批本部门审核/归档配置规则绩效目标与评分自评部门成员评分绩效结果管理指标库配置工资条查看仅本人仅本人月度薪资核算薪资项配置用户与权限管理无无用户信息维护角色授权、菜单管理实际开发时我并没有在前端写死这些权限而是把菜单和按钮都注册成权限码。比如员工端登录后返回的菜单只有打卡、我的考勤、我的绩效、我的工资条主管端多出团队考勤、待办审批HR端则有考勤汇总、薪资核算、绩效管理。后端每个接口都做权限注解校验避免前端隐藏菜单后接口还能被直接调用。1.2 考勤、薪酬、绩效三件事的流转关系很多初学管理系统的人会把考勤、薪酬、绩效做成三个孤立模块这是不对的。这三个模块的数据是串起来的员工的打卡流水生成月度考勤汇总考勤汇总里的迟到次数、缺勤天数、加班时长直接进薪资计算绩效评分经过加权汇总得到绩效等级绩效等级映射成绩效系数再乘以绩效奖金基数影响最终实发工资。我把这条链路简化成一个公式应发工资 基本工资 岗位工资 绩效奖金基数 × 绩效系数 加班费 - 缺勤扣款 - 五险一金个人部分 - 个人所得税。考勤和绩效都成为薪酬计算器的输入源而不是各自独立的数据孤岛。这也是为什么系统里工资独立于打卡和绩效存在但每个月核算时又必须读取这两个模块的结果。1.3 先把Excel里的潜规则翻译成系统规则我花了一天时间和HR对需求发现Excel时代有很多“人肉规则”。比如迟到15分钟以内不扣钱只记录超过15分钟按半小时扣比如绩效评分自评占30%、主管评分占70%比如工资条要求保留两位小数但是必须四舍五入而非银行家舍入。如果这些潜规则不提前变成系统里的配置项开发到一半就会因为“这不对我们以前不是这么算的”反复推翻。所以我在系统里专门建了基础配置模块把考勤班次、迟到宽限分钟数、绩效系数映射表、薪资项配置全部做成数据库表由管理员在界面上维护。代码里只写通用计算逻辑具体规则全部读配置这是这套系统能落地的关键一步。2. 技术选型Python Vue3这个组合解了什么题技术栈看起来是“python vue3”但Python生态里Web框架那么多Vue3也有一堆配套库真正决定开发效率的是具体选型。这一章我把我最终选的方案和理由讲清楚顺便说说对比之后放弃的方案。2.1 后端为什么用FastAPI而不是Flask或Django后端框架我对比过三个Flask、Django、FastAPI。Flask灵活但要自己拼很多组件搭一个带数据库迁移、参数校验、API文档的项目需要额外装一堆扩展Django生态最全自带Admin后台和ORM但比较重对前端Vue3这种前后端完全分离的开发模式来说它的模板系统基本用不上还有点约束FastAPI是后起之秀基于Pydantic做参数校验能少写很多重复代码自带Swagger文档方便联调性能在纯Python框架里也很能打。最终我选了 FastAPI SQLAlchemy MySQL。FastAPI还有两个很实用的特性一个是依赖注入我用来做角色权限校验另一个是异步接口打卡这类高频写入接口在上班高峰期不会因为数据库连接阻塞拖垮服务。对于中小型企业的考勤并发量这个组合非常够用。2.2 前端为什么直接上Vue3 TypeScript Pinia Element PlusVue3的组合式APIComposition API相比Vue2的选项式API最大的优势是逻辑复用。考勤、审批、薪资核算这些页面都有“查询列表、加载状态、分页”的重复逻辑我用组合式函数封装了通用的useTable、useForm每个页面只用几行代码就能接上接口。因为这套系统角色多、权限逻辑复杂我选了TypeScript接口返回体、用户信息、表格数据全部定义类型前端字段写错在编译期就暴露了不用等到运行时一脸懵。状态管理用的Pinia而不是Vuex。Pinia的API更简洁没有Mutation那层概念store之间互相调用很自然。我用一个userStore存登录用户信息和角色一个permStore存动态路由和按钮权限。Element Plus负责后台管理系统的UI组件表格、表单、弹窗、日期选择器这些都能满足图表部分直接上ECharts工资趋势、部门迟到率这些可视化报表用它画非常简单。2.3 前后端联调约定前后端分离的项目最怕接口规范不统一。我一开始就定了一套标准响应结构所有接口统一返回 stateCode、message、data 三个字段成功时 stateCode 为200业务错误用400或自定义错误码。前端封装了一个axios实例拦截器里统一处理token过期、错误提示后端接口只需要返回数据或抛异常前端不用在每个页面写重复的错误判断逻辑。登录认证用的JWT前端登录后把token存在localStorageaxios请求头自动带Authorization: Bearer 。前端路由守卫每次跳转前检查token和角色无权限直接重定向到登录页或401页面。这套约定让前后端可以并行开发后端写接口的同时前端用Mock联调最后合在一起只处理少量字段差异。3. 数据库建模考勤流水、绩效表、薪酬表怎么设计才不打架数据库设计是这类系统的地基。我的经验是宁可多拆表不要把什么都塞进一张大宽表。考勤流水是高频插入数据薪资是每月结算数据绩效是按季度或月度产生的评估数据它们的数据特征完全不同拆开建表不仅逻辑清晰还能避免一张表字段爆炸导致后来加需求无从下手。3.1 用户、部门与角色的表结构用户这块我建了三张核心表sys_user 用户表、sys_dept 部门表、sys_role 角色表。用户表不直接存角色名字符串而是通过关系表把用户和角色关联起来因为一个用户可能有多个角色。用户表里必须包含的基本信息包括用户名、密码哈希、姓名、手机号、部门ID、岗位、入职日期、在职状态。密码存储必须用哈希我用的bcrypt绝对不允许明文存密码。部门表做了一层父子级结构用 parent_id 表示层级关系方便主管能看到子部门成员的数据。角色表里除了角色名还可以加一个 data_scope 字段比如“仅本人”“本部门”“全公司”HR的考勤汇总页就是靠这个字段控制数据范围的。3.2 考勤流水与月度汇总考勤模块我建了班次表、打卡流水表、月度汇总表。班次表存上下班时间规则比如上午 09:00-12:00、下午 13:30-18:00可配置弹性宽限分钟数。打卡流水表记录每一次打卡事件字段包括用户ID、打卡日期、打卡时间、打卡类型上班/下班以及打卡来源指纹机导入、手机定位、二维码还有GPS坐标方便后端做位置校验。这里有个重要设计打卡流水只负责“记”不负责“算”。每天每个人可能多条流水比如早上打了一次、中午补打了一次判断哪条是有效打卡的逻辑比较复杂。所以我单独建了一张考勤月度汇总表每天晚上通过定时任务自动计算每个人的出勤天数、迟到次数、早退次数、缺勤天数、请假天数、加班时长。这样HR核算薪资时直接查汇总表不需要实时重算全部流水。3.3 绩效指标、评分与等级映射绩效模块我拆成三张表绩效指标表存“客户满意度”“项目完成度”“团队协作”这类评分项每个指标有名称、分值上限、权重绩效评分表存具体的评分记录包含被评人、评分类别自评/主管评、各项得分、评分状态绩效结果表则存最终加权总分、绩效等级、绩效系数。为什么要把评分记录和最终结果分开因为评分过程是动态的主管可以多次修改自评未提交、主管未评分这些状态需要区分。而绩效结果一旦确认就要冻结供薪资计算使用不能因为改了某个评分项让历史工资跟着变。等级映射规则也是可配置的总分≥90定A级绩效系数1.280-89定B级系数1.070-79定C级系数0.8低于70定D级系数0.5。3.4 薪酬表薪资项配置与月度工资薪酬模块我建了薪资项配置表和月度工资表。薪资项配置表用来定义有哪些薪资组成比如基本工资5000、岗位工资3000、绩效奖金基数2000、全勤奖200以及扣款项养老保险、医疗保险、失业保险、公积金、个税。这些配置项通过一个 type 字段区分是加项还是减项加项在计算时相加减项在计算时扣除。月度工资表保存每个员工某个月的核算结果字段包括月份、用户ID、各薪资项金额、应发工资、应扣部分、实发工资、核算状态。核算状态我用了一个状态流草稿、待审核、已发布、已归档。新版工资算出来后先存草稿HR核对没问题审核审核后员工才能看到工资条归档就锁定数据不能修改。这个状态机帮我和HR省去了大量“手滑改错工资”的麻烦。4. 后端核心逻辑打卡判重、迟到计算、薪资核算怎么实现数据库设计好后真正见功夫的是业务接口里的核心算法。这一章全是能直接复用的Python逻辑包括打卡接口怎么写才能防止重复提交、迟到早退怎么和班次配置关联、薪资计算怎么保证金额精度。4.1 打卡API与防重复提交打卡接口是员工每天用最多的接口高频操作最需要防重。我设计的接口流程是拿到当前登录用户的ID、打卡时的GPS坐标、定位半径后端先查班次表得到今天的上下班时间范围然后判断当前时间落在哪个时段。如果当前时间在上班时段内打卡类型为上班如果在下班时段内打卡类型为下班。判重逻辑很简单但容易被忽略同一个用户、同一天、同一种打卡类型只能有一条记录。我用了一个数据库唯一约束 guarantee在打卡时间上直接加“同一个 user_id、clock_date、clock_type 不能重复”的联合唯一索引从数据库层面防止重复插入而不是只靠代码里的if判断。GPS校验则使用 geopy 计算打卡坐标和公司坐标的距离超过设置的半径比如200米就拒绝打卡提示“不在考勤范围内”。router.post(/attendance/clock) async def clock_in(payload: ClockPayload, user: User Depends(get_current_user)): today datetime.now().date() # 同一用户同一天同类型只能打一次卡数据库联合唯一索引兜底 exists db.query(AttendanceRecord).filter( AttendanceRecord.user_id user.id, AttendanceRecord.clock_date today, AttendanceRecord.clock_type payload.clock_type, ).first() if exists: raise HTTPException(status_code400, detail今日该时段已打卡不能重复提交) distance geopy.distance.distance((user.lat, user.lng), (payload.lat, payload.lng)).meters if distance configured_radius: raise HTTPException(status_code400, detail不在考勤范围内) record AttendanceRecord( user_iduser.id, clock_datetoday, clock_timedatetime.now().time(), clock_typepayload.clock_type, latpayload.lat, lngpayload.lng, sourceAPP, ) db.add(record) db.commit() return JSONResponse({stateCode: 200, message: 打卡成功})4.2 迟到、早退、缺勤的判定逻辑打卡流水记录好了考勤汇总逻辑才是HR真正关心的。我用了两个配置参数上班时间 09:00、宽限分钟数 15。打卡时间晚于9:00且不超过9:15判定为“正常但迟到一次”不扣款只是记录晚于9:15但不超过9:45判定为迟到按迟到时长扣工资超过9:45则直接判定为半天旷工。早退的逻辑刚好反过来下班时间早于18:00判定早退早退超过1小时算半天旷工。这一整套判定我写成一个纯函数输入是班次配置和打卡流水输出是考勤状态方便单元测试和复用。def calc_daily_attendance(on_time, off_time, config): # 判断上班状态 if on_time is None: work_status ABSENT # 无打卡记录默认缺勤 elif on_time config.late_grace_deadline: # 比如 09:15 work_status NORMAL elif on_time config.late_cutoff: # 比如 09:45 work_status LATE else: work_status HALF_ABSENT # 判断下班状态 if off_time is None: leave_status ABSENT elif off_time config.off_time: # 18:00 leave_status NORMAL elif off_time config.early_cutoff: # 17:00 以前走算早退严重 leave_status EARLY else: leave_status HALF_ABSENT return work_status, leave_status加班时长计算我用了这样的规则工作日下班后超过30分钟开始累计不足1小时按1小时计周末加班需要走审批有审批单才算加班。加班费倍率默认工作日1.5倍休息日2倍法定节假日3倍这些倍率也全部放配置表。4.3 薪资计算器的精度问题处理工资计算最容易被忽视的问题就是浮点数精度。Python里 0.1 0.2 的结果不是0.3而是0.30000000000000004如果工资表用float直接算最后实发工资会出现一分的误差。我所有金额字段在数据库里用 Decimal(10,2)后端代码里全程用 decimal.Decimal 运算绝对不碰float。四舍五入规则也要统一。Python内置的 round 做的是银行家舍入round(2.675, 2) 结果不是2.68而是2.67这在工资里会出大问题。我封装了一个统一换算函数使用 ROUND_HALF_UP 模式所有金额先算到小数点后4位再四舍五入到2位。from decimal import Decimal, ROUND_HALF_UP def money(value): return Decimal(value).quantize(Decimal(0.01), roundingROUND_HALF_UP) def calc_salary(base, post, perf_base, perf_coef, overtime_hours, late_count): total money(base) money(post) money(perf_base) * perf_coef total money(overtime_hours) * money(25) # 每小时加班费示例 total - money(late_count) * money(30) # 每次迟到扣30示例 return total薪资计算的输入数据来自三个地方员工基础档案里的基本工资和岗位工资、考勤汇总表里的迟到次数与加班时长、绩效结果表里的绩效系数。我写了一个核算任务先把全公司所有员工的薪资明细算成草稿HR界面上能看到每个人的计算明细确认无误后一键审核发布。算错了也不用慌未发布之前可以重新核算覆盖草稿。4.4 绩效评分加权汇总与系数联动绩效评分做了一个加权汇总每个员工每个指标有两个分自评分和主管评分最终指标分 自评分 × 0.3 主管评分 × 0.7。总分是所有指标分乘以权重的总和。比如“项目完成度”权重40%自评90主管评80那这个指标分值就是 90×0.380×0.783再乘以40%得到33.2分所有这样算出来的分数加起来就是总分。总分出来之后用等第映射函数转换成A/B/C/D等级和绩效系数。这个系数会传到薪酬接口作为绩效奖金基数的倍数。我在代码里明确返回等级和系数避免HR手工填数字。绩效结果的每次修改都保留审计记录谁改的、改了什么、什么时候改的全部存在记录表里防止绩效申诉时说不清。5. 前端Vue3落地四个角色怎么进入各自的页面前端是我花时间最多的部分不是因为Vue3难而是角色权限和页面交互细节多。我的核心思路是动态路由 组合式函数 组件化页面让四个角色登录后看到完全不同的操作台。5.1 动态路由、菜单权限与登录跳转前端权限的核心逻辑在路由守卫里。我定义了一份静态路由包含login、404、403这些公共页面业务页面全部走动态路由后端根据登录用户的角色返回对应的菜单和路由表。前端在Pinia里存路由表每次路由跳转前先判断当前用户的路由是否存在不存在就动态 addRoute。四个角色登录后的默认首页我做了差异化员工默认跳打卡页主管默认跳待办审批HR默认跳考勤汇总看板系统管理员默认跳用户管理。这样登录后不用点来点去找入口。菜单权限用v-permission这样的自定义指令控制按钮级别比如员工没有“导出工资表”按钮渲染时直接过滤掉不能只靠隐藏按钮来防越权后端接口权限依然要严格校验。router.beforeEach(async (to) { const userStore useUserStore(); if (!userStore.token) { if (to.path /login) return true; return { path: /login, query: { redirect: to.fullPath } }; } if (!userStore.routesLoaded) { const menuRoutes await userStore.fetchUserRoutes(); // 从后端拉角色路由 menuRoutes.forEach((route) router.addRoute(route)); return { ...to, replace: true }; } if (to.meta?.roles !to.meta.roles.includes(userStore.role)) { return /403; } return true; });5.2 员工端定位打卡页和工资条页打卡页我用了Vue3的响应式API页面加载时调用浏览器的定位接口拿到经纬度后实时显示当前位置和距公司的距离。如果定位失败用户拒绝授权页面只能手动输入定位验证码或者改用公司WiFi名匹配方案这个作为兜底。打卡按钮会根据今天是否已打卡自动置灰并显示打卡时间避免员工反复点击。工资条页是一个典型的“一人一张表”页面。我查接口拿到当前用户某个月份的应发项、扣款项、实发工资用卡片式布局展示。下面再用ECharts画一个最近6个月的工资趋势折线图应发和实发两条线员工能直观看到自己的工资变化。因为工资数据敏感前端拿到后我还要做脱敏显示默认部分字段用星号遮挡点击“查看”按钮才展示完整金额。5.3 主管端补卡、请假与绩效评分审批流主管端最核心的页面是待办审批。我做了两个Tab考勤审批补卡、请假和绩效评分。考勤审批列表用卡片显示申请人的姓名、申请类型、申请理由、申请时间主管点进详情能看到该员工当天考勤流水和所在部门的平均考勤时间作为审批参考。批准或驳回接口会更新申请单状态同时通知员工用的简短的站内信。绩效评分页我面对一个现实问题主管同时给多个下属评分单个页面来回切换效率低。所以评分页做了表格批量模式一屏显示该主管名下的所有下属、所有评分指标主管在表格里直接打分支持草稿保存全部打完再一键提交。提交后状态变更员工端马上能看到自评分和最终分。5.4 HR端考勤看板与月度薪酬核算页HR端的首页是考勤看板我用ECharts做三块可视化一个日历热力图显示全公司每天的打卡率一个柱状图统计各部门迟到率排名一个饼图展示出勤状态分布正常、迟到、早退、缺勤、请假。每张图都可下钻点击某个部门跳转到部门明细列表让HR能快速定位考勤异常集中点。薪酬核算页则用了类似Excel的表格界面左侧勾选要核算的部门点“生成草稿”后表格里列出每个员工的基本工资、绩效奖金、加班费、扣款、应发、实发。HR双击某个单元格可以查看这笔金额的计算明细比如绩效奖金后面有个小链接点开能看到绩效系数来源。确认无误后点“审核通过”系统给所有员工发工资条生成通知这个页面是最能体现系统替代Excel价值的。6. 联调、部署与踩坑记录一个项目做完不难难的是联调阶段遇到的问题排查。这一章记录我在这个项目里真实踩过并且解决了的坑每一个都有具体原因和修复办法复现概率很高。6.1 时区Bug打卡时间凭空多了8小时部署上线第一天HR就发现所有员工的打卡时间显示成了下午而不是上午。这个Bug的根因是前端浏览器用的是本地时区而服务器默认时区是UTC前端传时间戳给后端后端直接 new Date() 格式化得到的是UTC时间导致显示时间比真实时间少了8小时。修复方案是在后端启动时强制设置时区为Asia/Shanghai同时数据库连接串里也要加 serverTimezoneAsia/Shanghai。前端统一用时间戳传参展示时再按客户端时区格式化。日期字符串不要用“YYYY-MM-DD HH:mm:ss”这种格式跨端传解析歧义太大我直接全换成了时间戳。6.2 计算金额出现一堆小数点联调时发现工资明细表里出现一串6666.666666666666之类的数字。排查后确认是薪资计算时把 Decimal 和 float 混用了比如绩效奖金基数存的是Decimal但绩效系数是float两者相乘后精度被污染。我定了一个硬规则金额字段在数据库、后端、前端任何环节都统一字符串或Decimal不在计算中途用float。前端表格拿到后端返回的数字也用toFixed(2)显示防止出现浮点尾巴。6.3 前端跨域与生产环境部署开发环境通过Vite的 proxy 把 /api 代理到后端地址能友好解决跨域问题。但部署到生产环境时不能依赖前端的代理配置我用Nginx做了统一的反向代理前端静态文件放在 /dist后端 uvicorn 监听 127.0.0.1:8000Nginx把 /api 开头的请求转发到后端服务。这样浏览器访问的始终是同一个域名不会产生跨域。Nginx配置里我特别加了处理history路由的规则Vue3用history模式时前端路由刷新会出现404需要在location / 里加 try_files $uri $uri/ /index.html;。这个配置忘了加生产环境刷新页面就白屏排查了半天。6.4 权限缓存员工切换角色后跳回旧菜单后台测试时发现一个隐蔽问题用户退出登录后再换一个账号登录显示的还是上一个账号的菜单权限。原因是动态路由在Pinia和Vue Router里已经注册过了退出登录时只清空了token没有重置路由表和新账号的权限store。修复方式是封装一个 resetPermission 函数退出登录时遍历当前路由表把动态添加的路由逐条移除再重置Pinia里对应的store状态。不能只是页面跳转整个应用状态必须重新初始化。同样的逻辑也适用于账号被管理员修改角色之后必须重新登录才能生效因为旧的路由表带着旧的权限码。我还遇到过pandas方式导入Excel考勤数据时时间字段被识别成数字的情况排查下来是Excel里的时间列被设置成了自定义格式标准pandas read_excel解析出来是datetime格式但有些导出工具生成的是文本需要在导入时先做统一转换。我的建议是规范导入模板列的格式并写try-except兜住异常数据宁可让HR手动改一行也不要导入时静默丢弃。这套系统内部跑了三个月累积了上千条打卡记录每月工资核算从过去的大半天缩短到十几分钟。给还在犹豫选什么技术栈的朋友一个建议管理系统这类业务系统别追求新框架Python FastAPI Vue3这套组合从开发效率、类型安全、生态成熟度上都够用了。里面最难的不是写接口而是把考勤迟到怎么算、绩效系数怎么映射这类业务规则真正做成可配置的能力这一层想透了系统才算真正立得住。