☰
Django与Flask选型实战:基于Python+Vue的人事管理系统开发指南
2026/10/14 17:01:04 网站建设 项目流程

很多团队接到“做一个企业人事管理系统”这种需求时,第一反应就是打开IDE开始写登录和员工表。等项目代码堆到一定程度,才意识到权限怎么收敛、考勤和薪资怎么和员工档案联动、前端到底该用什么框架配合后端。这篇文章想分享的不是某个成品项目源码,而是从零搭建一套基于Python的Django后端 + Vue前端的人事管理系统时,我实际走的完整路径,包括框架选型、目录结构、接口设计、跨域联调和部署上线的关键细节。如果你正打算用Django或Flask配合Vue做类似系统,并且习惯在PyCharm里开发,这篇文章应该能帮你少踩不少我踩过的坑。

我会以Django + Django REST Framework(DRF)+ Vue3 + Element Plus作为主线讲。不是因为Flask不好,而是做完对比你会发现,人事系统这种业务形态对Django的“全家桶”特性依赖度很高。标题里既然同时提到了django和flask,我会专门用一章分析两者的取舍,顺便告诉你什么情况下才值得换用Flask。

1. 人事系统到底在管什么:先拆需求再谈技术

很多人上来就写代码,结果做着做着发现“员工”这个实体和“用户”纠缠不清,“考勤记录”不知道挂在员工还是部门下。所以第一步不是建项目,而是把人事管理的核心域拆清楚。

1.1 人事管理系统的四大核心业务域

不管公司规模多大,人事系统的数据模型几乎都围绕四个域展开:

  • 组织架构域:部门、岗位、职级。部门是树形的,岗位挂在部门下,员工挂在岗位上。
  • 员工档案域:员工基本信息、入职日期、转正日期、合同信息、教育经历、工作经历。这个域是系统的心脏,考勤、薪资、绩效都要引用它。
  • 考勤域:打卡记录、请假申请、加班申请、出差记录。考勤数据往往要按月度汇总,和薪资计算联动。
  • 薪资域:基本工资、绩效、补贴、社保公积金扣除。这个域的数据非常敏感,权限控制必须单独做。

我见过一个常见的败笔设计:把“用户表”当成“员工表”用。登录账号确实是员工的一种属性,但员工有大量和登录无关的信息,比如学历、紧急联系人、入职来源。正确做法是User表管认证,Employee表管人事档案,两者通过OneToOneField关联。这样以后做绩效模块,不需要关心员工是怎么登录系统的。

1.2 角色权限模型:RBAC是及格线

人事系统天然多角色:系统管理员、HR专员、部门主管、普通员工。不同角色看到的数据范围完全不同。

普通员工只能看自己的档案、自己的考勤、自己的工资条。部门主管能看本部门员工的请假审批。HR专员能看全公司的员工档案,但薪资明细只有HR经理和系统管理员能看。这就是典型的RBAC(基于角色的访问控制):User → Role → 权限点。权限点建议细化到“视图级”而不是“菜单级”,比如employee:detail:salary,表示查看员工薪资详情的权限。

在Django里,我一般不直接用django.contrib.auth自带的Group和Permission裸做,而是定义一个Role模型,然后在视图层封装一个权限检查装饰器或DRF的权限类,控制每个API能被谁访问。

1.3 MVP版本的功能边界

做个能用的MVP,别一上来就把所有模块都铺开。我建议第一版只做四件事:

  1. 登录认证 + 用户角色分配
  2. 部门管理(树形结构)
  3. 员工档案CRUD + 按部门/岗位筛选 + 分页
  4. 考勤打卡记录和月度汇总

薪资模块可以放第二期。为什么?因为薪资涉及大量Excel导入导出、计算公式审核、安全审计,逻辑复杂度远超想象。MVP先把组织架构和员工档案打扎实,薪资模块接进来的时候才不会推倒重来。

2. Django和Flask怎么选:两个框架的真实差距

先回应一下标题里提到的“django flask”二选一。我两个都做过商业项目,差距不是“能不能做”的问题,而是“做到什么程度需要多少成本”的问题。

2.1 Django是自带引擎的整车,Flask是手动组装的零件箱

