news 2026/9/9 15:01:57

Django实战:构建农产品溯源系统,从二维码防伪到部署全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django实战:构建农产品溯源系统,从二维码防伪到部署全解析

这阵子帮一个农业合作社搭了一套基于Django的农产品溯源系统,从需求调研到上线部署来回折腾了大概三周。整个过程走下来,我觉得这个项目特别适合拿来当作Django项目实战的完整案例:它既有典型的数据建模,也涉及二维码生成、文件上传、状态流转、防伪校验,最后还有部署环节。正好把这段时间踩过的坑、想明白的逻辑和写出来的代码一并整理出来,希望能给正在做类似项目,或者准备拿Django接一个全流程业务系统的朋友一些参考。

农产品溯源这个需求听起来很直白,就是“扫码看农产品的来龙去脉”,但真往细了做,里面的门道不少。比如一个码怎么做到不可伪造,生产批次和销售批次怎么关联,图片视频这类溯源证据怎么安全地传到服务器,以及真正上线后如何让不懂技术的农户方便录入。这些都不是单纯写几个增删改查接口就能糊弄过去的。

我提前说清楚这篇博文的定位:我不会把整套项目代码贴出来,而是把核心设计的思考过程、关键代码片段和部署链路讲透。你拿到的是一套可以迁移到同类项目里的方法论和踩坑笔记。

1. 农产品溯源到底在追什么:先理清业务再谈 Django

很多人第一次接触溯源系统,觉得搞一个二维码、做一个查询页面、存几条记录就完事了。实际上,溯源系统的核心在于“链路闭环”和“防篡改”。

1.1 溯源系统不是给农产品贴个二维码那么简单

我们最终要达成的效果是这样的:消费者拿到一盒带有追溯码的草莓,扫码之后可以看到这个批次草莓的产地、种植户、定植时间、施肥记录、用药记录、采摘日期、农残检测报告、包装日期。每一条记录都对应一个操作时间、一个操作人员和一组现场照片或视频。整个链路从种植端一路走到消费端,中间任何一个环节断了,消费者就会质疑这条溯源信息的可信度。

从业务角色上看,这个系统至少要服务三类人:

  • 录入端(农户/合作社操作员):负责创建产品档案,新建生产批次,在每个生产环节补充记录和证据。
  • 消费端(扫码用户):扫二维码,看链路信息,提交对产品的评价或投诉。
  • 管理端(合作社管理员/监管人员):审核录入数据,查看扫码统计,处理异常预警。

我一开始犯的错就是把录入端想得太“重”,觉得要做一个完整App或者后台工作站。后来实际上线才发现,90%的录入操作都发生在田间地头或者车间里,操作员手里只有一部手机。所以后端接口和前端界面都得往“轻”了设计,Django Admin当然可以用,但更适合运营人员拿电脑集中维护,真正的现场录入还得靠移动端页面。

1.2 这个系统的核心数据流程

整个系统的数据流我梳理成下面这样:

  1. 合作社管理员创建产品档案,比如“红颜草莓”,包含品种、产地、图片、简介。
  2. 针对每一个种植批次创建批次档案,记录定植时间、预计采收时间、批次编号。
  3. 在种植、采收、加工、包装、仓储、物流等节点逐步追加流转记录,每条记录可以带若干图片或视频。
  4. 批次在“包装完成”之后,系统为这个批次生成一批唯一追溯码,每个码对应一件(或一箱)商品。
  5. 印刷厂(或者普通打印机)把这批追溯码打印成标签,贴到商品包装上。
  6. 消费者扫码后,系统通过追溯码反查出批次链路的完整记录。
  7. 每次扫码都会落一条扫码日志,管理员后台可以看到扫码分布和异常。

理解了这个流程,你就会发现,Django在这个场景里最合适的地方就是ORM数据建模内置Admin后台。业务表之间的关联关系非常复杂,但Django的ForeignKey和ManyToManyField能把这些关系管理得井井有条,而且后台可以直接变成运营人员的录入台,省掉一大块定制开发量。

