做工厂信息化的朋友大概都熟悉一个尴尬局面网上讲电商系统、内容管理系统的Django教程一抓一大把但一讲到生产执行管理系统MES资料立刻就稀薄起来。前阵子我在一个技术社区翻源码看到编号为15244的Django框架生产执行管理系统源码包项目体量不大业务却非常典型正好适合拿来当案例拆解。我把它完整拉下来跑通之后整理了这份理解从MES的需求原点、数据模型设计、工单流转逻辑到部署时的几个坑一次性聊透。如果你正准备做MES方向的毕业设计、刚接触制造企业信息化或者想在Django项目里参考一套生产执行系统的业务建模思路这篇文章可以配合源码一起读比你直接对着空项目猜结构要快得多。这个项目最打动我的不是某个功能写得多么花哨而是它的数据流非常完整一个生产工单从创建、派工、报工到完工每一步都有清晰的落库逻辑。能做到这一点的开源项目其实不多大多数Demo只做了增删改查生产业务里最关键的状态流转和数量追溯反而没人讲。所以下文会重点拆这两块也会把我觉得值得直接复用的功能点单拎出来说明。1. MES要解决什么从车间黑箱到可追溯的工单流转1.1 工厂里最常见的三个效率黑洞我做工厂信息化项目的前期调研时问得最多的一个问题是车间现在到底怎么管生产进度答案十有八九是看纸质工单或者打电话问车间主任。第一个黑洞是进度不透明。计划员排完产工单打印出来发到各个工位一线的完工数量靠下班前手工统计日报表第二天上午才能出来。等于整个车间在计划员眼里是一个黑箱里面的实时状态完全靠人去问。第二个黑洞是异常信息丢失。生产过程中出现设备故障、物料不良、工艺参数偏差一线工人的处理方式通常是先填一张纸质异常单扔在班长桌上。这张纸什么时候被看到、最后怎么处理的没人能追踪等到月底复盘的时候根本说不清楚责任和原因。第三个黑洞是追溯成本高。客户投诉某个批次有质量问题时工厂要返查当时的操作员、设备、工艺参数、检验记录。如果这些数据散落在纸质单据和老师傅的记忆里一次追溯可能要翻两天档案。生产执行管理系统MES就是来解决这三个黑洞的把计划-执行-反馈的闭环搬进系统里让工单状态实时可见让异常记录可追踪让每一次报工都变成一条可查询的数据记录。1.2 为什么选Django来做这类系统我见过不少工厂用的MES是外包团队拿Spring Boot写的也有用.Net的但如果你是中小型制造企业的IT部门或者学生团队做一个MES课程设计Django确实是一个更务实的选择。首先是Django Admin自带完整后台。生产执行系统里大量工作是维护产品主数据、工艺路线、用户权限这类相对静态的信息Django Admin把列表、搜索、筛选、外键联动全部帮你做好了省掉一大半前端工作量评审演示的时候拿Admin页面顶着效果完全不输定制界面。其次是Django的ORM和迁移机制很适合业务频繁调整的阶段。MES业务最容易出现的状况是模型字段今天加一个工位编码明天调一下工序顺序。Django的makemigrations和migrate让你改表结构的过程比较顺滑不熟练的开发者也能用一个星期把主流程跑通。第三是权限模型和用户体系是内置的。车间里不同角色的人看到的内容理应不同Django自带的Group、Permission、装饰器直接就能实现按角色收权限不需要自己重写一套认证逻辑。1.3 系统中的四个核心角色与工作台划分项目里定义了四个核心角色分别对应车间里的真实职能计划员负责创建生产工单、排定生产顺序、调整优先级他关心的是哪些工单该做了、哪些要延期了。车间操作员接收派给自己的工单在工位上执行报工、登记不良品他关心的是今天做什么、这个批次做了多少。质检员对完工批次做检验记录判定合格或不合格他关心的是这批货的检验数据在哪里。系统管理员维护产品数据、工序路线、用户账号和权限分配他关心的是主数据是否准确、系统是否稳定。这套角色划分直接影响了菜单结构和页面设计。数据权限上用Django自带的user.groups做粗粒度隔离就够了不用上django-guardian那种对象级权限毕竟车间场景下操作员之间不需要互相隐藏数据。2. 数据库模型设计生产执行系统的地基怎么搭2.1 从订单到成品中间至少拆几张表很多初学MES的人第一反应是建一张大表工单号、产品、数量、完成数量、状态、操作员全塞进去。这个做法在演示阶段看起来很直观但实际一旦跑了三个月你的查询会越来越别扭因为你丢失了过程。这个15244项目采用了比较标准的拆法核心是五张业务表。Product产品主数据存产品编码、名称、规格型号、默认工艺路线。这里比较容易被忽视的是工艺路线字段——一个产品要经过哪几道工序、每道工序的标准工时是多少都应该在产品上挂一个Process列表。Process工序每道工序属于某个产品有工序序号、工序名称、对应工位、标准工时。注意工序是有顺序的所以必须有一个seq字段做排序否则显示顺序会乱。WorkOrder生产工单这是整个系统的核心表字段包括工单号、产品外键、计划数量、状态、计划开始时间、计划结束时间、实际开始时间、实际结束时间。WorkReport报工记录每一条代表某个操作员在某道工序上的一次完工汇报记录良品数、不良数、工时、报工时间。这张表是后面所有统计报表的数据来源。DefectRecord不良品明细不良不能只在报工记录里给个总数还要记录缺陷类型和数量比如外观划伤12件、尺寸超差3件。这样才能支撑质量分析。把报工单独拆成一张表而不是在WorkOrder上累加已完工数量是我特别认可这个项目的一点。因为报工明细天然是一个时间序列后续做产量趋势图、班组绩效统计、产品追溯都是围绕这张明细表做聚合而不是去死磕一张不断被Update的主表。2.2 状态字段设计用整数状态机而不是字符串这里的核心经验是工单状态字段别用CharField存中文字符串理由很简单一个是数据库查询效率另一个是状态约束。项目里用的是类似DjangoIntegerChoices的写法状态值都是整数常量from django.db import models class WorkOrder(models.Model): class Status(models.IntegerChoices): DRAFT 0, 草稿 RELEASED 1, 已下达 IN_PROGRESS 2, 生产中 COMPLETED 3, 已完工 CANCELLED 4, 已取消 status models.IntegerField( choicesStatus.choices, defaultStatus.DRAFT, db_indexTrue, )为什么推荐整数而不是字符串第一整数字段在数据库里占的空间更小索引效率更高第二用框架的choices约束可以把合法值限定死防止脏数据比如生产中 带空格这种录入进去第三代码里写WorkOrder.Status.IN_PROGRESS比直接写生产中可读性和可维护性都更强。还有一个我在真实项目里踩过的坑状态流转一定要集中控制不要在每个视图里随手改状态。最好写一个独立的transition_status方法在里面校验当前状态到目标状态是否合法非法就抛出异常。比如草稿可以直接到已取消但已完工不能再跳回生产中生产中也不能直接跳到已下达。集中校验之后你再也不会在代码里发现某个地方偷偷把状态改乱的情况。2.3 可参考的核心表结构清单我把这个项目里值得参考的表结构整理成一个表方便你对照源码看表名核心字段设计要点Productcode, name, spec, unit产品编码唯一spec存规格描述Processproduct(外键), seq, name, workstationseq控制工序顺序workstation绑定工位WorkOrderorder_no, product(外键), plan_qty, statusorder_no唯一且非自增由业务规则生成WorkReportwork_order(外键), process(外键), user, good_qty, bad_qty, work_hours记录实际工时用于后续效率统计DefectRecordreport(外键), defect_type, qtydefect_type用代码表关联不要直接存中文字段命名上它统一用了小写下划线风格时间字段用了auto_now_add和auto_now这算常规操作。唯一要注意的是WorkReport的work_hours字段采集的是小时数精度保留到小数点后两位这样日报表统计班组效率时才够用。3. 工单流转的核心代码链路从创建到完工3.1 生成工单号并发才是真正的坑工单号生成看起来简单就是拼接字符串但真实生产环境里最怕的就是两个用户同时创建工单生成了同一个单号。这个项目用的方案是WO 日期 当日序号例如WO20250612-0001。关键在于序号怎么取。很多初学写法是today_count WorkOrder.objects.filter(created_at__datetoday).count() order_no fWO{today:%Y%m%d}-{today_count 1:04d}这个写法在并发场景下会出问题两个请求同时count到同样的数字然后生成同一个单号后一个记录就会因为唯一约束冲突而报错。更稳的方案是用数据库的序列或单独一张序号表加行锁。项目里的做法是单独建一张SequenceTable每次生成单号时用select_for_update锁住对应行取到当前序号加一后再写回整个过程放在事务里from django.db import transaction def generate_order_no(self): with transaction.atomic(): seq, _ SequenceTable.objects.select_for_update().get_or_create( datedate.today(), defaults{current_no: 0}, ) seq.current_no 1 seq.save(update_fields[current_no]) return fWO{date.today():%Y%m%d}-{seq.current_no:04d}第一次跑通时我还没觉得这个设计有多了不起直到后来同事在测试环境里用两个浏览器同时点创建工单一下就复现了单号冲突我才意识到这种小细节才是生产系统稳不稳的分水岭。3.2 报工写入一次点击更新多张表的一致性报工是MES里最核心的操作。操作员在界面上输入良品数50、不良数2、工时6.5点击提交后端要做三件事在WorkReport表插入一条报工记录更新WorkOrder上的累计完成数量判断累计数量是否达到计划数量如果达到了把状态从生产中改成已完工。这三个操作必须是一个不可分割的整体任何一步失败都要回滚否则就会出现报工记录存在但工单数量没变这种脏数据。项目里用了transaction.atomic()包住整段逻辑我实际写的时候也是照这个思路来from django.db import transaction with transaction.atomic(): report WorkReport.objects.create( work_orderwork_order, processprocess, userrequest.user, good_qtygood_qty, bad_qtybad_qty, work_hourswork_hours, ) work_order WorkOrder.objects.select_for_update().get( pkwork_order.pk ) work_order.completed_qty good_qty if work_order.completed_qty work_order.plan_qty: work_order.status WorkOrder.Status.COMPLETED work_order.save(update_fields[completed_qty, status, updated_at])这里有个关键细节更新工单累计数量前一定要用select_for_update重新查一次并锁住这行。否则两个操作员同时给同一个工单报工时读到的完成数量都是旧值后写的那个会把前一个的报工覆盖掉。我在测试时故意开了两个窗口模拟并发报工没加锁的情况下确实丢了一次报工记录加锁之后才稳定下来。3.3 看板上的完成率是怎么算出来的车间大屏和计划员页面上都要显示工单完成率计算公式很简单完成率 累计合格数 ÷ 计划数量。这里要小心的是查询效率。新手最容易写出来的糟糕代码是在模板里循环{% for order in work_orders %} {{ order.completed_qty|default_if_none:0 }}/{{ order.plan_qty }} {% endfor %}这行代码本身没问题但如果你还想显示该工单最后一条报工的更新时间你就不得不在循环里再查一次WorkReport一个页面上20个工单就是20次额外查询这就是经典的N1问题。项目里用的是annotate聚合一步查完from django.db.models import Sum, Max, OuterRef, Subquery latest_report WorkReport.objects.filter( work_orderOuterRef(pk) ).order_by(-reported_at).values(reported_at)[:1] work_orders WorkOrder.objects.annotate( last_report_timeSubquery(latest_report), )这样页面上每行只带一个last_report_time字段不再逐条查子表。这种能一次聚合就不要循环查的习惯在报表页和大屏页特别重要尤其是数据量上来之后。4. 这个源码里值得直接复用的四个功能点4.1 用Django Admin快速搭建后台管理界面这个项目没有做复杂的前端后台而是把Django Admin的潜力发挥得很充分。它在admin.py里做了几件比较典型的事第一列表页配置。list_display里只放工单号、产品、计划数量、已完工数量、状态、计划交期list_filter按状态和创建日期过滤search_fields搜工单号和产品编码。这些看起来基础但已经是生产环境里最常用的三个配置了。第二Inline关联编辑。Product的编辑页里用TabularInline把工序列表直接嵌进去录入产品资料的时候顺手就能把工艺路线维护完不用来回切换菜单。第三自定义Action。选中一批工单在下拉框里执行批量下达或者批量完工一个actions函数几十行代码就搞定了避免了为这种低频操作写整页前端界面。Admin改造完之后整个系统的后台管理界面基本上就不用另外开发了真正的业务操作界面只需要聚焦操作员的报工页面和大屏看板。4.2 车间看板超轻量实现方案车间需要一块显示实时生产进度的大屏这个项目用的不是重量级的WebSocket实时推送而是前端定时刷新。做法很实在给看板单独写一个接口返回JSON数据页面加载后用JavaScriptsetInterval每隔10秒钟fetch一次拿到最新数据后替换数字。一线操作员对10秒延迟完全没有感知但对开发者来说这个方案比搭一套Django Channels加WebSocket要简单得多也不需要在服务器上多维护一个常驻进程。我在实际部署时给看板的查询加了一层django.core.cache缓存缓存时间为8秒左右。这样即使车间有20台工位电脑同时开着看板页面请求也会先打到缓存上数据库不会因为轮询而压力过大。4.3 异常上报与待处理清单生产现场永远有例外情况系统里必须有一个异常上报的入口。项目里实现得很朴素一张ExceptionRecord表记录了工单外键、异常类型、描述、上报人、上报时间、处理状态和处理备注。操作员遇到物料短缺、设备报警、工艺异常在页面里点一下上报异常填几句描述这张单就进入了质检员或车间主任的待办清单。待办清单的数字用一条annotate统计出来pending_count ExceptionRecord.objects.filter( statusExceptionRecord.Status.PENDING ).count()这个功能的价值在于把找班长这个线下动作线上化了异常不被遗忘处理进度随时可查。对于一个MES演示项目来说这是非常能体现业务理解深度的一个模块。4.4 Excel导出报表项目里的导出功能用了openpyxl库报表内容包括工单明细、报工汇总、不良品统计等。我这里说几个容易被忽略的细节。第一是中文文件名。用Content-Disposition设置下载文件名时最好做一下URL编码否则在部分浏览器里中文文件名会变成乱码。第二是数字格式。openpyxl写入数值时如果单元格给了文本格式后面的求和公式就没法算。所以写报表前先给需要统计的列设置NumberFormat为0.00。第三是大批量导出。如果工单数量上万用openpyxl逐行写会非常慢。项目里推荐的做法是先按需筛选字段再用Worksheet.append批量写入避免每次写入都新建一个单元格对象。5. 本地跑通这个项目的完整过程与踩坑5.1 环境准备清单我拿到源码后在本地的环境是这样的操作系统Windows 11用Linux部署也是一样的步骤Python版本3.11Django版本4.2 LTS数据库SQLite开发环境生产可以换成MySQL依赖安装直接用pip install -r requirements.txt。如果上面列表里没有写版本号建议在虚拟环境里重新生成一个requirements.txt方便后续交接部署。提示创建虚拟环境这个步骤别跳过我就见过有人图省事直接在全球环境里装依赖结果Django版本冲突把别的项目搞崩了。python -m venv venv然后激活虚拟环境这步不麻烦但能省很多事。5.2 初始化数据库和创建管理员整个系统跑起来只要四步命令python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver但这里有个隐藏问题如果你跳过了loaddata这一步系统里没有任何产品数据和工序数据登录Admin之后看到的是空页面你很难判断系统是否真的正常。我建议第一次跑通后先手工建立一个产品、给这个产品配三道工序然后再创建一个生产工单。如果这一步能顺利完成说明模型之间外键关联基本没问题。演示给别人看的时候再准备一份包含几十条报工记录的演示数据展示效果会好很多。5.3 我实际遇到的三个典型坑第一个坑静态文件404。跑起来之后Admin页面完全没有样式打开浏览器控制台发现静态文件全部404。原因是Django的DEBUGFalse时要执行collectstatic收集所有静态文件到STATIC_ROOT然后再通过配置的静态文件服务路径访问。开发时直接把DEBUG保持为True就好部署时再处理静态文件托管。第二个坑时间错乱。系统设置启动后录入的报工时间比本地时间快了8个小时。原因出在USE_TZTrue而系统里没有配置TIME_ZONE数据库里存的是UTC时间。解决方案是在settings.py里明确写上USE_TZ True TIME_ZONE Asia/Shanghai如果是其他时区需要按实际地区调整。这个坑在MES里特别要命因为日报表是按自然日统计的时区不对当天产量全算错。第三个坑导出Excel中文乱码。用openpyxl导出时如果文件里全是一堆乱码字符大多数情况是字段编码问题而不是openpyxl本身的问题。项目里导出报表的数据源都加了str()转换和strip()去空格导出前再统一处理一遍就正常了。5.4 看板性能不要每次都全表COUNT最后说一个容易被忽视的性能点。看板页面上如果显示当前在产工单数和今日总报工次数新手第一反应是每次刷新都执行两个COUNT查询。数据量小的时候没事但当报工记录超过10万条并发刷新一多数据库响应时间会明显变慢。这台项目里的处理方式很务实看板数据的聚合结果套上了缓存缓存有效期5到10秒。用户看到的数据最多延迟10秒但数据库负载能下降一个数量级。对于大多数MES应用场景这个权衡是划算的。如果你真的需要秒级实时再去引入消息队列和独立的实时计算服务绝大多数工厂根本不需要走到那一步。跑完这个项目我对MES系统的一个核心认知更清晰了生产执行系统的骨架是数据流而不是页面流。工单号怎么生成、报工怎么落库、状态怎么流转、完成率怎么聚合把这几条主链路的数据设计想通剩下的界面都是围绕这些数据长出来的枝叶。15244这个项目最值得学习的也正是这层业务骨架。如果你正打算做类似的系统我的建议很直接先别急着抄页面代码把你的数据库ER图先画出来然后用一句话把工单从生到死的过程说清楚——说不清楚就先不写代码。另一个我的切身体会是状态枚举和事务处理这类地基代码千万别图省事这两个位置是我见过在真实工厂环境里踩坑率最高的。我在跑通这个项目后又顺手加了设备点检和物料批次两个模块整体下来还算顺滑说明这套骨架的可扩展性也经得住考验。希望这份拆解能帮你把代码里的业务逻辑看明白少走几步弯路。