Django默认就给了你:

  • 自带ORM,迁移工具makemigrations和migrate可以管理数据库变更
  • 自带Admin后台,几乎所有人事系统的“基础数据维护”界面都靠它快速搞定
  • 自带认证框架,扩展一下就能做JWT
  • 自带表单和序列化能力,配合DRF后API开发效率非常高

Flask则默认什么都没有。你要自己选SQLAlchemy还是peewee,自己配Alembic做迁移,自己找Flask-Login或PyJWT处理认证。数据库表一多,手工维护迁移脚本会让人崩溃。

2.2 为什么人事系统更需要Django

人事系统的特点是:数据模型多、关联关系密集、权限维度复杂。拿员工档案来说,一个Employee表至少关联部门、岗位、学历、合同、家庭成员、紧急联系人。Django的ORM可以随手写:

employee = Employee.objects.select_related("department", "position").prefetch_related("educations")

一条链式调用完成多表关联查询。写Flask+SQLAlchemy也能实现,但你需要自己注意查询时机、懒加载、序列化时是否触发N+1查询,心智负担明显更重。

另外Django自带一个非常实用但常被低估的Admin后台。人事系统里那些“改个部门名称”“调整一下岗位层级”的小需求,HR自己就能在Admin里完成,不用每次找开发改代码。

2.3 Flask适合什么场景

Flask并不是没有价值。如果你的项目只是做一个轻量级的员工请假API,总共四五张表,没有复杂的权限模型,而且团队更熟悉Flask,那用Flask完全没问题。它可以写成非常干净的单文件API服务。可一旦业务超过“作坊级”,开始有多个模块、多级权限、复杂报表,Flask的灵活就变成自由的代价——你必须在框架之外的“约定”上下更多功夫。

我做这套人事系统时选Django+DRF,核心判断是:业务复杂性最终取决于团队的时间和维护成本,而Django把最常见的Web层场景全都内置了,剩下的时间可以花在业务本身。

3. PyCharm环境搭建与依赖管理:别在第一步就埋雷

3.1 为什么用PyCharm Professional而不是社区版

PyCharm Community是免费的,但它不支持Django模板语法高亮,不内置数据库工具窗口,也没有专业的Flask/Django运行配置。做企业人事系统这种全栈项目,PyCharm Professional里的Database面板帮了大忙:可以直接看SQLite/Mysql的表数据、执行SQL、可视化关系,不用反复在浏览器和命令行之间切换。

如果你是学生或在开源项目上有贡献,可以留意一下官方针对教育/开源者的授权政策。再不济,社区版配合VS Code做前端,也不是不能跑,就是开发体验割裂一点。

3.2 虚拟环境是底线

创建Django项目前,一定先建虚拟环境。我见过太多人把依赖直接装到全局环境中,项目换台电脑就跑不起来。

# 在PyCharm的Terminal中,先确认虚拟环境,直接新建项目时勾选 Virtualenv python -m venv venv # Windows激活方式 venv\Scripts\activate # Linux/macOS激活方式 source venv/bin/activate

激活后命令行前面会出现(venv)标记,这时候再装依赖才干净。后续在PyCharm的Settings -> Project -> Python Interpreter里可以校验解释器路径是否指向venv目录。

3.3 requirements.txt 的版本锁定策略

我在项目的requirements.txt里放了这些依赖,这个组合在生产环境实测稳定:

Django==4.2.9 djangorestframework==3.14.0 django-cors-headers==4.3.1 PyJWT==2.8.0 mysqlclient==2.2.0 python-dotenv==1.0.0 django-filter==23.5 gunicorn==21.2.0

几个重点说明一下:

  • Django版本选4.2LTS,不要追最新的5.x激进版。LTS意思是长期支持,人事系统这种企业项目稳定压倒一切。
  • django-filter必须装,人事系统的员工列表必然要按部门、岗位、入职时间范围做组合筛选。
  • mysqlclient在Windows上编译经常会失败,建议提前装好VC构建工具,或者改用pymysql并在__init__.py里做兼容:
import pymysql pymysql.install_as_MySQLdb()
  • 开发阶段用默认的SQLite就够了,部署上线再切换MySQL。两个数据库的数据类型差异可能带来迁移问题,所以上线前一定要在MySQL上完整跑一遍迁移。

