ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django实战:制造企业绩效管理系统的设计与部署

Django实战:制造企业绩效管理系统的设计与部署 简介面向制造行业企业管理需求这套基于Django框架开发的内部绩效管理系统提供了用户认证授权、多维度绩效评估、目标设定与跟踪、报告与数据分析、反馈沟通等核心模块覆盖员工绩效指标配置与进度监控等典型业务场景。资源包共424个文件、约2.48MB其中247个JavaScript文件用于前端交互63个Python文件涉及Django视图、模型与表单逻辑26个HTML模板组织页面结构另有CSS、SVG、字体文件及nginx.conf等部署配置前后端结构完整便于本地运行与二次开发。已有129人学习下载。借助Django的MVT架构系统代码分层清晰适合计算机专业学生作为毕业设计、课程项目或Web开发练手素材也可供企业技术团队参考权限管理、评估模型与API集成思路。资源包含完整工程目录可直接在本地配置运行是理解企业管理业务与Django实战结合的典型案例。1. 从“考核靠Excel”到Django绩效系统制造企业为什么需要重写一遍制造行业车间绩效管理一直靠Excel和纸质单据Django这类Web框架进入企业视野后最先解决的不是打分而是规则和权限的闭环。班组长月底收表、车间主任二次调整、人事部再录入这套流程在几十人规模时勉强能转一旦涉及多车间、多班组、多指标权重Excel的版本冲突和公式覆盖就会让月度考核变成对账现场。用Django重写一套企业内部绩效管理系统核心收益是“规则可执行”谁能在什么周期给谁打分、权重怎么计算、报表怎么导出都从口头约定变成代码约束。适合两类人一类是制造企业信息科想替换人工考核流程另一类是Python学习者想找一个能完整讲清MVT、ORM、权限和报表导出的项目。接下来按一个可运行系统的拆解顺序展开。2. 数据模型与权限先定义制造车间的考核维度再谈MVT落地2.1 用Django创建app把绩效域和业务模块划分开搭建Web项目时很多入门项目把全部代码放在一个app里Model一多就开始混乱。常见做法是用django-admin startproject performance创建工程后再执行命令创建两个appassessment负责绩效核心数据accounts负责用户和角色扩展。设计上不要直接改Django自带的User表而是用OneToOne扩展Profile。# 项目根目录下创建两个app python manage.py startapp assessment python manage.py startapp accountsstartapp是Django内置命令执行后自动生成models.py、views.py、admin.py等骨架文件。app名使用小写和下划线不要用驼峰因为ORM反向查询时下划线命名更顺手。创建后需要把两个app加入INSTALLED_APPS否则执行makemigrations时会报AppRegistryNotReady。这个拆分决定了后续权限模块和考核模块不互相引用具体表结构只通过外键关联便于独立测试。2.2 核心Model设计绩效指标、考核周期与评分记录制造行业考核维度常见的是产量完成率、一次交检合格率、设备点检、5S、纪律。设计Model时不要把所有维度做成字段否则后期加考核项必须改表。正确做法是“指标表 分数记录表”的纵向结构每个员工在每个考核周期下可以有多条指标得分记录。from django.db import models from django.contrib.auth.models import User class Department(models.Model): name models.CharField(max_length50, uniqueTrue) leader models.ForeignKey(User, nullTrue, on_deletemodels.SET_NULL, related_namedept_leader) def __str__(self): return self.name class KpiIndicator(models.Model): code models.CharField(指标编码, max_length20, uniqueTrue) name models.CharField(指标名称, max_length50) weight models.DecimalField(权重, max_digits5, decimal_places2) category models.CharField(分类, max_length20, choices[(output, 产量), (quality, 质量), (safety, 安全)]) enabled models.BooleanField(启用, defaultTrue) def __str__(self): return f{self.code} {self.name} class AssessmentPeriod(models.Model): name models.CharField(周期名称, max_length50) start_date models.DateField() end_date models.DateField() status models.CharField(状态, max_length10, choices[(draft, 草稿), (published, 已发布), (closed, 已关闭)], defaultdraft) class Meta: ordering [-start_date] class PerformanceScore(models.Model): period models.ForeignKey(AssessmentPeriod, on_deletemodels.PROTECT, verbose_name考核周期) employee models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name被考核人) indicator models.ForeignKey(KpiIndicator, on_deletemodels.PROTECT, verbose_name考核指标) score models.DecimalField(得分, max_digits5, decimal_places2) scorer models.ForeignKey(User, on_deletemodels.PROTECT, related_namescored_records, verbose_name评分人) comment models.CharField(评分说明, max_length200, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (period, employee, indicator)字段类型用途注意点weightDecimalField指标权重不要用FloatField累加计算时浮点误差会留下烂账on_deletePROTECT外键约束防止误删指标/周期存在评分记录时禁止删除unique_together联合唯一同一个人同一指标只允许一条记录避免重复评分statusCharField控制草稿/发布/关闭发布后不允许再改分这里重点是PerformanceScore表的联合约束。同一员工在同一个考核周期内某个指标只能有一条评分记录避免班长重复提交。on_deletePROTECT表示考核周期或指标一旦被评分引用数据库会拒绝直接删除防止历史报表失去关联。权重用DecimalField而不是FloatField因为0.1加0.2在浮点数下不是0.3后续加权汇总会出现千分位误差。MVT的Model层不只是建表它同时承载权限判断和业务规则的起点比如unique_together就是数据库层的最后一道防线。2.3 基于内置User的权限与角色控制制造企业里班长、主任、人事专员看到的菜单和能做的操作不同。Django自带的User.is_staff、is_superuser只能区分能否登录Admin表达不了“班长只能给本班人评分、主任才能发布周期”这类细粒度规则。常见做法是自定义EmployeeProfile后用装饰器限制视图。# accounts/models.py from django.contrib.auth.models import User from django.db import models class EmployeeProfile(models.Model): ROLE_CHOICES [ (worker, 工人), (leader, 班长), (director, 主任), (hr, 人事), ] user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) employee_no models.CharField(工号, max_length20, uniqueTrue) department models.ForeignKey(assessment.Department, nullTrue, on_deletemodels.SET_NULL) role models.CharField(角色, max_length10, choicesROLE_CHOICES, defaultworker)# accounts/decorators.py from django.core.exceptions import PermissionDenied from django.contrib.auth.views import redirect_to_login from functools import wraps def require_role(*roles): def decorator(view_func): wraps(view_func) def wrapper(request, *args, **kwargs): if not request.user.is_authenticated: return redirect_to_login(request.get_full_path()) profile_role None if hasattr(request.user, profile): profile_role request.user.profile.role if profile_role not in roles: raise PermissionDenied(当前角色不允许执行该操作) return view_func(request, *args, **kwargs) return wrapper return decoratorrequire_role(leader, director)用在视图函数上方先判断是否登录再通过OneToOne反向查询读取角色。不要用is_staff判断业务角色因为超级管理员没有EmployeeProfile时取出的是None会被误拦截。wraps保留原函数名称避免URL路由匹配和Admin日志出现重复函数名。实际使用中如果发现request.user.profile不存在说明员工档案没初始化建议直接记录异常并拒绝操作不要静默放行。3. 考核流程实现目标设定、提交评分与删除重算3.1 目标设定与跟踪的视图逻辑制造企业的目标通常由计划部下达到班组例如本月产量目标、一次交检合格率目标。系统里可以用独立的TargetValue模型保存目标和实际值然后在视图中计算完成率。# assessment/models.py class TargetValue(models.Model): period models.ForeignKey(AssessmentPeriod, on_deletemodels.CASCADE) department models.ForeignKey(Department, on_deletemodels.CASCADE) indicator models.ForeignKey(KpiIndicator, on_deletemodels.CASCADE) target models.DecimalField(目标值, max_digits10, decimal_places2) actual models.DecimalField(实际值, max_digits10, decimal_places2, default0)# assessment/views.py from django.views.generic import ListView from .models import TargetValue class TargetTrackView(ListView): model TargetValue template_name assessment/target_track.html context_object_name targets def get_queryset(self): qs super().get_queryset() period_id self.request.GET.get(period) if period_id: qs qs.filter(period_idperiod_id) return qs.select_related(indicator, department) def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) for t in context[targets]: if t.target: t.completion_rate round(float(t.actual / t.target) * 100, 2) else: t.completion_rate 0 return contextselect_related对indicator和department外键执行JOIN避免模板中访问t.indicator.name时产生N1查询。completion_rate在get_context_data中动态追加不写入数据库因为完成率随着实际产量每日更新实时的计算结果更有意义。period_id通过GET参数过滤适合列表页的下拉筛选。这里要注意除零保护目标值尚未填写时不能抛异常。如果企业后续要求按班组批量录入目标可以改成FormSet一次提交多行但核心查询逻辑保持不变。3.2 表单校验与状态流转评分表单需要满足三条规则分数不能超过指标上限、同一周期同一指标不能重复提交、已发布周期禁止修改。这些逻辑放在forms.py的clean方法里最合适因为字段级校验不具备跨字段视角。# assessment/forms.py from django import forms from .models import PerformanceScore class ScoreForm(forms.ModelForm): class Meta: model PerformanceScore fields [period, employee, indicator, score, comment] def clean(self): cleaned super().clean() period cleaned.get(period) if period and period.status ! draft: raise forms.ValidationError(考核周期已发布禁止修改评分) employee cleaned.get(employee) indicator cleaned.get(indicator) if period and employee and indicator: exists PerformanceScore.objects.filter( periodperiod, employeeemployee, indicatorindicator ).exclude(pkself.instance.pk).exists() if exists: raise forms.ValidationError(该员工在此指标下已有评分记录) return cleanedclean方法在字段级校验完成后执行此时可以同时拿到period、employee、indicator。第一道检查限制周期状态只有草稿状态允许录入这是最容易被忽略的边界。第二道检查联合唯一exclude(pkself.instance.pk)保证编辑场景排除当前记录本身否则每次保存都被误判为重复。表单中员工下拉框建议用ModelChoiceField且按工号排序展示时带工号因为制造企业经常出现重名员工。状态流转建议在Model中显式定义方法而不是在视图里随便赋值def publish(self): if self.status ! draft: raise ValueError(只有草稿周期可以发布) self.status published self.save(update_fields[status])发布动作传入update_fields可以减少不必要的更新时间戳变更。如果项目已经上线并且需要更复杂的审批流可以用django-fsm定义状态机但内部绩效系统通常两到三个状态就够不需要引入额外依赖。3.3 执行查询-删除对象注意ORM的级联行为日常维护中管理员发现某条评分录错了需要删除。写一行PerformanceScore.objects.get(pkid).delete()在单条数据上没问题但误删考核周期时会发现被外键挡住。# 错误示例删除已有评分的考核周期 period AssessmentPeriod.objects.get(pk3) period.delete() # django.db.models.ProtectedError # 正确处理先清理评分记录再关闭周期 scores PerformanceScore.objects.filter(periodperiod) print(scores.count()) scores.delete() period.status closed period.save(update_fields[status])Model定义时Period和KpiIndicator的on_deletePROTECT是有意为之直接删除会被数据库约束拒绝。PerformanceScore.objects.filter(...).delete()返回一个元组(n, {assessment.PerformanceScore: n})表示删除的记录数。批量删除不会触发每个实例的delete()方法如果需要删除前做审计日志必须用循环逐条删除或使用信号。操作on_delete结果删除考核周期PROTECT存在评分时抛ProtectedError删除评分记录CASCADE关联记录直接删除删除UserCASCADE关联评分与Profile同时删除常见误用是把get换成filter后直接.delete()导致不确定是否删掉了预期记录。Django执行查询时filter().delete()不会检查unique_together约束可能把历史周期下的同名记录一并删掉。生产环境删除前建议先把待删记录的employee__username和indicator__name导出留作审计报表。3.4 用重定向传递数据避免重复提交和刷新提示表单提交后如果直接把消息放在render里返回用户刷新页面时浏览器会询问是否重新提交表单。用POST/Redirect/GET模式可以避免同时用Django messages框架在重定向后传递提示文本。from django.contrib import messages from django.shortcuts import redirect, render def submit_score(request): if request.method POST: form ScoreForm(request.POST) if form.is_valid(): form.save() messages.success(request, 评分已提交等待主任审核) return redirect(score_list, period_idform.cleaned_data[period].id) else: form ScoreForm() return render(request, assessment/score_form.html, {form: form})redirect(score_list, period_id...)中第二个参数对应URL path中捕获的变量名比如路径int:period_id/scores/。messages写入session模板里通过for message in messages循环显示。这里有一个django重定向传递数据的细节如果想要保留筛选条件可以在redirect URL后加?period3dept2查询参数但中文部门名需要用urlencode否则会被浏览器转码导致过滤失效。不建议把业务提示文案塞进查询参数因为URL长度有限且会残留浏览历史。4. 绩效报表、导出与Admin界面美化4.1 基于聚合查询的部门绩效看板制造企业晨会最常看的报表是“各车间综合得分排名”。由于每个指标权重不同不能简单对score做平均值要先算出加权分再按部门聚合。from django.db.models import F, Sum, FloatField, ExpressionWrapper from django.db.models.functions import Coalesce from .models import PerformanceScore def dept_summary(period_id): summary ( PerformanceScore.objects .filter(period_idperiod_id) .annotate(weightedF(score) * F(indicator__weight)) .values(employee__profile__department__name) .annotate( total_weightCoalesce(Sum(indicator__weight), 0.0, output_fieldFloatField()), weighted_sumCoalesce(Sum(weighted), 0.0, output_fieldFloatField()), ) .annotate(avg_scoreExpressionWrapper( F(weighted_sum) / F(total_weight), output_fieldFloatField() )) ) return summaryF()表达式让数据库在SQL层执行乘法不把数据拉到Python计算。values(employee__profile__department__name)先按部门分组annotate再聚合出总分和权重。Coalesce把SUM结果为NULL时替换成0.0否则weighted_sum / total_weight会出现NULL导致整条数据从报表消失。这套查询可以直接在Django shell中调用dept_summary(period_id1)验证输出不必先写模板。如果做前端看板可以把结果转成JsonResponse返回但注意Decimal需要float()转换才能被JSON序列化。4.2 用StreamingHttpResponse导出考核明细CSV导出评分明细时最怕的是几十万行数据一次性拼成字符串塞进HttpResponse。正确做法是使用django streaminghttpresponse配合content_type和content_disposition两个参数控制文件下载行为。import csv from django.http import StreamingHttpResponse class Echo: def write(self, value): return value def export_scores_csv(request, period_id): scores (PerformanceScore.objects .filter(period_idperiod_id) .select_related(employee, indicator) .order_by(employee__username, indicator__code)) echo Echo() writer csv.writer(echo) def generate(): yield writer.writerow([工号, 姓名, 部门, 指标编码, 指标名称, 得分, 权重]) for s in scores.iterator(chunk_size2000): yield writer.writerow([ s.employee.profile.employee_no, s.employee.username, s.employee.profile.department.name if s.employee.profile.department else , s.indicator.code, s.indicator.name, f{s.score:.2f}, f{s.indicator.weight:.2f}, ]) response StreamingHttpResponse(generate(), content_typetext/csv) response[Content-Disposition] attachment; filenamescores_{}.csv.format(period_id) return responseStreamingHttpResponse第一个参数必须是可迭代对象这里传生成器generate()。Echo是伪文件对象因为csv.writer需要调用write方法写数据这里每次直接返回字符串由StreamingHttpResponse把迭代块发送给浏览器。content_typetext/csv告诉浏览器这是CSV文件Content-Disposition中的attachment表示强制下载filename指定文件名。如果文件名含中文需要使用RFC 5987格式filename*UTF-8...否则老浏览器会截断。iterator(chunk_size2000)分批从数据库取结果避免一次性加载所有行导致内存暴涨。4.3 Django Admin界面美化select2、datepicker与静态资源配置Django Admin默认界面虽然朴素但内部管理系统够用。最简单的django admin界面美化不是改前端框架而是开启autocomplete_fields让员工下拉框支持搜索。Django原生使用Select2组件实现异步搜索这也是生产环境中员工数量过万后唯一能用的方案。# assessment/admin.py from django.contrib import admin from .models import PerformanceScore admin.register(PerformanceScore) class PerformanceScoreAdmin(admin.ModelAdmin): list_display (period, employee, indicator, score, scorer) list_filter (period, indicator__category) search_fields (employee__username, employee__profile__employee_no) autocomplete_fields (employee, indicator) def get_queryset(self, request): qs super().get_queryset(request) return qs.select_related(employee, indicator, scorer)属性作用说明autocomplete_fields员工/指标下拉框支持搜索对应ModelAdmin要声明search_fieldslist_filter按考核周期、指标分类过滤使用indicator__category跨表get_queryset优化列表页查询减少关联表N1项目正文中的style.min.css、materialdesignicons.min.css、bootstrap-datepicker3.css都属于Admin美化资源。引入自定义样式的标准做法是在项目根目录建static/并在settings.py设置STATICFILES_DIRSSTATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]然后通过Admin的class Media引入CSS和JS。注意加载顺序jquery-confirm依赖jquery必须放在jquery之后materialdesignicons会引用字体文件需要一并复制到static目录否则只能看到空方块。使用bootstrap-datepicker3.css时记住Admin的日期控件和普通Bootstrap日期控件HTML结构不同不能直接复制前端页面代码。5. 部署到生成环境nginx.conf、mysqlclient和二次开发边界5.1 先解决mysqlclient安装与Django连接配置生产环境大多用MySQL因为企业已有数据库。Django连接MySQL首选mysqlclient但在Linux上编译经常报错。Debian系系统先装系统依赖再pip安装sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pip install mysqlclientWindows下没有Linux依赖可以下载预编译whl包或者改用PyMySQL并在manage.py中加一行pymysql.install_as_MySQLdb()。安装完成后settings.py配置连接参数DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: performance_db, USER: perf_user, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, sql_mode: STRICT_TRANS_TABLES, }, } }utf8mb4才能完整存储中文和生僻字符STRICT_TRANS_TABLES让超长数据在数据库层报错而不是被静默截断。执行python manage.py migrate时如果遇到Specified key was too long通常是字符集排序规则的问题需要在建库时指定utf8mb4_unicode_ci。5.2 nginx.conf里的静态文件路由与反向代理生产环境由gunicorn或uwsgi跑Django动态请求nginx负责静态文件和反向代理。项目正文中的nginx.conf是一个标准参照核心配置如下upstream django_backend { server 127.0.0.1:8000; } server { listen 80; server_name perf.example.com; location /static/ { alias /srv/performance/static/; expires 7d; access_log off; } location / { proxy_pass http://django_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }location /static/的alias必须指向python manage.py collectstatic的输出目录不能指向开发时的源码static目录。expires 7d给资源加缓存版本升级后通过Django的ManifestStaticFilesStorage自动改变文件名避免客户端缓存旧CSS。proxy_set_header X-Real-IP和X-Forwarded-For用于权限审计记录真实IPDjango侧还需要配置USE_X_FORWARDED_HOST或第三方中间件否则request.META[REMOTE_ADDR]始终是nginx地址。5.3 前后端分离视角下的改造建议如果企业已有MES或钉钉想把考核数据开放给其他系统可以考虑django前后端分离方案。把render替换为JsonResponse认证从session改用JWT。但私有化部署环境里JWT密钥管理和token刷新反而增加维护成本。常见过渡做法是保留Django模板渲染只把报表接口单独做成API这样既支持大屏看板又不影响内部员工操作。需要对外开放接口时优先用Django REST framework的ModelViewSet配合permission_classes控制粗粒度权限细粒度角色判断继续沿用2.3节的装饰器。5.4 给二次开发者的三个验证技巧迁移检查部署前执行python manage.py makemigrations --check --dry-run如果返回No changes detected说明模型和迁移文件同步避免上线后才发现缺字段。本地数据库回放用django.db.connection.queries打印列表页SQL重点检查PerformanceScore列表是否出现重复查询。正常一个列表页的查询次数应控制在1到2次超过10次就要检查是否有N1。权限回归测试在TestCase中用Client.force_login分别切换工人、班长、主任角色断言require_role返回403。比手动在浏览器反复登录快得多也能防止后续改动误放开角色限制。本文还有配套的精品资源点击获取
返回列表