多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Python+Vue企业人力资源管理系统搭建与部署实战

Python+Vue企业人力资源管理系统搭建与部署实战 从零搭建一套Python Vue的企业人力资源管理系统前前后后折腾了大半个月市面上类似的成品源码不少但要么阉割得厉害要么文档一笔带过真正能落地跑起来还带完整权限体系的并不多见。这套系统虽然叫“人力资源管理系统”但涉及的技术栈跨度其实不小后端Python负责核心业务逻辑和接口服务前端Vue负责页面交互和状态管理中间还要处理好数据库表关系、跨域联调、权限控制这些绕不开的环节。如果你是正在自学Python/Vue的开发者或者手里恰好有一个人事管理的小项目需求这篇记录应该能帮你少走弯路。整套源码加数据库脚本加部署文档加起来不到几十兆但麻雀虽小五脏俱全员工管理、部门组织、考勤记录、薪资核算、招聘流程、系统用户与角色权限这些模块都有覆盖。下文就按我实际开发的顺序来拆解先讲清楚整体设计和技术选型为什么这样做再分别展开数据库、后端接口、前端页面三个大头最后把调试和部署过程中真实踩过的坑一一列出来。1. 内容整体设计与思路拆解1.1 为什么选Python Vue这个组合先说选型。企业级管理系统最怕的就是工期拖沓和技术栈太冷门导致后续没人维护。Python这边的生态成熟Django和Flask都能快速搭出稳定可靠的后端服务Vue在前端SPA开发里属于主流中的主流招聘成本低组件生态齐全配合Element UI这类桌面级组件库做人资管理系统这种表单密集、表格繁多的项目简直天然契合。后端我选的是Django。原因很直接Django自带Admin后台、ORM、认证体系和迁移工具这对“管理系统”这个场景是极大的加分项。人资系统最大的工作量往往不在炫酷的前端效果而在数据的增删改查、字段校验、关联查询和权限控制。Django的ORM能让我少写大量原生SQL而且模型类一旦定义好数据库表自动生成拿来开发这种业务系统非常适合。Flask虽然灵活但很多基础能力比如Admin、表单验证、多数据库迁移都要自己拼装在这种规模的系统里反而拖慢进度。前端选Vue 2 Element UI。有人会问为什么不用Vue 3实话说这套系统启动的时候Vue 3的生态还没完全成熟Element Plus也刚出来不久考虑到稳定性和教程的齐全程度Vue 2 Element UI是最稳妥的选择到今天依然能覆盖绝大多数管理系统的开发需求。Vuex管理登录状态和用户信息Vue Router负责页面路由和权限拦截axios统一处理HTTP请求这套组合在各类中小型后台项目中久经考验。1.2 系统功能模块拆解一套人资系统要覆盖哪些业务如果没做过很容易低估或漏项。我按企业人事日常操作的逻辑来拆组织架构部门的新增、修改、删除、停用部门与上级部门之间的层级关系。员工管理员工建档、部门调动、离职办理、员工信息检索和导出。考勤管理每日考勤记录维护、请假/加班申请、考勤统计。薪资管理基本工资、岗位工资、奖金的录入月度薪资汇总。招聘管理招聘计划发布、简历信息录入、面试记录跟踪。系统管理用户账号、角色、菜单权限的分配。这个功能划分参考了市面上主流开源人资项目的模块思路既有典型的CRUD操作也有部门树和权限分配这类稍微带点逻辑深度的内容。把每个模块对应的页面和数据表理清楚整个项目的骨架就出来了。1.3 技术方案的关键取舍技术上要解决的核心问题有三个认证授权怎么设计、部门层级怎么存储、前后端接口怎么约定。认证授权我用了JWT方案Django后端签发token前端每次请求带上token后端通过中间件解析并校验。相比Session方案JWT无状态、便于前后端分离部署也不需要额外维护Session存储。部门层级直接用自引用外键来解决一张部门表里加一个parent字段指向自己的主键前台展示时递归构建树形结构。这个方案简单直观性能上对中小规模系统完全够用。接口约定统一走RESTful风格前后端交互全部通过JSON。这样前端只要维护一套axios封装的请求工具后端每个视图函数对应一条接口路由调试的时候也容易定位问题。提示一开始规模不大的时候不要迷信微服务或者复杂的代码生成器。单体Django Vue SPA已经能扛住中小企业的并发量架构简单反而好维护等业务量大了再拆不迟。2. 数据库设计与模型关系2.1 核心数据表结构数据库选用MySQL原因是企业里MySQL普及率最高部署、备份、运维资料都齐全。系统涉及的数据库表格有以下核心几张表名说明关键字段sys_user系统用户id, username, password, role_id, statussys_role角色表id, role_name, remark, permissionsdept部门表id, parent_id, dept_name, leader, statusemployee员工表id, dept_id, name, gender, phone, email, position, hire_date, statusattendance考勤表id, employee_id, work_date, check_in, check_out, statussalary薪资表id, employee_id, base_salary, bonus, deduction, salary_month, totalrecruit_plan招聘计划表id, position, total_count, publish_date, requirement, statusresume简历表id, plan_id, name, phone, education, experience, status这几张表之间的关系非常清晰一个部门下挂多个员工一个员工有多条考勤和薪资记录一个招聘计划对应多份简历。系统用户与员工之间通过employee_id字段关联可以做到“一个系统账号绑定一个员工信息”也可以独立存在比如管理员账号仅用于后台管理不对应真实员工。2.2 Django模型定义示例用Django的ORM来定义核心模型代码简洁且可维护性高。这里以部门表和员工表为例from django.db import models class Dept(models.Model): 部门表 parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namechildren, verbose_name上级部门) dept_name models.CharField(max_length128, verbose_name部门名称) leader models.CharField(max_length64, nullTrue, blankTrue, verbose_name负责人) phone models.CharField(max_length20, nullTrue, blankTrue, verbose_name联系电话) status models.IntegerField(default1, verbose_name状态 1启用 0停用) create_time models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: db_table dept verbose_name 部门表 def __str__(self): return self.dept_name class Employee(models.Model): 员工表 dept models.ForeignKey(Dept, on_deletemodels.SET_NULL, nullTrue, related_nameemployees, verbose_name所属部门) name models.CharField(max_length64, verbose_name姓名) gender models.SmallIntegerField(choices((0, 女), (1, 男)), verbose_name性别) phone models.CharField(max_length20, nullTrue, blankTrue, verbose_name手机号码) email models.EmailField(nullTrue, blankTrue, verbose_name邮箱) position models.CharField(max_length128, verbose_name岗位) hire_date models.DateField(verbose_name入职日期) id_card models.CharField(max_length18, nullTrue, blankTrue, verbose_name身份证号) status models.IntegerField(default1, verbose_name在职状态 1在职 0离职) class Meta: db_table employee verbose_name 员工表需要注意两个细节部门自引用外键要用on_deletemodels.CASCADE但让parent字段允许为空否则顶级部门没办法创建员工表关联部门用SET_NULL这样部门被删掉的时候员工记录还在不会因为删部门连带把员工数据也抹掉这对真实人事系统特别重要。2.3 数据初始化与测试数据开发阶段最耽误时间的就是录入测试数据。我习惯写一个单独的初始化脚本在Django的management/commands下面自定义一个命令来批量生成演示数据而不是一条条往数据库里插。# app_name/management/commands/init_data.py from django.core.management.base import BaseCommand from app_name.models import Dept, Employee, SysUser import random import datetime class Command(BaseCommand): help 初始化演示数据 def handle(self, *args, **options): # 创建部门 tech_dept, _ Dept.objects.get_or_create( dept_name技术部, defaults{leader: 张三, phone: 13800000001} ) hr_dept, _ Dept.objects.get_or_create( dept_name人事部, defaults{leader: 李四, phone: 13800000002} ) # 创建员工 for i in range(20): Employee.objects.get_or_create( namef测试员工{i}, deptrandom.choice([tech_dept, hr_dept]), defaults{ gender: random.choice([0, 1]), position: random.choice([前端工程师, 后端工程师, HR专员]), hire_date: datetime.date(2022, random.randint(1, 12), random.randint(1, 28)), } ) self.stdout.write(self.style.SUCCESS(初始化数据完成))用这种脚本方式的好处是就算数据库被删了一条命令就能恢复基础演示环境。项目交付时源码里带上这样一个初始化模块接收方第一眼看到“初始化完成”就知道系统通了。3. 后端接口开发与核心实现3.1 JWT认证与登录接口后端接口我按“前段所有请求、除了登录和验证码之外都要校验token”的思路设计。用Django自带的Token其实也可以但JWT的无状态特性更适合前后端分离。具体实现用djangorestframework-simplejwt这个库配置简单# settings.py 部分配置 REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }登录逻辑很简单验证用户名密码后返回access token和refresh token前端把access token存到localStorage每次请求在header里带上Authorization: Bearer token。为了防止token过期导致用户无感退出我设置了过期时间24小时对后台管理系统来说一天一登录不算频繁。3.2 员工管理接口实现员工管理的核心接口是列表查询、新增、详情、修改、删除。列表接口要支持模糊搜索和分页否则员工多了以后页面加载会非常慢。from rest_framework.views import APIView from rest_framework.response import Response from app_name.models import Employee from app_name.serializers import EmployeeSerializer class EmployeeListView(APIView): def get(self, request): keyword request.query_params.get(keyword, ) page int(request.query_params.get(page, 1)) limit int(request.query_params.get(limit, 10)) queryset Employee.objects.all().order_by(-created_at) if keyword: queryset queryset.filter(name__icontainskeyword) total queryset.count() start (page - 1) * limit end page * limit data EmployeeSerializer(queryset[start:end], manyTrue).data return Response({ code: 0, data: data, total: total, })这里有个小技巧即使前端要求一次性展示所有数据后端也要强制分页。这不是为了省那点带宽而是防止某个用户误操作把全量数据加载到浏览器导致卡死。分页参数page和limit统一约定前端表格组件直接对接。3.3 考勤与薪资模块的设计逻辑考勤和薪资是人事系统里逻辑相对复杂的部分。考勤要做的是在页面上按日期展示员工打卡信息支持手动补录和批量导入。薪资则是每月按员工汇总工资明细薪资明细里包含了基本工资、绩效奖金、社保扣款、实发工资等字段。薪资计算这块可以稍微灵活一些后端提供一个“计算”接口读取员工基本信息和当月考勤数据按规则自动生成薪资记录生成后支持人工修改避免出现完全套公式导致特殊场景没法处理的情况。最简单的计算规则可以是实发工资 基本工资 绩效奖金 - (迟到次数 * 50) - 社保扣款。这些公式都放在后端一个独立的service文件里后续调整规则只改一处。# service/salary_service.py def calculate_salary(employee, year, month): attendance_list Attendance.objects.filter( employeeemployee, work_date__yearyear, work_date__monthmonth ) late_count attendance_list.filter(statuslate).count() base_salary employee.base_salary bonus employee.bonus social_security base_salary * 0.08 late_deduct late_count * 50 total base_salary bonus - social_security - late_deduct return { base_salary: base_salary, bonus: bonus, social_security: social_security, late_deduct: late_deduct, total: total, }实际项目中我发现薪资规则几乎每个月都可能微调比如临时加个高温补贴、节日福利等。所以千万别把薪资逻辑写死在视图函数里做成service类独立维护是更好的做法。3.4 权限控制与角色管理权限这块我采用的是RBAC模型基于角色的访问控制。系统初始化时创建三种角色超级管理员、人事专员、普通员工。每个角色在菜单权限表中勾选自己的可见菜单后端接口则通过装饰器或自定义权限类来限制访问from rest_framework.permissions import BasePermission class IsAdminPermission(BasePermission): message 只有管理员可以操作此功能 def has_permission(self, request, view): return request.user.role.role_code admin需要说明的是角色属性一开始可以挂在用户表上但如果后续要扩展多个角色给一个用户就需要改成多对多关联。开发初期为了省事用单角色字段没问题但要留好扩展空间。提示前端隐藏只是体验层面的保护真正的权限校验必须以接口为准。前端就算能把按钮渲染出来后端也必须拒绝无权限的请求这个原则贯穿了整个系统的所有接口。4. Vue前端页面与交互开发4.1 项目初始化与目录结构创建Vue项目我用的是官方Vue CLI。vue create hr-frontend然后安装Element UI和axios、vuex、vue-routercd hr-frontend npm install element-ui axios vuex vue-router目录按业务模块划分避免所有代码堆在components里难以维护src/ ├── api/ # 接口请求模块按业务拆分 │ ├── employee.js │ ├── salary.js │ └── login.js ├── components/ # 通用组件 ├── layout/ # 后台布局框架 ├── router/ # 路由配置 ├── store/ # Vuex状态管理 ├── views/ # 页面视图 │ ├── employee/ │ ├── attendance/ │ ├── salary/ │ └── system/ └── utils/ # 工具类api目录单独拆出来是我强烈建议的因为多个页面可能复用同一个接口。举个例子员工选择器组件需要调用员工列表接口薪资页面也需要按员工检索接口如果写两份请求代码一旦后端地址调整就要改两处。统一放api目录页面直接import函数省心得多。4.2 路由配置与登录拦截后台系统的路由一般有静态路由和动态路由之分。静态路由是登录页、404页这些无需登录就能访问的页面动态路由是根据用户角色生成的路由表比如普通员工看不到系统管理菜单。登录拦截的核心是路由守卫// router/index.js router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() } else { if (!token) { next(/login) } else { if (!store.state.userInfo) { store.dispatch(getUserInfo).then(() { next() }) } else { next() } } } })这里有个细节token存在本地不代表用户信息一定已经加载刷新页面后Vuex里的状态会丢失所以路由守卫里要判断store.state.userInfo是否为空为空则重新请求一次用户信息接口再放行。很多新手项目刷新后菜单消失大概率就是漏了这个判断。4.3 员工管理页面实现员工管理页面是标准的CRUD模板搜索栏、表格、分页、新增/编辑弹窗、删除确认。表格部分我用Element UI的el-table渲染数据状态字段用el-tag显示不同颜色的标签在职绿色、离职灰色操作栏放“编辑”“离职”“薪资详情”几个按钮。弹窗表单封装成独立组件EmployeeForm.vue父组件通过visible属性控制显示隐藏保存成功后刷新表格数据。表格列定义代码示例template el-table :datatableData v-loadingloading border stripe el-table-column propname label姓名 width100 / el-table-column propdeptName label所属部门 width120 / el-table-column propposition label岗位 width150 / el-table-column propphone label手机号 width140 / el-table-column prophireDate label入职日期 width120 / el-table-column label状态 width80 template slot-scopescope el-tag :typescope.row.status 1 ? success : info {{ scope.row.status 1 ? 在职 : 离职 }} /el-tag /template /el-table-column el-table-column label操作 min-width180 template slot-scopescope el-button typetext clickhandleEdit(scope.row)编辑/el-button el-button typetext clickhandleLeave(scope.row)离职/el-button /template /el-table-column /el-table /template表格加border和stripe属性能让大量数据行更好辨别操作列的按钮用文字型页面整体会更清爽。4.4 部门树与筛选联动部门树型结构是管理系统很常见的需求。部门选择器我用el-tree组件支持单选和多选。筛选联动逻辑是点击某个部门节点员工列表只显示该部门及其子部门的员工。前端处理树的关键是把后端返回的扁平列表转成树形结构。这一步可以在后端生成树结构返回也可以前端递归处理。我更倾向后端返回扁平列表、前端构建树因为部门数量一般不超过几百个前端递归轻松搞定后端只需查一次数据表即可function buildTree(list, parentId null) { const tree [] list.forEach(item { if (item.parentId parentId) { const children buildTree(list, item.id) if (children.length) item.children children tree.push(item) } }) return tree }注意处理树形结构时千万别在Vue里直接修改每个节点的数据否则会碰到Vue响应式系统对深层对象变更检测不到的问题。如果修改了节点属性但页面没变试试this.$set或者重新给数组赋一个新对象。4.5 axios封装与统一异常处理axios统一封装是后台系统必备的一步不然几百个接口散落各处改个baseURL都要全局搜索。我封装的核心逻辑包括请求头自动附带token、响应拦截处理业务状态码、特殊处理401过期跳转登录页// utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 0) { Message.error(res.msg || 请求错误) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(网络请求异常) return Promise.reject(error) } )这里编码时需要严格自律前端的业务状态码和后端约定为0表示正常非0则抛出错误提示HTTP状态401只在token失效或未登录时出现。不要搞两套状态码混杂的判断逻辑否则后期维护会非常难受。5. 常见问题与排查技巧实录5.1 跨域问题排查开发阶段最常遇到的第一个大坑就是跨域。前端在localhost:8080运行后端服务在localhost:8000浏览器默认会拦截非同源的请求。解决办法有两种。最简单的调试办法是在Django后端安装django-cors-headers配置允许的域名列表# settings.py INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:8080, ]生产部署阶段更推荐用Nginx反向代理把前端静态文件和后端接口放在同一个域名下彻底规避跨域。比如/api/开头的请求转发到后端服务其他请求返回前端静态文件。跨域问题如果在生产环境还没解决每次访问都要带着OPTIONS预检请求网络开销会明显增大。5.2 token失效与重复登录另一个高频问题是登录后很快又跳回登录页。排查思路分两种一是token过期时间配置得太短比如把ACCESS_TOKEN_LIFETIME设置成了5分钟用户操作稍微一慢就过期二是前端axios拦截器里把任意非200状态码都当成未登录导致后端返回参数错误也清了token。解决办法是后端返回401才清理本地token其余错误正常弹出提示。另外如果前端要静默刷新token可以在响应拦截器里捕获token过期调用refresh接口换新token后重新发起原请求这个机制加上之后基本不用再频繁登录了。5.3 日期格式处理问题前后端日期传递也是容易踩坑的点。Django的DateField序列化出来是一个字符串比如2024-01-15前端直接展示没问题但传到el-date-picker组件的value-formatyyyy-MM-dd必须显式设置否则组件内部Date对象和字符串之间转换会出错。建议后端序列化时统一把日期输出成yyyy-MM-dd格式接口层就约定好前端不再做二次转换。5.4 数据备份与恢复开发过程中我吃过一次大亏改表结构时不小心删掉了一张测试表半天数据没了。之后我养成了每次改动前先mysqldump备份的习惯。项目交付时也要在文档里写清楚数据库备份恢复的完整命令# 备份 mysqldump -u root -p hr_system hr_system_backup.sql # 恢复 mysql -u root -p hr_system hr_system_backup.sql5.5 常见问题速查表问题现象可能原因解决建议前端请求接口报403未带token或token过期检查axios拦截器是否附加Authorization头员工列表为空但数据库有数据接口未加分页或查询条件过滤太严清空关键字参数重试部门树无法展开后端parent_id字段返回类型不一致统一返回数字类型薪资计算结果不对考勤状态枚举值与计算逻辑不匹配核对状态码映射关系刷新页面后菜单消失Vuex状态丢失路由守卫中补充getUserInfo请求前端启动端口占用8080被其他项目占用修改vue.config.js的devServer端口6. 系统部署与项目交付6.1 前后端分离部署流程系统开发完成后部署我建议用最简单稳定的方案后端Gunicorn跑Django服务前端打包成静态文件交给Nginx托管。# 后端 pip install gunicorn gunicorn hr_system.wsgi:application -w 4 -b 0.0.0.0:8000 # 前端构建 npm run build # 构建产物在 dist/ 目录拷贝到服务器指定目录Nginx配置核心部分server { listen 80; server_name your-domain.com; location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root /var/www/hr-frontend; try_files $uri $uri/ /index.html; } }try_files那行必须写因为Vue Router在history模式下前端路由跳转到/employee时刷新页面会导致404这行配置让所有请求都回退到index.html由前端路由接管。6.2 部署时的静态文件与媒体文件处理Django的Admin后台和接口返回的图片上传功能都需要静态文件服务。生产环境一定不要让Django直接处理静态文件。收集静态文件的命令要提前执行python manage.py collectstatic然后在Nginx里增加对应的location配置让Nginx直接负责静态文件不要把这些请求转发给Gunicorn。员工头像、简历附件这类用户上传的媒体文件单独配置一个/media/路径的alias否则接口能返回数据但图片永远加载不出来。6.3 项目文档与交付清单这套系统和源码配套的文档我按“部署指南”、“用户手册”、“开发说明”三部分整理。交付时需要保证以下材料齐全完整源码前端后端数据库初始化SQL和备份文件环境依赖清单requirements.txt package.json部署文档尽可能细化到每一行命令的执行结果演示账号与密码明确说明权限范围文档最容易被忽视但也最重要的是环境版本号Python 3.8、Django 3.2、MySQL 8.0、Node 16这些版本一旦不匹配接收方复现时就会遇到一堆莫名其妙的依赖冲突。把这些版本写清楚能减少一半的售后问题。7. 开发体会与扩展方向7.1 一些实际问题个人看法这套Python Vue的人资管理系统做下来我个人的体会是这类管理系统最大的门槛不在于功能做不出来而在于功能之间的数据和权限组织关系。员工表、部门表、考勤表、薪资表看着独立实际上每个操作都互相牵连。开发时我坚持了几个习惯数据库字段命名统一带前缀比如所有表都带create_time接口返回结构统一为{code, msg, data}前端每个模块的页面操作后必须刷新列表数据。这些约定看似琐碎但多模块协作开发时极其重要。实际开发中我也是不断体会到Django和Vue组合的优势。Django的ORM遇到复杂查询比如按月统计某部门的总薪资几分钟就写出来了Vue的组件化让员工表单、部门树这类组件复用率非常高三个页面用同一个组件的情况很常见。所以如果你也在考虑用这套组合做后台系统可以放心往下推进。7.2 后续扩展方向这套系统在架构上预留了不少扩展空间后面如果业务需要优先可以考虑这几个方向第一个是更多维度的统计分析。目前薪资、考勤数据都有了加一个可视化面板比如用ECharts展示月度成本趋势和各部门人数分布会让人事决策更直观这类图表组件和Vue集成非常顺手。第二个是流程审批功能。请假、调薪、离职都可以走审批流。要给每个流程配置独立的审批节点Django后端存流程模板和审批记录两张表前端新增一个待办中心模块。第三个是定时任务集成。比如每月1号自动生成上月的考勤汇总和下月的薪资初始数据只需在Django里接入django-celery-beat写两个定时任务业务上可以减少人事专员的大量手工操作。如果真要上生产环境还有两件事不能省一是引入操作日志记录记录每个用户对关键数据的增删改查二是给服务器配置每日自动备份。这些不属于功能开发但直接关系到系统的安全性和可用性。先把基础打好再根据实际业务场景慢慢迭代功能系统的生命力比一次性写完更长久。
返回列表