1.3 为什么是Django而不是Flask或FastAPI

选型的时候我也纠结过要不要用Flask,毕竟Flask轻量,写起接口来快。但仔细列了一下需求,最后还是定了Django,原因很实在:

  • 这套系统是“后台重、前端轻”的典型业务,Django自带Admin、认证、权限、ORM、迁移工具,几乎每一样都用得上。
  • 溯源的业务模型强关联、强状态流转,ORM的F对象、select_related、prefetch_related能高效处理关联查询。
  • 项目后期大概率要加REST API给小程序端用,Django REST Framework依然是这个场景最成熟的方案。
  • Django的媒体文件处理、静态文件处理、模板引擎足够支撑快速出活。

这不是说Flask不行,而是Django在这个场景下能让你少写大量重复代码,尤其是Admin后台,直接省掉了平时最烦的“给运营做一套CRUD界面”的苦力活。

2. 一物一码的生成逻辑:二维码、批次号与防伪校验

二维码是整个溯源系统面对消费者的唯一入口,所以它的设计不能想当然。我见过很多溯源项目,二维码就是一个普通URL,谁拿到都能复制,复制出来的码还能正常用,完全没有任何防伪能力。这套方案做出来和贴一张普通标签没区别,消费者花钱买到的只是一段“能打开网页”的字符串。

2.1 码的结构设计:不能只放个URL

我最终采用的追溯码结构并不是单纯的URL,而是一串带签名参数的短码,扫码之后跳转到查询页面再带上这个码参数。

一个完整的追溯码路由是这样设计的:

https://trace.example.com/v/{trace_code}

trace_code本身是一串包含信息的字符串,例如:

B202503110001-A3F9K2-7C3D9E

拆开来看,这串码包含三个部分:

  • B202503110001:批次编号,B开头,后面是日期和流水号。
  • A3F9K2:该批次下的一个随机序列号,支持一物一码。
  • 7C3D9E:签名校验位,由服务端使用密钥和前面内容计算得出。

这个结构的核心是“签名校验位”。普通用户改了中间序列号,或者自己编一个码,最后一段签名对不上,服务端一验就会拒绝。这才是“一物一码”和“不可伪造”的关键所在。

2.2 HMAC签名防伪:让每个码都不可伪造

生成签名校验位的时候,我用了Python标准库里的hmac模块,配合一个服务端密钥。思路是:

  1. 取批次编号和序列号拼接成一个字符串。
  2. 用密钥对这个字符串做HMAC-SHA256计算。
  3. 取结果的前6位十六进制字符作为校验位。

具体代码写出来大概长这样:

import hmac import hashlib SECRET_KEY = b"your-trace-server-secret" def generate_trace_code(batch_no: str, seq: str) -> str: raw = f"{batch_no}:{seq}" sign = hmac.new(SECRET_KEY, raw.encode(), hashlib.sha256).hexdigest()[:6].upper() return f"{batch_no}-{seq}-{sign}" def verify_trace_code(code: str) -> bool: try: batch_no, seq, sign = code.split("-") except ValueError: return False raw = f"{batch_no}:{seq}" expect = hmac.new(SECRET_KEY, raw.encode(), hashlib.sha256).hexdigest()[:6].upper() return hmac.compare_digest(expect, sign)

这里需要注意一个细节:校验时不要直接用==比较字符串,而是用hmac.compare_digest。这个函数用常数时间比较,能避免简单的时序侧信道攻击。虽然一个溯源码系统的攻击面没有那么大,但好习惯还是得养成。

生成二维码图片我用的是qrcode库,配合Pillow把图片输出成PNG。批量生成的时候,把追溯码写入Excel,再逐一生成二维码图片文件。

import qrcode def make_qr(trace_code: str, output_path: str): qr = qrcode.QRCode( version=4, error_correction=qrcode.constants.ERROR_CORRECT_M, box_size=8, border=2, ) qr.add_data(f"https://trace.example.com/v/{trace_code}") qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") img.save(output_path)

