1. 项目定位与整体设计思路
1.1 即时零售场景下,一套系统到底要解决什么问题
超市即时零售,说白了就是顾客在线上选好商品下单,超市这边从自己的仓里拣货、打包,然后骑手取走送到顾客家门口。听起来和普通电商差不多?其实差得远。普通电商的库存是"中心仓"模式,今天下单明天发货,错了还能第二天补救;即时零售的核心是"快"——顾客半小时内要拿到东西,那你的库存数据就必须是实时的,订单和仓储必须咬合得非常紧。这套系统名字里的"即时零售"和"仓储管理"两个词放到一起,就已经把核心矛盾点出来了:既要让顾客端顺畅下单,又要让门店端高效履约,缺了哪头都会出问题。
那中小超市的现状是什么?很多店还在用外卖平台撑着线上订单,但平台抽成高,用户数据也在别人手里,说到底就是一锤子买卖。想自己搭建一套系统,又发现市面上成套的进销存软件要么贵得离谱,要么功能臃肿,压根不适合自己的业务形态。所以很多开发者选择用 Python + Django 做一套轻量级、可定制、能跑通的系统,既有用户端商城,又有管理端的库存和订单处理,一台普通服务器就能跑起来。
如果你是在学校做课程设计或者毕业设计,这个标题更是标配里的热门选题。因为它覆盖了 Web 开发中最典型的能力点:用户认证、商品管理、购物车、订单、库存联动、后台管理、权限控制、部署。一套系统做下来,很多知识都能串起来。无论你是要做课题,还是真给店里做工具,这个项目的设计思路和实现细节都值得从头到尾捋一遍。
1.2 为什么技术栈锁定 Python + Django
先说结论:在这个业务量级和需求特征下,Django 几乎是"性价比"最高的选择,没有之一。
第一,Django 自带 Admin 后台。做过进销存类系统的人都懂,后台管理的开发量经常比前台还大。商品列表、订单列表、库存表、用户管理,这些功能如果用别的框架,你需要先搭一堆增删改查页面。而 Django 的 Admin 拿过来就能用,稍微配置一下字段列表、搜索、筛选,一个能用的管理后台就出来了。虽然正式上线时很多页面要定制,但这个底子能帮你省掉大量重复劳动。
第二,Django 的 ORM 非常成熟。这类系统的查询天然复杂:按分类过滤商品、按状态查订单、关联用户和订单明细、库存联表查询,用 Django ORM 写起来顺手,而且天然躲避了手写 SQL 带来的注入风险。后面我要讲的下单锁库存、库存流水这类事务性强的逻辑,ORM 配合事务管理也能写得很干净。
第三,Python 生态省心。Pillow 处理商品图片、OpenPyXL 导出 Excel 报表、APScheduler 跑定时任务,都是拿来即用。你用 Java 或 Node.js 也不是不行,但这套系统本身没有高并发、高性能的硬性要求,中小超市一天撑死几百个订单,一台 2G 内存的服务器绰绰有余。把复杂度降到最低,把开发效率拉满,才是这类项目的正确打开方式。
1.3 系统功能全景图
整个系统我建议拆成三个端来看,这样无论编码还是演示都有清晰的节奏:
| 端 | 核心模块 | 主要使用角色 |
|---|---|---|
| 顾客端 | 商品浏览、分类搜索、购物车、下单支付、订单查询 | 普通顾客 |
| 管理端 | 商品管理、库存管理、入库出库、订单处理、拣货履约 | 店员、库管 |
| 系统层 | 用户权限、登录认证、操作日志、数据统计 | 店长、系统管理员 |
其中管理端又可以往下拆一层,因为超市的日常运营里,库管关心的是"仓库里有什么、缺什么、怎么补",店员关心的是"今天哪些订单要拣货、货在哪",店长关心的是"今天卖了多少、哪些商品该下架"。这三个视角对应的功能模块都不同,所以在设计数据库和功能边界的时候,一定要把角色的诉求理清楚。下一章就从数据库设计开始拆,因为几乎所有功能后期改起来最痛苦的,就是表结构没设计好。
2. 数据库设计:先把数据模型想清楚再动手写代码
2.1 核心实体与关系梳理
数据库设计是这类系统的地基。我见过太多人一开始就埋头写代码,写到订单和库存联动的时候发现"库存放哪都不对",然后回来大幅改表结构。所以这块一定要想透了再动手。
先梳理核心实体。整套系统最少需要这些表:
- 用户表(继承 Django 内置用户模型扩展)
- 商品分类表(支持多级分类)
- 商品表(存商品基础信息、价格、图片)
- 库存表(存某个商品当前总库存、锁定库存、预警阈值)
- 购物车表(可选,访客购物时也可以存 Session)
- 订单表(存订单主信息:订单号、状态、金额、时间)
- 订单明细表(存订单里的每一条商品快照)
- 库存流水表(记录每一次库存变动的来龙去脉)
核心关系是这样的:用户对订单是一对多,订单对订单明细是一对多,商品对库存是一对一,库存流水对商品是多对一。这里我特别强调,商品和库存一定要分成两张表,哪怕是单仓模式。原因后面会详细说,但先从逻辑上讲,商品表存的是"这个商品叫什么、卖多少钱",库存表存的是"这个商品现在有多少、被锁了多少",两者变化的频率完全不同,拆开才能互不干扰。
2.2 关键表结构详解
下面给出几张核心表的关键字段,这是我个人认为比较稳妥的设计方案,照着建不会踩太多坑。
商品表(Product):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | AutoField | 主键 |
| category | ForeignKey | 关联商品分类表 |
| name | CharField | 商品名 |
| sku_code | CharField | 商家自定义 SKU 编码,建议唯一 |
| barcode | CharField | 条码,扫码枪录入用,可能为空 |
| price | DecimalField | 销售价,精确到两位 |
| cost_price | DecimalField | 成本价,用于毛利统计 |
| image | ImageField | 主图,配合 MEDIA 存储 |
| status | BooleanField | 上架/下架状态 |
| sales_count | IntegerField | 累计销量,用于"销量排序" |
| created_at | DateTimeField | 创建时间 |
库存表(Inventory):
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | AutoField | 主键 |
| product | OneToOneField | 与商品一对一 |
| quantity | IntegerField | 当前实际库存 |
| locked | IntegerField | 锁定库存(下单未支付占用的) |
| warn_threshold | IntegerField | 预警阈值,低于这个值提示补货 |
| updated_at | DateTimeField | 更新时间 |
这里有个关键点:quantity代表仓库里实际有的货,locked代表被订单锁住但还没付完款的货。那顾客下单时能看到的"可购买库存"怎么算?就是quantity - locked。
订单表(Order):
| 字段名 | 类型 | 说明 |
|---|---|---|
| order_no | CharField | 唯一订单号,建议用时间+随机数生成 |
| user | ForeignKey | 下单顾客 |
| status | IntegerField | 订单状态机,见下方枚举 |
| total_amount | DecimalField | 商品总金额 |
| pay_amount | DecimalField | 实付金额,考虑优惠后的值 |
| address | CharField | 收货地址(或自提门店) |
| pay_time | DateTimeField | 支付时间,可为空 |
| created_at | DateTimeField | 创建时间 |
订单状态可以这样定义:待支付、已支付待拣货、拣货中、已出库配送中、已完成、已取消、售后中。注意这里不能用随便的字符串,用 IntegerField 加枚举常量管理,后期改起来和扩展起来都更稳定。
订单明细表(OrderItem):
| 字段名 | 类型 | 说明 |
|---|---|---|
| order | ForeignKey | 关联订单 |
| product | ForeignKey | 关联商品 |
| product_name | CharField | 下单时的商品名快照 |
| price | DecimalField | 下单时的单价快照 |
| quantity | IntegerField | 购买数量 |
库存流水表(StockFlow):
| 字段名 | 类型 | 说明 |
|---|---|---|
| product | ForeignKey | 关联商品 |
| change_type | IntegerField | 入库、销售出库、报损、盘点调整等 |
| change_quantity | IntegerField | 正数代表增加,负数代表减少 |
| before_quantity | IntegerField | 变动前库存 |
| after_quantity | IntegerField | 变动后库存 |
| related_order | ForeignKey | 关联单号(订单号或入库单号) |
| remark | CharField | 备注 |
| created_at | DateTimeField | 时间 |
2.3 三个值得注意的设计决策
第一,金额一律用 DecimalField,不要用 FloatField。这是老生常谈但对新手来说最容易忽略的坑。浮点数在计算机里的表示是有误差的,你算 0.1 加 0.2 得 0.30000000000000004,这在订单金额、对账的时候就是灾难。Django 的 DecimalField 在数据库层就直接映射为 decimal 类型,配合 Python 的 Decimal 运算,精确可靠。所有涉及金额的地方,包括单价、总价、优惠、退款,必须统一用 Decimal。
第二,库存单独建表而不是往商品表里塞两个字段。很多人图省事,在 Product 表里直接加一个stock字段,结果到了后面做入库、盘点、多单并发的时候,这个字段会被改得面目全非。库存和商品的生命周期完全不同:商品是"稳定基础数据",库存是"高频变动数据",天然就该分开。而且库存表一对一关联商品,逻辑清晰,后面如果要做多仓、分门店,扩展也容易。
第三,订单明细一定要存"商品名+单价快照",不能只关联商品 ID。因为商品改价、改名甚至在系统里被删掉,都是可能发生的。如果订单明细只是外键关联到商品表,你查历史订单的时候看到的可能就是"商品已删除"或者新价格,那对账就乱套了。做系统要记住一个原则:订单是历史事实,不能被后续变更篡改。
3. 核心后端逻辑:从购物车到订单履约
3.1 商品管理模块的实现要点
商品模块算是整个系统的门面,顾客端绝大部分页面都在和商品打交道。实现上主要有几个点值得展开。
第一是多级分类。超市里商品分类不会像电商平台那么深,一般两级就够了:大类(粮油副食、生鲜、日化)+ 小类(粮油里的米、面、油)。Django 里实现多级分类最常用的是自关联外键,models.ForeignKey('self', null=True, blank=True),一级分类的 parent 为 None,二级分类指向父级。查询某个分类下所有商品时,先取该分类及其所有子分类 ID 列表,再用Category.objects.filter(id__in=category_ids)关联查询。
第二是搜索和排序。搜索用Q 对象组合就行:
from django.db.models import Q keyword = request.GET.get("keyword", "").strip() products = Product.objects.filter(status=True) if keyword: products = products.filter( Q(name__icontains=keyword) | Q(barcode__icontains=keyword) )排序一般支持综合排序、销量排序、价格升序/降序。综合排序可以用is_recommend优先再按sales_count降序;销量排序直接order_by("-sales_count");价格排序用order_by("price")或order_by("-price")。注意这里的排序字段最好先在模型里加个索引(db_index=True),数据量大一点的时候查询速度差距明显。
第三是商品图片处理。Django 的 ImageField 依赖 Pillow 库,上传时会校验文件是不是真图片。但这里有一个我踩过很多次的坑:用户上传一张手机拍的 5MB 照片,直接塞服务器,页面加载慢、磁盘也很快爆掉。建议在上传视图里对图片做压缩处理:打开图片,用Image.resize或Image.thumbnail限定到 800x800 以内,质量压缩到 85%,再保存。这样一张图从 5MB 压到 200KB 左右,页面体验完全不一样。
3.2 购物车:Session 还是数据库
购物车有两种实现方式,各有利弊。Session 购物车:顾客不用登录就能加购,实现简单,但用户一清浏览器缓存、或者换设备,购物车就没了。数据库购物车:数据安全,跨设备同步,但需要一个登录流程,访客使用起来门槛高一点。
我的建议是:如果这是课程设计或演示项目,用 Session 购物车就够,因为访客体验好,演示的时候不用每次先登录再逛店。如果是要真实上线给店里用,那就用数据库购物车,因为它关联用户,后续可以做"加购未下单"的分析,也能跨端同步。
数据库购物车表设计很简单:Cart表存用户外键、商品外键、数量、加入时间。用户加购时先查是否已有同款记录,有就加数量,没有就新建记录。用户登录状态下加入购物车时,可以把 Session 里的临时购物车数据合并进数据库,这个逻辑不复杂,但很提升体验。
3.3 下单与库存扣减的正确姿势
这是整套系统最核心,也是并发场景下最容易出 bug 的地方。千万别写成"先检查库存够不够,够就生成订单,然后把库存减掉",这在单用户演示时完全没问题,但一旦有两个顾客同时下一个只剩一件的商品,就会出现超卖。
正确做法是:下单时用数据库事务把对应库存行锁住,再检查、再扣减。Django 里用select_for_update()配合transaction.atomic()实现。
from django.db import transaction @transaction.atomic def create_order_with_lock(user, cart_items): order = Order.objects.create( user=user, order_no=generate_order_no(), status=Order.Status.PENDING_PAYMENT, total_amount=Decimal("0.00"), ) total = Decimal("0.00") for item in cart_items: inventory = Inventory.objects.select_for_update().get( product_id=item.product_id ) available = inventory.quantity - inventory.locked if available < item.quantity: raise StockNotEnough( f"商品 {item.product.name} 库存不足,剩余 {available} 件" ) inventory.locked += item.quantity inventory.save() OrderItem.objects.create( order=order, product=item.product, product_name=item.product.name, price=item.product.price, quantity=item.quantity, ) total += item.product.price * item.quantity # 商品累计销量同步增加 Product.objects.filter(id=item.product_id).update( sales_count=F("sales_count") + item.quantity ) order.total_amount = total order.pay_amount = total order.save() return order这里最关键的三个点:事务保证整体要么全部成功要么全部回滚;select_for_update()在事务里把这一行库存记录锁住,其他事务必须等这个事务提交才能操作同一行,这样就避免了超卖;锁定库存而不是直接扣减实际库存,是为了给"未支付订单"预留缓冲。等顾客支付成功,再把locked转成实际扣减,也就是执行:
# 支付回调成功时执行 inventory.quantity -= item.quantity inventory.locked -= item.quantity如果顾客超时未支付或主动取消订单,就把locked减回去,库存重新释放。
提示:
select_for_update必须放在事务里才生效。如果你只是随手调用了select_for_update但没有用transaction.atomic()包住,它是不起作用的,这是我见过最隐蔽的 bug 之一。
支付这一块,真实对接微信支付或支付宝需要商户号,个人项目通常申请不下来。我建议做一个"模拟支付"模块:前端生成一个支付二维码页面或直接放一个"模拟支付成功"按钮,后台拿到支付回调后更新订单状态。但即使是模拟支付,也要在代码里留好签名校验的接口,因为真实支付回调是可以被伪造的。用 Django 做回调接口时,必须验证支付平台发过来的签名参数,验签失败直接拒绝,且要处理重复回调的问题——同一个支付结果回调可能发两次,你的回调逻辑必须幂等,也就是"订单已经是已支付状态就不重复执行扣库存"。
4. 仓储管理:超市的"前店后仓"怎么管
4.1 入库管理:从采购到上架
中小超市的仓储模式和大型电商仓库完全不同,它是典型的"前店后仓":门店前面是展示销售区,后面是个小仓库,货架、冷藏柜、杂物间。这种模式下,入库操作发生得很频繁,比如供应商每天早晨送来一批饮料和生鲜,店员要接货、验货、往货架上摆。所以入库管理的核心诉求是:简单、快,别搞一堆审批流。
入库单表可以这样设计:入库单(StockInBill)存供应商、入库类型、入库单号、总件数、状态、经手人。入库单明细(StockInItem)存商品、数量、进货价、生产日期、保质期到期日。入库操作接货时,一次建单,挨个录明细,确认后一次性提交。
提交入库单的事务逻辑如下:逐条明细锁定对应库存记录(select_for_update),然后quantity增加,同时写一条库存流水(change_type=入库、change_quantity=正数、关联入库单号)。这里有一个细节:如果系统里也管保质期,那入库时就要录入production_date和expire_date,并且在商品列表或者库存预警里提示临期商品。别小看这个功能,超市卖食品饮料的,临期处理是刚需。
4.2 出库履约:订单驱动拣货
订单支付成功后,真正把货从货架上拿下来的动作叫"拣货"。这套流程的设计直接影响顾客收到货的时效。
我的做法是:订单支付成功、状态变为"已支付待拣货"时,系统自动生成一张出库单(StockOutBill),状态为"待拣货"。店员在管理端打开"今日待拣"列表,看到的就是一张张出库单,点进去显示订单明细:商品名、数量、在哪个货架、有没有备注。店员拣完货后,点击"拣货完成",订单状态改为"已拣货待打包",再点"交接出库",状态改为"配送中"。如果接入了配送逻辑,这一步可以关联骑手信息。
整个状态流转其实对应的是一个非常朴素的道理:库存减少不是发生在顾客下单那一刻,而是发生在拣货出库那一刻。这一点很多人想不明白,导致库存数据总对不上。记住:下单只是"锁定",出库才叫"扣减"。
缺货场景也要处理。门店拣货时经常发现商品缺货,要么是盘点不准,要么是摆错位置。这时允许店员在出库单上对某个商品做"缺货登记",系统自动将该商品从订单明细里扣除并退款相应金额,剩余部分正常出库。这样顾客收到的是部分商品,但至少不会被整个卡住不出货。
4.3 盘点与库存预警
盘点在超市里通常是定期干的活,比如每周盘点一次生鲜区,每月盘点一次整个仓库。盘点的意义在于账实相符——系统里的数字和货架上的实物对得上。
盘点流程建议这样设计:创建盘点单 -> 录入盘点范围内每个商品的"实际盘点数量" -> 提交后系统逐项对比系统和实际数量 -> 差异自动生成"报损单"或"报溢单" -> 库存数量调整并写流水。
代码层面就是对比后执行一个简单的库存变更:
if actual_quantity != inventory.quantity: change = actual_quantity - inventory.quantity inventory.quantity = actual_quantity inventory.save() StockFlow.objects.create( product=inventory.product, change_type=StockFlow.Type.ADJUSTMENT, change_quantity=change, before_quantity=inventory.quantity, after_quantity=actual_quantity, related_order=check_bill, remark="盘点调整", )库存预警比较简单。两个做法:一是给Inventory表加一个warn_threshold阈值字段,在每次入库、出库、盘点后判断quantity < warn_threshold就标记一个"需补货"状态;二是写一个定时脚本,每天凌晨扫描一遍库存表,把低于阈值的商品汇总发到管理员的待办列表。小项目用第二种就够了,没必要上 Celery 那一整套重东西,Django 自带的django-extensions或者一个简单的 cron 配合管理命令就能搞定。预警阈值的设置,参考公式是"日均销量 × 补货周期天数",比如某商品日均卖 10 件,供应商 3 天送一次货,那安全库存就设 30 件上下。
5. 权限设计与安全防护
5.1 三种角色的权限模型
这个系统里角色很清晰:顾客、店员(含库管)、店长(管理员)。Django 自带的 auth 系统已经帮我们解决了 90% 的问题,剩下的就是怎么用。
首先,项目一开始就要自定义用户模型,用AUTH_USER_MODEL = "users.User"指向自定义用户,并继承AbstractUser,扩展一个role字段。这一点极其重要:Django 官方文档说得很清楚,AUTH_USER_MODEL一旦执行过迁移再改,是极度痛苦的操作,基本上要删库重来。很多人开始偷懒用了默认 User,后面要加角色字段才发现改不动,只能另搞一张 Profile 表绕来绕去。所以开工第一件事就是把用户模型定好。
其次,Django 自带的Group可以拿来当角色用。创建三个 Group:Customer、Staff、Manager,权限分别配置。视图层面用@permission_required或@login_required控制访问即可。管理端的接口要保证只有 Staff 以上角色能访问,顾客端页面只能操作自己的数据。门店日常运营里,店员和店长都可以看订单,但只有店长能设置商品特价、调整库存阈值,这个粒度在权限配置里分清楚,后期就不会出现店员误操作改价的问题。
5.2 几个容易忽略的安全细节
这一节全是我在真实项目里遇见过的坑。
CSRF 防护默认是开着的,但很多人写 API 的时候图省事,把csrf_exempt到处用,这就等于把系统大门打开了。尤其是订单创建、支付回调这类接口,千万不能豁免 CSRF。如果你要对接小程序或者前后端分离,那也是用 token 认证方案,而不是直接关掉 CSRF。
支付回调接口的验签问题前面提过,再强调一次:接口收到请求后先验签名,验签字段不能用 GET 参数,要用 POST body,验签失败直接返回失败,不要做任何业务逻辑。回调里对订单状态的判断要幂等,确保同一条支付成功回调访问两次,第二次不做任何操作。
图片上传要做双重校验。前端可以限制文件类型,但用户完全可以绕过前端直接 POST 文件。后端在用ImageField的时候,Django 会做基本校验,但更稳妥的做法是在保存前用 Pillow 打开文件,校验真实图片格式(是 JPEG 就真的是 JPEG,而不是伪装成图片的脚本),并限制最大分辨率,防止有人传一个几十 MB 的"图片炸弹"把服务器内存打爆。
操作日志也别忘了。库存调整、订单改价、商品上下架这类敏感操作,都应该记录操作人、时间、操作内容。Django 里可以用一个简单的OperationLog模型,在视图函数里通过request.user写入。这东西现在觉得麻烦,等真出事了排查数据异常的时候,你会感谢当初的自己。
6. 部署上线与演示数据准备
6.1 从本机到服务器的常规部署
很多项目做完,代码在本地跑得欢,一部署到服务器就各种报错。这套系统要真正能跑起来,我建议按下面这个流程走一遍。
首先是配置文件分离。创建一个config/目录,里面放settings_base.py、settings_dev.py、settings_prod.py。base放公用的配置,dev放 DEBUG=True 和 SQLite,prod放 DEBUG=False、MySQL 连接、静态文件路径。生产环境里的SECRET_KEY、数据库密码、支付密钥之类的敏感信息,一律从环境变量读取,千万别硬编码进代码里。
其次是静态文件收集。DEBUG=False 之后,Django 不会自动提供静态文件,必须先在服务器上执行一次python manage.py collectstatic,把 Admin 后台、上传的图片、CSS 和 JS 都收集到STATIC_ROOT目录。很多人部署完发现后台页面没有样式、图片打不开,八成就是这一步没做。
再次是数据库。中小型项目一般用 MySQL。注意 Django 连接 MySQL 时,最好在OPTIONS里配置init_command: "SET NAMES 'utf8mb4'",同时创建数据库时也把字符集设为utf8mb4。否则商品名里有生僻字或 emoji 时,你会发现数据存进去就变问号,查都查不回来。
服务进程建议用 Nginx + Gunicorn 的组合。Nginx 负责接管外部请求、转发给 Gunicorn 并处理静态文件和媒体文件。Gunicorn 最好是 2~4 个 worker,不需要多,多了内存也不够。如果你不熟悉 Linux 运维,也可以直接用宝塔面板这类工具,点点鼠标就能配好站点,但对于学习项目,手动配一遍能理解的东西比面板多得多。
6.2 演示环境怎么造出像样的数据
不管是答辩还是给客户演示,一个空荡荡的系统没有任何说服力。造数据这一步,建议提前准备,不要手动一条条在后台录,要写脚本批量生成。
Django 里造数据最方便的是用django-seed库,也可以自己写一个management command。我个人推荐自己写一个seed_demo()函数,逻辑完全可控,生成的数据也更贴合超市场景。
造数据的优先级是:先造商品分类和商品(100 个左右,覆盖粮油、饮料、零食、日化、生鲜几个大类),然后造用户(一个管理员、两个店员、几十个顾客账号),再造一两周的模拟订单(1000 条以上,状态随机分布),最后让订单生成对应的出库单和库存流水。这样演示的时候,顾客端有丰富的商品列表,管理端有今日待拣列表,统计页面也有数据可看。
这里有个细节:造订单数据时,创建时间和库存流水的创建时间要匹配,否则看起来会非常假。写脚本时可以指定从datetime.now() - timedelta(days=14)开始随机构造每天的订单,同时同步调整对应日期的库存流水。数据造完之后,演示的时候无论点哪一页,都有真实自然的数据撑着。
7. 常见问题与排查实录
7.1 高频问题速查表
这两个月里给好几个做类似项目的同学排查过问题,把最高频的几个整理成一张速查表:
| 现象 | 可能原因 | 排查思路与解决 |
|---|---|---|
| 页面显示时间比实际少 8 小时 | 没设置中国时区 | TIME_ZONE = "Asia/Shanghai",USE_TZ = True,注意数据库里存的是 UTC 时间,模板渲染时会自动转换 |
| DEBUG=False 后静态文件全挂 | 没有collectstatic | 执行python manage.py collectstatic,配置 Nginx 的静态目录指向STATIC_ROOT |
| 商品图片上传后页面打不开 | MEDIA 路径配置错误 | 检查MEDIA_ROOT和MEDIA_URL是否配对,Nginx 有没有把媒体目录代理出去 |
| 中文变问号 | 数据库字符集不对 | 数据库、表、连接串统一用utf8mb4 |
| 本地连不上 MySQL | 没装驱动或鉴权问题 | 安装mysqlclient或PyMySQL,检查 MySQL 用户是否允许远程连接 |
| 商品卖出去了库存没减 | 扣库存逻辑只写在了下单处,没写在支付回调处 | 检查代码里有没有从locked转quantity的逻辑 |
7.2 几个让人抓狂的"隐形坑"
有些问题不是一眼能看出来的,但遇到了能卡你一整天。
第一个是select_for_update和事务的关系。很多人代码里写了Inventory.objects.select_for_update().get(...),但外面的transaction.atomic()没包好,导致锁不生效,超卖照旧。排查办法很简单:看 SQL 日志,确认有没有出现FOR UPDATE语句。没有就说明事务上下文不对,Django 的select_for_update只会在事务内生效。
第二个是模型迁移的"幽灵问题"。改了 models.py 之后忘记跑makemigrations和migrate,然后怎么测都不对,Admin 里看不到新字段,接口报不存在字段。这个问题太常见了,每次改完模型第一件事就是跑这两条命令,别留到之后再回想。
第三个是订单号生成。如果并发量上来,简单用time.time()拼随机数可能撞车,生成重复订单号。建议订单号用时间戳 + 用户ID + 4位随机数的组合,或者在数据库层加唯一约束后捕获 IntegrityError 重试。演示时撞车的概率低,上线后就是必现问题。
第四个是开发服务器跑久了内存越来越大。runserver是给调试用的,不是给生产用的。多次上传大文件、模板加载杂项,内存占用会一路飙升。部署到正式环境一定要用 Gunicorn 之类的独立进程,否则服务器挂着挂着就"人间蒸发"了。
最后一个隐形坑跟懒加载有关。订单列表页要显示每个订单的明细和商品名,如果直接遍历订单再查询明细,会触发 N+1 查询,几十条订单就把数据库打满了。处理办法是在视图查询时用prefetch_related("items__product"),把关联数据一次性加载出来。这个小优化,能让列表页速度从 5 秒降到 0.1 秒。
做完整套系统下来,我最大的感受是,这类系统的魂不在某个高深的技术点上,而在订单和库存这两条链路怎么咬合。把订单状态机定义清楚,把库存流水表坚持写下去,后面所有功能都是水到渠成。另一个很实际的建议是,无论你是做毕设还是给小店做工具,都先把核心链路跑通——顾客下单、锁库存、支付回调、库存扣减、出库拣货——这五个环节能闭环了,再去补装饰性的功能,演示的时候最不容易翻车。我自己就吃过一次亏:把订单超时自动解冻库存做成独立脚本,结果上线后忘了挂定时任务,两天下来一大半库存都被未支付订单锁死,顾客下单全部提示库存不足。从那以后,我列部署清单的第一条永远是"定时任务有没有跑起来"。希望这篇文章能让你少走这些弯路,把时间花在真正有价值的功能上。