Python Django构建社区物资采购系统:从订单管理到库存预警的完整方案 如果不是真的跑过一套社区物资保障业务我不会明白一个朴素的道理单纯把页面堆出来的“采购系统”在真实业务面前往往脆弱得不堪一击。居民在微信群里接龙下单社区管理员靠复制粘贴做 Excel供应商发完货还要人工逐项核对到货记录——这套手工流程看上去也能转可一旦遇到集中下单漏单、错项、库存对不上、谁改过数据完全查不出来。于是有了这套用 Python 基于 Django 实现的便民物资采购系统目标就是把“居民下单—管理员审核—供应商供货—到货核销—库存预警”这条完整链路收进系统里。这篇博客我会按照需求拆解、数据模型、项目骨架、核心交易链路、便民边界逻辑、上线踩坑、部署运营这条线来写。如果你是在校学生做课设或毕设重点看前四部分如果你要把它用到社区、园区、物业、单位保供这类真实场景后三部分更值得慢慢读。每个关键设计我都会尽量讲清楚“为什么这么做”而不是只贴一段能跑的代码。1. 为什么一上来就做角色分离需求端的三个真实噪音1.1 集中采购场景下的业务现状便民物资采购最常见的场景是某个街道、小区或园区临时启动集中采购居民在统一渠道把米面油、蔬菜包、消毒水等商品报给管理员。以前的做法大多是微信群接龙接龙结束后管理员把聊天记录拷贝到 Excel 里做汇总再发给供应商货到了之后又由几名工作人员按名单分发最后在纸面表格上打钩确认谁领了、谁没领。这个流程在人数少的时候问题不大三五十个人、十几种商品Excel 完全可以应付。但真实业务不是这样。同一个小区可能有几百户同时下单商品品类十几二十种同一个商品还分不同规格有的按份、有的按斤订单状态从“刚提交”到“已出库”再到“已签收”会经历好几个环节。于是出现了三个非常现实的噪音信息不一致群里接龙到一半换了管理员新管理员拿到的 Excel 可能是旧版漏单和重复下单随后就出现。流程不透明居民不知道自己的订单卡在哪一步管理员每天被私聊问几十遍“我的菜到了吗”。责任说不清某件商品是谁上的架、谁改的价格、谁确认可以发货全部没有记录出问题只能互相扯皮。这个阶段最需要的不是花哨的电商功能而是把“谁在什么时候对订单做了什么”全部留下痕迹。系统设计的第一步不是写代码而是先把角色和权限拆分清楚。1.2 四种角色的权限矩阵我最终把用户分成四类居民、社区管理员、供应商、系统管理员。这个拆分不是拍脑袋而是对照真实操作流程里的人来分的。角色核心职责能做的事不能做的事居民下单、收货、反馈浏览商品、加购物车、提交订单、查看物流/核销状态、申请取消改价格、审核订单、看别人订单社区管理员日常运营审核订单、维护商品上下架、确认发货、扫码核销、导出汇总表改系统角色权限、改核心配置供应商供货协同查看汇总订单、上传批次号/物流单号、维护供货记录审核订单、触碰价格系统管理员平台运维管理所有用户、角色、公告、权限分配、日志审计、库存预警阈值不应有单笔订单随意改价的操作痕迹即便操作也要留日志这套矩阵里最容易被忽略的是“供应商”角色。很多初级设计会把供应商完全撇开让管理员手工录入物流单。实际上供应商如果能在自己的账号里看到已审核通过的订单汇总并上传物流单号管理员后续核销会省掉大量来回询问的口舌。我给供应商开放的权限非常克制只能看和自己供货相关的汇总不能看到居民个人住址、电话等隐私字段。1.3 功能模块按“人”切分功能模块没有按传统电商的“商品、订单、支付”大类去分而是按“人”的视角切成了四个 appusers注册、登录、实名信息、收货地址、用户类型和权限。catalog商品分类、商品管理、库存、批次有效期、上下架状态。orders购物车、下单、审核、发货、核销、取消、退款、订单导出。notices公告、站内消息、库存预警提醒。这样切的好处是需求分析时可以直接拿着模块列表跟管理员对话他不需要理解“order_item”“foreign key”这些概念只需要说“我们每天要导出一次按商品汇总的订单表”我就能很快定位到 orders 模块的导出功能。后面维护阶段如果某个社区想增加“家庭配额”功能也只需要在 orders 或 users 里扩展不至于牵一发动全身。2. 先把数据模型画清楚一张订单怎么从“新建”走到“完成”2.1 核心实体与几张关键表数据模型是整个系统真正的地基。功能可以后期加表设计错了后面改起来非常痛苦。我先说几个所有类似系统都应该遵守的约定用户模型必须自定义而不是直接用系统默认的 Django User。哪怕现在只用了手机号登录以后要加社区、加实名、加家庭配额默认表根本放不下。金额字段一律用 DecimalField别用 FloatField采购系统天天和钱打交道浮点精度问题会在一次累计汇总后给你上一课。订单明细里必须保存当时的商品名称和单价快照而不是用外键去关联商品表。商品之后会调价、改名历史订单不能跟着变。业务表尽量别物理删除用软删除。订单被误删了要能追溯尤其在便民保障场景审计比整洁更重要。下面这段用户模型是整套系统的起点from django.contrib.auth.models import AbstractUser class User(AbstractUser): USER_TYPE_CHOICES ( (resident, 居民), (admin, 社区管理员), (supplier, 供应商), ) user_type models.CharField( 用户类型, max_length20, choicesUSER_TYPE_CHOICES, defaultresident ) phone models.CharField(手机号, max_length20, uniqueTrue) community_name models.CharField(所在社区/园区, max_length50, blankTrue) is_verified models.BooleanField(已实名, defaultFalse) class Meta: verbose_name 用户 verbose_name_plural verbose_name商品表要额外处理“规格”和“库存”。这里有一个很多新手会犯的错只用一个 stock 字段表示库存。我后来加了一个 reserved_stock 字段含义是“已经被订单锁定但还没发货的数量”。这样能计算出真实的可售库存class Product(models.Model): name models.CharField(商品名, max_length120) spec models.CharField(规格, max_length255, blankTrue) price models.DecimalField(单价, max_digits10, decimal_places2) unit models.CharField(单位, max_length10) category models.ForeignKey(catalog.Category, on_deletemodels.PROTECT) stock models.IntegerField(物理库存, default0) reserved_stock models.IntegerField(已锁定库存, default0) limit_per_user models.IntegerField(每人限购数量, default0) status models.CharField(状态, max_length10, choices..., defaulton) produce_at models.DateField(生产日期, nullTrue, blankTrue) expire_days models.IntegerField(保质期天数, default0) image models.ImageField(upload_toproducts/, blankTrue) property def available_stock(self): return max(self.stock - self.reserved_stock, 0) class Meta: verbose_name 商品订单表与订单明细表是交易的核心。我单独设计了 order_no 而不是直接用自增 id 作为订单号。原因很朴素管理员在微信群回复居民时说“订单号 202405120018”比说“记录 id 10086”要自然得多。生成规则也简单日期加当天流水class Order(models.Model): STATUS_CHOICES ( (draft, 草稿), (submitted, 已提交), (confirmed, 已确认), (delivering, 配送中), (signed, 已签收), (cancelled, 已取消), (refunded, 已退款), ) order_no models.CharField(订单号, max_length32, uniqueTrue) user models.ForeignKey(users.User, on_deletemodels.PROTECT) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultdraft) total_amount models.DecimalField(订单总金额, max_digits10, decimal_places2) contact_name models.CharField(联系人, max_length30) contact_phone models.CharField(联系电话, max_length20) address models.CharField(收货地址, max_length255) remark models.TextField(备注, blankTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) confirmed_at models.DateTimeField(审核时间, nullTrue, blankTrue) delivered_at models.DateTimeField(发货时间, nullTrue, blankTrue) signed_at models.DateTimeField(签收时间, nullTrue, blankTrue) class OrderItem(models.Model): order models.ForeignKey(orders.Order, related_nameitems, on_deletemodels.CASCADE) product models.ForeignKey(catalog.Product, on_deletemodels.PROTECT) product_name models.CharField(商品名快照, max_length120) product_spec models.CharField(规格快照, max_length255, blankTrue) price models.DecimalField(单价快照, max_digits10, decimal_places2) quantity models.PositiveIntegerField(数量) amount models.DecimalField(金额, max_digits10, decimal_places2)这里需要的模型定论OrderItem 的 product_name、price 和 product_spec 是快照字段。它们不参与关联查询库存只用作展示和流水记录。很多人觉得这样做冗余但半年后当商品表价格从 39.9 改成 42.0 时你会感谢这些冗余字段帮你保住了每一张历史订单的真实面貌。2.2 订单状态机宁可多几个状态也别让流程“只能前进不能后退”订单状态是整个系统最容易失控的地方。如果只设计“待处理、已完成”两个状态就会出现一个尴尬情况管理员已经按订单配好货居民忽然说不要了这时候该走“已完成”还是“待处理”说不清。状态机设计成七个状态draft - submitted - confirmed - delivering - signed \ \ \ | \ \ \ ---- refunded \ \ ---------------- cancelled \ ---------------------------- cancelled这张图表示订单在每个阶段可能的去向。故意保留“draft”这个草稿态是因为居民从购物车生成订单后可能还没有最终提交。提交后管理员有一个“confirmed”动作这意味着订单进入正式供货清单。为什么必须有一个确认节点因为在社区采购的边界下管理员往往需要根据供应商库存调整数量比如供应商说今天只有 500 份菜那就得先确认订单再分配。确认动作完成后订单才允许被纳入汇总导出供应商才会看到。还有一个约定非常重要取消订单不能一刀切。订单在“submitted”状态取消只需要释放锁定库存在“delivering”状态取消就要通知管理员确认货物是否已经装车在“signed”之后居民再说不要那就不是取消而是走退款/退货流程。这种状态设计会让代码里每个操作分支都清晰避免某一个 View 里堆一堆 if status xxx 然后互相覆盖的情况。2.3 库存的两个数字物理库存与锁定库存我先讲讲我在第一版里犯过的错误。最初只在商品表写了一个 stock 字段用户下单时执行“如果 stock 大于买的数量就把 stock 减掉”。逻辑在单用户、低并发时完全正常可一旦两个人同时抢同一件商品最后库存可能被扣成负数。后来引入 reserved_stock 概念核心变化是下单成功reserved_stock 增加物理 stock 不动。意思是“这 10 份菜已经被某几个订单占住了其他人不能再买”。订单取消reserved_stock 减少物理 stock 不动。意思是“以前占的坑释放别人又可以买了”。商品实际发出物理 stock 减少reserved_stock 跟着减少。到这一步才是真正从仓库里拿走货物。这样设计有一个额外的好处管理员在后台看到“可售库存 物理库存 - 锁定库存”他心里清楚不是系统算错了而是那部分货已经有了订单归属。这对便民采购这种强时效业务非常友好。3. Django 项目骨架和配置里容易被忽略的几项3.1 技术选型为什么是 Django以及版本怎么挑做这个系统前我反复比较过 Flask、FastAPI 和 Django。论轻量FastAPI 确实快但论“一套后台管理系统”Django 的管理后台、认证体系、ORM、迁移机制都是现成的尤其这种以管理审核流程为主的系统Django 的生态能省下一半工作量。版本上推荐 Python 3.10 或 3.11 搭配 Django 4.2 LTS。Django 5.x 虽然新特性多但第三方库的兼容性不一定跟上。社区采购系统对稳定性要求高于对新特性的追捧选 LTS 版本是稳妥的做法。数据库方面开发期用 SQLite 起步很方便真正上线建议切换成 MySQL 8.0 或 PostgreSQL。如果你所在的云服务器内存只有 1GMySQL 8 可能有点吃紧那 5.7 也能跑但字符集一定要设置 utf8mb4否则存居民留言里的表情符号会报错。3.2 项目目录和 settings 里必须提前想好的配置项目目录按业务域拆分而不是按技术层拆分。这样每个人看到一个 app 就知道它负责哪个业务域config/ settings/ base.py dev.py prod.py urls.py apps/ users/ catalog/ orders/ notices/ static/ media/ manage.py requirements.txt开发和生产配置文件分离是为了避免上线时把 DEBUGTrue 带到服务器。下面的 base.py 配置有四个容易忽略但非常关键的点INSTALLED_APPS [ # django 自带 app django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, # 自建 app apps.users, apps.catalog, apps.orders, apps.notices, ] AUTH_USER_MODEL users.User LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_I18N True USE_TZ True MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media STATIC_URL /static/ STATIC_ROOT BASE_DIR / staticfilesAUTH_USER_MODEL 这个配置必须在第一次执行 makemigrations 之前就设好。如果项目已经用默认 User 跑过一次数据库迁移再想换自定义用户模型会非常麻烦基本只能重建库。网上很多教程在入口处没提新手照着写做到一半才发现卡住了。TIME_ZONE 和 USE_TZ 是一对容易出问题的“好兄弟”。USE_TZTrue 时Django 在数据库里存的是 UTC 时间页面展示时会自动转成 Asia/Shanghai但如果你在某些地方用 Python 的 datetime.now() 生成时间就会出现比本地时间少 8 小时的情况。这里我的建议是除非项目未来一定会有海外用户否则一个纯国内项目可以更省心地设置 USE_TZFalse同时把 TIME_ZONE 定为 Asia/Shanghai这样所有时间字段都以本地时间存储排查日志时不用心里默算时差。3.3 管理后台与初始化命令Django admin 不是花架子这个系统里它真的能当运营后台用。把 User、Product、Order、OrderItem 都注册进 admin 之后管理员不需要自己写复杂页面就能完成大多数查询和修改操作。不过要注意运营人员与开发人员的需求不一样。开发时我们在 admin 里直接改 Order 状态很方便上线后普通管理员不应该有直接进入 admin 的权限因为 admin 的修改操作没有二次确认弹窗误点一下就可能把整个订单链路搞乱。正确的做法是普通管理员用独立的前台管理页面admin 入口只给系统管理员。初始化数据我建议写成 management command比如python manage.py init_demo里面创建超级管理员、几个商品分类、几件测试商品。这样每次换测试环境一条命令就能把系统拉起来而不是靠手工在页面里点点点。很多同学卡在“系统写完了但不知道从哪点起”其实就是初始化数据没准备体验极差。4. 核心交易链路实现从“加入购物车”到“确认核销”4.1 商品浏览与购物车商品列表页做了分类筛选、按名称搜索、按销量/价格排序。真正让我动脑子的不是这些而是购物车要不要存数据库。用 session 存购物车看起来简单但居民换个设备或清一下浏览器缓存购物车就丢了。对民用系统来说这是不可接受的。所以我建了 CartItem 表把用户选中的商品和数量都落库class CartItem(models.Model): user models.ForeignKey(users.User, on_deletemodels.CASCADE) product models.ForeignKey(catalog.Product, on_deletemodels.CASCADE) quantity models.PositiveIntegerField(数量, default1) checked models.BooleanField(结算时勾选, defaultTrue)提交订单时只需要读取该用户 checkedTrue 的购物车记录。购物车的数量校验与订单创建放在一起下面会讲。4.2 下单服务的三层校验与并发保护这个环节是整个系统的“心脏”也是我之前线上被压测打穿的地方。我把下单逻辑拆成三层第一层身份校验。用户必须登录账号没有被冻结。第二层业务校验。逐条检查商品是否在售、可用库存是否足够、是否超过限购数量、购物车数据是否与商品当前价格冲突。第三层并发控制。在同一事务中锁定商品行防止两个请求同时扣同一份库存。第三层的核心代码是这样from decimal import Decimal from django.db import transaction from django.db.models import F def create_order(user, address, remark): cart_items ( CartItem.objects .filter(useruser, checkedTrue) .select_related(product) ) if not cart_items.exists(): raise ValidationError(购物车为空) with transaction.atomic(): # 锁定商品行防止并发超卖 product_ids [item.product_id for item in cart_items] product_list list( Product.objects .select_for_update() .filter(id__inproduct_ids, statuson) .order_by(id) ) product_map {p.id: p for p in product_list} if len(product_map) ! len(product_ids): raise ValidationError(存在已下架商品请刷新后重试) total_amount Decimal(0.00) order_items [] for item in cart_items: prod product_map[item.product_id] available prod.stock - prod.reserved_stock if available item.quantity: raise ValidationError(f商品 {prod.name} 库存不足) if prod.limit_per_user and item.quantity prod.limit_per_user: raise ValidationError(f商品 {prod.name} 超出限购数量) # 锁定库存 prod.reserved_stock item.quantity prod.save(update_fields[reserved_stock]) amount prod.price * item.quantity total_amount amount order_items.append( OrderItem( productprod, product_nameprod.name, product_specprod.spec, priceprod.price, quantityitem.quantity, amountamount, ) ) order Order.objects.create( order_nogenerate_order_no(), useruser, statussubmitted, total_amounttotal_amount, contact_nameuser.contact_name, contact_phoneuser.phone, addressaddress, remarkremark, ) OrderItem.objects.bulk_create(order_items) CartItem.objects.filter(useruser, checkedTrue).delete() return order这里有几个细节很多人会忽略。select_for_update()的查询结果一定要按 product_id 排序否则在多个订单一起提交时可能出现两个请求分别锁定相反顺序的产品行导致数据库死锁。Django 检测到死锁后默认会抛异常但你的前端只会看到“服务器错误”。所以我在代码里显式.order_by(id)来统一锁顺序。reserved_stock的增加和订单创建必须放在同一个transaction.atomic()块里。如果分开事务中途出异常要么库存锁了但订单没建成要么订单建了但库存没锁都会出乱子。这一点我在后面踩坑章节会再提到。4.3 审核、发货和核销的闭环订单从“已提交”到“已签收”中间每个操作都应该有权限校验。管理员审核时我只能操作 statussubmitted 的订单发货时只能操作 statusconfirmed 的订单核销时只能操作 statusdelivering 的订单。这样设计可以避免管理员无意中把一个刚提交的订单直接点成“已签收”跳过了整个配货流程。发货动作我设计得比较“重”。管理员点击发货时系统会重新核对订单所有明细如果发现某个商品库存不足就阻止发货并提示先补货或取消部分明细。补货在真实场景里经常发生供应商送来的实际数量可能少于订单汇总数管理员需要能对单个订单改数量。这个操作会影响订单总金额和商品锁定数量所以我在代码里单独做了一个“改单记录”谁改的、从多少改成多少都写入日志。这样居民询问“为什么少了一袋米”时管理员能直接找到原因。核销环节本来计划做成扫码核销但后来考虑居民手上不一定有二维码不如支持两种方式管理员输入订单号后六位或者手动按手机号后四位查询。核销成功时订单状态变为 signedsigned_at 写入当前时间同时减少物理库存和锁定库存。这一步是库存数据最终“落地”的地方。4.4 导出汇总表被严重低估的高频功能在做这套系统之前我一直把商品、下单这些当主角导出功能当配角。实际部署后发现导出功能才是管理员每天打开最多的页面。管理员每天要给不同的供应商发对应的订单汇总要按日期统计“今天出了多少份菜、多少袋米”。Community采购场景中Excel 是绕不开的协作工具。导出用 openpyxl 实现按商品维度和订单维度提供两种工作表from openpyxl import Workbook from django.http import HttpResponse from django.db.models import Sum def export_order_summary(request): wb Workbook() ws wb.active ws.title 按商品汇总 ws.append([商品名, 规格, 单位, 总数量, 总金额]) summary ( OrderItem.objects .filter(order__status__in[submitted, confirmed, delivering]) .values(product_name, product_spec, product__unit) .annotate(total_qtySum(quantity), total_amountSum(amount)) .order_by(product_name) ) for row in summary: ws.append([ row[product_name], row[product_spec], row[product__unit], row[total_qty], str(row[total_amount]), ]) response HttpResponse( content_typeapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet ) response[Content-Disposition] attachment; filenameorder_summary.xlsx wb.save(response) return response有人可能会问为什么不直接用 pandas 来导出。在这个项目里导出的数据量最多几千行还没有到需要 pandas 的规模引入它反而增加依赖体积和内存消耗。openpyxl 足够用了。5. 便民场景特有的边界需求限购、批次有效期、公告提醒5.1 限购与家庭配额便民采购和普通电商最大的不同是它是“按需分配”而不是“按钱抢购”。物资有限时必须防止个别人把商品囤空。我的做法是给商品表加了一个 limit_per_user 字段开发期觉得这样就可以但上线后管理员反馈一个问题居民可以分多次下单每次都不超限购数量但累计下来还是能把商品买空。于是我把限购逻辑改成“跨订单累计校验”。判断一个居民能不能再买这个商品要看他对这个商品在所有未取消、未退款订单里的数量合计再加上这次提交的数量是否超过限购数。实现上并不复杂在 create_order 里增加一个查询from django.db.models import Sum existing_qty ( OrderItem.objects .filter( order__useruser, order__status__in[submitted, confirmed, delivering, signed], productproduct, ) .aggregate(totalSum(quantity))[total] or 0 )真正难的是“家庭配额”。一个家庭可能有多个人分别注册了账号要限制整个家庭的采收总量就需要先有“家庭”这个实体再把多个账号绑定到同一个家庭里。这套系统第二版才开始做家庭配额第一版因为需求不明确设计出来非常复杂反而是拖累。我给新手一个建议如果业务方没有明确说“必须按户限购”先做“按人限购 管理员审核时手动调整数量”就够了。多数异常单子管理员在审核这一步能拦下。5.2 批次与临期提醒物资采购里大量是食品和日用品能不能在系统里管理生产日期和保质期直接决定了运营工作量。第一版我只在商品表放了 produce_at 和 expire_days每天凌晨用一条管理命令扫描所有商品把距离保质期不足 7 天的商品标成“临期”列表页加红色角标。这个方案简单但有个盲区同一件商品可能来了好几个批次一个批次临期另一个批次还很新鲜。居民登录后看到的商品是一个整体很难区分批次。更严谨的做法是建立 StockBatch 表商品入库时根据到货批次生成多条记录每批次有自己的库存、生产日期和保质期。下单时按照“先进先出”规则分配批次。但这样会让下单逻辑复杂很多事务里要维护多批次的锁定明细。考虑到便民系统的实际用户量我最终采用了一个折中方案商品表继续保留简单的日期字段同时让管理员在每次批量入库时可以手动更新“最近入库日期”和“生产日期”。系统只负责提醒“该商品有多批次库存需要人工确认是否有临期不建议把多批次混在一起展示”把最终判断权交给管理员。5.3 公告、消息通知和未取单提醒采购业务里居民最关心两件事什么时候能下单什么时候去取货。所以系统在首页放了一个公告栏管理员可以发布“今晚八点开放明天物资预约”“明天下午三点到货请在门卫处领取”这类信息。公告可以指定接收范围比如只推送给某个社区或只推送给某个商品类目的购买者。站内信功能起初我觉得很鸡肋真上线后才发现订单状态变化时的站内通知非常重要。居民提交订单后如果管理员直接点了“已确认”系统会发一条站内信“您的订单已确认预计发货时间请留意公告”。虽然只是简单文案但大大减少了“我的订单到底成没成”的咨询量。发送逻辑不复杂在 Order.save 方法的 status 变化回调里判断也可以用 Django signal 实现。我选了后者因为 signal 不侵入业务代码维护起来清爽一点。6. 联调和上线过程中踩到的六个坑6.1 并发超卖第一次压测就翻车第一次压测的场景是 50 个并发用户同时抢最后 20 份菜品。测试结果是系统生成了 48 个订单但库存只有 20。第一时间查日志发现我的第一版在下单时用的是“先读取 stock 字段判断大于 0再写入 stock stock - 1”两个用户同时读到 stock 1就都通过了校验。修复方案就是前面代码里写的select_for_update()加transaction.atomic()。但这里还有个容易被忽略的细节Django 的 select_for_update 只对 InnoDB 等支持行锁的数据库生效SQLite 下它是静默无效的。开发环境用 SQLite 测不出这个问题所以我从一开始就强制团队在本地用 MySQL 或 PostgreSQL 跑测试。否则你代码写得再漂亮上线数据库一换问题照样复现。6.2 时间差 8 小时日志对不上有一阵子管理员反馈居民在晚上九点下的单后台显示的创建时间是下午一点。查了很久才发现代码里有几处用了 Python 的 datetime.now()而 Django 配置是 USE_TZTrue数据库存的是 UTC。两个时间混在一起前后相差 8 小时。排查过程很狼狈。我先看了 MySQL 里的 created_at发现存的是 13:00可页面上显示 21:00说明 Django ORM 在读取时做了时区转换而几处 datetime.now() 写进来的时间就直接是 21:00读出来还是 21:00造成同一张表里两种时段逻辑并存。修复方式是统一把所有取当前时间的地方都改成django.utils.timezone.now()并把日志输出时间强制标记为本地时间。这个坑不是代码逻辑多难而是约定混乱造成的。6.3 文件上传 413照片传不上去管理员给商品传图片时某些大图会直接失败浏览器里显示“413 Request Entity Too Large”。一开始以为是 Django 代码限制后来发现请求根本还没到 Django是在 Nginx 这一层就被拦了。Nginx 默认的client_max_body_size是 1m一张手机照片随随便便超过这个数。修复就是在 Nginx 配置里加client_max_body_size 10m;同时 Django settings 里设置DATA_UPLOAD_MAX_MEMORY_SIZE。这两个地方要一起调否则 Nginx 放行了Django 又因为默认 2.5MB 内存限制而报错。调试完强烈建议写进部署文档因为后面每次换服务器都会遇到。6.4 重复下单居民双击了“提交订单”居民在弱网环境下点击“提交订单”没反应自然反应就是再点一下。结果等网络恢复两个请求都成功执行生成了两笔订单。这不是并发问题而是典型的重复提交。前端可以很轻松地做一层防护提交按钮点击后置灰文案变成“提交中”。但前端防护只能拦截正常人手的双击拦不住请求重放。后端方案是在订单表里加 order_no 唯一约束同时在下单事务里校验“同一用户一分钟内是否已有相同订单内容”如果订单内容一模一样直接返回已有订单而不是新建。这个策略不复杂但确实把重复下单从根上掐住了。6.5 权限只挡了前端后端裸奔某次演示时一个普通居民账号直接访问/admin/路径竟然看到了管理后台入口。原因是我在模板里判断了如果用户不是管理员就不显示管理菜单但后端路由并没有做权限控制。演示时用户手动输入了 admin 的 URL页面就直接打开了。这个问题说严重很严重说好修也好修。所有 View 统一加装饰器或权限校验类比如staff_member_required只放行管理员角色普通居民的请求直接 403。重点不在于这一次修复而在于树立一条原则后端视图里出现的每一个接口都要问自己“这个接口允许谁访问”而不是“前端还记不记得隐藏那个入口”。6.6 列表页慢查询订单多了直接卡三秒上线运行一周后订单量到两三千条管理员打开订单列表就卡三秒。用 django-debug-toolbar 一看是 N1 查询问题订单列表加载后每一条 OrderItem 都去单独查一次 Product 表。几千条订单等于几千条 SQL。修复分三步查询订单时用.select_related(user, product)一次性把关联对象带出来。列表页分页每页 20 条不要一次性渲染全部。给 Order.status、Product.status 这些高频筛选字段加索引models.Index(fields[status])。优化后列表页稳定在 300 毫秒以内。这个问题再次验证一个老生常谈性能问题多半不是服务器配置差而是查询次数创建了太多。7. 部署和运营用简单工具保持长期稳定7.1 服务器与进程方案系统运行时我选择了 Linux 服务器上的 Nginx Gunicorn Supervisor 组合。Gunicorn 负责跑 Django 应用Nginx 负责托管静态文件和反向代理Supervisor 守护进程保证服务挂了能自动拉起。这套组合不是最花哨的但足够稳定出了问题社区也能找到人排查。推荐进程数量等于 CPU 核心数的两倍加一比如 2 核服务器就-w 3。更多 worker 对缺少异步支持的 Django 同步请求不一定有好处反而增加内存占用量。配置示例gunicorn config.wsgi:application -w 3 -b 127.0.0.1:8000 --timeout 60 --access-logfile /var/log/gunicorn/access.log --error-logfile /var/log/gunicorn/error.logSupervisor 配置里把 user 设为非 root 用户避免安全问题。启动前先把静态文件收集好python manage.py collectstatic --noinput否则 Nginx 会一直报 404。7.2 数据库备份宁可无用不可没有运营社区采购系统最怕的不是系统卡而是数据没了。订单记录、核销记录这些东西丢了居民不会理解“服务器故障”这个词他们只知道“我订的东西去哪了”。数据库备份用最简单可靠的方案每天凌晨用 mysqldump 全量备份保留最近 7 天再额外保留每周一份。cron 示例0 2 * * * mysqldump -u backup_user -pbackup_password --single-transaction community_db | gzip /backup/community_db_$(date \%Y\%m\%d).sql.gz find /backup -name *.sql.gz -mtime 7 -delete定期在测试环境做一次恢复演练这个动作看起来麻烦但只有亲手把备份文件恢复成能跑起来的系统你才敢说“有备份”。7.3 长期运营的迭代清单系统上线不等于结束。我后续的迭代基本围绕运营反馈展开优先级如下管理员反馈导出字段缺了优先加。导出是每天都会用的功能字段不齐等于每天手工补。某个商品频繁被反馈临期就要把批次管理提上日程。居民端最常问的问题整理成常见问题入口减少人工咨询。加入简单的订单统计报表按天、按周统计下单量、核销量、取消率这些数字对后续备货非常有参考价值。这套系统在真实环境中运行一段时间后我最深刻的体会是技术难点往往不在写一个多复杂的推荐算法而是每天都在处理那些不起眼的边界情况——库存为什么少了、订单状态为什么卡住、导出为什么和手工台账对不上。把这些边界情况处理明白系统才真正从“能跑”变成“好用”。最后分享一个细节上线初期我在后台看到一条核销记录订单显示“已签收”可对应的库存却一直没有减少。一查才发现发货时库存扣减逻辑只写在“发货”按钮里而管理员是通过导入 Excel 批量标记发货的那条导入路径漏掉了库存扣减。修好之后我在所有批量操作里都加了一件事完成后立刻跑一次对账脚本把订单状态和库存数字做一次交叉校验。这套对账脚本的成本很低但它在后续几百天里多次帮我提前发现数据异常。如果你要把这套系统长期用下去建议从第一天就开始写对账而不是等出问题再补。