我特别选择了ERROR_CORRECT_M级别的容错率,因为农产品包装上的标签容易在运输过程中被磨损、或者被水渍弄脏。容错率太低会导致扫码失败率高,太高又会把二维码图案搞得太过细密,小尺寸打印出来反而更糊。M级别是一个相对均衡的选择。

2.3 扫码次数异常:比防伪码本身更值得关注

代码可验证只是防伪的第一道防线。真正上线之后,你会遇到一个更现实的问题:一个码被反复扫。比如有个人拿到产品,扫了码,觉得页面信息不够详细,又转发给朋友扫;或者某个码被恶意刷接口,每分钟请求几千次。

我的处理方式是把扫码行为全部落到ScanLog表里,记录trace_codeipuser_agentlocationcreated_time。扫码查询接口返回数据的时候,会顺带返回一个scan_count字段。当同一个码的扫码次数超过一个阈值(比如20次),页面上就会显示一条提醒:“该追溯码已被多次查询,请确认产品来源。”这个提示能让消费者对异常情况保持警觉,也能对内部的防伪判断提供参考依据。

另外,为了防接口被恶意循环刷,查询接口我加了简单的频率限制。Django上可以用django-ratelimit这类库,也可以自己在cache里计数。我用的是后者,因为实现起来只有几行代码,而且不引入额外依赖:

from django.core.cache import cache def is_rate_limited(trace_code: str, limit: int = 30, window: int = 60) -> bool: key = f"trace_scan:{trace_code}" count = cache.get(key, 0) if count >= limit: return True cache.set(key, count + 1, window) return False

3. 批次流转的核心数据模型:从播种到上架的状态机

数据模型是整个溯源系统的地基。地基打不好,后面写接口的时候会异常痛苦。我在这个项目里把核心表拆成了五张:产品表、批次表、流转记录表、追溯码表、扫码日志表,外加几张辅助表。

3.1 产品档案、批次、流转记录三张表的拆法

产品表存的是相对静态的信息:

class Product(models.Model): CATEGORY_CHOICES = [ ("fruit", "水果"), ("vegetable", "蔬菜"), ("grain", "粮油"), ("tea", "茶叶"), ] name = models.CharField("产品名称", max_length=100) category = models.CharField("分类", max_length=20, choices=CATEGORY_CHOICES) origin_place = models.CharField("产地", max_length=200) description = models.TextField("产品描述", blank=True) cover_image = models.ImageField("封面图", upload_to="product/%Y/%m/", blank=True) created_at = models.DateTimeField(auto_now_add=True)

批次表是溯源链路的枢纽,一个产品可以有多个批次:

class Batch(models.Model): STATUS_CHOICES = [ ("planting", "种植中"), ("harvested", "已采收"), ("processing", "加工中"), ("packaged", "已包装"), ("shipped", "已发货"), ("finished", "已完成"), ] product = models.ForeignKey(Product, on_delete=models.PROTECT, related_name="batches") batch_no = models.CharField("批次编号", max_length=50, unique=True) producer = models.CharField("种植户/生产单位", max_length=150) planting_date = models.DateField("定植日期") harvest_date = models.DateField("采收日期", null=True, blank=True) status = models.CharField("当前状态", max_length=20, choices=STATUS_CHOICES, default="planting") created_at = models.DateTimeField(auto_now_add=True)

流转记录表是溯源链路的“日记本”,记录每一个环节发生了什么:

class FlowRecord(models.Model): NODE_CHOICES = [ ("plant", "种植"), ("fertilize", "施肥"), ("pesticide", "用药"), ("irrigate", "灌溉"), ("harvest", "采收"), ("process", "加工"), ("quality_check", "质检"), ("package", "包装"), ("warehouse", "仓储"), ("logistics", "物流"), ] batch = models.ForeignKey(Batch, on_delete=models.CASCADE, related_name="flow_records") node_type = models.CharField("环节类型", max_length=20, choices=NODE_CHOICES) record_time = models.DateTimeField("操作时间") location = models.CharField("操作地点", max_length=200, blank=True) operator = models.CharField("操作人", max_length=100) description = models.TextField("环节描述", blank=True) created_at = models.DateTimeField(auto_now_add=True)