3.4 新建项目时目录结构的坑

用PyCharm直接File -> New Project创建Django项目,默认生成的目录结构其实不够用。一个中等规模的人事系统,我建议至少拆成下面这样:

hr_system/ ├── manage.py ├── requirements.txt ├── config/ # 项目配置目录 │ ├── __init__.py │ ├── settings/ │ │ ├── base.py # 公共配置 │ │ ├── dev.py # 开发配置 │ │ └── prod.py # 生产配置 │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── accounts/ # 用户、角色、认证 │ ├── organization/ # 部门、岗位 │ ├── employee/ # 员工档案 │ └── attendance/ # 考勤 └── frontend/ # Vue项目独立目录,后面细说
  • apps目录里放多个子应用,而不是把员工、考勤、薪资全塞进一个app里。Django一个app只干一件事,这是最佳实践。
  • settings拆成多个文件为的是开发和生产环境用不同配置。dev.py里DEBUG=True,prod.py强制DEBUG=False。

4. 后端接口设计:数据模型、序列化与JWT认证

4.1 核心数据模型设计

从实际业务出发,我们至少要设计这样几个模型。我直接给出精简版代码,你可以在PyCharm里动手敲一遍:

# apps/organization/models.py from django.db import models class Department(models.Model): name = models.CharField(max_length=64, unique=True, verbose_name="部门名称") parent = models.ForeignKey( "self", on_delete=models.CASCADE, null=True, blank=True, related_name="children", verbose_name="上级部门", ) leader = models.ForeignKey( "employee.Employee", on_delete=models.SET_NULL, null=True, blank=True, related_name="leading_department", verbose_name="部门负责人", ) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): return self.name

注意这里用的是"employee.Employee"字符串引用,因为Employee模型在另一个app里,这样写避免循环导入。

# apps/employee/models.py from django.db import models from django.contrib.auth.models import User class Employee(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="employee_profile") emp_no = models.CharField(max_length=20, unique=True, verbose_name="工号") name = models.CharField(max_length=32, verbose_name="姓名") gender = models.CharField(max_length=4, choices=[("male", "男"), ("female", "女")], default="male") department = models.ForeignKey("organization.Department", on_delete=models.SET_NULL, null=True, related_name="employees") position = models.CharField(max_length=64, blank=True, verbose_name="岗位") hire_date = models.DateField(verbose_name="入职日期") phone = models.CharField(max_length=20, blank=True) email = models.EmailField(blank=True) emergency_contact = models.CharField(max_length=32, blank=True, verbose_name="紧急联系人") emergency_phone = models.CharField(max_length=20, blank=True, verbose_name="紧急联系人电话")

关于on_delete策略,部门被删掉时,员工不能跟着被删,应该用SET_NULL并允许部门字段为空。用户被删时,员工档案反而需要保留,因为工资历史、考勤历史还挂在这个员工上,所以User的删除要谨慎,通常开发时我会在User模型上加一个is_active=False的软删除逻辑,而不是物理删除。

考勤模型:

# apps/attendance/models.py class AttendanceRecord(models.Model): employee = models.ForeignKey("employee.Employee", on_delete=models.CASCADE, related_name="attendance_records") work_date = models.DateField(verbose_name="日期") check_in_time = models.DateTimeField(null=True, blank=True, verbose_name="上班打卡") check_out_time = models.DateTimeField(null=True, blank=True, verbose_name="下班打卡") status = models.CharField( max_length=16, choices=[ ("normal", "正常"), ("late", "迟到"), ("early", "早退"), ("absent", "缺勤"), ], default="normal", )

一个很实用的约束:Meta类里加上unique_together = ("employee", "work_date"),保证同一天同一人只能有一条考勤记录。

4.2 DRF序列化器与视图的坑

模型建好之后,用DRF写序列化器:

# apps/employee/serializers.py from rest_framework import serializers from .models import Employee class EmployeeSerializer(serializers.ModelSerializer): department_name = serializers.CharField(source="department.name", read_only=True) username = serializers.CharField(source="user.username", read_only=True) class Meta: model = Employee fields = ["id", "emp_no", "name", "gender", "department", "department_name", "position", "hire_date", "phone", "email", "username"] read_only_fields = ["id"]

