做Python+Django的连锁超市销售商城项目,算是我近几年接过的颗粒度最细的业务系统之一。连锁超市不像单店,牵扯到多门店库存、采购、调拨、员工排班、促销定价,还有一堆老板天天要看的销售报表。这套东西用Django重写一遍之后我才发现,框架本身不难,难的是把“进销存+员工+分析”揉进一套合理的数据模型里。
这套系统是给某连锁超市做的,门店几十家,主营生鲜和日用品。客户原来的状态是:总部要数据靠微信群,门店采购靠经验,月底盘点靠人海。系统要解决的就是把“人、货、账”串起来——商品从哪个供应商采购,在哪个门店入库,卖出去了怎么扣库存,员工用什么权限操作系统,以及每天的销售数据如何自动变成老板看得懂的分析报表。文章里我会把整套系统的设计拆开讲,包括数据库建模、核心流程的Django实现、权限控制、统计报表,以及我实际踩过的几个坑。如果你是正在做Django后台系统、或者准备接零售类外包项目的开发者,这篇应该能直接当参考。
先交代技术选型。Python版本用3.10,Django用4.2 LTS,数据库MySQL 8.0,缓存和分布式锁用Redis,后端接口基于Django REST Framework,后台管理直接沿用了Django Admin并做了定制。前端没有过度设计,Bootstrap + jQuery,重点在业务流程和数据准确性,不整花活。后面所有代码,我都按这套环境来写。
1. 项目背景与整体设计思路
1.1 连锁超市业务的真实痛点
单店超市的系统很多开源项目都做过,但连锁超市完全是另一个复杂度等级。首先是多门店维度的库存,同一款可乐在A店有货、B店缺货,总部必须能实时看到每个门店的SKU库存。其次,采购不是门店自己说了算的,通常是总部或区域负责人统一跟供应商谈价、下单,然后分货到各门店,这就需要一个“采购单—入库单—门店库存”的流转关系。再就是员工管理,收银员、店长、采购员、总部运营,甚至财务,角色不同,能看到和操作的数据完全不一样。
我最初设计时很容易犯一个错:先把所有表建出来,再回头塞逻辑。后来发现零售系统里“数据模型决定业务规则”非常明显——比如库存字段放在商品表里还是独立的库存表里,直接决定了并发扣减好不好写;员工和门店的关系是一对一还是多对多,直接决定了排班和调拨的逻辑复杂度。所以我在设计前花了大量时间做业务梳理,把核心流程画成“状态机”而不是“画页面”。
1.2 为什么选Python和Django
选Python+Django的原因很简单,一是开发效率高,二是后台管理系统本来就是Django的强项。Django自带的ORM、Admin、Migration、权限认证,几乎覆盖了进销存系统80%的基础设施。特别是Admin,在项目早期给运营人员录入商品、供应商、员工数据时,几乎不用额外开发,节省了大量时间。
有人会质疑Django的性能,但这类零售后台系统的核心瓶颈从来不是高并发,而是数据一致性。MySQL加上Redis做缓存,配合Django的事务与行锁,完全可以支撑几十家门店的日常操作。当然,如果你要处理的是千万级日订单的电商平台,那Django可能不是最优选,但连锁超市的进销存和分析系统,Django的性价比明显更高。
提示:Django版本建议直接选LTS版本(当时是4.2)。不要追新,很多第三方库和文档都围绕LTS演进,踩坑会少很多。
1.3 模块划分与系统边界
整个系统分成四个核心模块:基础数据(门店、供应商、商品、员工)、进销存(采购、入库、销售、盘点、调拨)、商城接口(给收银端和线上下单用)、分析报表(销售统计、库存分析)。模块之间通过统一的服务层调用,不在视图里堆业务逻辑。
my_supermarket/ ├── accounts/ # 员工、角色、权限 ├── stores/ # 门店管理 ├── products/ # 商品、分类、SKU、供应商 ├── inventory/ # 入库、盘点、调拨、库存流水 ├── sales/ # 订单、收银、商城接口 ├── analytics/ # 统计报表 └── common/ # 公共工具、分页、通用配置这样的目录结构配合Django的应用拆分,可以让不同开发并行不冲突。更重要的是,后续每加一个新报表或新流程,都知道代码往哪里放。模块之间我尽量做到单向依赖,比如sales只依赖products和inventory的服务接口,不直接操作库存表,避免循环引用的“意大利面条”。
2. 数据库建模与核心数据表设计
2.1 商品、SKU与库存模型
零售系统的商品建模有一点很关键:商品(Product)和SKU(Stock Keeping Unit)要分开。比如“农夫山泉550ml”是一个SKU,“农夫山泉1.5L”是另一个SKU,它们可能属于同一个商品品牌,但价格、条码、库存都是独立的。我用的模型简化如下:
class Category(models.Model): name = models.CharField('分类名称', max_length=64) parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE) class Product(models.Model): name = models.CharField('商品名称', max_length=128) category = models.ForeignKey(Category, on_delete=models.PROTECT) brand = models.CharField('品牌', max_length=64, blank=True) class Sku(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='skus') spec = models.CharField('规格', max_length=64) # 550ml/箱 barcode = models.CharField('条码', max_length=32, unique=True) cost_price = models.DecimalField('成本价', max_digits=10, decimal_places=2) sale_price = models.DecimalField('销售价', max_digits=10, decimal_places=2)Inventory表必须单独建,不能把stock字段放到SKU上。原因很简单:库存是“门店+SKU”维度,同一个SKU在十个门店就有十条库存记录。我把库存表设计成:
class Inventory(models.Model): store = models.ForeignKey(Store, on_delete=models.CASCADE) sku = models.ForeignKey(Sku, on_delete=models.CASCADE) quantity = models.DecimalField('库存数量', max_digits=12, decimal_places=3) updated_at = models.DateTimeField(auto_now=True) class Meta: unique_together = ('store', 'sku')这里有一个容易忽视的细节:生鲜类商品的秤重商品按千克卖,库存要支持小数,所以用DecimalField而不是IntegerField。这也是零售建模和普通电商不一样的地方,你如果全用整数,后面做生鲜损耗调整就会非常痛苦。
2.2 供应商与门店模型
供应商表比较简单,但要注意和采购单的关联。每家供应商可以对应多个门店的采购价,所以采购价不能直接写在供应商表里,而要写在采购明细里,以采购单为准。门店表除了名称、编码,还要维护地址、联系电话、开业时间,这些字段在统计数据时经常被拿来当筛选维度。
class Store(models.Model): code = models.CharField('门店编码', max_length=20, unique=True) name = models.CharField('门店名称', max_length=64) region = models.CharField('区域', max_length=32, blank=True) status = models.SmallIntegerField('状态', default=1) # 1营业 0停业 class Supplier(models.Model): name = models.CharField('供应商名称', max_length=128) contact = models.CharField('联系人', max_length=32, blank=True) phone = models.CharField('联系电话', max_length=20, blank=True)门店编码别看是个小字段,后期做门店间调拨、对账、导入导出都靠它,最好从一开始就用统一的编码规则,比如“区域+序号”,别让运营自己随意填。
2.3 员工与权限模型
员工和门店之间是多对一关系,一个员工属于一个主门店,但可以兼职到其他门店。系统里的员工不直接关联Django内置的User,而是用OneToOne扩展:
class Employee(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) store = models.ForeignKey(Store, on_delete=models.PROTECT, verbose_name='主门店') name = models.CharField('姓名', max_length=32) employee_no = models.CharField('工号', max_length=20, unique=True) role = models.CharField('岗位', max_length=20, choices=ROLE_CHOICES)岗位我用一个简单字段加choices,而不是一上来就full-blown的RBAC。因为超市岗位相对固定:店长、收银员、采购员、仓管员、总部运营、财务。不同岗位的权限差异用Django的Group和Permission解决,员工→岗位→权限,三层关系清晰。
注意:员工工号要设置唯一约束。很多运营录入时喜欢偷懒,工号重复会让后续排班、工资核算、销售提成全部错乱。唯一约束能从源头挡住这种脏数据。
2.4 订单与销售流水设计
销售模块既要支撑线下收银,也要支撑线上商城下单,所以我设计了两层:Order主表和OrderItem明细表,订单状态用整数字段表示,避免用字符串散得到处都是。
class Order(models.Model): order_no = models.CharField('订单号', max_length=32, unique=True) store = models.ForeignKey(Store, on_delete=models.PROTECT) employee = models.ForeignKey(Employee, null=True, on_delete=models.SET_NULL) total_amount = models.DecimalField('订单总额', max_digits=12, decimal_places=2) pay_type = models.CharField('支付方式', max_length=16) status = models.SmallIntegerField('状态', default=1) # 1待支付 2已支付 3已取消 created_at = models.DateTimeField('下单时间', auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') sku = models.ForeignKey(Sku, on_delete=models.PROTECT) price = models.DecimalField('成交价', max_digits=10, decimal_places=2) quantity = models.DecimalField('数量', max_digits=12, decimal_places=3)这里要注意金额字段一律用DecimalField,绝对不要用FloatField。浮点数在金额累计和税费计算中会出现0.1+0.2不等于0.3的问题,这在零售系统里是致命的。报表里金额差一分钱,财务都会找你麻烦。
下单时间用auto_now_add虽然省事,但如果以后要跨时区做统计,最好还是自己显式写入门店本地时间。我在项目里把created_at统一存成UTC,再在展示层转换到Asia/Shanghai,后面会详细讲这个坑。
3. 进销存核心流程的实现
3.1 采购入库流程
采购入库我设计成:采购单创建 → 供应商发货 → 门店收货入库 → 库存增加并写流水。采购单头记录供应商和总金额,明细记录SKU、数量、采购价。入库时必须使用事务,避免出现“订单状态改了但库存没加”的中间状态。
from django.db import transaction @transaction.atomic def receive_purchase(purchase_order_id, store_id, operator_id): po = PurchaseOrder.objects.select_for_update().get(id=purchase_order_id) if po.status != PurchaseOrder.STATUS_SHIPPED: raise BizException('当前状态不允许入库') for item in po.items.all(): inv, created = Inventory.objects.get_or_create( store_id=store_id, sku_id=item.sku_id, defaults={'quantity': 0} ) inv.quantity += item.quantity inv.save() StockChange.objects.create( store_id=store_id, sku_id=item.sku_id, change_type='purchase_in', quantity=item.quantity, before=inv.quantity - item.quantity, after=inv.quantity, operator_id=operator_id, ref_no=po.purchase_no ) po.status = PurchaseOrder.STATUS_RECEIVED po.save(update_fields=['status'])每一个库存变动都写入StockChange流水表,这是后期对账和分析的数据基础。没有流水,出了问题只能两眼一抹黑。
这里有个经验:get_or_create在并发场景下可能会引发唯一约束冲突,所以在库存维度上,我建议在事务内先用select_for_update尝试读取,不存在再创建。上面的示例为了清晰保留get_or_create,实际项目里可以改成带重试的get_or_create。
3.2 销售扣减库存与并发控制
销售扣减库存是整个系统最容易出bug的地方。如果直接用:
inventory.quantity -= sold_quantity inventory.save()在高并发下一定丢更新。我最初的版本就是这种写法,压测时多线程同时下单,库存数据直接乱掉。后来改成悲观锁:
@transaction.atomic def create_order_and_deduct(store_id, cart_items, operator): for item in cart_items: inv = Inventory.objects.select_for_update().get( store_id=store_id, sku_id=item['sku_id'] ) if inv.quantity < item['quantity']: raise BizException('库存不足') order = Order.objects.create(...) ...select_for_update会在事务内对命中的库存行加锁,第二个会话只能等待前一个事务提交后再读取,这样就不会出现超卖。当然,悲观锁会牺牲一点并发吞吐,但超市收银场景并发量有限,正确性优先。如果后续量上来了,再考虑Redis预扣库存或者乐观锁方案。
注意:select_for_update必须放在事务里才生效,而且一定要在事务内尽早锁定目标行,锁定的行越少越好。如果锁定范围过大,会变成表级锁,反而拖垮整个系统。我在压测时还遇到一个情况:订单明细里同一个SKU出现多行,必须合并成一行去锁,否则会自己锁死自己,也就是死锁。
3.3 盘点与损耗处理
超市每天都有盘点,盘点会产生差异,差异要么是损耗(生鲜坏了、丢了),要么是系统误差。我设计的盘点流程是:生成盘点单 → 录入实盘数量 → 审核确认 → 系统自动调整库存并生成“盘盈亏流水”。审核环节用了两段式,防止录入错误直接改库存。
@transaction.atomic def confirm_check(check_id): check = InventoryCheck.objects.select_for_update().get(id=check_id) if check.status != InventoryCheck.STATUS_RECORDED: raise BizException('只能审核已录入的盘点单') for diff in check.diffs.all(): if diff.diff_qty == 0: continue inv = Inventory.objects.select_for_update().get( store_id=check.store_id, sku_id=diff.sku_id ) inv.quantity += diff.diff_qty inv.save() StockChange.objects.create( store_id=check.store_id, sku_id=diff.sku_id, change_type='check', quantity=diff.diff_qty, before=inv.quantity - diff.diff_qty, after=inv.quantity, ref_no=check.check_no ) check.status = InventoryCheck.STATUS_CONFIRMED check.save()盘点单审核后不允许再修改,只能做一笔“复盘”流程作为反向调整,这样审计链条是完整的。实际运营中,盘点录入很容易多打一个零,所以我在录入页面加了“差异率”提示,如果某个SKU的差异数量超过当前库存的50%,页面会强制二次确认。这个小功能虽然简单,但为运营省了很多麻烦。
3.4 门店调拨流程
连锁超市还有一类常见业务是门店调拨:A店库存多,B店缺货,总部发起调拨。调拨涉及两个库存变动,一个是A店出库,一个是B店入库,必须放在同一个事务里。调拨单还有在途状态,也就是A店已出库但B店还未入库时,货实际上在路上。
我在调拨单上加了一个status字段:待出库、已出库、已入库、已取消。A店确认出库后,A店库存扣减,B店库存不变,但系统会记录“在途库存”逻辑视图;B店确认入库后,B店库存增加,整条调拨单才算闭环。这种设计比A店直接扣、B店直接加更能追踪货损和延迟,物流环节出了问题也有据可查。
4. 员工管理与权限控制
4.1 多门店员工体系
员工的主门店是归属门店,但总部运营可能需要查看所有门店数据。我采用了一个简单的规则:查询数据时,权限过滤优先看角色;总部运营角色可以传空门店来查询所有门店;门店角色必须带上自己的store_id,并且在SQL中强制加store过滤。
Django默认的User模型可以直接塞自定义字段,但我最终用OneToOne扩展成Employee,因为员工岗位、工号这些信息和登录认证信息最好分开,以后接入企业微信或钉钉登录也不影响认证表。对于员工离职,我做了软删除:只把员工的User is_active设为False,保留历史订单和操作记录的外键完整。如果要物理删除,所有历史单据都会变成孤儿数据,那是不行的。
4.2 基于角色的权限控制
权限控制我用Django内置Permission + 自定义校验函数。比如收银员只能新建订单、查询自己的销售记录;店长可以查看本店销售报表、审核盘点;库管可以操作入库和调拨。视图上统一用装饰器和检查函数:
from django.contrib.auth.decorators import login_required, permission_required @login_required @permission_required('sales.add_order', raise_exception=True) def checkout(request): ...Django的permission_required很方便,但有一个坑:权限是缓存在request.user上的,如果后台改了某个用户的角色,该用户要重新登录才能生效。如果是小系统可以接受,但被管理员问到“为什么我加了权限他还是没权限”时,需要马上反应过来是缓存问题。
为了细粒度控制,我在store级别的数据上还封装了自己的check_store_access(request, store_id)函数:
def check_store_access(request, store_id): if request.user.has_perm('analytics.view_all_store'): return True if request.user.employee.store_id == store_id: return True raise PermissionDenied('无权访问该门店数据')这个函数会在每个门店粒度的视图中调用,确保数据越权查询被卡住。这里还有一个隐藏细节:如果员工被调到新门店,但session里还是旧门店ID,就会一直报权限错误。所以我在employee变更的视图里同步更新session中的store_id,避免用户“明明换店成功了却打不开数据”。
4.3 操作日志记录
零售系统涉及钱和货,操作日志不能省。我在每个写操作里统一埋点,记录操作人、操作时间、操作类型、请求参数和核心对象ID。最开始我用Post信号量自动记录,但信号量在批量导入时很难控制粒度,后来改成在服务层显式写日志。比如库存调整时,除了StockChange流水,还有一份员工操作日志:
class OperationLog(models.Model): employee = models.ForeignKey(Employee, on_delete=models.PROTECT) action = models.CharField('动作', max_length=32) # stock_change / order_create object_type = models.CharField('对象类型', max_length=32) object_id = models.CharField('对象ID', max_length=32) detail = models.TextField('详情', blank=True) created_at = models.DateTimeField(auto_now_add=True)有了这套日志,客户遇到“谁动了这批货”的纠纷时,我能直接拉出时间线。做这类项目,审计合规永远是加分项,别嫌麻烦省掉。
5. 数据分析与报表模块
5.1 销售趋势分析
分析模块的核心是“聚合”,而不是查明细。我用了Django ORM的annotate功能,按天、按门店、按SKU粒度做统计。下面是一个常见需求——查询某门店最近30天每日销售额:
from django.db.models import Sum, Count from django.db.models.functions import TruncDate daily_sales = ( Order.objects.filter( store_id=store_id, status=Order.STATUS_PAID, created_at__gte=start_date ) .annotate(day=TruncDate('created_at', tzinfo=local_tz)) .values('day') .annotate( total=Sum('total_amount'), order_count=Count('id') ) .order_by('day') )TruncDate时区参数一定要传。否则Django会用服务器默认时区截断,可能和门店所在时区相差几个小时,导致销售额落在错误的日期桶里。这个问题我排查了很久,后面在常见问题里展开。
5.2 畅销与滞销商品分析
畅销滞销用两个指标:销量和销售额占比。我写了一个Rank查询,按SKU汇总销量后排序,再计算每个SKU销售额占总销售额的比例,从而形成ABC分类。A类商品是贡献80%销售额的少数SKU,重点管理;C类是长尾商品,可以适当减少采购频次。
sku_sales = ( OrderItem.objects.filter(order__store_id=store_id, order__status=2) .values('sku__barcode', 'sku__product__name', 'sku__spec') .annotate(total_qty=Sum('quantity'), total_amount=Sum(F('quantity') * F('price'))) .order_by('-total_amount') )拿到这个结果后,我会在内存里做占比和排名计算,数据量级是几千个SKU,性能完全没问题。数据量大以后,再考虑物化视图或者定时任务提前算好存到报表表里。实际运营中,老板最喜欢看的是“门店Top10爆款”和“滞销超过30天的商品清单”,后者能直接指导清仓决策。
5.3 库存周转率计算
分析模块除了销售,还缺不了库存分析。库存周转率是零售行业很看重的指标,计算方式是“期间出库总数量 / 平均库存”。如果平均库存太高,说明资金压在了仓库里;太低则可能断货。我用SQL或者ORM先求出期间的出库总量,再按天采样库存总和,最后得到周转天数。这个功能让我在客户那里加分不少,因为大多数现成的超市系统都只做销售统计,不做库存健康度分析。
5.4 报表导出与可视化
老板们看系统的习惯是“能导出Excel就一定要Excel”。我封装了一个公共导出方法,用pandas生成DataFrame再写Excel,或者直接用Django的StreamingHttpResponse输出CSV。Excel更友好,但数据量大的时候CSV更快,所以做了两个入口。
图表方面,我选的是ECharts,后端只返回JSON,前端用Ajax拉数据。一个销售趋势折线图、一个品类占比饼图、一个门店销售排名柱状图,基本就能满足日常运营看板需求。这套东西不值得自己造轮子,选成熟的前端图表库,成本最低。
提醒:报表接口一定要做好缓存,比如同样的30天门店销售汇总,如果每五分钟没有新订单,缓存命中率会非常高。我用Redis给报表接口加了5分钟缓存,数据库压力直接下降一大截。但要注意缓存key的设计,必须包含筛选条件和门店ID,否则会出现“A门店的人看到了B门店的数据”这种事故。
6. 项目实战中的坑与排错实录
6.1 并发扣库存导致的超卖问题
这个坑我前面已经提过,这里补充一个定位技巧。当时压测超卖后,我先用Redis的INCR模拟并发写入同一个key,发现不影响;改用直接并发更新MySQL行后才复现,定位到是数据库更新丢失。方案改成select_for_update后问题消失。
这里的关键结论是:不要在生产环境用“先读后写”的方式更新库存,任何情况下都要保证读取和更新在同一事务里,并且用锁或乐观版本号保护。我甚至建议在库存操作视图上加一层统一入口,禁止绕过服务层直接改Inventory表的代码出现,这是代码评审时的红线。
6.2 时区导致统计日期错乱
Django开启USE_TZ=True后,数据库默认存UTC时间。TruncDate默认用的UTC,导致上午8点前的订单被归到前一天。排查时我发现页面和归档明细都对,只有日报统计错位,最后定位到是TruncDate时区参数。解决方式是统一使用Asia/Shanghai时区,并且在所有日期截断函数中显式传入tzinfo。
后来我在项目配置里把TIME_ZONE设为'Asia/Shanghai',同时保留USE_TZ = True,存库用UTC,展示用本地时区。这个方案虽然多了一层转换,但跨门店、跨服务器部署时不会乱。如果哪天要在海外开店,改一下显示时区配置就行,历史数据完全不受影响。
6.3 DecimalField金额精度问题
金额字段试过用FloatField快速建表,结果在求和时出现0.30000000000000004这种数字,打印到Excel里非常难看。后来全部迁移到DecimalField,max_digits=10, decimal_places=2。涉及百分比、累计金额时,也统一用Decimal而不是float运算。这个改动看似琐碎,但对财务场景来说是底线。
我写了一个公共的金额计算工具函数,传Decimal类型,返回Decimal类型,杜绝在视图里直接用float做加减。代码评审时我专门强调过:只要涉及钱,一律禁止float,这条规则写进了项目规范。
6.4 报表查询的N+1问题
刚开始查询销售明细去关联SKU、商品、门店时,产生了大量重复SQL。用了Django的select_related和prefetch_related之后,查询次数从几百次降到几次。写ORM查询时,建议直接无脑加上相关联的select_related,FK外键能省则省。排查时可以用Django Debug Toolbar看SQL次数,非常直观。
报表模块还有一个优化点:不要在前端做总金额汇总。我见过同事把明细数据全部返回给前端,在前端用JS累加,数据量大时又慢又容易出错。正确做法是在数据库端用Sum聚合,前端只展示结果。这样即使导出几万条明细,也不影响页面响应速度。
6.5 权限缓存导致角色变更不生效
Django的权限在第一次请求后缓存到session,后台改了用户权限,用户不重新登录就不会刷新。后来我在自定义的权限检查函数里加了一个版本号机制:每个用户维护一个权限版本,变更权限时递增版本,请求时对比session里的版本号,不一致就强制刷新权限缓存。这个小改动解决了运营反复登录的问题,但注意别把性能搞坏。
这个版本号我直接存在Redis里,key是user_perm_ver:{user_id},权限检查时先取版本号,和session里的对比,不一致才重新加载权限并更新session。实测下来多了一次Redis读,但换来了权限即时生效,还是挺值的。
6.6 数据库索引与查询性能
随着历史数据积累,订单表会越来越大。我一开始没建组合索引,导致按门店+时间查询时全表扫描,一个月的订单查询要好几秒。后来给Order表加了(store_id, status, created_at)组合索引,给OrderItem加了(order_id, sku_id)索引,查询速度从秒级降到毫秒级。零售系统订单表通常会无限增长,所以索引策略一开始就要和DBA确认好,否则上线三个月后会被查询卡死。
经验:在进销存系统里,库存流水表是只增不减的,建议按月或按季度做分区表。我的做法是设定一个归档任务,把三个月前的StockChange导入归档库,主库只保留近期数据。查询历史对账时,先查主库再查归档库,对业务透明。
最后分享一个我自己总结的小技巧
个人经验来说,做一个零售管理类系统,前期花在“理清流程”上的时间,应该比写代码的时间多。我第一次做的时候急着建表写接口,结果做到一半发现库存模型设计错了,回炉重改的成本远高于一开始多画两天流程图。所以每次接到这类“进销存+角色权限+分析报表”项目,我都先拉着业务方把采购、销售、盘点、调拨、员工归属这些核心流程确认清楚,再动手写模型。这个习惯帮我少踩了非常多坑。
关于这套系统的扩展,我目前正在做的是给报表模块加“同期对比”和“库存周转率”指标,其实库存周转率就是“期间出库量/平均库存”,SQL一行能算出来,但业务上很说明问题。后面如果门店数量破百,我会考虑把Django App拆成微服务或者上消息队列,不过在此之前,先把单体的稳定性和数据准确性做扎实,比什么花活都重要。希望这篇实战记录能给你在类似系统的设计和实现上提供一点参考,少走弯路。