流转记录和媒体证据之间我选择拆了一张Evidence表,而不是直接在FlowRecord上挂多个图字段。因为图片和视频是“多条对一条”的关系,用单独的关联表维护起来更灵活,也方便以后扩展其他类型的附件。

3.2 用Django的choices和状态字段控制流转

批次状态不是随便就可以来回改的。比如一个批次已经“已发货”了,就不应该再回到“种植中”。这个状态机的约束,我在模型层加了一层校验逻辑。

Batch模型里重写save()方法:

ALLOWED_TRANSITIONS = { "planting": {"harvested"}, "harvested": {"processing"}, "processing": {"packaged"}, "packaged": {"shipped"}, "shipped": {"finished"}, } def can_transition_to(self, new_status: str) -> bool: if self.status == new_status: return True allowed = self.ALLOWED_TRANSITIONS.get(self.status, set()) return new_status in allowed

其实我并没有在save()里强制拦截所有不合法的流转,因为有些特殊的业务场景确实需要管理员手工纠正状态(比如录入错误)。所以我的做法是:在业务接口层做严格校验,普通操作员只能按状态机顺序流转;在Admin后台放开给管理员修改的权利,毕竟管理员需要处理各种人工异常情况。

3.3 删除对象时的一个大坑:别让硬删除毁掉整条溯源链

Django文档里最基础的Model.objects.get(...).delete()用起来非常爽,但在溯源系统里,这是个危险操作。我在测试阶段就踩过一次:在Admin后台想删一个录入错误的批次,结果Django默认级联把该批次下的所有流转记录和追溯码全部删了。也就是说,已经打印出来贴到包装上的二维码,一夜间全部作废。

我当时处理的方式是全面转向“软删除”。

具体做法是给核心表都加上is_active字段,默认是True,然后用自定义管理器覆盖默认查询:

class ActiveManager(models.Manager): def get_queryset(self): return super().get_queryset().filter(is_active=True) class Batch(models.Model): # ... existing fields ... is_active = models.BooleanField("是否有效", default=True) objects = ActiveManager() all_objects = models.Manager()

这样,普通查询只会返回有效数据,而管理员如果需要看被隐藏的批次,可以用Batch.all_objects.filter(...)。真正需要物理删除的场景非常少,一般只是开发测试环境里用用。

这个坑也提醒了我:在项目一开始就要和数据需求方明确“关键业务数据一律不允许物理删除”的约定。宁可让后台看起来多几条脏数据,也不能让已经对外流通的追溯码失联。

4. 图片视频等溯源证据的上传链路:模型、存储与安全隐患

溯源系统的“证据链”如果只有文字描述,说服力大打折扣。消费者更愿意看到的是农药残留检测报告的照片、田间施肥的视频、包装车间的实拍图。所以,图片和视频上传是整个系统里最贴近实际业务需求的功能模块。

4.1 ImageField和FileField的正确打开方式

模型层面,我用了一个单独的Evidence表来关联流转记录和媒体文件:

class Evidence(models.Model): flow_record = models.ForeignKey( FlowRecord, on_delete=models.CASCADE, related_name="evidences" ) file = models.FileField("媒体文件", upload_to="evidence/%Y/%m/") media_type = models.CharField( "媒体类型", max_length=10, choices=[("image", "图片"), ("video", "视频")] ) uploaded_at = models.DateTimeField(auto_now_add=True)

关键配置在settings.py里:

MEDIA_URL = "/media/" MEDIA_ROOT = os.path.join(BASE_DIR, "media")

开发阶段跑起来没问题,URL路由里加两行就能访问上传的媒体文件:

from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... your urls ... ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

但生产环境里一定不能靠Django来服务媒体文件。原因很简单:Django自带的开发服务器是单线程的,一旦同时有几个视频在被下载,整个服务就会被卡死。生产环境必须交给Nginx来处理/media/路径的请求,这个在后面部署部分会细说。

4.2 上传接口的权限与校验