视图直接用ModelViewSet可以快速拿到标准的CRUD接口:

# apps/employee/views.py from rest_framework import viewsets, filters from django_filters.rest_framework import DjangoFilterBackend from .models import Employee from .serializers import EmployeeSerializer class EmployeeViewSet(viewsets.ModelViewSet): queryset = Employee.objects.select_related("department", "user").all() serializer_class = EmployeeSerializer filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields = ["department", "gender"] search_fields = ["name", "emp_no", "phone"] ordering_fields = ["hire_date", "emp_no"]

select_related一定要在queryset里写,否则前端每次获取员工列表,都会额外产生N次查询,这就是著名的N+1查询问题。列表接口性能慢,十次有八次是这个原因。

4.3 JWT认证与登录接口实战

前后端分离后,Session认证就不太方便了。我用PyJWT自己写了一个轻量认证模块,避免引入复杂的认证框架。

先用base64生成用户Token:

# apps/accounts/auth.py import jwt from django.conf import settings from datetime import datetime, timedelta def generate_token(user): payload = { "user_id": user.id, "username": user.username, "exp": datetime.utcnow() + timedelta(hours=12), "iat": datetime.utcnow(), } return jwt.encode(payload, settings.SECRET_KEY, algorithm="HS256") def decode_token(token): try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"]) return payload except jwt.ExpiredSignatureError: return None

登录视图:

# apps/accounts/views.py from django.contrib.auth import authenticate from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .auth import generate_token class LoginView(APIView): def post(self, request): username = request.data.get("username") password = request.data.get("password") user = authenticate(username=username, password=password) if not user: return Response({"detail": "用户名或密码错误"}, status=status.HTTP_401_UNAUTHORIZED) token = generate_token(user) return Response({"token": token, "username": user.username})

DRF全局权限配置:

# config/settings/dev.py REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": [ "apps.accounts.auth.JWTAuthentication", ], "DEFAULT_PERMISSION_CLASSES": [ "rest_framework.permissions.IsAuthenticated", ], }

这里要写一个JWTAuthentication类继承DRF的BaseAuthentication,在authenticate方法里解析Header中的Authorization: Bearer <token>。

4.4 权限控制的三个层级

人事系统光靠IsAuthenticated远远不够。我做了三个层级的权限:

  • 登录即可访问:如修改自己的密码
  • 登录且角色匹配:如HR专员才能创建员工档案
  • 登录且数据范围匹配:如部门主管只能审批本部门的请假单

DRF自带的BasePermission可以这样写:

# apps/accounts/permissions.py from rest_framework.permissions import BasePermission class IsHRAdmin(BasePermission): def has_permission(self, request, view): role = getattr(request.user, "role", None) return role in ["hr_admin", "super_admin"] class IsDepartmentManager(BasePermission): def has_permission(self, request, view): role = getattr(request.user, "role", None) return role in ["manager", "hr_admin", "super_admin"]

在具体视图上叠加权限类:

from apps.accounts.permissions import IsHRAdmin class EmployeeViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated, IsHRAdmin]

数据范围的控制要再写一层逻辑,比如部门主管的queryset要过滤成department__leader=request.user.employee,这部分逻辑可以放在get_queryset方法里覆写。权限和过滤不要混在同一个类的同一个方法里,拆开维护会清晰很多。

5. Vue3前端开发:从登录页到员工管理页

后端接口就绪后,开始前端工程。我用的是Vue3 + Vite + Element Plus + Pinia + Vue Router的组合。这套组合是当下Vue生态里最适合做后台管理系统的,组件库现成,状态管理简洁,构建速度快。

5.1 前端项目初始化

在frontend目录下执行:

npm create vite@latest frontend -- --template vue cd frontend npm install element-plus @element-plus/icons-vue axios pinia vue-router

Vite创建的项目结构很干净。我习惯再手动加几个目录:

frontend/src/ ├── api/ # 按后端模块拆分的API请求 │ ├── auth.js │ ├── employee.js │ └── department.js ├── views/ # 页面组件 │ ├── Login.vue │ ├── EmployeeList.vue │ └── DepartmentTree.vue ├── router/index.js ├── stores/user.js ├── utils/request.js └── App.vue

5.2 axios请求封装是前端的重中之重

