资讯动态

基于Django+Vue的超市货品管理系统开发实战:从建模到部署全流程

发布时间:2026/10/8 8:45:43 来源:尧图企业网站定制
做超市货品管理系统这类项目最容易被技术光环带着跑。很多新手一上来就纠结用 Django 还是 Flask、Vue 用几、Pycharm 怎么配结果框架搭了一周业务逻辑还是一片空白。这个标题里同时出现了 python、vue、django、flask、pycharm 几个关键词其实正对应了一套完整全栈项目的技术选型问题。我基于实际开发经验把整个系统从需求梳理、后端建模、前端页面到联调部署完整走了一遍写这篇文章主要是给准备做毕业设计、课设或者想入门全栈开发的朋友一份可以直接照着抄的路线图和避坑清单。1. 先把业务拆明白超市货品管理系统到底要管什么写代码之前一定要先想清楚超市里每天发生哪些动作。很多人觉得超市货品管理就是商品增删改查真去调研一圈会发现业务至少覆盖进货、销售、库存变动、供应商、报表这些环节。如果你只做个 CRUD答辩或者实际试用时很快会被问倒。1.1 从收银台和仓库两个视角看业务边界我习惯把超市货品管理系统拆成两个视角来看。收银员视角要解决的是卖得快扫条码或者搜商品名称把商品加入购物车改数量看合计金额点结算生成销售单自动扣减库存打印小票。这个流程里最核心的动作是销售单 库存扣减。仓管员视角要解决的是管得清给商品建档、维护分类、设置进价和售价、记录供应商信息、做入库操作、定期盘点、处理报损。这个视角的核心是商品档案 库存流水。两个视角合起来就构成了一个最小可用系统。我建议你把功能模块先画成清单商品管理新增、编辑、上下架、搜索、条码录入分类管理一级分类维护供应商管理供应商信息与供货记录销售管理创建订单、订单列表、订单详情库存管理入库、库存查询、低库存预警数据统计销售额、热销商品、库存金额这里多说一句如果你是在做课设不用一上来就做采购审批、多门店、会员积分这些复杂逻辑。把收银和库存两条链路跑通系统就已经立住了。1.2 技术选型为什么我选了 Django Vue而不是 Flask标题里同时出现了 django 和 flask说明很多人对这两个框架的选择有困惑。我的结论很直接做这种业务管理系统优先选 Django。Flask 的优势是轻量、自由适合做 API、微服务、快速原型。但超市货品管理系统不是单纯的接口服务它需要 Admin 后台、数据模型关联、表单处理、认证权限。这些 Django 都是开箱即用自带 ORM、Admin 后台、迁移机制、用户认证DRFDjango Rest Framework又能帮你把模型快速变成 Restful API。你要是用 FlaskORM 要自己配 SQLAlchemyAdmin 要自己写或者接第三方迁移工具要自己搞 Alembic。项目一大这些自由全变成成本。前端选 Vue 是因为它和 Django 前后端分离配合非常顺。Vue 3 的 Composition API Element Plus 组件库做后台管理界面的效率极高。相比 Django 自带的模板语法渲染页面前后端分离的好处是前端只负责展示和交互后端只负责数据和业务规则以后想扩展小程序、App后端接口可以直接复用。技术栈最终定下来是后端Python 3.12 Django Django REST Framework SQLite开发/ MySQL生产前端Vue 3 Vite Vue Router Pinia Axios Element Plus开发工具Pycharm后端、VS Code 或 Pycharm前端有同学可能会问为什么不上 FastAPIFastAPI 性能确实好自动生成 OpenAPI 文档也很香但它的生态和 Django 不一样。超市货品管理这种重业务的系统性能瓶颈根本不在框架本身而在数据库设计和业务逻辑正确性。Django 的成熟生态能让你少写大量基础设施代码。真要说对比Flask 与 FastAPI 都适合做 API但做完整业务系统Django 的全家桶优势无人能比。1.3 开发环境准备Pycharm、Python 3.12、Node 20 一套配齐环境准备看着简单但很多人栽在这里。我推荐直接用 Pycharm 做主力开发工具专业版对 Django 的支持很完善社区版也够用不需要折腾什么激活问题正规渠道下载就行。Python 环境建议装 3.12 或者 3.11避免用太老的 3.7、3.8。Node 环境用 18 或 20 LTS 版本Vite 5 需要 Node 18 以上。数据库这块本地开发我直接用 Django 自带的 SQLite零配置、跑得稳等部署到生产再切 MySQL通过 Django 的 settings 切换非常方便。我自己创建项目的标准流程是用 Pycharm 新建项目选择虚拟环境 venv终端执行pip install django djangorestframework django-cors-headers执行django-admin startproject supermarket进入项目目录执行python manage.py startapp goods、python manage.py startapp sales前端目录另起一个文件夹npm create vitelatest web -- --template vue这套流程的好处是后端和前端目录彻底分离职责清晰。目录结构大致是supermarket-backend/ # Django 后端 goods/ # 商品模块 sales/ # 销售模块 supermarket/ # 项目配置 manage.py supermarket-web/ # Vue 前端 src/ package.json2. 后端落地的关键代码建模、序列化与库存扣减后端是整套系统的业务核心。我按建模 - 序列化 - 视图 - 核心业务逻辑的顺序来讲这个顺序也是你真正开发时应该遵守的顺序。2.1 Django 项目与 App 的创建顺序很多人创建 Django 项目喜欢一股脑把所有表都塞进一个 App 里这是最难受的维护方式。我建议按业务域拆分 Appgoods管商品、分类、供应商、库存sales管销售单、销售明细。如果以后要做用户管理可以单独usersApp。创建完 App 后记得在settings.py的INSTALLED_APPS里注册INSTALLED_APPS [ ... rest_framework, corsheaders, goods, sales, ]同时配置 DRF 和 CORS注意中间件顺序corsheaders.middleware.CorsMiddleware要放在尽量靠前的位置MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOW_ALL_ORIGINS True # 开发环境放开生产环境要指定域名这里有个新手常见坑CORS_ALLOW_ALL_ORIGINS True必须在DEBUG True时配合使用如果DEBUG False还全放开生产环境会有安全隐患。2.2 数据模型设计钱用 DecimalField库存要加锁商品表和销售表的关系是典型的一对多。直接看我用的模型定义# goods/models.py from django.db import models class Category(models.Model): name models.CharField(分类名称, max_length50, uniqueTrue) def __str__(self): return self.name class Goods(models.Model): name models.CharField(商品名称, max_length200, db_indexTrue) barcode models.CharField(条码, max_length32, uniqueTrue, db_indexTrue) category models.ForeignKey(Category, on_deletemodels.SET_NULL, nullTrue, blankTrue, related_namegoods) cost_price models.DecimalField(进价, max_digits10, decimal_places2, default0) sell_price models.DecimalField(售价, max_digits10, decimal_places2) stock models.IntegerField(当前库存, default0) status models.BooleanField(是否上架, defaultTrue) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue) def __str__(self): return self.name# sales/models.py from django.db import models from goods.models import Goods class SaleOrder(models.Model): order_no models.CharField(销售单号, max_length32, uniqueTrue) total_amount models.DecimalField(总金额, max_digits10, decimal_places2, default0) created_at models.DateTimeField(auto_now_addTrue) class SaleItem(models.Model): order models.ForeignKey(SaleOrder, on_deletemodels.CASCADE, related_nameitems) goods models.ForeignKey(Goods, on_deletemodels.PROTECT, related_namesale_items) quantity models.IntegerField(数量, default1) price models.DecimalField(成交单价, max_digits10, decimal_places2)有几个地方我要特别提醒。商品金额必须用DecimalField绝对不能用FloatField。Float 存储金额会出现 0.10.2 不等于 0.3 的经典问题哪怕价格上架时没错订单合计也可能出现 0.30000000000000004 这种诡异数字。数据库层面解掉精度问题比前端写一堆处理逻辑靠谱得多。商品名称加了db_indexTrue条码加了uniqueTrue和db_indexTrue。超市收银最常见的场景就是按条码精确查找没有索引等于每次都要全表扫描数据量大了直接卡顿。销售明细里的价格字段为什么要冗余一份成交单价因为商品售价可能调整但历史订单必须保留成交时的价格。如果只是通过外键取 Goods 的当前售价历史数据就被篡改了。这是进销存系统里的标准做法叫价格快照。2.3 用 DRF 把模型变成 API 接口模型定义好之后用 DRF 快速暴露接口。序列化器这样写# goods/serializers.py from rest_framework import serializers from .models import Goods, Category class CategorySerializer(serializers.ModelSerializer): class Meta: model Category fields [id, name] class GoodsSerializer(serializers.ModelSerializer): category_name serializers.CharField(sourcecategory.name, read_onlyTrue) class Meta: model Goods fields [id, name, barcode, category, category_name, cost_price, sell_price, stock, status, created_at]视图集用ModelViewSet最省事# goods/views.py from rest_framework import viewsets from .models import Goods from .serializers import GoodsSerializer class GoodsViewSet(viewsets.ModelViewSet): queryset Goods.objects.all().select_related(category) serializer_class GoodsSerializer路由配置# supermarket/urls.py from django.urls import include, path from rest_framework.routers import DefaultRouter from goods.views import GoodsViewSet from sales.views import SaleOrderViewSet router DefaultRouter() router.register(rgoods, GoodsViewSet) router.register(rorders, SaleOrderViewSet) urlpatterns [ path(api/, include(router.urls)), ]这里有个小坑queryset里我用了select_related(category)。因为GoodsSerializer里读取了category.name如果不用select_related每查一条商品都会额外发一次 SQL 查分类表这就是经典的 N1 查询问题。数据量小可能感觉不出来但列表页一多数据库连接数立刻飙升。2.4 下单扣库存事务和并发是最容易翻车的地方系统的核心价值就在这个函数里。超市收银场景下同一商品可能同时被两个收银台售卖如果不做并发控制库存会扣成负数或者数据覆盖。解决方案是事务 select_for_update行锁。# sales/views.py import uuid from datetime import datetime from decimal import Decimal from django.db import transaction from django.utils import timezone from rest_framework import status from rest_framework.response import Response from rest_framework.views import APIView from goods.models import Goods from .models import SaleOrder, SaleItem def generate_order_no(): return datetime.now().strftime(%Y%m%d%H%M%S) uuid.uuid4().hex[:6] class CreateOrderView(APIView): def post(self, request): items_data request.data.get(items, []) if not items_data: return Response({error: 订单商品不能为空}, statusstatus.HTTP_400_BAD_REQUEST) try: with transaction.atomic(): order SaleOrder.objects.create( order_nogenerate_order_no(), total_amountDecimal(0) ) total Decimal(0) for item in items_data: goods_id item.get(goods_id) quantity int(item.get(quantity, 1)) if quantity 0: raise ValueError(商品数量必须是正整数) goods Goods.objects.select_for_update().get(idgoods_id) if goods.stock quantity: raise ValueError(f商品 {goods.name} 库存不足当前库存 {goods.stock}) goods.stock - quantity goods.save() price goods.sell_price SaleItem.objects.create( orderorder, goodsgoods, quantityquantity, priceprice ) total price * quantity order.total_amount total order.save() return Response({order_no: order.order_no, total_amount: str(order.total_amount)}, statusstatus.HTTP_201_CREATED) except Goods.DoesNotExist: return Response({error: 商品不存在}, statusstatus.HTTP_404_NOT_FOUND) except ValueError as e: return Response({error: str(e)}, statusstatus.HTTP_400_BAD_REQUEST)这段代码有三个关键点。select_for_update()会锁住这条商品记录直到当前事务提交。在线程 A 读取并扣减库存的过程中线程 B 的同样操作会被阻塞等 A 提交后才读到底。没有这一步高并发情况下两个订单都可能读到同一个库存数字导致超卖。整个创建订单和扣库存的过程包在transaction.atomic()里任何一个环节报错所有数据库操作全部回滚。不会出现订单建成功但库存没扣或者库存扣了但订单没建成的问题。异常统一抛出来由 APIView 层捕获返回 JSON 错误信息。前端拿到error直接弹提示就行。还有一点经验手动创建SaleOrder时不设置total_amount最后再统一回填是为了避免在循环里反复更新订单表。这种先建壳、后填数的方式在复杂业务里很常见。3. Vue 3 前端页面拆解从脚手架到收银台前端的目标是把后端接口转化成真实可用的操作界面。Vue 3 的写法比 Vue 2 清爽太多我直接讲核心部分。3.1 Vite 搭建项目骨架与依赖安装创建前端项目用 Vite命令是npm create vitelatest supermarket-web -- --template vue cd supermarket-web npm install npm install vue-router4 pinia axios element-plus依赖装完我把main.js改成这样import { createApp } from vue import { createPinia } from pinia import ElementPlus from element-plus import element-plus/dist/index.css import App from ./App.vue import router from ./router const app createApp(App) app.use(createPinia()) app.use(router) app.use(ElementPlus) app.mount(#app)main.js里只做全局注册业务逻辑全部拆到router、views、api目录里。我见过很多新手把所有代码堆在App.vue里最后改一个需求要滚几千行代码那是灾难。3.2 路由、布局和页面模块划分路由表按业务模块划分这样后面加页面只需要在routes数组里追加一条。// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layout/MainLayout.vue), children: [ { path: , redirect: /goods }, { path: goods, component: () import(../views/GoodsList.vue) }, { path: cashier, component: () import(../views/Cashier.vue) }, { path: orders, component: () import(../views/OrderList.vue) }, { path: stats, component: () import(../views/Stats.vue) }, ] } ] const router createRouter({ history: createWebHistory(), routes }) export default router一点提醒createWebHistory用的是 HTML5 History 模式开发环境没问题但部署到 Nginx 之后刷新非首页路由会出现 404。因为前端路由是前端在管服务器收到/goods请求时不知道应该返回index.html。解决办法是在 Nginx 配置里加一句location / { try_files $uri $uri/ /index.html; }这个坑我摔过一次刷新收银台页面直接白屏排查了半天才发现是路由模式的问题。页面布局用一个MainLayout.vue做侧边栏顶部栏结构侧边栏放菜单内容区放router-view。Element Plus 的el-menu和el-container已经很成熟直接组合即可不用自己造轮子。3.3 商品管理页表格、分页和搜索的实现细节商品管理页是最常规的后台列表页但细节很多。核心是搜索、分页和表格联动。搜索逻辑要分两种场景按条码精确匹配、按名称模糊搜索。条码匹配是收银台高频操作必须走精确查询名称搜索用模糊查询就行。后端视图集需要支持搜索# goods/views.py from rest_framework import filters class GoodsViewSet(viewsets.ModelViewSet): queryset Goods.objects.all().select_related(category) serializer_class GoodsSerializer filter_backends [filters.SearchFilter] search_fields [name, barcode]前端表格用el-table数据通过 Axios 获取import request from /api/request export function fetchGoods(params) { return request.get(/api/goods/, { params }) }Axios 实例统一配置基础地址和拦截器// src/api/request.js import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.response.use( response response.data, error { ElMessage.error(error.response?.data?.error || 请求失败) return Promise.reject(error) } ) export default request分页这块建议用 DRF 自带的PageNumberPaginationREST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20 }前端表格底部分页器绑定page、pageSize两个变量切页时重新请求接口同时保持搜索关键词状态避免翻页后搜索词丢了的问题。还有个小坑Django 的SearchFilter默认是 OR 逻辑search_fields [name, barcode]表示 name 匹配或 barcode 匹配都会命中。如果你希望名称模糊搜索时条码字段也参与这个默认行为正合适。3.4 收银台组件前端购物车的状态管理收银台页面是整个系统里交互最复杂的地方。我把购物车状态放在 Pinia 里统一管理// src/stores/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [] }), getters: { totalAmount(state) { return state.items.reduce((sum, item) { return sum item.sell_price * item.quantity }, 0) } }, actions: { addItem(goods) { const existing this.items.find(item item.goods_id goods.id) if (existing) { existing.quantity 1 } else { this.items.push({ goods_id: goods.id, name: goods.name, sell_price: goods.sell_price, quantity: 1 }) } }, removeItem(index) { this.items.splice(index, 1) }, clear() { this.items [] } } })结算函数逻辑async function checkout() { if (cart.items.length 0) { ElMessage.warning(购物车为空) return } const payload { items: cart.items.map(item ({ goods_id: item.goods_id, quantity: item.quantity })) } const res await createOrder(payload) ElMessage.success(下单成功单号 ${res.order_no}金额 ${res.total_amount}) cart.clear() }金额合计这里要特别小心。JavaScript 的浮点数问题在后端已经通过DecimalField解决了但前端计算合计时还会踩坑0.1 0.2会得到0.30000000000000004。建议前端所有金额运算都做一步保护const total cart.items.reduce((sum, item) sum item.sell_price * item.quantity, 0) // 显示时固定两位小数 const displayAmount total.toFixed(2)如果要严谨可以把金额单位换成分来运算避免一切浮点问题。业务量小的情况下toFixed(2)已经够用了。4. 前后端联调和部署阶段踩过的坑前端后端都写好真正的痛苦才刚刚开始。我把联调和部署阶段遇到的高频问题集中说一下。4.1 CORS 跨域问题前后端分离的第一道坎前后端分离后前端跑在http://localhost:5173后端跑在http://localhost:8000浏览器会拦截跨域请求。这个问题的标准解法在后端加 Django CORS 头# settings.py CORS_ALLOW_ALL_ORIGINS True # 开发阶段生产环境一定不要全放开。正确的做法是CORS_ALLOWED_ORIGINS [ https://your-domain.com, ]一旦忘记配置 CORS现象就是前端控制台报Access to XMLHttpRequest at http://localhost:8000/api/goods/ from origin http://localhost:5173 has been blocked by CORS policy这问题本身不难难在开发阶段就把CORS_ALLOW_ALL_ORIGINS配好省得每加一个接口都报一次错。4.2 本地联调Vite 代理和环境变量怎么配开发时前端的 Axios 请求地址如果硬编码成http://localhost:8000/api等部署到生产环境还得全局替换太麻烦。更优雅的方案是 Vite 代理。在vite.config.js里import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8000, changeOrigin: true } } } })这样前端代码里所有请求都写相对路径/api/xxx开发时 Vite 把请求转发给 Django生产时 Nginx 再把/api转发给后端前端代码完全不用改。前端和后端的环境变量也分开管理前端.env.development只管前端的变量后端环境变量靠python-dotenv或系统环境变量注入各管各的不要混在一起。4.3 Pycharm 调试技巧断点、Django Console 和运行配置Pycharm 调试 Django 项目我最常用的三个功能是断点调试、Django Console 和 Run Configuration 环境变量。断点调试在CreateOrderView里加断点用 Pycharm 的 Debug 模式启动项目请求一到断点处就能看到items_data、goods.stock、total这些变量的实时值。否则你只能靠print()输出效率差太多了。Django Console在 Pycharm 底部工具栏选中 Python Console它会自动加载 Django 配置。可以很方便地测试 ORM 查询比如执行Goods.objects.filter(stock__lt10).count()不用启动整个项目就能验证查询逻辑。Run Configuration如果你在不同环境开发、生产切换数据库或密钥可以在 Run Configuration 里配置环境变量不要写在代码里。这样代码移动到别的机器上也安全。4.4 上线部署DebugFalse 之后的一连串问题本地跑通和上线部署是两回事。我刚接触部署时把settings.py的DEBUG改成False之后遇到了一系列问题静态文件全部 404Django 开发模式会自己服务静态文件DEBUGFalse后就不会了。生产环境要么用 Nginx 直接服务静态文件要么执行collectstatic把静态文件收集到指定目录。ALLOWED_HOSTS必须配置不配置的话Django 会拒绝非 localhost 的请求现象是页面返回 400 错误。数据库连接参数SQLite 切 MySQL 时DATABASES配置要改ENGINE和连接参数同时注意 MySQL 的字符集要用utf8mb4否则存中文可能报错。进程管理身边没有专业的运维环境时用 Gunicorn Supervisor 托管 Django 进程最稳定。Gunicorn 负责处理请求Supervisor 负责进程守护进程挂了自动拉起。部署真不是代码能跑就行建议课设阶段别急着上服务器先把本地生产模式的配置跑通理解DEBUGFalse到底改变了什么。5. 从演示项目到可交付系统的扩展方向如果你已经完成了上面的商品管理、收银台和订单模块这套系统已经是一个完整的最小可用产品了。但从课设演示走向真正能用的系统还差几步。5.1 权限控制与操作日志超市货品管理系统至少要有两种角色管理员和收银员。管理员能管商品、看报表收银员只能收银和查看订单。Django 自带的auth模块可以直接用在GoodsViewSet里通过权限类限制from rest_framework.permissions import IsAuthenticated, IsAdminUser class GoodsViewSet(viewsets.ModelViewSet): permission_classes [IsAuthenticated]操作日志我建议用 Django 的 Middleware 实现。每次用户执行的写操作都记一条日志包括操作人、时间、操作内容。万一出现问题有迹可查。收银员的误操作、商品价格误改都是靠操作日志定位的。5.2 用 ECharts 做销售数据看板销售数据报表是超市老板最关心的模块。后端写一个统计接口按天聚合销售额# sales/views.py from django.db.models import Sum, F from django.db.models.functions import TruncDate from rest_framework.views import APIView from rest_framework.response import Response from .models import SaleItem class SalesStatsView(APIView): def get(self, request): data (SaleItem.objects .annotate(dayTruncDate(order__created_at)) .values(day) .annotate(totalSum(F(quantity) * F(price))) .order_by(day)) return Response(list(data))前端用 ECharts 把接口数据渲染成折线图整个看板就出来了。这一步技术难度不大但对答辩或实际演示的效果提升非常明显。5.3 这套架构能复用到哪些场景写完这个系统你会发现它的架构可以快速复用到大量类似场景药店进销存、便利店 POS、图书管理、仓库货品管理本质上都是商品档案 库存变动 订单流水。业务规则变化不大核心是把商品、订单、库存三张表的关系维护好、把并发扣库存的事务做好。如果你打算深入学习我会建议在这个项目基础上做三件事加一个用户中心模块、加一个采购入库流程、再加一个库存盘点功能。做完这三个你基本就掌握了企业级进销存系统的通用模型。我做完这个项目的最大感受是全栈系统开发的核心不是会写几个接口而是理解业务怎么跑、数据怎么流、什么时候该锁、什么时候该快照。超市货品管理系统正好是一个绝佳的练手题材业务不复杂但该有的技术点全都有模型关联、事务并发、前后端联调、权限控制、数据统计。最后分享一个小技巧开发前先把所有模块的数据库表关系画清楚实体关系弄明白了代码基本不会走大弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