上传接口必须要做权限校验,不然任何人都可以往你的服务器上扔文件,还可能塞一个恶意脚本进去造成更大麻烦。

我是这样设计的:

  1. 接口要求登录,用Django自带的@login_required装饰器。
  2. 加上csrf_exempt,方便移动端上传时不用带CSRF Token。
  3. 对文件大小做限制,图片不超过5MB,视频不超过100MB。
  4. 对文件类型做校验。

文件类型校验这里特别提一下。很多人只看文件扩展名,比如后缀是.jpg就放行,但在实际生产中,把可执行文件改成图片后缀上传的情况非常常见。我用了python-magic库去读文件的真实MIME类型:

import magic def validate_file_type(uploaded_file): mime = magic.from_buffer(uploaded_file.read(1024), mime=True) uploaded_file.seek(0) allowed_images = ["image/jpeg", "image/png", "image/webp"] allowed_videos = ["video/mp4", "video/webm", "video/quicktime"] return mime in allowed_images + allowed_videos

关于python-magic的安装,在Debian/Ubuntu系系统上要先装系统库libmagic1,否则会报错。这一点在部署时特别容易忽略,我第一次就是漏了这个依赖,导致生产环境上传接口直接500。

4.3 视频上传的兼容处理和分片思路

视频上传是另一个容易出问题的点。Django默认情况下,一个超过2.5MB的文件会被放到临时目录,然后再写入目标位置。对于100MB级别的视频,这个流程虽然能跑,但有两个问题:一是网络不稳定时容易失败;二是只有一个大文件上传接口的时候,用户只能干等。

我当时业务量不大,采取了一个折中方案:

  1. 前端用<input type="file" accept="video/*">选择视频。
  2. 上传前先在浏览器端检查文件大小,超过20MB时给出提醒。
  3. 后端用FILE_UPLOAD_MAX_MEMORY_SIZE控制内存缓冲阈值,大文件走临时文件落盘,避免撑爆内存。
  4. 视频统一转成MP4格式再展示,前端用HTML5的<video>标签播放。

如果你的项目需要支持几百MB甚至更大视频的上传,推荐去了解一下django-chunked-upload这个库,它能把大文件切分成多个小块依次上传,后端再逐个拼接,能极大降低上传失败率。

视频转格式我用的是FFmpeg。在模型保存后,用Celery异步任务去处理转码。不过因为我们项目体量不大,我一开始并没有上Celery,而是用了一个简单的后台管理命令去批量处理待转码的视频,每天定时跑一次。这个方案的优点是部署简单,缺点是不够实时。如果以后视频多了,再平滑迁移到Celery也不迟。

4.4 媒体文件命名与目录问题

还有一个细节,就是文件名冲突。两个农户同时在田间上传照片,如果文件名都叫IMG_0001.jpg,后面那个就会覆盖前一个。规避方法:

def upload_to_path(instance, filename): ext = filename.split(".")[-1] timestamp = timezone.now().strftime("%Y%m%d%H%M%S") random_hex = uuid.uuid4().hex[:8] new_name = f"{timestamp}-{random_hex}.{ext}" return f"evidence/{timezone.now().strftime('%Y/%m')}/{new_name}"

这个函数会自动生成evidence/2025/06/20250610143001-3f2a9b7c.jpg这种路径,基本不会重名,而且按年月分目录,方便后续归档清理。

5. 扫码查询页与后台管理的落地细节

溯源系统的面向消费者查询页和内部运营后台,这两个界面的落地方式完全不同。查询页追求的是“快”和“直观”,后台追求的是“好用”和“可控”。

5.1 扫码查询页面的性能优化

扫码查询页的数据链路是:从URL里取出trace_code,反查TraceCode记录,再通过batch关联查出Product和所有FlowRecord,最后还要把每条流转记录的Evidence文件列表带出来。

如果直接按模型关系一层层查,会产生N+1查询问题。比如一个批次有10条流转记录,每条记录下有两张图片,那你需要查询1次TraceCode、1次Batch、1次Product、10次FlowRecord、20次Evidence,合计超过30次数据库查询。对于访问量很小的系统可能无所谓,但一旦遇到直播带货这类突发流量,数据库马上会顶不住。