不封装axios,直接在每个组件里写axios.get(),Token失效时你就知道什么叫连环爆炸。我会在utils/request.js里做统一处理:

import axios from "axios"; import { ElMessage } from "element-plus"; import router from "../router"; const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || "/api", timeout: 10000, }); request.interceptors.request.use((config) => { const token = localStorage.getItem("token"); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); request.interceptors.response.use( (response) => response.data, (error) => { if (error.response && error.response.status === 401) { localStorage.removeItem("token"); router.push("/login"); } ElMessage.error(error.response?.data?.detail || "请求失败"); return Promise.reject(error); } ); export default request;

这个封装的核心是统一处理401跳转和错误提示,页面代码里就不需要到处写try...catch了。

5.3 登录页与路由守卫

登录页的代码不复杂,关键是登录成功后要把用户信息和token写入Pinia和localStorage:

<script setup> import { ref } from "vue"; import { useRouter } from "vue-router"; import { login } from "../api/auth"; import { useUserStore } from "../stores/user"; const router = useRouter(); const userStore = useUserStore(); const username = ref(""); const password = ref(""); const loading = ref(false); async function handleLogin() { if (!username.value || !password.value) return; loading.value = true; try { const res = await login({ username: username.value, password: password.value }); localStorage.setItem("token", res.token); userStore.setUser(res.username); router.push("/employee"); } finally { loading.value = false; } } </script>

路由守卫要求每个需要登录的页面都检查token:

router.beforeEach((to, from, next) => { const token = localStorage.getItem("token"); if (to.path !== "/login" && !token) { next("/login"); } else { next(); } });

5.4 员工列表页:表格、分页、弹窗表单

员工列表是整个系统最核心的页面。Element Plus的el-table配合el-pagination基本是标配。关键点在于后端接口要传哪些参数:

async function fetchEmployees(page = 1) { const params = { page: page, page_size: pageSize.value, department: currentDepartment.value || undefined, search: keyword.value || undefined, ordering: "hire_date", }; const res = await getEmployees(params); employees.value = res.results; total.value = res.count; }

一个容易踩的坑是后端DRF的分页返回格式。ModelViewSet配合默认分页类时,返回结构是{"count": 100, "next": "...", "previous": null, "results": [...]}。前端取数据时一定要用res.results和res.count,别直接拿整个response当数组。

表单弹窗我建议用el-dialog包一个el-form。提交时用Promise.all做表单校验和接口请求分离:

async function handleSubmit() { await formRef.value.validate(); const payload = { ...form.value }; if (form.value.id) { await updateEmployee(form.value.id, payload); } else { await createEmployee(payload); } ElMessage.success("保存成功"); dialogVisible.value = false; fetchEmployees(); }

保存成功后立刻刷新列表,这个体验小但很重要。

6. 前后端联调:跨域、字段命名与常见报错排查

6.1 跨域问题:Django的CORS配置

开发阶段前端跑在http://localhost:5173,后端跑在http://localhost:8000,两者端口不同,浏览器会拦截跨域请求。解决方案是装django-cors-headers,然后配置:

# config/settings/dev.py INSTALLED_APPS = [ # ... "corsheaders", ] MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", # 注意放在CommonMiddleware之前 ] CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", "http://127.0.0.1:5173", ]

一个非常容易忽略的细节:CorsMiddleware的位置要放在CommonMiddleware上面,否则预检请求OPTIONS可能拿不到CORS头。我在这里卡了整整一下午,最后才注意到中间件顺序的问题。

6.2 前后端字段命名风格的对接

后端遵循Python的惯例,字段名用snake_case,比如hire_date、emergency_contact。前端如果直接用v-model="form.hire_date"倒也相安无事。麻烦的是后端某些字段返回值是个对象,比如部门字段:

{ "department": 3, "department_name": "技术部" }

前端表格展示时要用department_name,但表单提交时要提交department的id值。处理方式是表单里用el-select绑定department的id,表格列用department_name展示。这个映射关系写清楚后,后面加字段就不容易混乱。

6.3 请求400/500的排查思路

联调阶段最常见的错误就是400 Bad Request和500 Internal Server Error。我总结出一套排查顺序:

  1. 打开浏览器开发者工具Network面板,确认请求URL、请求方法、请求体。
  2. 看后端日志。Django开发服务器会在控制台打印异常堆栈。
  3. 400错误多半是序列化器校验不过,响应体会返回具体字段的错误,比如{"hire_date": ["Enter a valid date."]}。这是因为前端传了"2025-03-15T08:00:00"这种带时间部分的字符串,而后端期望的是"2025-03-15"。解决办法是前端form里日期控件用format="YYYY-MM-DD"。
  4. 500错误多半是后端代码有问题。常见的是字段名打错TypeError,或者数据库约束冲突。把堆栈贴出来基本一眼定位。

6.4 一套实测有效的联调技巧

在PyCharm的后端日志里加一条中间件,把请求时长打出来,这样明显能感知到哪些接口慢:

# app_middleware.py import time from django.utils.deprecation import MiddlewareMixin class LoggingMiddleware(MiddlewareMixin): def process_request(self, request): request._start_time = time.time() def process_response(self, request, response): total = time.time() - getattr(request, "_start_time", time.time()) if total > 0.5: print(f"[SLOW] {request.method} {request.path} took {total:.2f}s") return response

这个中间件对线上性能排查也很实用,我至今还留着。

7. 从开发机到服务器:部署上线要调整哪些配置

7.1 生产环境的关键开关

部署时最容易犯的错是把settings/dev.py直接带到生产环境。至少要改这几项:

# config/settings/prod.py DEBUG = False ALLOWED_HOSTS = ["你的域名或服务器IP"] INSTALLED_APPS += ["whitenoise.runserver_nostatic"] MIDDLEWARE = ["whitenoise.middleware.WhiteNoiseMiddleware"] + MIDDLEWARE STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")

Whitenoise是解决Django静态文件服务的标准方案,它让Django在没有Nginx辅助的情况下也能高效吐出静态文件。但这只适合轻量场景,正式项目仍建议用Nginx托管静态文件。

数据库从SQLite切MySQL:

DATABASES = { "default": { "ENGINE": "django.db.backends.mysql", "NAME": "hr_system", "USER": "hr_user", "PASSWORD": os.getenv("DB_PASSWORD"), "HOST": "127.0.0.1", "PORT": "3306", "OPTIONS": { "charset": "utf8mb4", }, } }

utf8mb4是中文存储的关键,否则生僻字和表情符号会乱码。连接数据库之前,一定要先在MySQL里创建好数据库和账号,并授权给该账号。Django的migrate不会帮你建库。

7.2 Gunicorn + Nginx的标准部署流

部署命令我建议写成一行,方便后续重启:

gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers 3 --timeout 60 --access-logfile -

3个worker对应小型团队的并发足够。--timeout 60是为了防止某些慢查询被Gunicorn误杀。

Nginx配置里,前端构建后的dist目录作为静态资源根目录,接口请求反向代理到8000端口:

server { listen 80; server_name your-domain.com; root /var/www/hr_system/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /static/ { alias /var/www/hr_system/staticfiles/; } location /media/ { alias /var/www/hr_system/media/; } }

try_files $uri $uri/ /index.html这行特别重要,没有它,前端路由跳转后刷新页面会404。

7.3 关于安全审计的一个提醒

人事系统的数据比普通业务系统敏感得多,上线前至少检查三件事:

第一,Admin后台没改路径,默认的/admin会被扫描器盯上,稳妥的做法是自定义Admin URL。

第二,员工薪资字段在API里是否暴露给了不该看的角色。建议薪资单独一个序列化器,只有HR经理权限才返回薪资字段。

第三,MySQL备份策略。用脚本每天凌晨自动mysqldump一次,保留七天,备份文件加密后存到另一台机器或对象存储。万一真出了数据误删事故,这份备份能救命。

我做这套系统最大的体会是,人事管理系统本身不难,难的是把所有边界情况都想到:部门被删除后员工怎么办,员工离职后账号怎么冻结,考勤记录被别人误删怎么恢复。这些细节决定了系统的可用性,也决定了一个开发者从“能跑通Demo”到“能交付生产系统”之间的距离。如果你正卡在某个报错上,或者犹豫Django和Flask的选型,希望这篇文章能帮你缩短一点纠结的时间。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询