基于Django的智慧农机信息管理系统开发实践

发布时间:2026/10/12 22:35:43
基于Django的智慧农机信息管理系统开发实践
接手这套智慧农机信息管理系统的时候交接人给我的是两样东西一沓写满手写字迹的农机台账还有一个塞满了合并单元格、公式链绕得人头晕的Excel。拖拉机、收割机、插秧机加植保无人机三百多台设备每台的型号、购置日期、车况分别记在几本不同的本子里。但真要问一句——现在哪台机器在用、这台收割机上次保养是什么时候、今年春耕总共干了几亩地现场没有一个人能立刻答上来。我要做的就是基于Python和Django把这堆纸质档案和分散表格变成一个能持续运转、随时可查、能够支撑决策的智慧农机信息管理系统。这篇复盘适合谁看如果你正在给农业合作社、种植大户或者农机服务站做管理系统或者想找一个完整度比较高的管理类项目作为练手、毕业设计方向这篇文章中的思路和细节应该能帮上忙。我不会只讲代码怎么写还会把需求梳理、数据建模、功能实现、测试上线和踩坑排雷的全过程都摊开来讲你觉得有用的部分可以直接抄走用。1. 先别急着写代码这个系统到底要管什么1.1 农机管理的真实痛点光靠Excel台账已经撑不住了很多没接触过农机行业的人会以为农机管理就是给机器编个号、记一下买了几年没什么复杂的。但实际上只要设备数量上了百台业务复杂度会快速超过Excel能承受的范围。我在前期调研时发现某合作社的日常管理里有这么几个长期存在的毛病一个是设备状态不透明。几十台拖拉机分布在好几个乡镇哪台在作业、哪台已经故障停机、哪台调配到外地跨区作业了调度员只能靠打一圈电话确认。经常出现两台收割机被同时安排到同一个地块、另一块地却空着没机器去的尴尬情况。另一个是作业数据靠手填。机手干完活就口头报个数或者拍张照片发到群里统计员月底对着聊天记录整理作业面积。到了申报补贴的季节还要把数据抄进专门的汇总表农机监理来回核对、打电话追问费时费力还容易对不上账。保养维护就更典型了。农机保养和汽车保养不一样很多大型农机靠的是工作小时数而不是里程。柴油机用到几百小时该换机油、液压系统几百小时该换滤芯。但这些节点基本都记在老师傅脑子里他要是忙起来忘了设备就得“带病作业”农忙时节一台机器趴窝影响的可能就是几千亩地的收成窗口。说白了这不是一个简单的“把Excel搬上网页”的过程。它背后的问题是设备档案不完整、作业过程无记录、维护保养无预警、调度决策靠经验。所谓智慧农机信息管理系统第一步要解决的其实是这些最基础的信息化问题然后才谈得上数据分析和决策支持。1.2 角色与需求拆解谁在用怎么用管到多细在动手设计数据库之前一定要先把使用系统的角色盘清楚。这个系统我最终拆出了四类角色每一类的核心诉求完全不一样。系统管理员通常是合作社的负责人或者信息员。他们关心的是“我的设备整体运转得怎么样”哪些机器还在折旧期、哪些机器利用率低、这一季总共产生了多少作业面积和油耗。权限上要能看全社数据能管设备、人员、基础配置是系统的超级用户。机手是一线使用者。他们会登录系统查看今天分配的任务、填报自己完成的作业记录。机手最需要的是简单——打开页面选机器、选作业类型、填面积和油耗最多拍张照片上传。搞太复杂的流程机手就不愿意用了。维修人员处理与保养维修有关的事情接到保养工单、查看设备故障描述、填写维修结果和更换的配件。他们要能看到这台设备的完整维修历史方便判断是慢性问题还是偶发故障。统计员/财务负责月底结算。他们关心的是数据和报表这个月各台农机干了多少活、油耗是否异常、机手的作业量排名以及对接补贴申报时需要导出的统计表。我把角色和功能整理成一个简单的对照表后面做权限设计和菜单设计都直接对齐它角色核心功能数据范围系统管理员设备管理、人员管理、系统配置、查看所有报表全局机手查看任务、上报作业、查看自己的历史记录仅本人相关维修员接收工单、填写维修保养记录、查看设备档案全部设备侧重维修维度统计员查询统计报表、导出Excel全局数据只读需求拆解阶段最重要的产出不是功能列表而是搞清楚数据的从哪来、到哪去。农机档案谁来录入作业记录谁来产生保养提醒由谁确认关闭这些数据流问题关系到后面的表结构设计。我最后把所有功能划成五个大模块农机档案管理、人员权限管理、作业记录管理、维修保养管理、统计报表中心。系统就围绕这五条线展开不多不少。2. 技术选型为什么我选了Django而不是其他框架2.1 Django的“全家桶”思路刚好匹配管理类系统选型的时候有人问我这种项目用Flask写不是更轻便吗我的答案很直接Flask确实轻但面对一个需要用户登录、权限分级、后台管理、数据建模、表单处理的管理系统轻量框架意味着大量组件要自己拼。数据库迁移得自己接认证得自己写Admin后台得自己造轮子。这些事不是不能做但会分散很多精力。Django从一开始就走的是“全家桶”路线。它的ORM能让我用Python类直接定义农机、作业记录这些实体一条命令完成建表自带的Admin后台不花一分钱就有了一个可用的数据管理界面开发早期拿来做数据维护工具非常顺手认证系统内置了用户、分组、权限配合自定义扩展就能满足角色隔离。再加上模板引擎、表单校验、CSRF防护这些现成能力整套下来开发效率明显高一个档次。还有一个很重要的点是代码的约束性。Django的MTV架构对项目结构有很强的约定新接手的开发者看代码会容易上手。我在项目后期把一部分代码交给同事维护他之前没写过Django但跟着项目的目录层级和Django惯例走基本没什么障碍。这一点在长期维护的项目里比性能优势更值钱。当然Django也不是没有缺点。它在高并发、复杂异步场景下表现不佳但农机管理系统属于典型的企业级内部系统并发量撑死百来号人同时在线数据库中规中矩Django的短板在这里根本不算问题。选型不是选最好的而是选最合适的。对于这种业务逻辑复杂、并发压力不大、强调开发效率和可维护性的系统Django就是很合理的答案。2.2 总体架构项目是怎么分层的整个系统我用的是比较经典的Django分层方式没有刻意追求微服务也没有做前后端完全分离。项目结构大致是这样的接入层用Nginx作为反向代理负责静态文件服务和请求转发。前端是Django模板渲染的页面配合少量JavaScript。应用层Django开发的应用按业务域拆成几个appmachines农机档案、operations作业记录、maintenance维修保养、systemmgmt用户与权限、reports统计报表。数据层MySQL存业务数据Redis做缓存和分布式锁文件系统存图片和导入的Excel文件。没有做前后端分离是经过权衡的。农机管理系统的用户集中在办公室和手机浏览器交互复杂度不算高Django模板加HTMX、少量Ajax足够支撑。这样做的直接好处是少了一套Node.js构建流程不用维护两套前端代码跨域问题也不存在一个人从后端写到页面报错链路短、排查快。但设计上我把后端接口逻辑尽量写在独立的service层这样以后要拆出API给移动端使用也不至于伤筋动骨。部署上我采用了Nginx Gunicorn MySQL Redis的标准组合。Django的静态文件配好收集命令之后一次性同步到服务器目录数据库定期备份到异地存储。整体架构说不上花哨但胜在简单可靠出了问题容易定位。2.3 数据库与前端方案怎么选、怎么配数据库选的MySQL 8.0没上PostgreSQL是因为现有服务器上已经有维护成熟的MySQL实例这种项目不需要给自己添麻烦。字符集设置成utf8mb4要留意的是排序规则如果没配对中文查询和排序会出现兼容问题。Redis用来做缓存和定时任务的分布式锁这些下面会展开说。前端没有引入重型框架。页面UI用了Layui和Bootstrap图表统一用ECharts地图部分用Leaflet加载轨迹。选择这些组件的原因是它们生态成熟、中文资料多而且对于表格式页面、统计图表的支持效果足够好。图表数据的格式在视图层组装成JSON传给模板前端拿数据绘图即可。3. 数据模型设计底座打好了后面全是顺水推舟3.1 农机档案模型设备信息的“户口本”农机档案是整个系统的地基其他所有业务数据都挂在这张表上。设计的时候我反复提醒自己字段宁可多列几个也不要后期频繁改表。第一次上线后如果再往表里加字段意味着所有历史数据的补录和维护成本都会增加。我设计农机表Machine时核心字段分为三类基础属性、购置属性、运营状态。基础属性包括农机编号、名称、类型、品牌、型号、出厂编号等购置属性包括购置日期、购置金额、补贴金额、使用年限、折旧方式运营状态包括当前状态、所属合作社、绑定机手、累计作业面积、累计油耗、下次保养时间。用Django模型写出来大概是这个感觉class Machine(models.Model): MACHINE_TYPE_CHOICES ( (tractor, 拖拉机), (harvester, 收割机), (planter, 插秧机), (drone, 植保无人机), (triller, 旋耕机), ) STATUS_CHOICES ( (idle, 空闲), (working, 作业中), (repairing, 维修中), (scrapped, 已报废), ) machine_no models.CharField(农机编号, max_length32, uniqueTrue) name models.CharField(农机名称, max_length64) machine_type models.CharField(农机类型, max_length32, choicesMACHINE_TYPE_CHOICES) brand models.CharField(品牌, max_length64, blankTrue) model_name models.CharField(型号, max_length64, blankTrue) factory_no models.CharField(出厂编号, max_length64, blankTrue) purchase_date models.DateField(购置日期) purchase_price models.DecimalField(购置价格(万元), max_digits8, decimal_places2, default0) subsidy_amount models.DecimalField(补贴金额(万元), max_digits8, decimal_places2, default0) service_life models.IntegerField(使用年限(年), default8) status models.CharField(当前状态, max_length16, choicesSTATUS_CHOICES, defaultidle) organization models.ForeignKey(systemmgmt.Organization, on_deletemodels.PROTECT, verbose_name所属组织) driver models.ForeignKey(auth.User, nullTrue, blankTrue, on_deletemodels.SET_NULL, verbose_name绑定机手) total_area models.DecimalField(累计作业面积(亩), max_digits12, decimal_places2, default0) total_fuel models.DecimalField(累计油耗(升), max_digits12, decimal_places2, default0) next_maintenance_hours models.IntegerField(距下次保养(小时), default100) current_hours models.IntegerField(当前累计工作小时, default0) is_deleted models.BooleanField(逻辑删除, defaultFalse) created_at models.DateTimeField(创建时间, auto_now_addTrue) updated_at models.DateTimeField(更新时间, auto_nowTrue) class Meta: db_table machine verbose_name 农机档案 indexes [ models.Index(fields[status]), models.Index(fields[machine_type]), ] def __str__(self): return f{self.machine_no}-{self.name}几个容易踩坑的地方先说清楚。金额字段选择DecimalField而不是FloatField是因为农机购置价格动辄几十万、补贴金额也要精确到分浮点数的二进制表示误差会让人抓狂直接用十进制字段最稳妥。农机编号加上唯一约束这样每台设备在系统里的身份是唯一的。状态字段单独建索引因为列表页最常见的查询条件就是“看当前有哪些机器空闲/作业中/维修中”不加索引的话几万条记录之后全表扫描会很吃力。我还在表里放了一个is_deleted逻辑删除字段。实际业务里误删一台农机后面牵扯到作业记录、维修记录、结算单的历史数据物理删除会造成大量记录失去关联对象。所以凡是核心业务表都做逻辑删除只在管理界面提供“回收站”功能。3.2 作业记录模型农业生产数据的核心沉淀农机管理系统和普通企业资产管理系统最大的不同在于它要记录的不是“这台机器今天借给谁了”而是一台机器在特定时间段、特定地块发生的农业生产作业。作业记录表WorkRecord是这个系统的业务核心。每次机手完成一块地的作业就生成一条记录。我最初设计的字段包含农机外键、机手外键、作业类型、开始时间、结束时间、作业面积、油耗、作业地点描述、备注、审核状态。后来又根据实际反馈加上了“补贴申报”相关的字段组合。class WorkRecord(models.Model): WORK_TYPE_CHOICES ( (plough, 耕地), (seed, 播种), (fertilize, 施肥), (spray, 喷药), (harvest, 收割), ) machine models.ForeignKey(Machine, on_deletemodels.PROTECT, verbose_name农机) operator models.ForeignKey(auth.User, on_deletemodels.PROTECT, verbose_name机手) work_type models.CharField(作业类型, max_length16, choicesWORK_TYPE_CHOICES) start_time models.DateTimeField(开始时间) end_time models.DateTimeField(结束时间) area models.DecimalField(作业面积(亩), max_digits10, decimal_places2) fuel_consumption models.DecimalField(油耗(升), max_digits8, decimal_places2, default0) work_location models.CharField(作业地点, max_length255, blankTrue) status models.CharField(审核状态, max_length16, choices( (pending, 待审核), (approved, 已通过), (rejected, 已驳回), ), defaultpending) remark models.TextField(备注, blankTrue) create_time models.DateTimeField(提交时间, auto_now_addTrue) class Meta: db_table work_record verbose_name 作业记录 indexes [ models.Index(fields[machine, start_time]), models.Index(fields[operator, start_time]), ]这里要专门说一下为什么machine和operator外键都用on_deletemodels.PROTECT。作业记录一旦生成它关联的农机和机手就不能随便删否则历史账目全乱套。用PROTECT的含义是如果要删除的农机已经被作业记录引用数据库会直接拒绝删除从机制上挡住误操作。联合索引的设计也有讲究。最常见的一组查询是“某一台机器在某段时间的作业记录”对应(machine, start_time)联合索引另一组是“某个机手在某段时间做了多少活”对应(operator, start_time)。这俩索引覆盖了大部分业务查询模式我就不再额外堆冗余索引了。作业面积这个字段在实际项目中牵扯到很多细节。补贴申报的时候上报面积和机手自报面积经常不一致所以我加了审核状态字段由统计员核对后置为“已通过”。审核不通过的要填驳回原因防止机器作业数据被随意虚报。3.3 维修保养与调度工单状态流转的设计要点维修保养和调度是两个状态流转很强的模块设计思路上比单纯的数据登记要复杂一些。维修保养表我分成了两块保养提醒计划和实际维修记录。实际业务里每台农机会有一个“下次保养工时”阈值系统根据累计工作小时来判断是否该保养了。保养记录表记录的是保养时间、保养项目、保养负责人、费用、更换配件明细。维修记录表偏故障处理记录故障现象、故障原因、维修方案、费用、维修时长。关键设计点在于保养和维修都要求关联“当前工时读数”和“上次工时读数”。这样做的目的是算清一个保养周期内设备到底跑了多少小时避免换个机油之后不到一个月又提示该保养的尴尬情况。调度工单表解决的是“设备谁用、什么时候用、去哪里用”的问题。字段包括关联农机、申请机手、调度时间段、作业区域、作业类型、调度状态、审批意见。调度状态有固定的流转路径待审批 → 已通过/已驳回 → 作业中 → 已完成 → 已取消。状态的流转用Django的F表达式配合事务来保证并发下的正确性。举个例子两台收割机同时被调度员查看并分配任务如果只靠前端按钮控制就可能出现同一台机器被调度到两块的冲突。我在分配任务前用条件更新Machine.objects.filter(idmachine_id, statusidle).update(statusreserved)返回结果影响行数如果为0说明这台机器已经被别人占用了。这个思路用一条SQL就解决了并发分配问题比先查询再更新的方式稳妥得多。权限层面为了让不同用户只能看到自己职责范围内的数据我没有在每个视图里写重复的过滤逻辑而是给主要模型定义了自定义管理器class MachineManager(models.Manager): def visible_to(self, user): if user.is_superuser: return self.all() if user.user_type org_admin: return self.filter(organizationuser.organization) if user.user_type driver: return self.filter(driveruser) return self.none()业务层的查询统一走这个管理器比如Machine.objects.visible_to(request.user).filter(statusworking)。后期加新页面的时候权限过滤逻辑不用重写直接复用管理器方法就行。事实证明这个抽象在项目后期收益很大审批流和报表页都用到了。4. 核心功能模块实现从增删改查到数据大屏4.1 农机档案管理界面列表、筛选与批量操作的实现思路农机档案模块是整个系统的入口。页面要支持按农机类型、状态、所属组织、购置年份、关键字编号/品牌/型号组合筛选同时支持批量修改状态和导出Excel。我把筛选条件放在列表页顶部每次查询用QuerySet的链式过滤实现几个条件叠加也不会让代码变乱。举一个多条件筛选的典型写法def machine_list(request): queryset Machine.objects.visible_to(request.user) keyword request.GET.get(keyword, ).strip() mtype request.GET.get(machine_type, ) status request.GET.get(status, ) org_id request.GET.get(organization, ) if keyword: queryset queryset.filter( Q(machine_no__icontainskeyword) | Q(name__icontainskeyword) | Q(brand__icontainskeyword) ) if mtype: queryset queryset.filter(machine_typemtype) if status: queryset queryset.filter(statusstatus) if org_id: queryset queryset.filter(organization_idorg_id) queryset queryset.select_related(organization, driver).order_by(-created_at) paginator Paginator(queryset, 15) ...注意我用了select_related(organization, driver)。页面列表要显示所属合作社名称和机手姓名如果不做预查询每一行都会额外发两条SQL查关联表15条数据就是30条SQL页面响应会明显变慢。加了select_related之后一条联表查询搞定这个优化在列表页是必须的。新增和编辑页面用的是Django的ModelForm表单校验直接复用模型字段定义。同时我在前端页面用了一些JavaScript做联动校验。比如选择了“已报废”状态就要求填写报废日期和报废原因选择了“维修中”就要弹出维修工单创建入口。这些联动规则没写在模型层而是放在页面的表单控制逻辑里因为修改状态时业务约束经常变化放前端灵活一些。4.2 作业统计与报表让汇总告别手工对账统计报表模块的目标很简单把原来统计员月底对着聊天记录手工整理数据的活全部变成界面上几个选择框加按钮的操作。最核心的一张表是“农机作业月报”按月份、农机、作业类型聚合出作业面积、油耗、作业次数和单亩油耗。Django的ORM做这种分组聚合很顺手from django.db.models import Sum, Count, F, Q monthly_report (WorkRecord.objects .filter(statusapproved, start_time__yearyear, start_time__monthmonth) .values(machine__machine_no, machine__name, work_type) .annotate( total_areaSum(area), total_fuelSum(fuel_consumption), work_countCount(id), ) .order_by(-total_area))这组查询返回的结果会渲染成两张图一张是各类农机的作业面积堆叠柱状图一张是各机手作业量排名条形图同时下方配一个明细表格。图表用ECharts绘制后端只需要传JSON格式的数据。做统计还有个经验不要硬啃年度大数据量。如果用户查的是全年数据前端一次性渲染几百个点没问题但要是按“近五年”查直接渲染会很卡。我把统计维度做了分层按天聚合成日汇总表报表查询默认查日汇总表的汇总值只有单条明细时才回明细表。这样不管用户查的时间范围多大响应时间都能控制在1秒以内。4.3 保养预警与消息通知让系统主动找人而不是人找系统农机保养提醒是本项目用户满意度最高的功能之一原因很简单——过去靠人记现在系统主动提醒。实现上分为三部分定时任务扫描、消息落库、用户查看和确认。定时任务我没有引入Celery那么重的东西项目里就用Django的manage.py自定义命令加系统Cron来实现。每天凌晨扫描所有状态为“使用中”的农机计算当前累计工作小时与next_maintenance_hours的差值。差值小于等于50小时时生成一条预警记录。核心逻辑大概是这样def scan_maintenance_due(): machines Machine.objects.filter(is_deletedFalse).exclude(statusscrapped) for machine in machines: remaining machine.next_maintenance_hours - machine.current_hours if remaining 50: Message.objects.create( usermachine.organization.admin, title保养预警, contentf农机[{machine.machine_no}]距下次保养仅剩{remaining}小时请安排保养。 )预警消息落库后用户在系统站内信和首页看板都能看到。这里有个细节消息不能每次扫描都重复生成否则第二天用户打开系统看到几十条一模一样的提醒。我给Message加了message_key字段存唯一键生成前先查message_key是否已经存在用唯一约束保证幂等。对于已确认处理的保养任务维修员在保养记录页填写保养数据后系统自动把机器的next_maintenance_hours更新为“当前工时 新的保养周期”同时相关预警自动消除。这样整个“预警 → 保养 → 重置”的闭环就走起来了。4.4 基于位置的轨迹查看与作业面积核算这是整个系统里技术上最“硬”的一部分。带有定位模块的设备会上报GPS轨迹点农机信息管理系统需要把这些点画到地图上方便调度员追踪跨区作业情况也用于辅助核算作业面积。轨迹点的存储我单独建了一张表字段包括农机外键、时间、经度、纬度、速度。这张表增长很快一台收割机干一天活可能产生几千个点所以设计了两个策略控制性能一是按车按月分表归档二是查询时做抽稀。抽稀的逻辑不复杂。前端回显轨迹不需要一万个原始点两百个点就够了。做法是按时间均匀抽样import math def sample_points(points, target_count200): if len(points) target_count: return points step len(points) / target_count return [points[min(int(i * step), len(points) - 1)] for i in range(target_count)]前端用Leaflet画折线同时标注作业地块名称。轨迹数据本身叠加到农机档案详情页点开任意一台机器就能看到最近三天的移动路径。作业面积核算做的是辅助估算。用轨迹点计算外包多边形面积再换算成亩。这个方法只能作为参考因为农机在地头转弯和重复路径会带来误差实际结算还是以机手填报、管理员审核为准。我把自动估算结果放在作业记录详情页备注“估算值仅供对账参考”避免被用户直接拿去做补贴申报。4.5 Excel批量导入导出历史数据迁移的救命稻草用户实际使用系统首日最痛的事就是把老Excel台账搬到新系统。几百台设备一张一张手动录入显然不现实所以我把批量导入功能作为上线前的必做项没有它这个项目根本推不下去。导入流程设计成下载模板 → 填写数据 → 上传 → 预览校验 → 确认导入。模板用openpyxl生成表头附带下拉选项和格式说明。上传后后端逐行解析校验必填项、数据类型、农机编号重复情况。校验结果集中展示哪一行、哪个字段、什么问题。全部通过后再一次性写入数据库。def parse_machine_excel(file): workbook load_workbook(file) sheet workbook.active errors [] machine_list [] for row_idx, row in enumerate(sheet.iter_rows(min_row2, values_onlyTrue), start2): machine_no, name, mtype, brand row[0], row[1], row[2], row[3] if not machine_no or not name: errors.append(f第{row_idx}行农机编号和名称不能为空) continue if Machine.objects.filter(machine_nomachine_no).exists(): errors.append(f第{row_idx}行农机编号[{machine_no}]已存在) continue machine_list.append(Machine(...)) if errors: return {success: False, errors: errors[:50]} Machine.objects.bulk_create(machine_list) return {success: True, count: len(machine_list)}要注意的是上传的Excel里的日期字段不是Python的date对象而是字符串。解析时必须用datetime.strptime显式转换并且预设格式容错比如同时支持“2024-03-01”和“2024/3/1”两种写法否则用户从不同版本Excel导出来的文件在导入时会报一堆格式错误。导出功能相对简单把查询结果用openpyxl写进表格设置列宽和表头样式做一个下载接口即可。导出还需要处理一个细节中文字符和文件名的URL编码问题。用HttpResponse返回文件时文件名要做URL编码否则在部分浏览器里下载的文件名会乱码。5. 常见问题与排查实录这些坑我替你们趟过了5.1 时区问题入库时间比实际少了8小时开发环境跑得好好的部署到生产环境后发现作业记录里用户提交的时间全部比真实时间慢了8小时。最后定位到是Django设置里的USE_TZ True和TIME_ZONE UTC在作怪。Django在USE_TZTrue时所有DateTimeField字段读写都按UTC处理只有在模板渲染时才转成TIME_ZONE指定的时区。如果数据库里存的是UTC时间前端展示时如果忘记转时区就会看到少8小时的“穿越数据”。而国内用户的大部分操作时间都是日常业务记录不是系统生成的时间戳所以必须保证存进去的就是北京时间。解决方案分两步。第一步把TIME_ZONE设置成Asia/Shanghai第二步在写入业务时间的地方用timezone.localize或者直接使用pytz处理。更稳妥的办法是对这类业务时间字段干脆把USE_TZ改成False让Django直接使用本地时间。考虑到这个系统只服务国内用户没有跨时区的需求最终我直接采用了USE_TZ False业务时间怎么填就怎么存不在转换上耗时间。如果你的系统未来会跨时区使用建议保留USE_TZ True但一定要清楚所有涉及用户填写的业务时间入库前必须转换成UTC存储展示时再转回用户时区。别混用。5.2 查询慢作业记录表几万行之后列表页秒变拖拉机系统上线运行三个月后作业记录表突破了5万行。这时候机手在手机上查询自己的作业历史页面转圈好几秒。排查过程很典型查看Django的debug工具栏和数据库慢查询日志发现慢的SQL主要集中在“按机手和年份查询”以及“按农机和月份汇总统计”这两类。解决方案首先是补索引。作业记录表里原本只有主键索引我在(operator, start_time)和(machine, start_time)上加联合索引之后查询速度立竿见影。其次是优化查询语句本身统计类接口里用values().annotate()避免一次性加载大量模型对象。还有个容易忽略的点是分页查询中的count()。Django的Paginator默认会执行一条SELECT COUNT(*)在大数据量表上这条语句也会挺慢。我把高频列表页的分页计数缓存到Redis设置60秒过期列表翻页就快了。5.3 定时任务“重复执行”多进程下的幂等问题保养预警上线第一天就翻车了。当天凌晨的扫描任务半小时内执行了三次用户早上打开系统看到几十条一模一样的保养提醒除了拍脑袋想骂人没别的。原因在于当时测试环境上跑了一个runserver进程生产环境配置的Gunicorn可能也会启动多个worker进程Cron每五分钟触发一次manage.py command而多个进程读到的“任务未执行”状态是相同的于是都插入了一次消息。解决办法是给定时任务加Redis分布式锁。执行命令前先尝试用SETNX获取一个带过期时间的锁只有成功拿到锁的进程才真正执行扫描其他进程直接退出。import redis r redis.Redis(host127.0.0.1, port6379, db0) lock_key maintenance_scan_lock if r.set(lock_key, 1, nxTrue, ex600): try: scan_maintenance_due() finally: r.delete(lock_key)加上锁之后这个问题再也没出现过。后来我把这招用在了所有涉及“同一时间只允许一个进程执行”的后台任务上包括Excel导入和补贴数据同步。5.4 Excel导入乱码和日期解析异常两个容易“小问题”的细节Excel导入模块联调测试时被不同人的Excel文件折腾了好几轮归纳起来就两类问题。一类是中文乱码。用户给的模板文件好多是从老系统导出再编辑的编码不一定是UTF-8。用openpyxl读xlsx格式没有编码问题但一旦用户把文件另存成CSV再传上来编码就乱了。最终我对导入文件格式做了限制只接受xlsx不接受csv。同时在模板里用数据验证限制了几个关键字段的可选值从源头挡住大量无效数据。另一类是日期解析。Excel里的日期在不同区域语言设置下可能显示为字符串“2024年3月1日”、数字“45001”或者其他格式。解析策略是分类型处理如果是datetime.datetime直接转换如果是字符串做多格式尝试如果是数字则用xlrd的日期规则计算。把这一层容错做好后导入的成功率从60%直接提到95%以上。5.5 权限数据隔离不能让别人看到别的合作社的农机这个系统上线前被客户提了一个很现实的要求同一个系统将来可能部署给多个农机合作社用每个合作社只能看到自己的农机和人员数据。这就是数据隔离问题不光是权限控制。我最初用Django自带的Group和Permission管理页面级权限但这解决不了行级数据隔离。某合作社的管理员虽然在登录后看不到别人的菜单位置但直接改URL访问某个农机详情页时如果数据不过滤就有越权风险。最终的方案是文章前面提到的自定义Manager统一过滤。我重新检查了所有涉及模型查询的视图确保每个入口都走Machine.objects.visible_to(request.user)这类方法而不是直接调用Machine.objects.all()。为了排查是否还有遗漏我写了一个管理命令遍历项目里所有视图函数的查询代码找出没有经过管理器过滤的查询并输出警告。上线前用这个方法揪出两处低级遗漏补上后权限自测才通过。写在最后的几点体会这个项目从需求确认到正式上线前后花了三个多月。整个过程中我最深的感受是真正难的往往不是Django代码本身而是如何把用户脑子里那堆“差不多就行”的模糊需求变成一张张精确到字段和状态的数据表。客户一开始只说要一个农机管理系统但我去现场跑了三天把机手、管理员、统计员的日常工作全部跟了一遍才搞明白系统到底要解决什么、数据的源头在哪里、哪些环节可以信息自动流转、哪些环节必须保留人工审核。这一步花的时间再久都值得。还有一个很大的经验教训是给农口用户做的系统界面上少做无用的动效和复杂的交互多做能直接导出的表格和能快速录入的表单。很多用户不习惯打字能点选就不要让用户手输。字段的提示文案不要写“请输入”要写“请选择作业类型如耕地、播种”。这些细节决定了用户愿不愿意把你的系统用下去。如果让我预判这类系统的下一步方向我觉得会朝两个方向演进一是接物联网数据农机的实时位置、油耗、故障码全部自动采集不再依赖人工填报二是跟补贴申报、保险理赔这类外部环节打通让农机数据产生更多资产价值。但无论未来怎么发展把基础档案、作业记录、维护记录这些数据模型设计清楚始终是底层的地基——地基稳了上面盖什么都靠谱。