Django的select_relatedprefetch_related就是解决这个问题的:

from django.shortcuts import get_object_or_404 def trace_detail(request, trace_code): code_obj = get_object_or_404( TraceCode.objects.select_related("batch__product"), code=trace_code ) batch = code_obj.batch flow_records = batch.flow_records.prefetch_related("evidences").order_by("record_time") context = { "product": batch.product, "batch": batch, "flow_records": flow_records, "scan_count": code_obj.scan_logs.count(), } return render(request, "trace/detail.html", context)

这里select_related("batch__product")把TraceCode到Batch到Product的JOIN查询合并成一次,prefetch_related("evidences")通过额外的IN查询把所有相关的Evidence一次性取出来。实测下来,这个页面最多4次查询就能出所有数据,效果还是很明显的。

如果后续扫码量变大,还可以考虑用Django的缓存框架,把查询结果缓存一段时间:

from django.core.cache import cache def get_trace_context(trace_code: str): cache_key = f"trace_context:{trace_code}" cached = cache.get(cache_key) if cached: return cached # 按上面的逻辑组装context cache.set(cache_key, context, 60 * 30) return context

注意缓存时长不要设太长,因为如果农户在后台补录了一条流转记录,消费者却没在页面上看到,追问起来就很尴尬。30分钟到一个小时的缓存窗口是一个比较稳妥的平衡。

5.2 Django Admin如何变成运营人员的溯源录入台

Django自带的Admin后台在这套系统里几乎是给运营人员量身定做的。但默认界面的体验比较朴素,字段展示也不够直白,需要花点时间定制。

我做的几个关键定制:

  • list_display:在批次列表里显示产品名、批次号、当前状态、创建时间,让运营人员一眼看清所有批次的情况。
  • list_filter:按产品分类、批次状态筛选,配合search_fields按批次号或种植户搜索。
  • inlines:在批次编辑页里内联展示流转记录,运营人员不用来回跳转页面就能维护整个链路。
  • readonly_fields:把batch_no设置为只读,不允许随便改,避免追溯码和批次号对不上。

还有一个很重要的优化是给Admin引入simpleui。这是一个第三方Django Admin主题,界面更现代化,操作也更友好。对于不熟悉技术的农户或合作社员工来说,一个清爽的后台界面能显著降低培训成本。

5.3 Django与Vue整合的一种务实方案

我见过很多团队纠结要不要把Django和Vue整合在一起,搞前后端分离。我的建议是:先把这个系统的使用场景分清楚再决定

对于消费者扫码查询页,用Django模板直接渲染就够了,没必要上一套SPA。页面本身很简单,就是产品信息、批次时间线、图片视频列表。模板渲染速度快,不需要单独部署前端项目,也省掉了跨域的问题。

对于运营后台,如果你觉得Django Admin不够用,要做一个定制化的数据看板或者复杂交互页面,那再引入Vue也是有价值的。我当时用的是“Django模板里嵌套Vue应用”的混合模式,在需要动态交互的看板页写上<div id="app"></div>,然后引入打包后的Vue文件。这样既享受了Django的认证和路由,又能在局部模块里用Vue做响应式界面,两边的开发效率都能保住。

6. 部署到服务器:宝塔、虚拟环境与Nginx静态文件

最后一步是部署。这个项目前后折腾了差不多两天,踩了不少坑,尤其是静态文件、媒体文件权限和uWSGI配置三个环节。如果你部署Django项目还不太熟,下面这一节可以直接当操作手册来看。

6.1 创建虚拟环境和安装依赖的干净姿势

服务器环境是Ubuntu 22.04,用的宝塔面板。我强烈建议在一台全新的服务器上部署时,先用宝塔装好Python 3.10+,然后为项目单独创建虚拟环境,而不是直接把包装到系统Python里。虚拟环境能避免不同项目之间的依赖冲突,也能让你在出问题的时候快速重建环境。

