Flask + Django 构建企业人力资源管理系统:双框架协同实战指南
在公司里跑过几个管理系统项目之后我越来越觉得人力资源管理系统HRMS这种“看起来谁都能做”的项目反而是最考验技术选型和数据建模功力的。这次要聊的这个项目标题是“Python flask django公司人力资源管理系统”说白了就是用Python生态里两个主流Web框架——Flask和Django——组合着把一个公司的HR业务搬到线上。你可能会问一个系统而已干嘛要俩框架这正是我拿到这个标题后最想拆解的点也是整个项目设计思路里最值钱的部分。这套系统能解决什么问题呢往小了说是把花名册从Excel表格变成在线数据库往大了说是把组织架构、入职离职、考勤统计、薪酬核算、招聘进度这些琐碎又敏感的流程统一收拢到一个权限分明的平台上。它适合谁如果你是刚带团队的后端开发者、准备做毕业设计的学生或者公司正好缺一套内部管理工具、想自己折腾出来的运维同学这个项目都值得从头到尾过一遍。我会把双框架的分工逻辑、核心数据表怎么设计、权限怎么控、部署踩过哪些坑全部摊开讲。1. 项目整体思路与框架分工1.1 为什么同时用Flask和Django很多初次接触这个项目的人第一反应都是Flask和Django二选一不就行了说实话绝大多数场景下确实该二选一。Django自带Admin后台、ORM、Migration、认证体系适合快速搭建业务密集型系统Flask轻量灵活适合做小服务、API转发、定时脚本的Web外壳。那为什么会有“同时用”的需求答案藏在“公司人力资源管理系统”这个标题的业务形态里。人力资源系统有很鲜明的特点主业务模块多且耦合度高。员工信息、组织架构、考勤、薪酬、招聘、培训这些模块之间强关联用Django的MTV架构加Admin后台开发效率极高。但与此同时HR部门经常有一些临时的、低耦合的工具型需求比如批量导入Excel花名册、定时跑考勤汇总邮件、对接钉钉/企业微信的审批回调、给外部系统提供报表接口。这类需求如果继续往Django主工程里塞会让业务代码越来越臃肿迁移表结构时还要小心翼翼怕影响主链路。所以实际项目里常见的做法是Django做主业务系统负责组织架构、员工档案、考勤记录、薪酬核算、权限管理这些核心数据Flask做辅助服务专门承载报表导出、Excel导入解析、定时任务、Webhook回调这类边界型功能。两个服务可以部署在同一台服务器上通过内部HTTP接口或共享数据库协作。这样做的好处是主系统保持稳定辅助功能挂了也不影响核心业务。1.2 双框架协同的边界划分我在设计这个项目时给双框架划了一条很清晰的边界线有状态的核心数据归Django无状态的工具型任务归Flask。所谓有状态是指数据需要长期保存、有复杂的关联关系、并且有严格的权限审计要求。组织架构里的部门层级、员工表里的在职状态、考勤表里每天每人的打卡记录、薪酬表里的工资项组成这些都属于Django的管辖范围。Django的ORM可以很方便地表达“一个部门下有多个员工一个员工属于一个部门”这样的关系而且自带的Django Admin可以省掉大量后台管理页面的开发时间。Flask这边则专注于三件事文件处理、定时任务、外部对接。例如HR拿着一份几百行的Excel交给你要求“按这个更新员工信息”Flask可以快速写一个上传接口用openpyxl解析后调用Django的API写入数据库。又比如每天晚上10点需要统计当日考勤异常并推送通知Flask里挂一个APScheduler就能轻松搞定完全不影响Django进程。再比如公司用的OA系统回调钉钉审批结果Flask起一个webhook接收端验签后转发给Django更新员工请假状态链路非常清爽。这种分工不是拍脑袋拍出来的而是基于一个很实际的运维考虑Django主服务升级重启时如果所有功能都塞在里面服务就得整体停摆。拆出Flask辅助服务后升级辅助功能完全不影响主服务运行——这在真实企业环境里是非常宝贵的特性。2. 核心模块拆解与数据模型设计2.1 HR系统的六个核心业务模块一个能真正在办公室跑起来的人力资源管理系统不是只有“增删改查员工”那么简单。按业务优先级排序我认为至少要覆盖以下六个模块组织架构管理部门树的维护、部门的合并与拆分、部门负责人的变更。这是所有业务数据的骨架员工、考勤、薪酬都必须挂在部门和职位下才能形成有效统计。员工信息管理包括基本资料姓名、性别、出生日期、联系方式、入职信息入职日期、试用期、转正日期、合同信息、教育经历、工作经历、紧急联系人等。这里要特别注意敏感字段的权限控制比如薪资信息只能薪酬专员看联系方式只能本部门主管看。考勤管理日常打卡记录、请假审批、加班申请、出差登记。考勤数据是薪酬核算的输入源所以这个模块的数据准确性要求极高。薪酬管理基础工资、岗位工资、绩效奖金、五险一金扣除、个税计算、实发工资。这个模块通常不和打卡记录直接耦合而是通过月度薪酬核算任务从考勤模块拉取“出勤天数”“请假时数”等统计结果。招聘管理职位发布、简历投递、面试安排、Offer审批。这个模块可以相对独立但候选人一旦入职就要在员工信息模块里自动或半自动地建档。培训与绩效管理培训计划、课程记录、考核分数、绩效目标、评价结果。这个模块在中小公司往往优先级最低但作为系统能力规划时应该预留数据模型。每个模块内部还有状态流转。比如员工的状态可能是待入职→试用期→正式→离职流程中→已离职。考勤记录的状态可能是正常、迟到、早退、缺卡、请假、出差。在设计数据库时这些状态字段建议用整数编码或短字符串存储前端再映射成中文标签避免把中文直接存进数据库导致索引和查询效率下降。2.2 数据库设计实例直接上核心表结构。我习惯用这种精简但够用的设计生产环境可以在这个基础上扩展。部门表department字段名类型说明idint主键namevarchar(64)部门名称parent_idint父部门ID支持树形结构manager_idint部门负责人关联员工表statustinyint1在职0已撤销员工表employee字段名类型说明idint主键emp_novarchar(32)工号唯一索引namevarchar(64)姓名gendertinyint1男2女0未知department_idint所属部门position_idint职位IDhire_datedate入职日期statustinyint1试用2正式3离职phonevarchar(32)手机号emailvarchar(128)邮箱考勤记录表attendance字段名类型说明idint主键employee_idint员工IDwork_datedate日期check_in_timedatetime上班打卡check_out_timedatetime下班打卡statustinyint1正常2迟到3早退4缺卡5请假6出差请假记录表leave_record字段名类型说明idint主键employee_idint员工IDleave_typetinyint1事假2病假3年假4调休start_timedatetime开始时间end_timedatetime结束时间reasonvarchar(255)请假事由approve_statustinyint0待审批1通过2驳回薪酬记录表salary_record字段名类型说明idint主键employee_idint员工IDmonthvarchar(7)月份格式2025-06base_salarydecimal(10,2)基础工资performancedecimal(10,2)绩效奖金overtime_paydecimal(10,2)加班费allowancedecimal(10,2)补贴deductiondecimal(10,2)扣除项社保、个税等net_salarydecimal(10,2)实发工资Django里定义这些模型很直白。用ForeignKey表达员工和部门的关系用unique_together约束员工工号和月份组合唯一性。特别提醒一下所有金额字段都用DecimalField而不是FloatFieldFloat的二进制精度问题在钱相关的场景下绝对不能妥协。2.3 权限模型与登录态设计HR系统的权限是整个项目里最敏感的环节。普通员工只能看自己的档案部门主管能看本部门员工的考勤和基础信息HR专员能维护全公司员工档案薪酬专员只有薪酬模块的权限系统管理员负责分配角色。我采用的是RBAC基于角色的访问控制模型Django自带的auth框架提供User、Group、Permission三件套可以很轻松地实现这套逻辑。具体做法是创建django_user表作为登录账号再把内部员工表employee和User做一对一关联这样HR系统里“员工即用户”的概念就建立起来了。权限控制通过装饰器和View的dispatch方法做校验例如from django.contrib.auth.decorators import permission_required permission_required(hr.view_salary, raise_exceptionTrue) def salary_list(request): ...对于API接口我用Django REST Framework实现认证方式选择JWT。为什么不选Session因为辅助服务Flask也要调主系统接口JWT无状态、跨服务验证方便。登录接口签发token前端存到localStorage请求时在Authorization头里带上Bearer token。Flask辅助服务需要调用Django API时直接用固定的服务账号换取token或者在Django侧开放内网白名单两者结合更稳妥。还有一点必须注意薪酬数据绝对不能跟着列表接口一起返回。我在员工列表序列化器里只暴露姓名、部门、职位、入职日期这些基础字段薪酬信息单独设置序列化器在权限满足时才拼接。这类细节就是好系统和烂系统的差别。3. 关键功能落地实操全过程3.1 环境准备与项目骨架搭建先说环境版本。我推荐一个经过大量生产验证的组合Python 3.103.8也能跑但3.10的语法和性能更好Django 4.2 LTS长期支持版别追新Django REST Framework 3.14Flask 2.3数据库选MySQL 8.0Django 4.2对MySQL 8支持很好Redis 6做缓存和JWT黑名单创建项目骨架的时候我习惯用虚拟环境隔离依赖Windows和Linux的操作略有不同核心命令一样# 创建主服务虚拟环境 python3 -m venv venv-main source venv-main/bin/activate pip install django4.2 djangorestframework pymysql cryptography django-cors-headers # 创建辅助服务虚拟环境 python3 -m venv venv-tool source venv-tool/bin/activate pip install flask flask-cors openpyxl requests apschedulerDjango项目的初始化用官方命令一行搞定django-admin startproject hr_server cd hr_server python manage.py startapp org python manage.py startapp employee python manage.py startapp attendance python manage.py startapp salary python manage.py startapp auth_plus每个app各司其职org管部门和职位employee管员工档案attendance管考勤和请假salary管薪酬auth_plus管权限扩展。INSTALLED_APPS里把这些app和rest_framework都注册进去再配置数据库连接。Django连接MySQL需要装pymysql并在项目__init__.py里加上import pymysql pymysql.install_as_MySQLdb()这一步不加运行migrate时会报“Did you install mysqlclient?”的错误。3.2 Django主服务员工模块与考勤API员工模块是整个系统的信息中枢API设计上我按功能拆分成几个端点GET /api/employees/员工列表支持按姓名、部门、状态过滤POST /api/employees/新增员工GET /api/employees/{id}/员工详情PUT /api/employees/{id}/更新员工信息DELETE /api/employees/{id}/删除员工逻辑删除即修改状态模型里就按2.2那套表结构来但我会额外加一层逻辑删除员工不真正DELETE只是把status改成离职。理由很实在——历史考勤数据、历史薪酬数据都要和员工记录保持关联万一数据被物理删了那条历史链路就断了。这个习惯是我在一个项目中吃过亏后养成的。序列化器方面列表接口用精简版详情接口用完整版。核心视图代码如下from rest_framework import viewsets from rest_framework.permissions import IsAuthenticated from .models import Employee from .serializers import EmployeeListSerializer, EmployeeDetailSerializer class EmployeeViewSet(viewsets.ModelViewSet): queryset Employee.objects.filter(is_deletedFalse) permission_classes [IsAuthenticated] def get_serializer_class(self): if self.action list: return EmployeeListSerializer return EmployeeDetailSerializer def perform_destroy(self, instance): # 逻辑删除标记离职而非物理删除 instance.status 3 instance.is_deleted True instance.save()考勤模块的关键是“日报表统计”。HR想看的是“这个月谁迟到了几次、谁缺卡了几次”而不是一堆原始打卡流水。所以我提供了一个统计接口用Django ORM的annotate按员工和月份聚合from django.db.models import Count, Q from django.db.models.functions import TruncMonth from .models import Attendance def attendance_summary(employee_id, month): return ( Attendance.objects .filter(employee_idemployee_id, work_date__startswithmonth) .values(status) .annotate(totalCount(id)) )前端拿到这个统计结果直接渲染成“迟到3次、缺卡1次”的卡片非常直观。实际做的时候我建议把work_date字段加一个索引因为按月份统计的查询会经常碰到。请假审批流程用状态机的方式推进。提交请假时创建一条leave_record状态是0待审批。主管审批通过后状态变1同时自动生成一条请假类型的attendance记录。这个动作通过Django的signal或直接在视图里手动处理都可以我倾向于在approve接口里一并完成保证事务一致性避免signal执行顺序的不可控问题。3.3 Flask辅助服务Excel批量导入现在说Flask这个辅助服务。最典型的应用场景就是HR拿Excel导入员工信息。这张表里可能有几千行数据让HR在网页上一个一个手动输入是不现实的但用Django Admin导入Excel又需要安装额外插件。这时候Flask轻量服务的优势就体现出来了。我用openpyxl读取Excel校验后调用Django REST API写入数据库。核心代码如下from flask import Flask, request, jsonify import openpyxl import requests app Flask(__name__) DJANGO_API http://127.0.0.1:8000/api TOKEN 内网服务账号的JWT Token app.route(/import/employees, methods[POST]) def import_employees(): file request.files.get(file) if not file: return jsonify({error: 未上传文件}), 400 wb openpyxl.load_workbook(file) ws wb.active headers [cell.value for cell in ws[1]] rows [] for row in ws.iter_rows(min_row2, values_onlyTrue): item dict(zip(headers, row)) # 简单校验工号和姓名必填 if not item.get(工号) or not item.get(姓名): continue rows.append({ emp_no: str(item[工号]).strip(), name: item[姓名].strip(), department: item.get(部门, ), position: item.get(职位, ), hire_date: item.get(入职日期, ), phone: item.get(手机号, ), email: item.get(邮箱, ), }) success_count 0 for row in rows: resp requests.post( f{DJANGO_API}/employees/, jsonrow, headers{Authorization: fBearer {TOKEN}} ) if resp.status_code in (200, 201): success_count 1 return jsonify({total: len(rows), success: success_count})这个接口有个需要注意的坑Excel里的日期单元格用openpyxl读出来是datetime对象但通过JSON传给Django时datetime对象需要序列化成字符串。我之前就碰到过日期导入后全部变成“2025-06-25T00:00:00”这种带时分秒的格式Django端的DateField倒是能自动转换但前端显示时会出现多余的时间部分。解决办法是在Django序列化器里对日期字段做格式化或者在Flask端先把date对象转成strftime(%Y-%m-%d)再传。类似的Excel导出接口也一样逻辑Django查数据生成Excel文件Flask提供下载端点。批量导入导出这类功能在真实系统里每天都会被HR用到用Flask做出来后主系统代码一点都不会被污染我需要什么功能临时加一个Flask接口就行。3.4 部署上线uWSGI/Gunicorn与Nginx部署部分直接讲生产环境方案。Django主服务用Gunicorn启动Flask辅助服务用uWSGI启动前面统一挂Nginx做反向代理和静态文件处理。Gunicorn启动Django的命令我常用这套参数gunicorn hr_server.wsgi:application \ --bind 127.0.0.1:8000 \ --workers 4 \ --threads 2 \ --timeout 60 \ --access-logfile logs/gunicorn-access.log \ --error-logfile logs/gunicorn-error.logworkers数量不是越多越好。经验值是2 * CPU核心数 1。比如4核机器开9个worker但每个worker都会占用一份数据库连接池所以还要在Django的数据库配置里适当调大CONN_MAX_AGE保证长连接能被复用。Flask辅助服务的端口我用5001和Django区分开Nginx里配置两条location规则server { listen 80; server_name hr.internal.company.com; # 静态文件交给Nginx减轻后端压力 location /static/ { alias /opt/hr_project/static/; } # Django主服务 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; } # Flask辅助服务 location /tool/ { proxy_pass http://127.0.0.1:5001/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个细节Flask接口的location用了/tool/前缀转发时proxy_pass的末尾加了/含义是剥掉/tool/再转发Flask端路由就可以从根路径写起。如果不加末尾的斜杠Flask端URL就需要写成带/tool/前缀的形式容易搞混。数据库的配置也要给生产环境做优化。Django的settings.py里数据库引擎用django.db.backends.mysql但我建议设置OPTIONSDATABASES { default: { ENGINE: django.db.backends.mysql, NAME: hr_db, USER: hr_app, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, CONN_MAX_AGE: 60, OPTIONS: { charset: utf8mb4, sql_mode: STRICT_TRANS_TABLES, }, } }utf8mb4这个字符集必须设否则员工名字里带生僻字或emoji时数据会报编码错误。sql_mode用STRICT_TRANS_TABLES可以避免字段超长时静默截断导致的数据错误。4. 高频问题与调试实录任何一个正经系统都免不了踩坑这个项目我在开发和上线阶段也遇到了不少问题。挑几个高频的列成表格方便你照方抓药。问题现象原因分析解决方案避坑提示中文乱码插入生僻字时报错MySQL表或连接没使用utf8mb4数据库创建时指定utf8mb4连接OPTIONS加上charset建库时直接选utf8mb4_unicode_ci排序规则时间格式不统一前端显示“T”分割Django默认ISO格式带T在序列化器里给DateTimeField指定format%Y-%m-%d %H:%M:%S全局统一format别在多个序列化器里重复设置Flask导入Excel日期变成带时分秒openpyxl读取的是datetime对象用strftime(%Y-%m-%d)转换后再传在Excel里先确认HR写的日期格式统一人员离职后登录还能访问系统没同步删除或禁用User账号离职审批通过时同步调用user.is_activeFalse用signal监听员工状态变更自动禁用账号JWT过期后用户还停留在页面前端没处理401响应写一个axios响应拦截器遇到401清除本地token并跳转登录页Token有效期不宜过长建议60分钟考勤统计接口越查越慢work_date字段没加索引给work_date、employee_id加复合索引先跑explain再上线别等慢了再优化并发导入Excel时数据库锁表批量写操作没做事物分批每次POST提交后Django自动事务提交大量数据时分批次写入单次导入控制在500行以内分批次处理更安全再补充几个我在代码层面反复踩过的细节。第一个是CSRF校验。Django默认给所有POST请求加CSRF校验如果是纯API接口不做表单渲染我建议在APIView里用csrf_exempt或直接在settings里关掉否则前端POST请求全给你403。当然如果你用的是DRF它默认把CSRF校验放在SessionAuthentication里用JWT时就不受影响这个要注意别两套混用。第二个是跨域问题。HR系统的前端通常是单独部署的域名比如hr-ui.company.com后端在hr-api.company.com两者不同源必然触发CORS。这问题我已经见太多新人在配环境时气急败坏。解决方案是装django-cors-headers在MIDDLEWARE里加corsheaders.middleware.CorsMiddleware然后配置CORS_ALLOWED_ORIGINS [ http://hr-ui.company.com, ]千万别为了省事直接用CORS_ALLOW_ALL_ORIGINS True。公司内部系统还好外部可访问的系统如果全开放CORS等于允许任何网站跨域调你的接口是非常严重的安全漏洞。第三个坑发生在ORM查NULL值。Django的filter(phoneNone)在Mysql里会翻译成phone IS NULL这没问题。但如果你写的是filter(phone)翻译出来的就是phone 而空字符串和NULL在数据库里是两种不同情况。员工手机号没填的时候Excel导入可能传进去的是空字符串Django模型里的CharField默认将空字符串存为而你查询时用的却是None两边永远匹配不上。建议统一逻辑所有可空字符串字段都用nullTrue, blankTrue并在序列化器里把空字符串转成None保证数据一致性。第四个值得说的是Django Admin和自定义API的冲突。Admin后台很方便但直接把Admin暴露给HR使用有一些问题Admin的UI是面向管理员的不适合给普通HR做数据录入而且Admin的权限模型和自定义的业务权限比如部门数据范围经常对不上。我的方案是Admin只给系统管理员做排查和数据修复用普通HR全部走自定义页面和API。你要给Admin设置专属URL前缀别让它霸占根路由——去掉urlpatterns里的path(admin/, admin.site.urls)改成path(system/admin/, admin.site.urls)既实用又稍微增加一点安全隐蔽性。第五个坑是文件上传大小限制。Flask默认接收上传文件的大小没有硬性限制但Nginx默认client_max_body_size是1M所以如果你没配HR传一个5MB的Excel文件时会直接被Nginx拒之门外报413错误。解决方法是Nginx配置里加client_max_body_size 20m;同时Flask端也用app.config[MAX_CONTENT_LENGTH] 20 * 1024 * 1024做一层应用级校验超过大小直接返回友好的错误提示给HR。最后说一下日志。生产环境调试最大的痛苦就是看不到错误日志。Gunicorn的错误日志要开Django的LOGGING配置也要配好最好把Django的请求日志、SQL日志、业务日志分开存。SQL日志在定位ORM问题时尤其有用你可以在settings里这样配LOGGING { version: 1, handlers: { sql_file: { level: DEBUG, class: logging.FileHandler, filename: logs/sql.log, }, }, loggers: { django.db.backends: { handlers: [sql_file], level: DEBUG, }, }, }这个SQL日志会记录所有执行的SQL语句包括耗时是排查性能问题的一把好手。生产上如果嫌日志太大可以只保留慢查询的SQL记录或定期轮转日志文件。对内部系统来说保留最近一个月的日志基本就够用了。我个人在实际操作中还有一个体会双框架的项目最容易出问题的地方其实不在代码而在于两个服务之间的接口契约管理。Django API返回什么字段结构Flask端必须严格遵守一旦两边同时改动没有契约测试就会悄无声息地出问题。我后来单独建了一个contracts目录把内部接口的请求响应示例以JSON文件形式固定下来任何一边改动都要更新契约文件代码评审时一眼就能看出结构有没有破坏。团队协作时这个习惯特别有用。这个项目管理系统的后续扩展空间也很大。比如接入企业微信消息推送员工请假审批通过后自动通知主管比如用定时任务每月自动生成薪酬报表并推送邮件再比如把考勤数据同步给财务系统减少二次录入。每一步扩展Django主系统保持稳定Flask辅助服务继续承载工具型需求整个架构可以平滑演进。