ARTICLE DETAIL

资讯详情

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

Flask全栈开发考勤工资管理系统:从数据库设计到Nginx部署实战

Flask全栈开发考勤工资管理系统:从数据库设计到Nginx部署实战 我去年接了一个中小企业的管理需求对方公司叫“一鸣企业”核心诉求就一句话把人事档案、考勤打卡、工资结算这三摊子事从Excel里解放出来。当时沟通完需求我心里大概就有数了——这就是一个典型的Flask全栈项目既不用上微服务也用不着前后端分离关键是怎么把考勤数据和工资计算在数据层面打通。这篇文章我就把这个项目从需求拆解到部署上线的完整过程写出来包括表结构怎么设计、工资计算流水线怎么落地、部署时踩过哪些坑。如果你正准备用Python做第一个完整的管理系统或者正在为毕业设计、课程设计找系统实现思路这篇内容应该能帮你省不少弯路。1. 需求拆解先搞清楚考勤和工资是怎么联动的很多人在做这类管理系统时第一步就栽了跟头——上来就画表、写代码结果做到工资模块时发现考勤数据对不上、请假扣款算不清楚回头重构表结构。我自己的经验是先别碰代码把业务流程掰开揉碎弄清楚考勤和工资之间的计算关系再谈数据库设计。1.1 一鸣企业的原始痛点先说背景。一鸣企业是一家员工规模在一百人上下的贸易型公司有行政、销售、仓储、财务几个部门。他们原来的流程是员工每天在钉钉群里打卡截图行政月底下载钉钉考勤报表到Excel再人工核对请假单、加班申请最后把结果发给出纳算工资。整个过程有三个明显的痛点考勤口径不统一。有人用钉钉打卡有人漏卡走线下补卡流程还有业务员长期外勤不知道按什么规则算考勤。工资计算规则散落在Excel公式里。迟到扣多少、加班费按什么基数算、请假半天怎么扣规则分散在几个人的脑子中换个人就说不清楚。数据没有形成闭环。员工的入离职状态、部门调动、基本工资调整这些人事变动没有及时同步到考勤和工资环节经常出现“人已经离职了还在发工资”的荒谬情况。所以这个系统的核心不是“把Excel搬到网页上”而是建立一套从人事数据到考勤数据再到工资数据的自动流转机制。1.2 功能边界哪些必须做哪些先砍掉我梳理需求时给客户提了一个原则第一版只做能跑通闭环的核心功能其他的一律往后放。最终确定的功能清单如下模块必须实现的功能第一版不做的事员工管理员工档案增删改查、按部门筛选、批量导入导出不接电子签、不做附件管理部门管理部门层级维护、员工部门调动不做复杂组织架构树考勤管理每日打卡、补卡申请、请假登记、加班登记、月度考勤统计不接硬件打卡机、不做GPS定位工资管理月度工资自动计算、工资条查看、发薪状态管理不做工资银行代发接口、不做个税专项附加扣除边界砍掉之后工作量一下子清晰了很多。特别是“不接硬件打卡机”这个决定让整个考勤模块变成了纯软件的逻辑处理后续如果客户想接钉钉考勤只需要在打卡入口做一个数据导入接口就行不影响核心计算逻辑。1.3 角色权限与业务流程梳理系统最终划分了三个角色系统管理员负责部门管理、员工档案管理、用户账号分配拥有最高权限。人事专员负责考勤审核、请假审批、工资核算与确认。普通员工仅能查看自己的考勤记录和工资条。业务流程上最关键的链路是员工打卡 → 月考勤汇总 → 工资计算 → 工资条确认。其中“考勤汇总”是承上启下的核心环节我在后面的表结构设计中单独为它做了冗余设计这里先不展开。顺便提一句这个角色模型也是跟客户反复确认过的——一鸣企业不需要多级审批流人事专员就是唯一审批人权限模型越简单后续开发越不容易出幺蛾子。2. 技术选型为什么是Flask而不是Django技术选型是这类项目里被问得最多的问题。我的答案很直接这个体量的项目Flask的灵活性和上手速度是最合适的。当然这不是说Django不好而是要看场景。2.1 Flask的轻量优势在哪里一鸣系统最终大概有7张核心表、15个左右的功能页面。这种规模用Django的话反而要面对一堆用不上的“自带电池”——Admin后台、ORM中间件、模板系统绑定的约定定制起来反而费劲。Flask的核心特性是路由、模板、请求响应这些基础能力都有了剩下的按需扩展。用Flask做主力的第二个理由是模块化扩展非常成熟。项目里我用到了Flask-SQLAlchemyORM层负责数据库操作Flask-Login登录会话管理Flask-WTF表单处理和CSRF防护Flask-Migrate数据库表结构迁移这些扩展都是Flask官方和社区维护的出问题时很容易找到解决方案适合中小型管理系统。第三点是部署成本。Flask应用打包成WSGI服务配Gunicorn加Nginx整个上线流程二十分钟能搞定。对比Spring Boot那种动不动就几十兆起步的方案在内存只有2G的轻量服务器上Flask跑得轻轻松松。2.2 数据库选型MySQL为主开发期可用SQLite数据库我选了MySQL 8.0原因很简单客户自己有运维能力MySQL他们熟。但开发阶段我强烈建议先用SQLite等本地功能全部跑通再切换MySQL。原因很实际SQLite是单文件数据库不用安装、不用配置账号密码拷贝一个文件就能跑起来极大降低了本地开发门槛。Flask-SQLAlchemy的代码在SQLite和MySQL之间几乎零差异切换时只需要改连接字符串。SQLite足够支撑单机开发环境下几十个用户并发测试。生产环境切MySQL时唯一要注意的是建库时指定字符集这个坑我后面专门说。2.3 前端方案Jinja2 Bootstrap就够了吗很多人会问为什么不用Vue做前后端分离我的答案很朴素——这个项目最大的复杂度在业务逻辑不在交互体验。用Vue意味着要额外维护一套API文档、处理跨域、搭构建工具链投入产出比很低。一鸣系统最终用了Jinja2模板引擎服务端渲染页面模板继承解决公共布局Bootstrap 4做响应式布局和基础组件jQuery 少量原生JS处理表单提交和局部刷新这里有个经验Jinja2模板的组织方式很重要我按模块来分模板目录templates/下再细分为staff/、attendance/、payroll/等子目录每个模块的列表页、编辑页、详情页放到对应用户角色的模板目录里这样后期维护时找文件非常快。3. 数据库设计让考勤和工资在数据层面“对话”这是整个项目中最不能急的部分。我的设计原则很简单每个表都围绕一条业务事实来设计表与表之间通过外键关联但关键业务数据要做字段冗余避免大量JOIN查询拖慢页面。3.1 核心表结构与字段说明最终设计出7张核心表分别是department部门表、employee员工表、user用户表、attendance考勤记录表、leave请假记录表、overtime加班记录表、payroll工资表。以员工表为例字段设计如下id: 自增主键 emp_no: 员工编号业务上唯一 name: 姓名 gender: 性别 department_id: 部门外键 position: 岗位 base_salary: 基本工资用于工资计算 hire_date: 入职日期 status: 状态在职/离职 user_id: 关联账号ID一个容易忽略的细节是离职员工的处理。员工表直接删除会破坏工资表的历史关联所以员工离职后只更新status字段不做物理删除。这样工资表和历史考勤数据还能继续查询到。3.2 工资表为什么要做“月度快照”这是我和一位做过财务系统的朋友聊出来的经验也是这个设计里最重要的一个决定。假设员工张三3月份基本工资是80004月份调薪到10000。如果4月发工资时直接更新员工表再去计算工资那4月的工资单里基础工资就会变成10000但这没问题——真正有问题的场景是你5月份想去核对3月份的工资时员工表里的基本工资已经被覆盖了。所以工资表的正确做法是在每月计算工资的那一刻把当时用到的基础工资、绩效、补贴、扣款明细全部以“快照”形式存到工资表里。class Payroll(db.Model): id db.Column(db.Integer, primary_keyTrue) emp_id db.Column(db.Integer, db.ForeignKey(employee.id)) pay_month db.Column(db.String(7), nullableFalse) # 格式: 2025-06 base_salary db.Column(db.Numeric(10, 2)) attendance_deduct db.Column(db.Numeric(10, 2)) overtime_pay db.Column(db.Numeric(10, 2)) bonus db.Column(db.Numeric(10, 2)) social_security db.Column(db.Numeric(10, 2)) personal_tax db.Column(db.Numeric(10, 2)) actual_salary db.Column(db.Numeric(10, 2)) status db.Column(db.String(20), defaultpending) # pending/confirmed这个设计在技术圈有个术语叫“保留历史事实”说白了就是工资计算出来之后一切输入参数都不可变只允许追加修正记录。这样做的好处是员工对工资有疑问时你可以逐项解释每一项数字是从哪个表、哪条记录算出来的经得起对账。3.3 考勤状态与请假、加班的关联设计考勤记录表的status字段我用了枚举字符串取值包括normal正常、late迟到、early早退、absent缺勤。每天每个员工有两条打卡时间记录早上上班时间存check_in_time晚上下班时间存check_out_time系统根据这个计算工作时长。请假表和加班表都和考勤表通过emp_id和work_date关联。这里有个很细节的问题同一天员工既请了假又有加班怎么处理我的处理是请假表和加班表里的记录状态字段增加approve_statuspending/approved/rejected工资计算时只统计approved状态的记录pending状态的不计入。这样可以避免一稿工资算完又被审批单影响的情况。4. 核心模块实现从登录到工资结算的代码级拆解4.1 基于Flask-Login的登录与角色权限控制登录认证是这类系统的安全基石我用Flask-Login实现会话管理密码哈希用Werkzeug自带的generate_password_hash不用存明文密码。from werkzeug.security import generate_password_hash, check_password_hash class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(64), uniqueTrue, nullableFalse) password_hash db.Column(db.String(128), nullableFalse) role db.Column(db.String(20), defaultemployee) emp_id db.Column(db.Integer, db.ForeignKey(employee.id)) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password)权限控制我写了一个role_required装饰器判断当前用户的角色from functools import wraps from flask_login import current_user def role_required(*roles): def decorator(func): wraps(func) def wrapper(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for(auth.login)) if current_user.role not in roles: abort(403) return func(*args, **kwargs) return wrapper return decorator用法示例app.route(/payroll/generate, methods[POST]) login_required role_required(admin, hr) def generate_payroll(): # 只有管理员和人事专员可以生成工资表 ...之所以要分角色是因为员工不能访问工资生成的页面而人事专员也不能改部门结构。用装饰器统一控制比在每个视图函数里手写if判断要清晰得多。4.2 考勤打卡接口迟到早退的判定逻辑打卡功能设计了两个入口员工在页面手动打卡以及人事代录。打卡的核心逻辑是记录时间并自动计算状态app.route(/attendance/check_in, methods[POST]) login_required def check_in(): emp_id current_user.emp_id today datetime.now().strftime(%Y-%m-%d) # 判断是否已经打过卡 today_att Attendance.query.filter_by( emp_idemp_id, work_datetoday ).first() if today_att and today_att.check_in_time: flash(今日已经打过卡了) return redirect(url_for(attendance.index)) now datetime.now() # 上班时间为9:00预留5分钟宽限 standard_in datetime(now.year, now.month, now.day, 9, 0, 0) status normal if now standard_in timedelta(minutes5): # 迟到超过5分钟 status late ...这里关键的设计点在于打卡时间点记录的是数据库当前时间而不是浏览器传过来的时间。也就是说HTML页面只负责触发请求服务器才是时间的权威来源。这样做避免用户修改浏览器时间进行作弊。另外补卡功能我做了一层审批逻辑——员工提交补卡申请人事审批后才更新考勤记录并保留一条补卡日志。4.3 工资计算从考勤汇总到最终实发工资的流水线工资计算是整个系统最核心的模块我实现了一个calculate_payroll函数处理逻辑严格按照业务规则走def calculate_monthly_salary(emp, month, attendance_stats): # 1. 基础工资取自员工表当前基础工资 base_salary emp.base_salary # 2. 日工资按21.75天计算法定月计薪天数 daily_salary base_salary / 21.75 hour_salary daily_salary / 8 # 3. 考勤扣款 attendance_deduct 0 late_count attendance_stats.get(late_count, 0) early_count attendance_stats.get(early_count, 0) absent_days attendance_stats.get(absent_days, 0) attendance_deduct late_count * 50 # 迟到一次扣50 attendance_deduct early_count * 50 # 早退一次扣50 attendance_deduct absent_days * daily_salary # 缺勤扣日工资 # 4. 加班费工作日加班1.5倍休息日加班2倍 overtime_pay 0 for ot in emp.overtimes.filter_by(monthmonth, statusapproved): if ot.ot_type weekday: overtime_pay ot.hours * hour_salary * 1.5 elif ot.ot_type weekend: overtime_pay ot.hours * hour_salary * 2.0 # 5. 社保按基本工资为基数按比例扣除简化规则 social_security base_salary * 0.105 # 养老8%医疗2%失业0.5% # 6. 应发工资 gross_salary base_salary overtime_pay bonus - attendance_deduct # 7. 个税简化计算不处理专项附加 personal_tax calculate_tax(gross_salary - social_security) # 8. 实发工资 actual_salary gross_salary - social_security - personal_tax ...这里有几个业务细节需要单独说明月计薪天数为什么要用21.75这是劳动法规定的月平均计薪天数。如果简单按30天或当月实际天数计算会出现加班费计算结果忽高忽低的问题客户虽然没要求但这个口径必须规范。考勤扣款和加班费不能互相抵消很多人会想“员工这个月迟到五次但加班了十小时能不能抵扣”业务上一定要分开计算否则财务做账时说不清。个税用简化算法实际企业个税会考虑累计预扣法、专项附加扣除第一版系统我没有把这些做进去而是留了一个calculate_tax函数占位后续可以扩展。4.4 月度考勤统计与工资确认流程每月月底人事专员点击“生成工资表”按钮系统会遍历所有在职员工汇总当月考勤数据、请假数据、加班数据调用工资计算函数生成当月的payroll记录。这个流程我设计成了两步先生成草稿statuspending人事逐条确认后批量标记为confirmed。工资条页面做了权限隔离app.route(/payroll/my) login_required def my_payroll(): if current_user.role employee: payrolls Payroll.query.filter_by(emp_idcurrent_user.emp_id)... else: payrolls Payroll.query.filter_by(pay_monthmonth)...员工登录后只能看到自己的工资条管理员和人事可以看到全公司的。同时工资条页面也加了打印样式员工可以打印或另存为PDF解决客户“要留底给员工签字”的需求。5. 部署与排坑本地能跑通不等于能上线写代码只是第一步真正让客户验收的是“系统能不能稳定跑起来”。这个项目的部署也踩了几个坑每个都值得单独说一说。5.1 开发环境的一键配置首先建议统一Python版本。这个项目我用的Python 3.10Flask版本锁定在2.3.x。为什么强调版本锁定因为Flask 3.x在一些API上有变化如果直接pip install flask装到最新版很久以前的代码很可能跑不起来。项目依赖统一放在requirements.txt里flask2.3.3 flask-sqlalchemy3.1.1 flask-login0.6.3 flask-migrate4.0.7 flask-wtf1.2.1 pymysql1.1.0 gunicorn21.2.0创建虚拟环境并安装python -m venv venv source venv/bin/activate # Windows下: venv\Scripts\activate pip install -r requirements.txt一个很实用的建议开发阶段就开debug模式跑起来用Flask自带的服务器做功能测试但生产环境一定要换Gunicorn。开发服务器是单进程的并发高一点页面就卡死而且它会在控制台打印大量调试信息直接暴露在公网上有安全隐患。5.2 生产部署Gunicorn Nginx 的组合方案生产环境我用的Gunicorn启动Flask应用gunicorn -w 4 -b 127.0.0.1:8000 wsgi:app-w 4是4个工作进程wsgi:app意思是导入wsgi.py文件中的app实例。之所以绑定到127.0.0.1而不是0.0.0.0是因为前面还挡着一层Nginx——由Nginx接收外部请求再反向代理到Gunicorn端口。Nginx配置核心片段server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /path/to/your/project/static/; expires 7d; } }location /static/这段很重要它是把静态文件交给Nginx直接处理而不是让Gunicorn去读取、返回静态资源。前后端分离的项目没有这个概念但服务端渲染的项目必须配好否则图片和CSS加载会非常慢。5.3 上线前遇到的三个经典坑坑一数据库连接超时断开。系统上线运行一段时间后页面突然报“Lost connection to MySQL server during query”一开始很懵后来查到原因是MySQL默认的wait_timeout是8小时如果连续8小时没有新的查询服务器会把已经空闲的连接自动断开而SQLAlchemy连接池里的连接并不知道这个变化还在复用旧的连接。解决方法是配置连接池回收app.config[SQLALCHEMY_ENGINE_OPTIONS] { pool_recycle: 3600, # 连接池中的连接1小时回收一次 pool_pre_ping: True, # 每次使用前先ping一下无效则重建 }这个坑最常见的表现形式是“每天早上第一次访问特别容易出错”排查思路很清晰。坑二MySQL建库字符集没指定中文变问号。生产环境建库时直接用了CREATE DATABASE enterprise_sys;结果插入中文员工姓名全部变成了????。原因在于没有指定utf8mb4字符集。解决办法是建库时明确指定CREATE DATABASE enterprise_sys DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;与此同时Flask应用的连接字符串也要带上charsetutf8mb4参数SQLALCHEMY_DATABASE_URI mysqlpymysql://root:passwordlocalhost/enterprise_sys?charsetutf8mb4坑三静态文件在Nginx下加载不全。开发时用Flask自带的静态文件处理一切正常但切到Nginx后CSS和JS没问题图片却偶尔加载失败。排查发现是Nginx的alias路径没写对。location /static/语义是“/static/后面的路径映射到alias指定的物理路径下”如果alias路径末尾少了一个斜杠就会匹配不到文件。这类问题没有日志基本看不出原因给Nginx配置加一行error_log可以方便定位。6. 从一鸣系统延伸出的扩展方向一鸣系统交付之后客户正常使用了三个多月基本稳定。但通过这次实践我也总结了一些后续扩展的空间如果你正在做类似系统可以提前规划进去。第一个方向考勤硬件对接。第一版不做硬件对接是为了快速上线但实际使用中员工手动打卡和人事代录的工作量不小。后续可以对接钉钉、企业微信的考勤API每天定时拉取打卡记录并写入attendance表这个接口本身不复杂主要是加密签名逻辑要处理好。第二个方向工资条的主动推送。目前员工是登录网页查看工资条可以扩展为生成PDF工资条之后通过邮件或企业微信应用消息推送给员工减少人事逐个通知的成本。第三个方向考勤和工资的数据分析。系统运行一段时间后积累的考勤数据、加班数据就很有价值了。可以增加部门维度的月度人力成本分析、加班趋势报表让老板一眼看清人力开销花在哪里。这些报表用Flask自带的模板就能实现不必上专门的可视化框架。我个人在接这种中小型管理系统时的体会是技术上永远不是瓶颈业务规则的重构成本才是。与其在演示时炫技用新技术不如扎扎实实把表结构设计对、把计算规则理清楚。毕竟管理者用系统是为了少出错不是为了看界面有多酷。最后再分享一个我做这类项目的小技巧第一次跑通工资计算闭环后一定要拿上个月的真实Excel工资表逐项核对一遍。不要只看总额对不对要逐人逐项比对基本工资、考勤扣款、加班费是否一致。这个环节能发现大量你根本没有想到的业务特例比如有人一天打三次卡、有人上了通宵班怎么算加班时长等等。系统上线前多揪出一个特例上线后就能少接一个求助电话。
返回列表