cd /www/wwwroot/trace_project python3 -m venv venv source venv/bin/activate pip install --upgrade pip pip install -r requirements.txt

提到依赖安装,现在很多人开始用uv替代pip,安装速度确实快很多,所以我也试了一下。uv可以从PyPI下载预编译的wheel包,在处理那些需要编译的依赖(比如Pillow、psycopg2)时优势非常明显。如果装依赖经常卡在编译环节,换成uv pip install -r requirements.txt会省很多事。

在虚拟环境里关联Python版本这一点要注意,宝塔的Python管理工具允许你给每个项目指定Python版本,如果创建虚拟环境时用了错误的Python解释器,后面装依赖经常会遇到“找不到gcc”或者“某个头文件缺失”的报错,排查起来特别痛苦。

6.2 uWSGI + Nginx的核心配置

生产环境我用的组合是Nginx + uWSGI,这也是Django官方文档推荐的主流方案之一。

项目根目录下创建一个uwsgi.ini

[uwsgi] chdir = /www/wwwroot/trace_project module = trace_project.wsgi:application virtualenv = /www/wwwroot/trace_project/venv master = true processes = 4 threads = 2 socket = 127.0.0.1:8001 chmod-socket = 664 vacuum = true die-on-term = true

这里processes设置为4,threads设置为2,是因为服务器只有2核4G内存,这个配置能兼顾并发能力和内存占用。如果是1核1G的小机器,改成processes = 2比较稳妥。

Nginx的站点配置:

server { listen 80; server_name trace.example.com; client_max_body_size 100m; location /static/ { alias /www/wwwroot/trace_project/static/; } location /media/ { alias /www/wwwroot/trace_project/media/; } location / { include uwsgi_params; uwsgi_pass 127.0.0.1:8001; } }

特别要说的是client_max_body_size 100m这一行。默认情况下Nginx允许的上传大小只有1MB,如果你不设置这一项,上传稍大一点的视频就会直接返回413错误。

静态文件收集这一步也别忘了,部署前要执行:

source venv/bin/activate python manage.py collectstatic --noinput

记得在settings.py里设置:

STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, "static")

6.3 媒体文件访问403的排查过程

部署的时候我遇到一个特别隐蔽的问题:静态文件能访问,但上传到/media/里的图片一直403。Django日志没报错,Nginx错误日志也没报错,页面就是加载不出来。

最后发现问题出在文件权限上。

宝塔默认的网站运行用户是www,而我在命令行操作时用root手动创建了media目录,导致这个目录的ownerroot。Nginx的worker进程以www用户运行,读取root拥有的目录时被拒绝了。

解决方案:

cd /www/wwwroot/trace_project chown -R www:www media chmod -R 755 media

排查这种问题的时候,可以先手动在浏览器里访问一个媒体文件,如果返回403,到服务器上执行:

ls -la /www/wwwroot/trace_project/media/evidence/

看看目录权限是不是正常。如果是drwxr-xr-x root root这种,基本可以断定是权限问题了。

6.4 关于删除虚拟环境的一个实际建议

热搜词里提到“django虚拟环境怎么删除”,这个我实际操作过。如果需要重建虚拟环境,一句话总结就是:直接删掉整个venv目录,然后重新创建,不需要任何特殊命令。

deactivate rm -rf venv python3 -m venv venv source venv/bin/activate pip install -r requirements.txt

有人习惯用pip uninstall把所有包卸载干净,但这样既慢又容易漏。我的经验是,虚拟环境本身就是一次性的,删掉重来永远是最干净、最省事的做法。

另外,requirements.txt一定要从虚拟环境里生成,而不是从系统环境生成:

pip freeze > requirements.txt

如果打包了系统环境里的包,部署到新环境时容易出现一堆没用的依赖,甚至版本冲突。

7. 复盘:这个项目做完之后的几点真实感受

项目上线之后,我自己也去合作社现场蹲了一天,观察农户是怎么录入数据的,消费者扫出来的页面长什么样。有些感受是在写代码时完全体会不到的。

