做管理系统这类项目,最怕的就是“需求一时爽,开发火葬场”。尤其是企业人事管理,牵扯到部门、员工、考勤、薪资、角色权限一大堆东西,如果技术选型或者表结构设计一开始没想清楚,后面每加一个功能都是在给自己挖坑。我最近刚好把一个基于 Flask Vue 的企业人事管理系统从零到一完整落地了,跑在 PyCharm 里开发和调试,整体架构清晰、前后端分离,数据层用 SQLAlchemy 管理,认证走 JWT。很多朋友会问为啥选了 Flask 而不是 Django,这个我后面会专门说,结论是:对于这种体量的系统,Flask 的自由度能帮你省掉一堆不必要的框架约束。这篇文章不是贴一堆代码就完事,我会把思路、数据表怎么拆、接口怎么设计、权限怎么做、以及我实际踩过的坑,全部摊开来讲。如果你正准备做类似的信息管理系统,或者正在纠结技术选型,这篇应该能给你省不少时间。1. 项目整体设计与技术选型思路拆解1.1 这件事最开始我应该先想清楚什么人事管理系统听起来简单,无非是“增删改查”。但你真去梳理需求的时候会发现,它的难点根本不在CRUD,而在“关系”和“状态”。员工和部门是什么关系,一个人能不能挂多个部门?考勤记录是一天一算还是月底统一算?薪资是固定工资还是可以配置绩效、补贴、扣款?角色权限要细到什么粒度,是每个按钮控制还是只控制菜单?这些问题如果不在设计阶段定下来,编码就是返工的开始。我的做法是先把角色分为三类:超级管理员、HR、普通员工。超级管理员管系统配置和全部数据;HR管员工信息、考勤、薪资的日常维护;普通员工只允许查看自己的信息、自己的考勤和工资条。这套权限模型几乎是这类系统的标准答案,既能满足企业日常管理,又不会让权限设计失控。1.2 为什么我跳过了 Django 选了 Flask标题里提到 Django,这里直接说结论:我有充足的理由用 Flask,但如果你更熟悉 Django,这套系统照样能实现,核心思路完全一致,只是代码风格不同。先说我选 Flask 的原因。Django 是个“全家桶”框架,内置 Admin 后台、ORM、表单、认证,开发快是真的。但也正因为太全,它把你钉死在一套项目结构里。对于企业人事这种需求变化频繁的系统,Django 的耦合有时反而碍事——你想换一种鉴权方案、想对某个字段做非常规的序列化处理,框架给你的默认行为总会出来“捣乱”。Flask 不是这样。它只给你路由、请求响应、模板渲染这几个最核心的东西,其他全由你自己拼装。这相当于给你一套毛坯房,Django 是精装房。毛坯房前期累,但你可以按自己的意愿改水电、搞布局。尤其是我要用 Vue 做完全的前后端分离时,后端就只是一个纯 API 服务,Flask 的轻量正好匹配这种需求。补充一句,如果你不需要这么细的控制粒度,或者你的团队全是 Python 新手、希望靠框架约束来减少低级错误,那 Django 是更稳妥的选择。工具这种东西,没有绝对的好,只有适合不适合。1.3 技术栈全景图先把我最终确定的技术栈完整列出来,后面的每一段都会围绕这套组合展开。层级选型说明后端框架Flask 2.x轻量、灵活,配合蓝图做模块化ORMFlask-SQLAlchemy操作数据库不用写原生SQL,省心数据库MySQL 8.x企业级系统基本都用MySQL,不用SQLite凑合认证方案JWTPyJWT前后端分离下Token最合适,天然支持跨域前端框架Vue 3 Vite组合式API写起来清爽,Vite启动速度比webpack快得多UI组件库Element Plus表格、弹窗、表单都很成熟,适合后台类系统HTTP客户端Axios统一封装请求、拦截器处理Token和错误码权限控制前端路由守卫 后端装饰器双重校验,前端控制体验,后端控制安全开发工具PyCharm Professional调试接口、管理虚拟环境、看数据库都方便这是一套非常标准的“前后端分离 轻量后端”组合,没有生僻技术,找你团队里任何一个会 Python 和 JS 的同事都能快速接手。2. 数据库表结构设计,这一步决定了后面你是否会返工2.1 核心表拆解,不要一张大表走天下很多人做管理系统,恨不得一张表存所有字段。那是大忌。员工表如果把自己个人信息、部门信息、岗位信息、考勤数据全堆在一起,存倒是能存,但一旦要统计“某个部门本月平均考勤率”,或者要改一个部门名称,你得遍历多少条记录做更新?我把数据按业务域拆分成了下面几张核心表:sys_user用户表:存登录账号、密码哈希、角色、状态。关联员工ID。employee员工表:存姓名、性别、手机号、身份证、入职日期、学历等员工属性。department部门表:部门名称、负责人、上级部门。attendance考勤表:打卡日期、上班时间、下班时间、考勤状态正常/迟到/早退/缺勤。salary薪资表:基本工资、绩效、补贴、扣款、实发工资、发放月份。leave请假表:请假类型、开始时间、结束时间、审批状态。这里我说一下为什么要把用户表和员工表分开。人事管理系统里面,“用户账号”和“员工档案”是两个概念。新人入职,先建档,不一定马上发账号;老员工离职,账号要禁用,但历史档案要保留。字段全塞一起会非常尴尬。拆开后,用户表只关心“谁能登录、有多大权限”,员工表关心“这个人的背景信息是什么”,各司其职,互不干扰。2.2 关系设计中的两个重点,多对多和自关联部门和员工的关系,我最终设计成了多对多。因为现实中确实存在一个人兼职多个部门的情况,比如某些公司的HR同时兼任行政。表结构就是中间的关联表 employee_dept,两个外键分别指向员工和部门。部门自关联是在部门表里加一个 parent_id 字段,0 或 NULL 表示顶级部门。这样做的好处是,任何层级的组织架构都能表达,不需要为每级部门单独建表,而且层级数量不受限制。权限表的设计我用了 RBAC 模型的简化版:user 表里直接存 role 字段,role 的值决定后续所以能访问的接口。我见过很多系统一上来就做用户-角色-权限三张表加权限点表,实际做完才发现维护成本极高,很多权限点根本就没人用。对这种中小型系统,角色直接用字符串字段存,线上运行后按需加判断,灵活性和可维护性都更好。只有当你需要“给张三这个特定用户开放某个特定按钮”这种细粒度场景,才需要完整的三表模型。2.3 字段设计的经验教训这个坑我踩得比较惨,必须单独拿出来说:时间字段一定要存标准类型,别用字符串。尤其是考勤和请假这种要拿来计算的数据,你用字符串存2024-01-15 08:30:00,看着挺规整,真要做“迟到超过15分钟算旷工半天”这种判定时,还得先解析字符串,纯属给自己找事。密码字段我建议存哈希值,不要做任何可逆加密。即使用简单一点的 SHA-256 加盐都比明文强,生产环境建议用 bcrypt。别嫌麻烦,出头鸟的事我不多做,但密码安全这种底线问题,不能含糊。新手容易忽略的还有一个:凡是金额字段,在 MySQL 里一律用 DECIMAL 类型,不要用 FLOAT。FLOAT 有精度损耗,工资算多了几厘钱,账对不上就麻烦。DECIMAL(10,2) 表示总长10位,小数占2位,对人事薪资完全够用。sqlalchemy 的表定义大致长这样:class Employee(db.Model): __tablename__ employee id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse, comment姓名) gender db.Column(db.String(10), comment性别) phone db.Column(db.String(20), uniqueTrue, comment手机号) id_card db.Column(db.String(18), uniqueTrue, comment身份证号) hire_date db.Column(db.Date, comment入职日期) education db.Column(db.String(20), comment学历) status db.Column(db.Integer, default1, comment在职状态:1在职,0离职)3. Flask 后端实现,从蓝图到接口权限的完整落地3.1 项目目录结构,别把所有路由堆在一个文件里一个小型 demo 你当然可以把所有路由写在 app.py 里,但人事管理系统至少十几个模块,堆一起你维护到后面想哭。我在项目里按模块拆成了蓝图(Blueprint):project/ ├── app.py # 应用入口 ├── config.py # 配置文件 ├── models/ # 数据库模型 │ ├── __init__.py │ ├── user.py │ ├── employee.py │ ├── department.py │ ├── attendance.py │ └── salary.py ├── blueprints/ # 路由蓝图 │ ├── __init__.py │ ├── auth.py # 登录/认证模块 │ ├── employee_api.py # 员工管理接口 │ ├── attendance_api.py # 考勤接口 │ └── salary_api.py # 薪资接口 ├── utils/ # 工具类 │ ├── __init__.py │ ├── jwt_helper.py # Token生成与校验 │ └── decorators.py # 权限装饰器 └── venv/ # 虚拟环境这样的好处是:每个蓝图只负责自己的业务模块,如果有人要加一个“招聘模块”,直接新建一个 blueprint 文件,在 app.py 里注册一下就行,对原有代码零侵入。3.2 JWT 认证与自定义装饰器前后端分离的架构下,session 方案非常别扭,因为前端和后端可能不在同一个域名,跨域时携带 Cookie 要处理一堆 CORS 的细节。JWT 要好用得多了,后端生成一个带过期时间的 Token,前端存到 localStorage 里,每次请求在 Authorization 头带上就行。JWT 的核心就是一个带签名的 JSON 字符串,它自己包含了用户标识和过期时间,服务端不用存任何 session 记录,天然适合水平扩展。生成 Token 的部分我直接封装了一个小工具:import jwt import time SECRET_KEY your-secret-key-change-in-production def generate_token(user_id, role, expires_hours24): payload { user_id: user_id, role: role, exp: int(time.time()) expires_hours * 3600 } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def verify_token(token): try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) return payload except jwt.ExpiredSignatureError: return None # Token过期 except jwt.InvalidTokenError: return None # Token非法然后自定义一个登录必需的装饰器,用于保护那些必须登录才能访问的接口:from functools import wraps from flask import request, jsonify def login_required(f): wraps(f) def wrapper(*args, **kwargs): auth_header request.headers.get(Authorization, ) if not auth_header.startswith(Bearer ): return jsonify({code: 401, msg: 未登录或Token缺失}), 401 token auth_header[7:] # 去掉 Bearer 前缀 payload verify_token(token) if not payload: return jsonify({code: 401, msg: Token无效或已过期}), 401 # 把用户信息挂到request上,方便后续接口直接取 request.user_id payload[user_id] request.role payload[role] return f(*args, **kwargs) return wrapper有了这个装饰器,任何需要认证的接口,只需要在路由上面加一行 login_required 就行。而角色控制则是再封装一个带参数的装饰器,比如 role_required(HR) 这样的形式。3.3 员工管理接口的设计细节员工模块是人事系统的核心,接口在设计时要考虑三个点:多条件筛选、分页、字段校验。我做的接口格式是 GET /api/employee/list,支持 keyword(搜索姓名/手机号)、department_id、status(在职/离职)、page、size 五个参数。后端用 SQLAlchemy 把这些条件组合起来,拼成一个动态查询。这里用 filter 就行,不用手拼 SQL,清晰安全。分页我统一返回一个结构:{ code: 200, data: { list: [...], total: 128, page: 1, size: 10 } }前端表格拿到这个结构,直接能算总页数,Element Plus 的分页组件也正好长这样,对接非常顺畅。我特别想强调一个细节:删除员工不要物理删除。员工一旦入职过,他的考勤、薪资、离职记录都是历史数据,你把他从表里 delete 掉,那些关联数据立马成孤儿,后面做统计报表一定炸。我的做法是逻辑删除,把 status 字段置为 0,表示“离职/禁用”,数据还留在表里,但在列表查询中默认过滤掉。3.4 考勤与薪资模块的计算逻辑考勤模块的核心不是记录,而是判定状态。企业考勤的常见规则是:上班晚于 09:00 算迟到,晚于 09:30 算迟到严重甚至算旷工半天;下班早于 18:00 算早退。我用 Python 的 datetime 模块做判定:from datetime import datetime, time def judge_attendance(check_in, check_out): late_time time(9, 0) severe_late_time time(9, 30) leave_time time(18, 0) status 正常 if check_in and check_in.time() severe_late_time: status 旷工半天 elif check_in and check_in.time() late_time: status 迟到 if check_out and check_out.time() leave_time: status 早退 if status 正常 else f{status}早退 return status实际项目中规则可能更复杂,比如弹性上班、外勤打卡、节假日排班等。这些可以往后扩展,但核心思路不变:原始打卡记录和业务判定逻辑分开,原始数据永远不改,判定函数可以随时调整规则重跑。薪资模块我留了一个漂亮的钩子——把薪资项做成了灵活配置结构。每个月发工资时,HR 填写基本工资、绩效、补贴,系统自动算:实发 基本 绩效 补贴 - 五险一金代扣 - 个税 - 缺勤扣款。每个月的工资单存一张表记录,连同月份字段一起,方便后续查历史工资条。这里有个防呆设计我建议一定做:同一员工同一个月只能有一条工资记录,用联合唯一索引 unique(employee_id, month) 约束。否则 HR 手一抖连点两次提交,就多了一条重复工资,月底发现钱对不上就火大。4. Vue 3 前端搭建,从页面骨架到接口联调4.1 前端项目结构与路由设计前端我用 Vite 创建 Vue 3 项目,目录长这样:frontend/ ├── src/ │ ├── api/ # 接口封装 │ ├── router/ # 路由 │ ├── store/ # Pinia状态管理 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ └── utils/ # 工具函数路由我分了两层:登录页是独立的路由,登录成功后的所有页面都套在一个 Layout 组件里,左侧菜单,顶部用户信息,中间内容区。路由守卫是必须做的。Vue Router 提供 beforeEach 钩子,我在里面判断:跳转任何页面之前,先看 localStorage 里有没有 Token。没有就跳去登录页,有就放行。这只是前端的体验层拦截,真正数据安全靠的是后端那些 login_required 装饰器,两层都做,但不互相依赖。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path ! /login !token) { next(/login) } else { next() } })4.2 Axios 统一封装与拦截器Axios 封装得好不好,直接影响前端开发的幸福感。我在 utils/request.js 里做了一个实例,统一配置 baseURL、超时时间,然后加请求拦截器和响应拦截器。请求拦截器干一件事:从 localStorage 拿 Token,放到 headers 里。service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer ${token} } return config })响应拦截器干两件事:第一,后端返回 code 401 时,自动清掉 Token,跳回登录页;第二,后端返回 data 直接拆掉外层包装,业务代码不用每次 res.data.data 这么套娃。service.interceptors.response.use( response { const res response.data if (res.code 401) { localStorage.removeItem(token) router.push(/login) return Promise.reject(new Error(登录状态已过期)) } return res.data }, error { return Promise.reject(error) } )4.3 表格页面的标准实现套路Element Plus 的 el-table 做后台表格很好用。我的员工列表页面基本长这样:搜索区(el-form 内联) 表格区(el-table) 分页区(el-pagination)。搜索区四个字段:关键字、部门下拉、状态下拉、搜索按钮。点搜索时把表单数据拼进查询条件,重新请求第 1 页。表格列绑定 employee 对象的属性,其中操作列放按钮,跳去详情页或弹窗编辑。比较值得讲一下的是下拉框数据从哪来。部门下拉的数据是从后端部门接口拉的,但你不能每次打开页面都请求一次部门列表,这种几乎不变的数据,可以放在 Pinia 里做全局状态,登录后拉一次,存起来,后面所有组件直接读 store。我的做法是 Pinia store 里定义一个 departmentList,页面加载时如果发现 store 里还没有数据才去请求。实际开发中我建议前后端同时开发时,前端先 Mock 一份假数据把页面跑通,后端好了再接真实接口。这样两边不互相等,效率翻倍。5. 联调环节,那些坑我建议你提前避掉5.1 跨域配置,一次配好一劳永逸前端跑在 localhost:5173,后端跑在 localhost:5000,端口不同就构成跨域。Flask 后端需要安装 Flask-CORS:from flask_cors import CORS CORS(app, supports_credentialsTrue)开了这个,前端 Axios 跨域请求就不会被浏览器拦下来。但注意,supports_credentialsTrue 这条要谨慎使用,它允许携带 Cookie。如果你全程使用 JWT 而不是 Cookie 认证,不需要开启这条。我当时踩过这个坑,开了之后反而会让调试的时候多出一些奇奇怪怪的安全限制。生产环境如果是同一个域名部署,后端接口放 /api/ 路径下,前端构建出来的静态文件直接托管在同域名下,基本不涉及跨域问题。所以 CORS 只是开发环境的临时方案,别让线上环境也大敞大开的跨域。5.2 PyCharm 里的调试技巧用 PyCharm Professional 跑 Flask 项目有个好处:可以像调试普通 Python 脚本一样,在接口代码里打断点,一个请求打过来,IDE 会自动停在断点上,你可以清晰地看到 request.json 里到底接了哪些字段、SQLAlchemy 拼出来的 SQL 到底长什么样。我强烈建议把数据库工具窗口用起来。PyCharm 自带的 Database 面板可以直接连 MySQL,能看表结构、能跑查询、能看当前表里的数据,排查“接口返回了但数据不对”这类问题,一边看接口返回值一边对着表里的原始数据,问题定位能快一倍。Flask 默认的 debug 模式只是“代码改完自动重启 显示错误页”,但真正排查逻辑问题还是靠断点调试和日志。建议在 app.py 里配置统一的日志,把每个请求的时间消耗和异常栈打出来,后面有问题还能追溯。5.3 接口联调时的数据不一致问题前后端联调时最常遇到的场景:前端觉得后端字段名不对,后端觉得前端传参不对。根因就是大家没有一份统一的接口文档。我们的解决办法虽然土,但特别有效:在项目根目录维护一个 API.md,每个接口写完就更新,包含 URL、方法、请求参数、返回示例。前端对着这个文档联调,后端对着这个文档自查,扯皮的频率立刻降下来。这也是为什么在很多团队,光有代码没有文档的项目最后一定是一地鸡毛的原因。6. 常见问题与排查技巧,都是真实踩过的坑6.1 问题速查表问题现象可能原因解决方案登录成功后下次刷新页面就退出JWT 过期时间太短把 expires_hours 调大,或改成刷新Token机制前端请求报401但Token明明有响应拦截器里处理401逻辑有误先看Network里实际请求头是否带了Authorization数据库中文乱码建库时没用 utf8mb4 字符集建库语句指定 DEFAULT CHARACTER SET utf8mb4员工列表接口很慢每次查询都在用like匹配大字段列表接口不要对长文本字段做模糊查询,索引要建在常用筛选字段上部门树形结构显示不对前端递归组件或后端返回的children结构有问题后端一次查出所有部门,前端递归组装children,减少请求次数薪资计算偶发对不上账金额字段用了 FLOAT 或重复提交工资单换成 DECIMAL 类型,加联合唯一索引防重复上传的图片头像无法显示静态文件路径没正确配置后端用 Flask 的 send_from_directory 暴露上传目录6.2 我记得最深刻的两个排查经历第一个是打卡时间混乱。开发考勤模块时,我用 MySQL 的 DATETIME 存打卡时间,一开始测试的数据正常,后来发现某些员工记录时间是“UTC”,跟本地时间差了八小时。查来查去发现是 MySQL 连接串里没有配时区参数,驱动默认按 UTC 处理。解决很简单,数据库连接配置里加一行?serverTimezoneAsia/Shanghai,整个世界就正常了。这个坑你要是没遇过,基本永远想不起来。第二个是登录功能的 Token 在前端异常消失。表现为:用户明明登录成功了,刷新页面后 Token 却读不到。后来发现是我在前端代码里把 Token 存到了 sessionStorage,浏览器标签页一关就清掉了。换成 localStorage 之后稳定解决。这个小问题,前端新手经常犯。6.3 系统上线前的安全检查清单功能都能跑通,不等于可以上线。我总结了几个必须检查的点:后端所有的写操作接口,必须有权限校验,不能只靠前端把按钮藏了就安全。密码不能明文存,而且要检查默认管理员账号是不是还在用默认密码。生产环境关闭 Flask debug 模式,否则报错页面会把堆栈信息暴露出去。 调试接口时用 Swagger 或者 Postman,但上线前注意这些接口文档是否对公网暴露。MySQL 连接串的账号密码不能写死在代码里,放到环境变量或者配置中心管理。7. 系统扩展方向与我的实操体会7.1 这套系统还能往哪些方向长人事管理系统最容易被要求扩展的功能,我列几个,提前给你打预防针:消息通知:员工薪资发放或者审批通过后,怎么通知到本人?短信、邮件还是公众号模板消息。我建议在设计时就把“事件”的概念抽象出来,比如提交申请、审批通过、薪资发放都是一个个事件,后续接通知系统就只是给事件绑定一个订阅者,不用大改代码。审批流:请假审批、入职审批、离职审批,这是人事系统里最容易让人头大的模块。单级审批还简单,复杂的是“部门主管审批 - HR复核 - 总经理批准”这种多级流。Flask 的灵活性在这种场景下会帮到你,因为你可以自由设计状态机,而不是被框架绑死。报表导出:领导最爱的“按部门统计人数”“年度离职率”“月度薪资汇总”。我建议不关用什么方案,直接后端生成 Excel 返回文件流,前端用 Blob 接收下载,效果稳定不容易被浏览器拦截。7.2 关于这个项目,我的最后几句心里话这个系统我从设计、编码到联调上线,前后花了两周多。如果说总结什么经验,我最想说的是:不要沉迷在技术炫技上——网上大把把系统搞得极其复杂的教程,又是微服务,又是各种消息组件,但一个企业内部人事系统,真正让用户觉得好用的,是“考勤判定准”“工资条不错”“查询不卡”。把基础模块打磨好,比堆什么高级技术都值钱。Flask 选型当初我一点也不后悔。它逼着我自己搞清楚每个组件的原理,而不是把行为都委托给框架。项目的结构清晰、代码可读性好,后来有个同事接手加了一个培训管理模块,他只看了半天代码就开始动手了,这就是模块化设计带来的好处。如果你要复现这个项目,照着文中的思路,先搭数据库,再写后端接口,然后衔接前端,这条路线是最顺的。不要在开始就纠结 UI 炫不炫,先把流程跑通,再逐步打磨。祝你在自己的项目里少踩点我踩过的坑,做出一套真正能用的系统。
阅读完成 · 觉得有帮助?