这个标题我第一眼看到就觉得很熟悉,典型的课程设计或者毕业设计项目代号——hx2008多半是你学号、班级名称或者老师给的项目编号,实际作用就是把一个完整项目标签化。但不管编号叫什么,这类“基于Python的连锁超市线上管理系统”几乎是每年都会大量出现的必修项目。它覆盖的知识点非常齐全:Python后端框架、数据库建模、Web页面交互、权限控制、进销存逻辑、报表统计,几乎把计算机专业的核心课都串起来了。
我当时帮人做过好几个类似的系统,用Python重写、用Django重建、甚至把前端换成Vue的都有。踩过的坑、走过的弯路能写一大堆。今天这篇就专门拆一个可复现的完整方案:从需求分析到数据库建表、再到核心代码实现和一个个真实会踩的坑,确保你拿到手上不只是一堆截图,而是能真正跑起来的成品。这篇内容适合正在做课设、毕设的Python初学者,也适合想快速搭一套B/S结构管理系统的开发者参考。
1. 项目定位与需求拆解:这类“线上管理系统”到底在做什么
1.1 连锁超市的核心业务模型
在动手写代码之前,先得把业务搞清楚。很多同学上来就建表建页面,做到一半发现逻辑对不上,返工成本极高。连锁超市线上管理系统,关键词是“连锁”,不是单店版进销存,它需要有“多门店”的概念。比如总部能查看所有门店的数据,门店店长只能管自己的门店,普通员工只能做销售录入这类操作。
一套完整的连锁超市线上管理系统,基本包含下面这几块:
- 总部后台:管理所有门店、商品档案、员工账号、采购入库审核、全局报表
- 门店端:日常销售、库存查询、入库登记、盘点、门店内部数据统计
- 线上商城端:会员浏览商品、下单购买、订单查询,订单自动扣减对应门店库存
- 公共模块:员工登录、验证码、个人中心、操作日志、系统公告
这里有一个容易被忽略的细节:线上管理系统不等于在线商城。很多毕设题目里的“线上”指的是“B/S网页访问、数据集中存数据库、远程操作”,不一定非要集成下单支付流程。我刚做的时候把支付宝支付都模拟了一遍,后来被老师指出偏离重点——这个系统考核的核心是进销存和权限管理,而不是支付。所以拿到题目先问清楚需求边界,重点放在库存、订单、商品和报表上,方向不会跑偏。
1.2 角色权限设计是全系统的地基
权限设计是整个系统的核心骨架。连锁超市就意味着有层级:集团管理员看所有门店,区域经理看区域内门店,店长看本店,员工只能操作销售和库存查询。
我建议用角色字段配合装饰器实现,不引入复杂的权限框架。数据库里给用户表加一个role字段,用整数区分角色等级:
0:超级管理员(总部)1:区域经理2:店长3:员工
登录后把用户信息写入session,写一个装饰器@check_permission(level),接口触发时把当前用户角色和所需等级做比较。为什么不用django-guardian这种权限框架?因为对于课设和中小型系统来说,角色等级模型足够用,实现简单、逻辑清晰、答辩时好讲。
权限设计还有一个容易疏忽的点:后端必须校验权限,不能只看前端隐藏菜单就完事。我一个同学的前端把“删除按钮”隐藏了,结果用Postman直接调删除接口照样能删。所以装饰器必须在View函数上做校验,前端隐藏只是提升体验,不是安全措施。
2. 技术选型分析:为什么用Django而不是Flask,以及配套环境搭建
2.1 Python主流方案对比
这套系统我见过用Flask做的,也见过用Django做的,还见过用纯Flask+Jinja2手工搭建的,但从完成度和开发效率来看,Django明显的更适合这种业务密集型系统。
- Django自带Admin后台,商品、订单、用户这些模型直接能用admin管理,演示和开发效率都极高
- Django的ORM非常成熟,分组聚合、连表查询的能力能直接支撑报表模块
- Django表单和模板系统自带CSRF防护,答辩时问安全性也有话可说
- 用户认证模块是现成的,配合session无需额外写登录逻辑
Flask的优势是轻量、灵活,但用户模块、ORM、Admin这些都需要自己组合第三方库,对新手来说踩坑概率更高。所以如果你是第一次做这类系统,直接用Django是更稳的选择。
看这个标题还有“线上”两个字,那用户端页面是必须的。前端层面不需要上Vue这类框架,用Django模板+Bootstrap就能实现得很好看。如果你想加分,可以单独引入ECharts做销售趋势图、门店销量对比图,这比纯表格展示要高出好几个档次,而且代码量不大。
2.2 环境搭建与项目初始化
Python环境版本建议用Python 3.10+,Django版本选择4.2 LTS(截至现在4.2是主流稳定版),数据库使用MySQL 8.0。这里为什么不选SQLite?虽然SQLite零配置,但并发写入性能差,且在大数据量联表查询上表现一般,管理系统演示数据稍微多一点就卡顿。MySQL 8.0在Windows和Linux上的安装都非常成熟,驱动用pymysql就能解决连接问题。
# 创建虚拟环境 python -m venv venv # 激活虚拟环境(Windows) venv\Scripts\activate # 激活虚拟环境(Linux / macOS) source venv/bin/activate # 安装Django、pymysql、django-cors-headers等 pip install django==4.2 pymysql mysqlclient cryptography pillowDjango项目初始化的命令是所有教程都会写,但我想强调的是目录结构规划。很多人喜欢把所有app塞到一个文件下,到后期改一次代码要滚半天,这里给一个清晰的模块拆分:
django-admin startproject hx2008 python manage.py startapp user # 用户登录、角色 python manage.py startapp store # 门店管理 python manage.py startapp goods # 商品、分类 python manage.py startapp stock # 库存、出入库 python manage.py startapp order # 订单、销售记录 python manage.py startapp report # 报表统计为什么拆这么细?这符合Django的哲学“一个app只做一件事”,之后每个模块的models和views独立维护,新增功能不会互相影响。
接着修改settings.py里的关键配置,最主要是把数据库引擎切到MySQL,同时加两个中文相关的配置:
DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'hx2008_db', 'USER': 'root', 'PASSWORD': 'yourpass', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', }, } } # 中文支持和时区设置 LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True然后在项目根目录的__init__.py里导入pymysql:
import pymysql pymysql.install_as_MySQLdb()这个操作是很多教程必写的,因为Django底层默认使用MySQLdb驱动,而MySQLdb在Python3环境下安装非常麻烦,pymysql兼容驱动可以直接顶替。
3. 数据库模型设计与核心业务流转逻辑
3.1 模型设计必须回答的业务问题
数据库设计是整个系统最核心的部分,很多人的代码写不下去就是因为表关系没理清。连锁超市系统至少需要以下这些表:用户表(含角色)、门店表、商品分类表、商品表、门店库存表、入库单表、入库明细表、销售订单表、订单明细表、会员表、操作日志表。
我直接给出核心模型代码,带注释讲解设计思路。用户和门店放一个文件里说明关系:
class User(AbstractUser): # 继承Django内置用户,自带username、password、email字段 role = models.IntegerField(choices=( (0, '超级管理员'), (1, '区域经理'), (2, '店长'), (3, '员工'), ), default=3, verbose_name='角色') store = models.ForeignKey('store.Store', on_delete=models.PROTECT, null=True, blank=True, verbose_name='所属门店') class Meta: verbose_name = '员工账号' verbose_name_plural = verbose_name def __str__(self): return f'{self.username}-{self.get_role_display()}'角色用整数存储,不直接存字符串,好处是数据库空间小、判断效率高,而且get_role_display()能自动映射成中文显示。store外键用PROTECT而不是CASCADE,就是防止用户被关联删除后连带门店数据一起消失——门店下有商品、订单、员工,怎么能因为某个用户删除就连坐?这个细节是过来人经验。
门店、分类、商品模型如下:
class Store(models.Model): name = models.CharField(max_length=50, verbose_name='门店名称') address = models.CharField(max_length=200, verbose_name='地址') phone = models.CharField(max_length=20, verbose_name='联系电话') status = models.BooleanField(default=True, verbose_name='是否营业') class Category(models.Model): name = models.CharField(max_length=50, verbose_name='分类名称') parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, verbose_name='上级分类') class Product(models.Model): name = models.CharField(max_length=100, verbose_name='商品名称') barcode = models.CharField(max_length=30, unique=True, verbose_name='条码') category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name='分类') sale_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='售价') cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='进价') spec = models.CharField(max_length=50, blank=True, verbose_name='规格') status = models.BooleanField(default=True, verbose_name='上架状态')外键全部用PROTECT而不是CASCADE,是连锁超市业务里特别重要的一点。分类下有商品就不允许删分类,门店下有员工就不允许删门店,强制保证数据的完整性。
库存表必须拆成“门店库存表”和“库存变动记录表”两张。门店库存表管当前库存数量,库存变动记录表管每一次出入库流水。为什么要拆?因为单看当前库存你不知道这批库存怎么来的,有了流水才能盘点、追责、审计。
class StoreStock(models.Model): store = models.ForeignKey(Store, on_delete=models.CASCADE, verbose_name='门店') product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name='商品') quantity = models.IntegerField(default=0, verbose_name='当前库存') low_stock_threshold = models.IntegerField(default=10, verbose_name='库存预警阈值') class Meta: unique_together = ('store', 'product') # 同一门店同一商品只有一条库存记录 class StockRecord(models.Model): RECORD_TYPE = ( ('in', '入库'), ('out', '出库'), ('sale', '销售'), ('check', '盘点'), ('transfer', '调拨'), ) store = models.ForeignKey(Store, on_delete=models.CASCADE, verbose_name='门店') product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name='商品') record_type = models.CharField(max_length=10, choices=RECORD_TYPE, verbose_name='变动类型') quantity = models.IntegerField(verbose_name='变动数量(正数增加/负数减少)') operator = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name='操作人') remark = models.CharField(max_length=200, blank=True, verbose_name='备注') created_at = models.DateTimeField(auto_now_add=True, verbose_name='时间')unique_together必须加,这是多门店体系里最容易漏的约束。没有这条组合唯一约束,同一门店下同一条商品可能插入多条库存记录,报表统计直接就乱了。为什么operator用PROTECT而不用SET_NULL?因为操作记录是审计的关键,就算员工离职也不能把流水里的操作人变成空。
3.2 订单与会员模块的设计细节
线上销售订单表和线下收银记录本质上是一回事,都是为了记录商品销售行为,区别只在于来源渠道。订单表用主单+明细分表设计,主单记录订单总金额、状态、门店,明细记录每件商品的单价和数量。
class Order(models.Model): STATUS = ( ('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ) order_no = models.CharField(max_length=30, unique=True, verbose_name='订单号') store = models.ForeignKey(Store, on_delete=models.PROTECT, verbose_name='下单门店') member = models.ForeignKey(Member, on_delete=models.SET_NULL, null=True, blank=True, verbose_name='会员') total_amount = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='订单总金额') status = models.CharField(max_length=10, choices=STATUS, default='pending', verbose_name='状态') pay_type = models.CharField(max_length=20, verbose_name='支付方式') created_at = models.DateTimeField(auto_now_add=True, verbose_name='下单时间') class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items', verbose_name='所属订单') product = models.ForeignKey(Product, on_delete=models.PROTECT, verbose_name='商品') price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='成交单价') quantity = models.IntegerField(verbose_name='购买数量') subtotal = models.DecimalField(max_digits=12, decimal_places=2, verbose_name='小计')订单号有一个很常见的新手坑:直接用时间戳作为订单号生成策略,并发时可能重复。建议生成规则为'HX' + datetime.now().strftime('%Y%m%d%H%M%S') + 4位随机数,再加上数据库层的unique=True约束,双重保障。会员用SET_NULL是因为线下订单可能没有会员,删会员不会破坏订单数据。
数据库模型建完后,每次改动模型都要跑迁移命令,这步很多人忘,结果启动时报“table not exists”:
python manage.py makemigrations python manage.py migrate如果项目里已经有数据,改模型还可能遇到迁移冲突,建议开发阶段一旦改了模型就马上迁移,不要攒草稿到最后一起执行。这个习惯很重要,我至少见过三位同学因为最后统一迁移导致外键对不上而整个库重建。
4. 核心功能实现:登录权限、入库、销售这三个关键链路的代码解析
4.1 登录与权限装饰器
Django自带的认证系统可以直接用,登录视图写法很固定,但有两个细节值得注意:登录成功后需要把用户名、角色、门店ID都放进session,这是后面所有权限判断的基础。
from django.contrib.auth import authenticate, login from django.shortcuts import redirect, render def login_view(request): if request.method == 'POST': username = request.POST.get('username') password = request.POST.get('password') user = authenticate(username=username, password=password) if user is not None: login(request, user) # 会话里保存关键身份信息 request.session['role'] = user.role request.session['store_id'] = user.store_id if user.store else None return redirect('dashboard_index') else: return render(request, 'login.html', {'error': '用户名或密码错误'}) return render(request, 'login.html')然后自定义一个权限校验装饰器:
from functools import wraps from django.shortcuts import redirect def check_permission(required_role): def decorator(view_func): @wraps(view_func) def wrapper(request, *args, **kwargs): role = request.session.get('role', None) if role is None: return redirect('login_view') if role > required_role: # 数值越大权限越小 return render(request, 'error.html', {'msg': '权限不足'}) return view_func(request, *args, **kwargs) return wrapper return decorator为什么用role > required_role来判断?因为我把角色定义为数值越小权限越大,超级管理员是0,员工是3。比如店长角色是2,访问需要角色0的功能时2 > 0为真,会被拦截。这种数值比较的逻辑在答辩时也很好解释。
依赖角色数值还有个好处,比如系统里想增加一个“仓库管理员”角色,只需要在User模型的choices里加一个选项,数值排在合适位置,装饰器逻辑不需要改动一行。
4.2 入库操作:库存更新的原子性保障
入库操作是连锁超市系统里最典型的“事务”场景。一次入库要同时完成三件事:生成入库单、写入库明细、更新门店库存表。这三步要么全部成功,要么全部失败,绝对不能出现入库单生成了但库存没增加的情况。
from django.db import transaction from django.utils import timezone @transaction.atomic def stock_in(request, store_id): # 入库商品数据格式: [{product_id: 1, quantity: 20}, ...] items_data = request.POST.getlist('items') total_items = [] for item_data in items_data: product_id = item_data['product_id'] quantity = int(item_data['quantity']) # 锁住库存记录行,避免并发超卖 stock, created = StoreStock.objects.select_for_update().get_or_create( store_id=store_id, product_id=product_id, defaults={'quantity': 0} ) stock.quantity += quantity stock.save() StockRecord.objects.create( store_id=store_id, product_id=product_id, record_type='in', quantity=quantity, operator=request.user, remark=item_data.get('remark', '') ) total_items.append(...) # 生成入库单主表记录 StockInBill.objects.create(...)select_for_update()这里非常关键,它走的是数据库行锁,保证同一时刻只有一条事务在读改这条库存记录。连锁超市多个门店同时从总部仓库调货,并发访问是很常见的场景。为什么更推荐行锁而非乐观锁版本号?因为对于库存这种高频强一致数据,行锁简单可靠,不会出现CAS失败需要重试的额外逻辑。
注意@transaction.atomic装饰器包裹整个函数,函数内任何一步抛出异常,数据库自动回滚。我见过同学的方案是“先加库存,再创建记录,出错了再手动补一条冲销记录”——这是给自己挖坑,会多出一堆补偿逻辑,而且容易混乱。
4.3 销售下单:库存扣减与订单生成的联动
销售模块是另一个必须用事务的地方。用户下单成功必须扣减库存,如果库存不够则订单创建失败,两者不可分离。这里还涉及到一个预先校验的细节,虽然最终扣减时也会判断,但先校验可以避免用户填了很多收货信息到提交时才发现库存不足。
def create_order(request): # 1. 校验库存是否足够 for item in cart_items: stock = StoreStock.objects.get(store_id=store_id, product_id=item.product_id) if stock.quantity < item.quantity: raise StockNotEnough(f'{item.product.name}库存不足') # 2. 事务内扣库存 + 生成订单 with transaction.atomic(): order = Order.objects.create(...) for item in cart_items: # 再次校验 + 扣减 stock = StoreStock.objects.select_for_update().get(store_id=store_id, product_id=item.product_id) if stock.quantity < item.quantity: raise StockNotEnough(f'{item.product.name}库存不足') stock.quantity -= item.quantity stock.save() OrderItem.objects.create(order=order, product=item.product, ...) StockRecord.objects.create(store_id=store_id, product_id=item.product_id, record_type='sale', quantity=-item.quantity, operator=request.user)可能有人问:第一步校验完后,事务里为什么还要再校验一次?因为在第一步和第二步之间,库存可能被其他销售订单抢先扣减了,这是并发场景下的经典“检查后失效”问题。第二次校验配合行锁,结论才是可靠的。这个设计拿出来讲,老师会认为你有真正的并发意识。
4.4 报表统计模块:ORM聚合的核心代码
报表模块是这类系统的加分大项。很多系统报表就是简单表格,但你用ECharts画出“各门店近30天销售趋势”“商品分类占比”之后,整个系统的高度立刻不一样。
报表本质是多维分组聚合查询。Django的ORM用annotate配合values就能实现:
from django.db.models import Sum, Count, F, DecimalField from django.db.models.functions import TruncDate def store_sales_report(request): # 按日期统计各门店销售额 report_data = ( Order.objects .filter(status='paid') .annotate(day=TruncDate('created_at')) .values('store__name', 'day') .annotate( total=Sum('total_amount', output_field=DecimalField()), order_count=Count('id') ) .order_by('day') ) return render(request, 'report/store_sales.html', {'data': report_data})TruncDate把datetime截断到日期,相当于SQL里的DATE(created_at),这是做日期维度统计最常用的函数。values('store__name', 'day')决定了分组的粒度。输出的是一个列表,每个元素是字典,直接能转成JSON给前端图表用。
ECharts的引入不复杂,在模板里加一个div容器,然后引入CDN,把后端数据序列化为JSON后传给JavaScript变量:
<div id="chart" style="height: 400px;"></div> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> <script> var chartData = {{ chart_data|safe }}; var chart = echarts.init(document.getElementById('chart')); chart.setOption({ xAxis: { type: 'category', data: chartData.days }, yAxis: { type: 'value' }, series: [{ name: '销售额', type: 'line', data: chartData.totals }] }); </script>模板里的{{ chart_data|safe }}需要后端先转JSON字符串,如果用Django模板默认转义,JSON里的引号和特殊字符会被转成实体,图表初始化就会报错。所以要么用|safe过滤,要么用json_script模板标签,后者更安全:
{{ chart_data|json_script:"chart-data" }} <script> var chartData = JSON.parse(document.getElementById('chart-data').textContent); </script>5. 常见问题与排查:这些坑我当初都踩过,能避开就避开
5.1 数据库连接与迁移问题速查
这类系统开发中出现频率最高的就是数据库相关报错,基本占据了所有运行错误的半壁江山。整理成快速排查表,比在看代码里逐行找高效得多:
| 报错现象 | 根本原因 | 解决方案 |
|---|---|---|
ModuleNotFoundError: No module named 'MySQLdb' | 没导入pymysql兼容驱动 | 在项目__init__.py写pymysql.install_as_MySQLdb() |
django.db.utils.OperationalError: (1045, "Access denied...") | MySQL用户密码错误或权限不足 | 检查settings.py中的USER/PASSWORD,用命令行试连 |
(2003, "Can't connect to MySQL server...") | MySQL服务未启动或端口不对 | 检查服务状态、端口3306是否监听、host是否localhost |
(1064, "You have an error in your SQL syntax...") | 表名与MySQL关键字冲突 | 给模型加db_table或改字段名,order容易撞关键字 |
Table 'xxx' doesn't exist | 忘记执行迁移命令 | 先makemigrations再migrate,注意app注册 |
中文全部显示成?或乱码 | 数据库字符集非utf8mb4 | 建库时CREATE DATABASE ... CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci |
我的一个真实经历:项目在Windows上开发一切正常,部署到Linux服务器后崩掉,报_mysql导入错误,最后发现是生产环境没装pymysql且没在__init__.py里导入。环境一致性必须从一开始就注意,建议用虚拟环境加requirements.txt锁死依赖。导出命令一行搞定:
pip freeze > requirements.txt到了新机器一条命令装完:
pip install -r requirements.txt5.2 常见代码逻辑隐患
除了环境报错,代码逻辑上的隐患更容易让人头大,尤其是数据同步问题。
库存为负进入数据库。用select_for_update()扣库存之后,仍建议在StoreStock模型加CheckConstraint约束,让数据库层面兜底:
from django.db import models class StoreStock(models.Model): quantity = models.IntegerField(default=0, verbose_name='当前库存') class Meta: constraints = [ models.CheckConstraint( check=models.Q(quantity__gte=0), name='quantity_not_negative' ) ]加了约束后,理论上任何非法扣减都会在数据库层报错,这是防呆兜底。为什么明明有事务和行锁还要加约束?因为开发过程中难免有别的代码绕过逻辑直接操作库存字段,约束能让你在测试阶段就发现问题。
另外一个高频问题:静态文件404。Django的DEBUG=True时能自动服务静态文件,但部署时DEBUG=False必须执行collectstatic,把分散在各app的静态资源收集到STATIC_ROOT目录,同时Nginx或Apache要配置静态目录指向。检查顺序是:STATIC_URL是否配置、STATICFILES_DIRS是否指向项目根目录的static文件夹、模板里是否加了{% load static %}标签。
还有一个莫名其妙的问题:上传的图片能显示,但刷新后没了。大概率是图片上传到了内存而非磁盘。MEDIA_ROOT配置漏掉或路径不对会导致文件根本没落盘,Django只在请求生命周期内把它放在内存里,一旦进程重启就丢。必须显式设置:
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'然后在根URL里加上:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)5.3 订单并发和超卖场景的测试建议
写create_order这种并发敏感代码时,不能只做功能测试,还要做压测验证。我测试并发时用的方法是写一个脚本模拟20个线程同时购买同一商品,预期结果是:只有库存数量的订单能成功,其余订单都回滚,最终库存不能为负数。
from concurrent.futures import ThreadPoolExecutor import requests def buy(): resp = requests.post('http://127.0.0.1:8000/order/create/', data={'product_id': 1, 'quantity': 1}) print(resp.status_code) with ThreadPoolExecutor(max_workers=20) as executor: for _ in range(20): executor.submit(buy)如果最终数据库里有超过库存量的订单,说明库存扣减逻辑有并发漏洞。比如你看到库存只有10件,结果20单都创建成功,那很可能你没加select_for_update(),或者事务没在正确位置开启。真遇到这种情况别慌,把事务和锁加上基本能解决。
5.4 关于“数据迁移”版本冲突的一个补充建议
开发过程中最让人头疼的操作是修改了模型字段类型后执行迁移,报依赖错误。比如User模型引用了Store模型,你先删了Store里的字段,又同步改User里的外键引用,迁移顺序乱了就报MigrationError。
我的做法是:一旦模型确定下来,不要频繁改字段类型,顶多加新字段;真需要改时,先在本地备份数据库,手动对比迁移文件再执行。开发阶段保留一个“能跑通”的干净版本,万一把数据库改崩了,能快速回退。
6. 几个提升项目档次的加分项建议
如果基础功能和报告都完成了,还有余力的话,下面这几个扩展点建议按难度从低到高依次加:
第一个是消息通知模块。用Django的signals信号,当库存数量低于low_stock_threshold阈值时,自动创建一条通知,推送给对应门店的店长。这个非常适合连锁超市场景,因为在页面上展示“A门店的农夫山泉库存仅剩5瓶”,比让店长自己一个个对库存高效太多。信号写法很简单:
from django.db.models.signals import post_save from django.dispatch import receiver from .models import StoreStock, StockAlert @receiver(post_save, sender=StoreStock) def check_low_stock(sender, instance, created, **kwargs): if instance.quantity <= instance.low_stock_threshold: StockAlert.objects.get_or_create( store=instance.store, product=instance.product, is_handled=False, defaults={'message': f'{instance.product.name}库存不足'} )get_or_create是为了避免同一个商品库存一直低于阈值时反复产生多条待处理通知,只有处理完才能产生新的。
第二个是Excel导入导出。连锁超市商品SKU动不动几百上千,手工在页面一个个录入根本不现实。用openpyxl库实现商品档案批量导入导出Excel,这属于实战里的刚需功能。代码逻辑无非是读Excel每行数据,Django ORM逐条建商品档案,重号条码直接跳过并生成导入错误报告。
第三个也是最能拉开差距的:数据大屏页面。所有门店销售趋势、热销商品排行、实时销售额、库存预警数量集中到一页,每5秒定时刷新,配全屏展示。前端用ECharts的仪表盘、地图、折线图组合,后端只需写几个聚合查询接口。这个页面挂在仓库大屏或导购台显示器上,演示时的视觉冲击力是普通表格页面无法比的。数据库层面不需要额外建表,就是那几个模型的聚合查询,但展示方式完全不同。
第四个:如果时间来得及,可以考虑加入简单的采购建议逻辑。按每个商品过去7天的平均日销量算出安全库存,再对比当前库存给出补货建议。公式也很简单:建议补货量 = max(安全库存 - 当前库存, 0)。安全库存可以设为平均日销量 * 补货周期天数 * 1.5,1.5是安全系数。这个属于简单的数据分析应用,答辩时可以讲“基于历史销量的库存预测”,非常加分。
7. 关于这个项目的一点个人体会
这类系统说白了就是“CRUD+权限+报表”,但把它做扎实并不容易。我见过太多只写出一堆页面却跑不通完整链路的人,也见过把库存、订单、权限处理得滴水不漏的人,差距往往不在代码量,而在最开始对业务逻辑的理解深度。
hx2008这个项目代号虽然简简单单,但它背后隐藏的是一个完整的多角色、多门店、多数据流的业务系统。做成什么样才算好?标准其实很朴素:用户能登录、有权限区分、商品能入库、顾客能下单、库存能扣减、报表能看趋势。这六个环节打通了,系统就能真正用起来,而不只是一堆网页截图。
最后分享两个自己反复用来检验系统质量的技巧:一是拿起计算器,手工计算一笔订单的总金额、库存变化、报表汇总,和系统后台结果比对,数据能对上才是真正确;二是换三个不同角色账号登录,逐一检查页面显示和按钮可见性是否符合预期,菜单隐藏和接口拦截双重验证。这两个测试看起来质朴,但很多功能齐全的系统恰恰死在这里。
如果这篇文章的过程中你有哪一步卡住了,优先看第5部分的排查表,绝大多数问题都能在里面找到答案。动手做永远比看教程学得快,系统跑起来之后再回来复盘整条业务链路,你会有完全不一样的收获。