这个立项编号有点长,但项目本身却是非常典型的一套内容——“基于python的超市购物商城采购销存系统”。我把它从里到外拆开整理了一遍,从数据模型讲到库存预警,从Django和Flask的混合架构讲到PyCharm里的调试心得。如果你正在做类似的进销存系统、商城后台或者期末课程设计,这篇文章可以直接拿来当参考。
1. 项目整体思路与架构设计
1.1 超市管理系统的业务痛点与功能定位
超市的业务链条看起来很简单,无非就是进货、卖货、存库存,但真正落地到系统里,痛点全都藏在细节里:商品品类多、条码杂、供应商动不动改名换联系方式、采购单和到货单对不上、库存账面数和实物数总有偏差、收银台的促销规则一堆、临期商品没人提醒。这套采购销存系统就是把“采购、销售、库存”三条主线拧成一套数据闭环,再叠加一个商城端让顾客能线上下单、到店自提或配送。
功能定位大致可以分成前台和后台两条线。前台是商城,面向顾客:商品浏览、搜索、购物车、下单、会员登录、订单查询。后台是管理端,面向店长、采购员、仓管员、收银员:商品管理、供应商管理、采购订单、入库审核、库存台账、销售订单、会员管理、促销设置、盘点、预警。总体来看就是典型的进销存加一个B2C商城外壳,业务边界清晰,数据关系复杂,非常适合用来练手或者作为中小型超市的初始版本。
说实话,这种项目最大的坑往往不是功能做不出来,而是数据关系和状态流转从一开始就没理清楚。下单后库存什么时候扣?是下单占库存还是支付后扣库存?采购单到货后是直接加库存还是审核后加库存?盘点差异能不能直接改数量?这些业务规则如果前期不聊透,后面全是返工。
1.2 为什么同时选Django和Flask,而不是二选一
这个项目同时用了Django和Flask,很多人第一反应是“是不是多此一举”。我当时的考虑其实很实际:Django的核心优势是全家桶,ORM、Admin后台、表单、权限认证全都内置,开发后台管理类功能特别稳;Flask则胜在轻量灵活,非常适合快速提供JSON接口。这个系统既要给收银员和仓管员这种内部角色用后台,又要给顾客提供商城的API接口,所以我把两块拆开:Django负责后台端的采购、库存、销售、数据管理,Flask负责商城前端的RESTful API服务。
从纯工程角度讲,用Django加Django REST Framework(DRF)也能一套吃完所有需求,很多生产项目就是这么干的。但我这里选混合架构有两个诉求:一是控制主工程的复杂度,Django这块专注管理端,Flask这块专注接口,互不干扰;二是想在实际项目中验证一下两个框架协同工作的边界和风险,这一点对团队后续在其他项目里的框架选型很有参考意义。不过要特别提醒:混合架构是有代价的,后面第6章我会专门讲两个框架之间怎么协作、怎么避免数据混乱。
1.3 项目结构规划:模块拆分与代码分层
项目结构如果不规划好,进销存这种数据关系复杂的系统很快就会变成一堆互相引用的“屎山”。我这边按业务模块拆分了Django应用,每个应用维护一套独立的models、views、services:
supermarket/ ├── manage.py ├── config/ # 项目配置,settings/urls/wsgi ├── apps/ │ ├── goods/ # 商品、分类、条码、SKU │ ├── supplier/ # 供应商、商品供应商关系 │ ├── purchase/ # 采购订单、采购明细、入库 │ ├── inventory/ # 库存台账、库存流水、盘点、预警 │ ├── sales/ # 销售订单、收银、购物车 │ ├── member/ # 会员、积分、余额、促销 │ └── common/ # 工具类、常量、基础模型 ├── flask_api/ # Flask商城接口服务 │ ├── app.py │ └── api/ │ ├── products.py │ ├── cart.py │ └── orders.py └── scripts/ # 数据初始化、定时任务、演示数据这里我特别强调一点:一定要加一个service层,不要把所有业务逻辑堆在views里。采购入库涉及库存流水、库存更新、订单状态改变、供应商欠款变动,这些逻辑如果直接写在视图函数里,当你需要在定时任务、命令脚本、API接口里复用同样的业务规则时就会很痛苦。我在每个app下都建了services.py,专门放业务事务逻辑,视图层只做参数校验和调用,这样责任边界清楚,测试也好写。
2. 数据模型设计:商品、供应商、库存与订单
2.1 核心数据表结构设计思路
进销存系统的核心是数据模型,模型设计错了后面怎么补都别扭。我把核心表分成几类:基础资料、业务单据、库存流水。基础资料包括商品表(Goods)、商品分类表(Category)、供应商表(Supplier)、会员表(Member);业务单据包括采购单头(PurchaseOrder)和采购明细表(PurchaseOrderItem)、销售单头(SalesOrder)和销售明细表(SalesOrderItem);库存核心则是库存台账表(Stock)和库存流水表(StockFlow)。
以商品表为例,超市商品必须有条码(Barcode)字段并加唯一索引,条码不仅用于收银扫码,还是商城搜索的快捷标识。商品要区分单规格和多规格,多规格商品建议把公共信息放商品主表,规格属性放SKU子表,但小超市商品种类没有电商那么大,我用一个商品表加一个规格字段就够用:规格内容直接记录“500ml/瓶”“200g/袋”,简单实用。价格字段一律用DecimalField,坚决不用FloatField,金额和价格这种字段用浮点存早晚要出精度问题,别给自己埋雷。
库存表是唯一反映当前实时库存的表,每次变动都要更新它,同时必须往流水表里写一条记录。库存流水表我设计了这些关键字段:
# apps/inventory/models.py class StockFlow(models.Model): goods = models.ForeignKey(Goods, on_delete=models.PROTECT) change_type = models.CharField(max_length=20) # IN采购入库 / OUT销售出库 / ADJ调整 / TAKING盘点 quantity = models.DecimalField(max_digits=12, decimal_places=3) # 正数为入,负数为出 before_qty = models.DecimalField(max_digits=12, decimal_places=3) after_qty = models.DecimalField(max_digits=12, decimal_places=3) ref_order_no = models.CharField(max_length=40, db_index=True) # 关联单号 remark = models.CharField(max_length=200, blank=True) created_at = models.DateTimeField(auto_now_add=True)记录变更前和变更后的库存数量,不是为了好看,而是为了审计。一旦某一天库存对不上账,你可以通过流水把任何商品的库存演变轨迹完整还原出来,这在排查数据和盘点差异时特别重要。
2.2 库存扣减的并发与事务处理
进销存系统最敏感的环节就是库存扣减。商城和收银台同时卖同一个商品,如果库存只剩5件,两个订单同时创建,都检查“库存充足”然后各自扣减,最后库存可能变成负数,这就是典型的超卖问题。解决思路是用数据库行级锁配合事务:先锁定库存行,再判断库存是否充足,最后扣减。
from django.db import transaction def deduct_stock(goods_id, quantity, ref_order_no): if quantity <= 0: raise ValueError("扣减数量必须大于0") with transaction.atomic(): stock = Stock.objects.select_for_update().get(goods_id=goods_id) if stock.quantity < quantity: raise InsufficientStockError(f"库存不足,当前库存:{stock.quantity}") before = stock.quantity stock.quantity = before - quantity stock.save() StockFlow.objects.create( goods_id=goods_id, change_type="OUT", quantity=-quantity, before_qty=before, after_qty=stock.quantity, ref_order_no=ref_order_no, )这里select_for_update()是关键,它会把对应的数据库行锁住,直到事务提交或回滚。在并发请求下,后到的请求会等待前一个事务释放锁,从而避免超卖。要注意的是这个方法只在支持事务的数据库上生效,SQLite在高并发下表现弱一些,生产环境还是用MySQL或PostgreSQL更稳妥。
还有一个更容易忽略的地方:如果在同一个事务里要扣多种商品库存,一定要让所有线程按相同的顺序去锁库存行。比如订单里有商品A和商品B,线程1先锁A再锁B,线程2如果先锁B再锁A,就可能互相等待形成死锁。我的做法是先对所有商品ID排序,然后按顺序逐个锁定库存,统一加锁顺序可以从根上规避死锁。
2.3 商品SKU与条码设计的注意事项
做超市项目,条码这块有几个实际经验值得记一下。第一,条码字段在数据库里用字符串类型存,不要用数字类型。超市条码很多是13位EAN,但有时候会出现带字母的内部码,用数字类型会丢失精度或解析报错。第二,前端输入条码时可能会丢失末尾的0,比如“6901234567890”在某些输入框里被转成科学计数法,所以所有涉及条码的接口都要按字符串处理。
库存单位也是新手容易踩坑的地方。超市场景里,整箱进货、单件销售,如果库存管理用“箱”而销售用“件”,中间就必须维护一个换算比例。我在这套系统里统一约定“最小销售单位”作为库存基准单位,比如货架上的“瓶”是库存单位,“箱”只是采购展示单位。商品表里加一个unit_ratio字段记录“箱含多少瓶”,采购入库时通过换算写入库存。这个设计让库存账永远只认一个单位,避免了“箱”和“件”在报表里对不上的尴尬。
另一个设计细节是软删除。商品下架、供应商停用这类操作,不要直接物理删除数据。进销存系统里历史单据要关联商品名称和条码,一旦物理删除,历史报表里就会找不到对应商品。我所有的业务主表都用了is_active布尔字段或删除时间字段,查询时默认过滤,历史数据永远保留。
3. 采购模块的完整流程实现
3.1 采购订单从创建到入库的状态流转
采购流程是超市库存的来源,也是库存数据的第一道关口。我把采购单的状态机定义得比较清晰:草稿(DRAFT)、已审核(APPROVED)、部分入库(PARTIAL)、全部入库(RECEIVED)、已取消(CANCELLED)。草稿是采购员创建的初始状态,可以修改和删除;审核后就不能直接改明细,只能走追加或取消流程;货物到店后仓管员按实际到货数量登记入库,采购单状态自动更新。
状态机在模型里用一个简单的choices字段加状态校验函数实现,不引入太重的工作流引擎。在采购明细上,我额外设计了两个数量字段:order_qty是预计采购数量,receive_qty是实际到货数量。为什么特意拆开?因为供应商实际发货往往和采购订单对不上,可能是缺货少发,也可能是破损退回。如果入库直接覆盖采购数量,账面上就丢掉了“订货和实收差异”这个重要信息,对供应商考核和结算都不利。
采购单审核之后,任何对明细的修改都不允许,这是为了保证业务留痕。万一真的需要调整,我提供一个“变更采购单”的功能,会生成一条变更记录,而不是直接去改明细。这套流程花在设计上的时间不多,却省掉了非常多后期扯皮的麻烦。
3.2 供应商管理与采购价格维护
供应商资料这块,除了名称、联系人、电话、地址这些基础字段,我专门加了一个供应商等级字段,用来区分主力供应商和临时供应商。主力供应商的响应速度和结算账期通常更好,这个数据在报表筛选、采购推荐时非常有用。
商品和供应商是多对多关系,同一个商品可以有多个供应商,价格不同。我在关联表里维护了当前采购价,另外挂了采购价格历史表,记录每一次价格调整的生效时间和旧价格。这样采购员新建采购单时,系统会自动带出该商品在某供应商下的最近采购价,同时允许手动修改并记录原因,避免之后对账时说不清楚价格为什么变。
价格历史表的实现也很简单,就是一张带生效时间的小表。这里有一点经验之谈:不要试图在主表里只存“最新价格”,因为历史价格一旦被覆盖就再也找不回来了。有了价格历史,你可以很轻松地按时间轴分析采购成本的波动,这一点在后续做毛利报表时尤其有价值。
3.3 采购入库与库存联动的实现细节
采购入库是整个采购流程里库存数据第一次被影响的地方,必须在一个事务里同时完成“更新订单状态”和“写入库存流水”。入库操作我封装在service层的一个函数里,仓库管理员录入实际到货数量后,系统将每个品项逐条更新库存台账,并生成一条change_type为IN的库存流水,流水号直接关联采购单号,方便对账。
一个容易疏忽的细节是:入库操作必须支持部分入库。仓库到了一部分货是常态,此时采购单状态变成PARTIAL,仓管员可以再次入库剩余部分,直到达到订单数量或超收一部分。超收场景在生鲜品类里很常见,比如供应商多送了一箱,我允许入库数量超过订单数量,但超过比例限制在10%以内,超过则要求采购员先修改采购单或在审批流里说明原因。
入库单提交后,不是立即就生效,还需要仓管主管做一次审核确认。审核的意义在于给入库操作加一道复核,避免有人录错数量导致库存虚增。一旦审核通过,库存数据和流水就正式落地,不允许再随便改。要是真的入错了,我提供的不是修改入口,而是“红冲”入口——生成一条负数的调整流水,把错误数量冲掉,再重新做一个正确的入库单。
4. 销售与商城模块的核心逻辑
4.1 商城购物车的实现方案
商城端购物车的实现本来可以上Redis,但我最终选择用数据库表加用户会话的方式来实现,原因很简单:这个系统里商城访问量没有到需要Redis撑高并发的程度,而数据库购物车的好处是数据可视化、调试方便,用户在多个设备之间也能共享购物车。真正到量级大了,再把购物车缓存层抽出去也不迟。
购物车表我设计成绑定会员ID和商品ID,数量字段用Decimal存储。加入购物车时有一个细节:同时把商品当前价格快照存进去,而不是只存商品ID。因为商品价格可能随时调整,如果购物车只存ID,用户在结算时看到的价格会和加入购物车时不一致,非常影响体验。我存了一个add_price字段,并且在商品详情页实时展示的是最新价,只有购物车里明确展示“加入时的价格”,这样两边都能说清楚。
结算逻辑要分两步走。第一步是预览:检查购物车每个商品是否还有效、价格是否有变化、库存是否够,把最终应付金额算出来给用户确认。第二步才是真正创建订单。预览不锁定任何库存,只有用户点了“提交订单”才进入锁定和扣减流程,这样能降低对库存格的占用时间,提高并发吞吐。
4.2 销售订单与收银流程
线下收银和线上商城在订单流程上是两条支线,但底层共享同一套SalesOrder和SalesOrderItem表。线下收银的特点就是快,收银员扫描商品条码加入购物车、选择会员、结算、打印小票。我的商城里没有引入复杂的支付通道,统一用一个“支付流水表”来模拟微信、支付宝和现金,后续如果需要接入真实支付,只需要在第三方接口层加一个适配器,业务层面完全不用动。
订单状态的流转我定为:待支付(PENDING)-> 已支付(PAID)-> 已完成(COMPLETED)-> 已取消(CANCELLED)。在“已支付”这个动作里,同时完成库存扣减和销售流水记录。为什么不是在“创建订单”时就扣库存?因为简化版商城没有预占库存机制,如果用户在创建订单后迟迟不支付,库存就被无效订单白白锁住。这个取舍对小超市是合理的:订单支付率很高,支付失败或取消的情况相对少,可以接受在支付那一刻再扣库存。
一旦订单支付,销售明细立即生成库存流水的出库记录,并同步更新库存台账。值得注意的是,销售出库和采购入库不同,出库不会像入库那样有二次审核环节,因为收银台本身已经有人工复核。不过我会在订单列表里保留“退货”入口,退货会走一条库存回补的流水,把商品退回库存,同时记录销售退回,方便算营业额净额。
4.3 会员管理与促销活动的工程实现
会员模块一开始我只存了姓名、手机号、等级、积分这几项,后来加上了储值余额。超市会员很常见的是存一笔钱送一点金额的玩法,所以余额字段必须有事务锁保护,充值和消费都要走流水,不能直接改余额。我建了一张BalanceFlow表记录每一笔余额变动,和库存流水同样的思路,钱的事更要留痕。
促销活动是超市系统里业务规则最杂的部分。满减、折扣、会员价、第二件半价、买一送一,各种玩法如果直接写在订单代码里,基本就是一场灾难。我采用策略模式的简化版:促销规则表存规则类型和参数,比如满减就存满X元减Y元,折扣就存打几折,并且每个商品可以绑定多个促销活动,结算时统一走一个价格计算服务,逐条匹配规则。
价格计算服务是整个销售模块最容易被改坏的地方,我强烈建议把它写成一个纯函数:输入购物车明细和会员等级,输出每组商品的实际成交价、优惠明细和应付总额。纯函数意味着不查数据库、不改状态,只做计算,这样写单元测试非常方便。我补了一张促销活动结果缓存表,结算完成后记录用户使用了哪些优惠,避免后续对账扯不清“这笔订单到底减了多少”。
5. 库存监控与预警机制
5.1 库存台账与进销存平衡公式
库存管理不是把数量存进数据库就完了,关键是要能自检。我每天凌晨跑一个对账任务,用进销存平衡公式验证所有商品的数据是否一致:
理论库存 = 上次结存 + 期间总入库 - 期间总出库 ± 期间调整
对账脚本算出理论库存后,和当前库存台账里的实际数量做比对,不一致直接标记为异常商品并生成异常记录。举例来说,某商品上月结存850件,本月采购入库1200件,销售出库1023件,报损调整-17件,那么理论库存=850+1200-1023-17=1010件。如果库存台账显示1005件,差了5件,系统就自动把一个差异告警推给仓管员,仓管员需要回到流水里查是哪一笔漏记或者录错了。
这种日检机制的价值是能把人工盘点从“每个月对所有商品挨个盘点”变成“只盘有差异预警的商品”。我做完这个功能后,盘点工作量至少降了一半,而且库存数据的可信度明显提升。
5.2 低库存与临期预警的任务调度
低库存预警的触发条件不能一刀切。水这种日用品日均卖100瓶,鸡蛋可能一天卖20盒,如果统一设“低于50件预警”就没意义。我采用一个简单而经典的安全库存公式:
安全库存 = 日均销量 × 采购补货周期天数 × 安全系数
安全系数取1.2到1.5,主要看供应商供货稳定程度。比如某饮料日均销量45件,从下单到到货需要3天,安全系数按1.2算,安全库存=45×3×1.2=162件。当库存台账数量低于162件时,系统自动在预警列表里生成一条低库存提醒,采购员直接看到“建议补货数量=安全库存-当前库存+采购周期消耗量”,不用自己再翻Excel算。
商品日期管理是超市另一个刚性需求。我引入goods_batch表存每个批次的效期,预警任务每天扫一遍即将临期的批次,按剩余天数分成三个级别:90天、60天、30天。30天内到期的商品自动标记为“临期品”,在商城端可以单独做一个临期特卖专区,减少损耗而不是直接报废。临期预警和低库存预警是两个独立的调度任务,但都统一由Django的management command配合系统定时任务驱动,保证即使Flask服务挂了也不会影响预警任务执行。
5.3 盘点差异处理的业务闭环
盘点流程如果不闭环,账永远盘不准。我在系统里把盘点设计成一个独立业务单元,不是简单改数字。盘点单建好后,系统根据库存台账自动生成盘点明细快照,仓管员逐个填写实盘数量。只要实盘和账面不一致,就自动算出差异数量和差异金额,但这个差异不会直接写入库存,而是先进入待审核状态,由店长或财务审核确认后,才生成一笔change_type为“盘点调整”的库存流水,把账面数修正为实盘数。
把“修改数字”和“确认修改”拆成两步,是为了避免仓管员自己盘点自己改账,这个权限控制非常重要。实际操作中我还加了限制:盘点差异超过一定金额必须填写差异原因,否则无法提交审核。这样一来,虽然流程上多了一道手续,但每笔差异都有解释、有责任人、有审核,真正做到业务闭环。
6. 关键接口与性能优化实录
6.1 Django与Flask的协作接口设计
Django和Flask同时存在,最核心的问题是怎么协同而不互相踩脚。我们的方案是:Django作为数据主服务,负责所有写操作和后台管理;Flask只提供商城前端的读接口和部分会话类操作,所有涉及下单、改库存、改价格的请求,Flask都调用Django侧开放的内部API来完成。
举个例子,商城端购物车里的商品信息,Flask可以直接从配置好的同一个MySQL数据库里联表查询,这是只读操作,不需要和Django做复杂的同步协议,共享同一个数据库副本即可。但用户点击提交订单时,Flask不能自己写销售订单和库存流水,必须POST到一个只对内部网络开放的Django API接口,以API密钥鉴权,由Django在自己的事务里完成订单创建和库存扣减。
# flask_api/api/orders.py import requests DJANGO_API = "http://django-service:8000/internal/api" def submit_order(user_id, items): resp = requests.post( f"{DJANGO_API}/orders/create/", json={"user_id": user_id, "items": items}, headers={"X-Internal-Token": "内部密钥"}, timeout=10, ) return resp.json()这样一来有一个很明确的红线:Flask不直接改库,Django不直接对外暴露写接口,两边各司其职。这个约束虽然牺牲了一点性能,但事务和数据一致性完全收口在Django一侧,避免了两个框架各自建事务连接互相锁库的混乱场景。如果你觉得这个混合架构太重,生产环境直接统一用DRF也完全没问题,本质思路是一样的。
6.2 缓存、分页与批量操作的优化
前端商城首页的热门商品和商品分类导航是典型的读多写少接口,我直接用Redis缓存起来,缓存过期时间设在5分钟。商品库存数量和价格这类高频变化的数据不做页面缓存,还是走实时查询,避免顾客看到的价格和结算时不一致。至于商品列表页,我用了DRF的PageNumberPagination分页,默认每页20条,避免一次查询上万商品把接口拖死。
数据查询的N+1问题是新手最容易犯的错,表现就是页面打开要几百毫秒甚至几秒。比如循环销售订单明细时,每条明细都要查询一次商品表,100条明细就多出100次查询。解决方案很简单,用select_related或prefetch_related把关联数据一次性查出来:
sales_items = SalesOrderItem.objects.filter(order_id=10086).select_related("goods")这个改动在一张订单的查询上可能只快了十几毫秒,但在“最近30天销售明细”这种报表接口上,性能提升可能是几十倍。我优化后拿同一个接口做对比,未优化时响应约2.1秒,优化后是230毫秒,肉眼可见的差别。
采购入库的明细批量写入我也建议用bulk_create。一次采购单可能有几十上百行明细,逐条save()要开几十次数据库事务,而bulk_create可以一次批量插入。我写了个benchmark,100条明细用bulk_create耗时比逐条save快大约8到10倍,这个优化在数据量上来后非常值。
6.3 安全与权限控制的落地
进销存系统内部角色多,数据敏感级别高,权限控制不能只有“登录”和“未登录”的区别。我建了五个角色:采购员、仓管员、收银员、店长、财务。Django后台的权限用自带的Group加Permission体系,视图层用login_required和permission_required装饰器做粗粒度拦截,并且为每个重要操作额外做对象级权限的检查。比如仓管员能编辑盘点单草稿,但不能审核盘点单;收银员能看到自己当班的销售数据,但不能看采购成本价。
商城端的Flask接口走的是JWT认证。用户登录后得到一个短期token,过期时间默认两小时,涉及提交订单和余额支付的操作还需要二次验签,用来防CSRF。所有输入参数都过ORM参数化查询和类型校验,从根本上杜绝SQL注入。Admin后台模板启用CSRF保护,商品描述这类富文本字段全部做HTML转义,防止存储型XSS。
密码字段永远不要自己写加密算法,直接用Django内置的PBKDF2哈希存储。第一次开发时我想用它自带的User模型,后来发现会员和员工是两种身份,就拆成了两个模型:员工表接Django的权限体系,会员表只用于商城登录和积分余额。两者不做一对一会话,登录逻辑也分开,逻辑上更干净。
7. 调试环境与PyCharm使用心得
7.1 虚拟环境与依赖管理
这套项目跑在Python 3.10上,我全程用PyCharm作为IDE,第一步就是把虚拟环境建好。命令行创建虚拟环境很简单:
python -m venv venvWindows下激活虚拟环境用venv\Scripts\activate,Linux和macOS用source venv/bin/activate。激活后安装依赖,版本一定要锁定,不锁定版本的最大风险是过两个月Django安全更新释放后,项目因为框架版本升级出现不兼容的异常。我最终导出的requirements.txt长这样:
Django==4.2.20 Flask==2.3.3 djangorestframework==3.14.0 redis==5.0.4 django-redis==5.4.0 requests==2.31.0 python-jose==3.3.0 PyMySQL==1.1.0 APScheduler==3.10.4在PyCharm里配置Project Interpreter时,直接指向venv里的python.exe即可。本地调试时我习惯用sqilite作为数据库,部署或联调时再把DATABASES配置切到MySQL。SQLite和MySQL共存的一套方案只适用于开发演示,生产必须用统一数据库,否则ORM底层行为差异容易出怪问题。
7.2 调试技巧与常见坑
PyCharm对Django和Flask都有一键调试的Run Configuration支持,断点调试是定位复杂逻辑最趁手的工具。我强烈推荐你设置条件断点,比如在订单明细循环里,只在商品ID等于某个值时才断下来,不然每次循环都停一次,调试效率会非常低。
数据库慢查询排查上,我装了django-debug-toolbar,在settings里把DEBUG设为True时它会自动在页面侧边显示SQL执行列表和执行时间。排查N+1问题时特别有用,你一看页面里SQL数量上百条,立刻就能意识到哪里缺了select_related。
还有一个很容易被忽略的坑是时区。settings里如果设置了USE_TZ=True,那么所有DateTimeField在取出时都是UTC时间,而你在本地调试时往往想看北京时间。我的解决方案是配置TIME_ZONE='Asia/Shanghai',同时USE_TZ=True,然后在模板和接口序列化时统一转成本地时间。前端商城展示订单时间如果直接取原值,会给顾客看到一个差8小时的“未来时间”。
7.3 测试数据准备与演示跑通
系统开发完成后,不能给验收方一个空空荡荡的后台。我写了一个自定义management command,一条命令初始化整套演示数据:
python manage.py init_demo_data这个命令会生成5个分类、60个商品、3个供应商、若干采购单和销售单、覆盖最近90天的库存流水,以及一个演示会员账号。初始化完毕的面板一打开就有数据,各种报表和预警界面不需要等待自然业务积累就能直接看到效果。为了避免每次运行命令重复造数据,我还在命令里加了幂等判断,检测到已有数据时提示是否清空重建。
对于商城端我也准备了模拟用户,生成一个带购物车和历史订单的账号,演示下单流程时直接登录就能看到端到端效果。这个命令脚本本身不复杂,但在评审和展示环节能省下非常多准备时间,我建议所有类似项目都配一个数据初始化工具。
8. 常见问题速查表与避坑指南
8.1 典型报错与解法
| 现象 | 原因 | 解法 |
|---|---|---|
| 库存扣成负数 | 事务里没用select_for_update | 锁定库存行后再判断和扣减 |
| no such table: inventory_stock | 忘了跑迁移 | 执行makemigrations和migrate |
| Decimal金额对不上,差0.01 | float参与金额计算 | 金额字段全用DecimalField |
| Flask端口启动报Address already in use | 上次进程没退出 | 结束占用进程或换端口 |
| 商城接口返回中文乱码 | Response未指定utf-8 | Flask返回JSON时设置charset=utf-8 |
| 定时任务重复执行 | 多进程同时启动 | 用Redis分布式锁做任务幂等 |
还有一个比较隐蔽的报错是PyMySQL版本与Django版本不兼容,安装后启动项目直接报“AttributeError: module has no attribute ‘escape_string’”。这个问题的根源是PyMySQL近几个版本调整了方法的位置,解决办法是锁定PyMySQL==1.1.0,或者升级Django到支持该版本的组合。
8.2 我踩过的几个坑
做这套系统期间,有几个坑花了我不少时间去排查,写出来给大家提个醒。第一个是金额精度问题。一开始我在销售明细里用FloatField存单价和金额,测试时发现某些订单的总金额和手工计算差0.01,原因就是二进制浮点数无法精确表示十进制小数。排查出来后把所有金额和数量字段全部改成DecimalField,decimal_places统一设为2,这个精度问题彻底消失。
第二个坑是订单明细里select_related用错了关联字段。销售明细表跟商品表是多对一,我用select_related是没问题的,但有一次我在商品分类那里误用了select_related,代码没报错,SQL却变成了LEFT OUTER JOIN,查询性能反而下降。select_related适合正向单值关联,prefetch_related适合反向多值关联,用错场景会适得其反。
第三个是Flask和Django共用数据库但数据读写时序不一致。商城端查商品列表时,Django后台管理员刚改完商品价格还没来得及刷新缓存,导致商城显示旧价格。解决方式是数据库和缓存一起做失效处理:Django保存商品价格时不仅更新数据库,还删除Redis里对应的商品详情缓存,强制Flask端下次回源数据库。
8.3 后续扩展建议
这个系统如果继续迭代,我建议按优先级做三件事。第一是接入真实第三方支付,把模拟支付流水表的支付通道换成真实接口,业务代码不用改动太多,主要是新增回调处理和退款处理的接口逻辑。支付回调要注意幂等性,同一笔支付通知可能到达多次,必须通过支付流水号去重。
第二是增加多门店和仓库的支持。当前库存表只按商品维度记录总量,扩展时改成按门店、仓库维度建库存台账,查询和报表都带上组织维度筛选即可。这个改动会影响所有库存相关查询,建议在项目初期就预留一个warehouse_id字段,哪怕暂时用不到,后面扩展会省很多事。
第三是报表可视化。Django后台可以用只读视图配合前端图表库展示销售趋势、采购成本、毛利排行等指标。数据可以从销售明细和采购明细表实时聚合,也可以在每天晚上把日结数据汇总到报表汇总表,避免报表查询拖垮在线业务。报表场景建议用汇总表,因为量大之后实时聚合查询性能堪忧。
这套系统做完之后,我最大的一点体会是:进销存项目的关键不在于用了多少新技术,而在于把每条数据流的状态变化管清楚。从采购入库、销售出库、库存盘点,到价格快照、支付流水,每一笔业务变动都留下完整的轨迹,整个系统才真正有价值。如果你也在做类似的项目,先把业务状态机和数据闭环画明白,再动手写代码,走的弯路会少很多。