第一,溯源系统的瓶颈往往不在技术,而在录入端的意愿和习惯。农户在地里忙了一整天,让他回到电脑前再逐条补记录,他大概率会拖到第二天甚至直接放弃。所以移动端录入界面必须做到“三步以内完成一条记录”,最好能以拍照为入口,照片拍完上传,再顺手选一个环节类型,就自动生成一条带时间地点的流转记录。

第二,二维码标签的印刷质量直接影响扫码成功率。有些合作社为了省钱,用普通喷墨打印机把二维码打在不干胶上,而包装是带纹路的牛皮纸,二维码被压出很深的纹路,手机很难识别。后来我们建议用热敏标签纸,打印清晰度高,粘连性好,扫码率从不到80%提升到95%以上。

第三,防伪不是单靠软件就能完成的。软件层能做的,是保证码本身不可伪造、扫码行为可追溯。但现实中一个码被复制打印到假冒产品上,除了每次都去后台人工比对扫码位置和内容之外,几乎无能为力。比较务实的做法是给标签增加一次性物理破坏结构,比如刮涂层、易碎纸,让标签一旦贴上就无法完好转移。

这半年里,我在这个系统上陆续加了Excel导入批量生成追溯码、开放了小程序端查询页面、还把检测报告的PDF文件也纳入了证据链。每一步改动,Django的模型迁移都稳稳接住了,没有一次需要返工重建数据表。这大概也是我为什么在这种重业务、重数据关系的项目里依然愿意推荐Django的原因——它的稳妥和成熟,在关键时候是替你把关的。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 15:01:46

从站协议栈Canfestival在STM32F103上的移植实战

简介&#xff1a;CANopen在STM32F103上的从机移植源码&#xff0c;是一套面向工业自动化与嵌入式开发者的从机节点实现方案&#xff0c;解决CANopen协议栈从零移植、对象字典配置、SDO/PDO通信联调等问题。压缩包共244个文件、3.59MB&#xff0c;核心代码由62个头文件与49个C源…

作者头像 李华
网站建设 2026/9/9 15:01:42

抗DDoS防御方案实战对比:流量清洗、CDN边缘防护与黑洞路由的取舍

1. 为什么写这篇对比&#xff1a;抗 DDoS 不是一个"装了就行"的事 前段时间接手了一个被流量打瘫的客户项目&#xff0c;业务本身不大&#xff0c;但一遇到攻击就全站 502&#xff0c;加钱买高防之后又发现误杀率奇高&#xff0c;正常用户都被拦在门外。排查了整整两…

作者头像 李华
网站建设 2026/9/9 15:00:15

双目立体视觉核线纠正:C++与OpenCV实现详解

简介&#xff1a;一套基于C实现的核线影像纠正程序&#xff0c;面向遥感、GIS及计算机视觉方向的开发者与研究人员&#xff0c;用于消除卫星或航空影像的几何失真并生成核线投影影像&#xff0c;为立体匹配、地形分析等任务提供几何精纠正基础。程序核心模块包括图像读取、传感…

作者头像 李华
网站建设 2026/9/9 14:59:48

零训练语义分割:用LLaVA先验知识实现开放词汇像素级定位

1. 项目概述&#xff1a;这不是又一个“调参炼丹”流程&#xff0c;而是一次对语义分割范式的重新定义最近在CVPR2026主会上看到这篇题为《The Power of Prior: Training-Free Open-Vocabulary Semantic Segmentation with LLaVA》的论文&#xff0c;我第一时间下载了原文和开源…

作者头像 李华
网站建设 2026/9/9 14:59:46

GPU-Util 100%算力却只有15%?从Warp调度到Tensor Core的深度解析

先别急着骂显卡是“虚标王”。GPU-Util 100%、SM满载、Warp调度打满&#xff0c;结果 nvidia-smi 里算力只有15%&#xff0c;这个场景在深度学习训练、高性能计算里太常见了。我最早遇到这问题是在调一个 transformer 推理服务&#xff0c;GPU 占用率显示接近 100%&#xff0c;…

作者头像 李华