Django+微信小程序在线点餐系统开发:登录、订单与库存设计实战

发布时间:2026/9/15 1:58:35
Django+微信小程序在线点餐系统开发:登录、订单与库存设计实战 简介面向计算机相关专业毕业设计、课程设计以及希望掌握Django开发技能的Python学习者提供一套可独立运行的在线点餐系统项目。系统后端采用Django框架完整实现用户注册登录、菜品分类与检索、购物车管理、订单生成与状态跟踪、后台数据管理等核心功能前端采用微信小程序提供扫码点餐、餐品展示、订单确认等交互体验前后端通过API对接整体代码规范、注释充足。资源包大小361.68MB共2000个文件其中1474个py文件承载业务逻辑与视图函数201个html模板搭配30个css样式构成管理端页面127个js文件负责动态交互13个wxss与11个wxml布局小程序界面另有配置文件、说明文档及数据库备份目录结构清晰便于按模块学习与二次修改。该资源已有1340人学习下载源码经过测试可稳定运行并附赠计算机答辩PPT模板能帮助快速完成毕设演示、项目讲解与材料整理。1. 为什么用 Django 微信小程序做在线点餐系统在线点餐系统看起来是“一个页面 一个购物车”的小项目真正压住毕业答辩的往往是登录态贯通和订单数据一致性。Django 提供的 ORM、Admin 后台和迁移机制加上微信小程序“无需安装、即开即用”的入口形态正好覆盖“后台管理端 用户扫码点餐端”两套使用场景。这套资源适合三类人要交课程设计的本科生、想快速出原型验证点餐流程的产品开发、以及正在学 Python Web 但缺少前后端联调经验的人。拿到源码后先不要急着跑 demo先把 Django 的目录结构和小程序的页面映射关系理清现场演示才不会卡壳。2. Django 后端工程搭建与三张核心表的设计2.1 从零初始化 Django 工程与 App拿到源码包后最好先按标准流程把工程骨架重新生成一遍这样答辩时被问“这个项目是怎么创建的”能直接答出来。Django 项目的标准做法是“一个项目 多个应用”点餐系统拆成 menu、order、user 三个应用最合理分别负责菜品管理、订单流转和微信用户映射互不掺和。# 创建虚拟环境避免把依赖装进系统 Python python -m venv venv source venv/bin/activate # Windows 下使用 venv\Scripts\activate # 安装核心依赖drf 用于写 APIcors-headers 解决小程序跨域 pip install django djangorestframework django-cors-headers Pillow # 在当前目录生成 manage.py 和项目配置包 django-admin startproject order_system . # 创建三个业务应用 python manage.py startapp menu python manage.py startapp order python manage.py startapp user # 固定依赖版本方便换电脑后一键复现环境 pip freeze requirements.txtpython -m venv venv会创建一个隔离的 Python 环境避免后续pip install污染系统环境startproject后面加的那个英文句点表示项目文件直接生成在当前目录不加的话会多套一层order_system/order_system后续迁移路径很容易写错。这里把django-cors-headers提前装上是因为微信小程序端在开发者工具里默认校验安全域名本地调试时往往会走 http 跨域不带这个中间件会遇到一连串 CORS 报错。startapp之后不要忘了打开settings.py把menu、order、user、rest_framework、corsheaders注册进INSTALLED_APPS否则后面执行makemigrations时 Django 根本不会扫描这三个目录下的模型。资源自带源码里已经写好了注册逻辑但自己动手重建一遍更容易记住这个点。requirements.txt里保存的依赖版本建议和源码说明文档保持一致换机器演示时直接pip install -r requirements.txt就能把环境恢复出来。2.2 三张核心表的字段设计与数据关系点餐系统的数据模型不复杂重点在于每个字段为什么这么选。用户表不在 Django 默认的auth.User上直接加字段而是新开一张WeChatUser表用openid当唯一键菜品表把价格做成DecimalField而不是FloatField订单表拆成主表和子表子表里保存下单那一刻的菜品快照。# menu/models.py from django.db import models class Category(models.Model): name models.CharField(分类名, max_length50) class Meta: verbose_name 菜品分类 verbose_name_plural verbose_name def __str__(self): return self.name class Dish(models.Model): category models.ForeignKey( Category, on_deletemodels.CASCADE, related_namedishes, verbose_name所属分类 ) name models.CharField(菜名, max_length100, db_indexTrue) price models.DecimalField(价格, max_digits7, decimal_places2) image models.ImageField(图片, upload_todishes/%Y/%m/, blankTrue) stock models.PositiveIntegerField(库存, default0) status models.BooleanField(是否上架, defaultTrue) created_at models.DateTimeField(创建时间, auto_now_addTrue) class Meta: verbose_name 菜品 verbose_name_plural verbose_name def __str__(self): return self.name这段模型里有几个值得在答辩时展开的参数on_deletemodels.CASCADE表示删除分类时级联删除其下菜品实际门店运营中建议改成PROTECT避免误删分类把整批菜品带走db_indexTrue给菜名加索引食材名称查询在小程序端很频繁PositiveIntegerField限定库存不能为负数这是第一层防超卖约束。价格精度是另一个高频追问点。FloatField在 Python 里会变成二进制浮点数0.1 加 0.2 可能得到 0.30000000000000004体现在订单金额上就是分单位误差。DecimalField(max_digits7, decimal_places2)在数据库层用定点数存储总量不超过 99999.99对单店点餐系统来说足够。下面是资源包中核心模型的字段速查表可以拿来做数据库设计的说明页。模型字段名类型与关键参数用途说明WeChatUseropenidCharField(max_length64, uniqueTrue)微信用户的唯一标识CategorynameCharField(max_length50)菜品分类名称DishpriceDecimalField(max_digits7, decimal_places2)避免浮点精度损失DishstockPositiveIntegerField(default0)库存不允许负值OrderstatusCharField(max_length20, choicesSTATUS)订单状态机的枚举字段OrderItemcountPositiveIntegerField()购买数量限定为正整数订单主表与子表的拆分逻辑要重点理解一张总表记录用户、总金额和状态一张子表记录订单里的每道菜品、数量、下单时价格。子表里保存“下单时价格”是关键菜品价格以后改了历史订单依然能还原当时的金额而不是跟着菜品表一起变动。如果不拆子表把菜品列表直接塞进一个 JSON 字段虽然查询时省事但后面要做“月度菜品销量统计”就会非常痛苦。2.3 Django Admin 的可用性与界面美化后端管理端直接用 Django Admin 可以省掉一整套后台页面开发。但原生 Admin 的输入框比较朴素答辩演示时观感不好。资源包里带了 bootstrap 和 font-awesome 样式文件就是用来给后台页面做装饰的通常的做法是在admin.py里重写DjangoAdmin的change_list_template或者直接在项目静态目录里覆盖admin/base_site.html。# menu/admin.py from django.contrib import admin from .models import Category, Dish class DishAdmin(admin.ModelAdmin): list_display (name, category, price, stock, status, created_at) list_filter (category, status) search_fields (name,) list_editable (price, stock, status) admin.site.register(Category) admin.site.register(Dish, DishAdmin)list_editable允许在列表页直接改价格、库存和上架状态演示时不用点进编辑页鼠标点两下就能完成“菜品下架”操作。status字段加进list_filter之后Admin 右侧会自动生成筛选栏方便展示“只看已上架”菜品的场景。如果想把 Admin 界面再做得现代化一点可以安装simpleui这类第三方库但毕业设计里不建议过度依赖因为答辩老师更可能问“这些样式怎么来的”而不是“这个按钮为什么这么好看”。Dish的图片字段ImageField需要确认settings.py里配好了MEDIA_URL和MEDIA_ROOT否则后台上传图片后前端拿不到可访问的 URL。常见的配置是MEDIA_URL /media/MEDIA_ROOT BASE_DIR / media并在根路由里用static()处理媒体文件。这个点经常湮没在源码里但现场演示上传菜品图时却最容易翻车。3. 微信小程序前端从登录到下单的联动实现3.1 code 换取 token 的登录链路小程序端没有 cookieDjango 默认的session机制在这里不适用。通用的做法是小程序调用wx.login拿到临时code后端用这个 code 去微信接口换openid再生成一个业务 token 返回前端。资源包的说明文档里把这个流程称为“code 换 token 登录”这也是微信小程序最常见的鉴权模型。# user/views.py import hashlib import json import time from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .models import WeChatUser def make_token(openid): raw f{openid}:{time.time()} return hashlib.sha256(raw.encode()).hexdigest() csrf_exempt def login(request): data json.loads(request.body) code data.get(code, ) # 本地联调时无法真正调用微信接口这里用固定前缀模拟 openid openid wx_demo_ (code[-6:] if code else 000000) user, _ WeChatUser.objects.get_or_create(openidopenid) token make_token(openid) # 正式项目还要把 token 存到服务端并设置过期时间 return JsonResponse({token: token, nickname: user.nickname})csrf_exempt是给本地联调用的因为小程序wx.request不会像浏览器那样先发 OPTIONS 预检或携带 CSRF tokenDjango 的 CSRF 中间件会拦截 POST 请求。生产环境里建议换成djangorestframework-simplejwt签发 JWT并去掉csrf_exempt。code[-6:]只是为了让不同微信账号在演示时拿到不同的openid真实项目中必须用 request 到微信code2Session接口。前端侧的小程序登录封装如下// utils/auth.js function login() { return new Promise((resolve, reject) { wx.login({ success: (res) { if (!res.code) { reject(new Error(微信登录失败)); return; } wx.request({ url: http://127.0.0.1:8000/api/login/, method: POST, data: { code: res.code }, success: (resp) { if (resp.statusCode 200) { wx.setStorageSync(token, resp.data.token); resolve(resp.data); } else { reject(new Error(resp.data.msg || 登录接口异常)); } }, fail: reject }); }, fail: reject }); }); }这里把登录封装成Promise后续所有页面都能用async/await方式调用避免回调地狱。wx.setStorageSync把 token 存进本地缓存下一次请求从缓存里取不需要每次启动都重新登录。答辩时可以把这段代码的核心参数列成表格评委如果追问“前端怎么保持登录态”直接指着token参数说明即可。参数名传输位置示例值说明codePOST body0b3XeDmkwx.login 返回的临时凭证tokenHeader5f4dcc3b5a...鉴权用后端可存入缓存nicknamePOST body测试用户联调阶段可随意指定提示CORS_ORIGIN_ALLOW_ALL True只适合本地联调。生产环境一旦放开任何网页都能跨域请求你的点餐接口必须换成白名单或者按小程序 referer 校验。如果熟悉uniapp的跨端方案也可以把前端换成 uniapp 工程但资源包里给的是原生小程序代码直接在微信开发者工具中打开即可省去二次构建。两种方案的登录流程都基于同一个wx.login后端逻辑不需要改动。3.2 菜品列表渲染与购物车数据绑定菜品列表页是整个小程序端访问量最大的页面。页面启动时从后端拉取上架菜品然后通过wx:for渲染卡片列表用户点“加入购物车”时用dish_id作为购物车对象的 key这样同一个菜品只存在一个条目数量递增。// pages/menu/menu.js Page({ data: { dishes: [], cart: {}, loading: false }, onLoad() { this.fetchDishes(); }, fetchDishes() { this.setData({ loading: true }); const token wx.getStorageSync(token); wx.request({ url: http://127.0.0.1:8000/api/dishes/, method: GET, header: { Authorization: Token token }, success: (res) { this.setData({ dishes: res.data.data }); }, complete: () this.setData({ loading: false }) }); }, addToCart(e) { const { id } e.currentTarget.dataset; const dish this.data.dishes.find(item item.id id); if (!dish) return; const key String(id); const cur this.data.cart[key]; this.setData({ [cart.${key}]: { ...dish, count: cur ? cur.count 1 : 1 } }); } });setData里的模板路径cart.${key}是微信小程序官方支持的特性可以动态更新对象某个属性的值而不用先取回整个cart再整体 setData在菜品数较多时性能更稳。这里的find依赖dishes已经加载完成如果网络慢用户在 loading 期间点了按钮dish会是undefined所以代码里做了if (!dish) return的防御性判断。答辩时如果问到“购物车数据存在哪里”回答“存在页面内存里退出页面即清空”即可真正的持久化下单接口在 3.3 节。对应的 WXML 结构可以写成这样view classdish-list view classdish-card wx:for{{dishes}} wx:keyid image src{{item.image_url}} modeaspectFill/image view classdish-info text classname{{item.name}}/text text classprice{{item.price}}/text button sizemini>radio-group bindchangeonSpecChange>submitOrder() { const cart this.data.cart; const items Object.keys(cart).map(id ({ dish_id: Number(id), count: cart[id].count, spec: cart[id].spec || })); if (items.length 0) { wx.showToast({ title: 购物车为空, icon: none }); return; } wx.request({ url: http://127.0.0.1:8000/api/orders/, method: POST, data: { items }, header: { Authorization: Token wx.getStorageSync(token) }, success: (res) { if (res.data.code 0) { wx.showToast({ title: 下单成功 }); this.setData({ cart: {} }); } else { wx.showModal({ title: 下单失败, content: res.data.msg, showCancel: false }); } } }); }这里把dish_id用Number()强转成数字是因为Object.keys()拿到的 key 全是字符串后端 Django 的模型主键是整数字符串匹配也能通过但日志里会出现类型转换警告。spec字段在后端如果为空字符串要统一处理成None或空值否则数据库里会存下大量空规格。下单成功后立刻清空购物车并提示用户刷新订单列表保证“订单已提交”和“购物车已重置”两个状态同时变化。4. 订单状态机与 Django 事务扣库存4.1 订单状态机的五个状态订单模块最容易“演示一分钟答辩十分钟”。常见做法是把订单状态定义成常量表存进CharField的choices里而不是直接散落在各处判断字符串。每个状态写清楚触发条件和下一状态后续接支付、接退款都能在这个模型上扩展。# order/models.py class Order(models.Model): STATUS_PENDING pending # 已下单未支付 STATUS_PAID paid # 已支付 STATUS_COOKING cooking # 制作中 STATUS_DONE done # 已完成 STATUS_CANCEL cancel # 已取消 STATUS_CHOICES [ (STATUS_PENDING, 待支付), (STATUS_PAID, 已支付), (STATUS_COOKING, 制作中), (STATUS_DONE, 已完成), (STATUS_CANCEL, 已取消), ] user models.ForeignKey(user.WeChatUser, on_deletemodels.CASCADE) total_price models.DecimalField(max_digits9, decimal_places2, default0) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultSTATUS_PENDING) created_at models.DateTimeField(auto_now_addTrue) updated_at models.DateTimeField(auto_nowTrue)updated_at字段配合auto_nowTrue在每次save()时自动更新时间后端在查询“哪些订单超过 30 分钟没支付”时可以直接用这个字段做时间差计算。订单状态改动建议统一走一个 service 方法而不是在视图里直接给order.status xxx赋值这样便于加权限校验和操作日志。状态机对应的表格可以放进文档也可以在答辩 PPT 里直接引用状态枚举值触发时机后续状态待支付pending用户提交订单已支付 / 已取消已支付paid模拟支付回调制作中制作中cooking后厨接单已完成已完成done用户确认收货终态已取消cancel用户取消或超时终态4.2 事务与行锁扣库存的正确姿势点餐系统最容易出 bug 的地方是库存扣减。假设菜品库存是 10 份两个用户同时在购物车各提交 8 份如果都是先查库存再扣减两次查询都会读到 10最终库存在内存计算后被写成 2超卖就发生了。Django 的解决方案是把扣库存放在一个数据库事务里并对菜品行加锁。from django.db import transaction transaction.atomic def create_order(user, items): order Order.objects.create(useruser, total_price0) total 0 for item in items: dish Dish.objects.select_for_update().get(iditem[dish_id]) if dish.stock item[count]: raise ValueError(f{dish.name} 库存不足) dish.stock dish.stock - item[count] dish.save(update_fields[stock]) total dish.price * item[count] order.total_price total order.save(update_fields[total_price]) return ordertransaction.atomic把整个方法包成一个原子操作中间任何一步抛出ValueError前面的order和dish.save()都会回滚数据库不会留下半成品订单。select_for_update是事务里的行级锁第一个事务没有提交前第二个事务执行到同一行时会阻塞等待而不是继续读到旧库存。这里不直接写dish.stock F(stock) - count是因为还需要先做库存是否足够的判断更新用update_fields可以只更新stock字段减少锁竞争。F()表达式是另一种避免竞态的写法比如Dish.objects.filter(idid, stock__gtecount).update(stockF(stock) - count)它把运算下推到数据库执行。但少了库存校验步骤返回值是受影响行数为 0 就说明库存不足。两种写法在答辩里都可以讲关键是理解“查询-修改-写回”三个步骤必须原子。4.3 用户订单列表的分页与状态筛选前端订单页需要高频查询“当前登录用户的历史订单”接口必然要支持分页和按状态筛选。Django 自带的Paginator够用不需要引入第三方分页框架。from django.core.paginator import Paginator from django.http import JsonResponse def get_user_orders(request): user request.user qs Order.objects.filter(useruser).order_by(-created_at) status request.GET.get(status, ) if status: qs qs.filter(statusstatus) page int(request.GET.get(page, 1)) page_size min(int(request.GET.get(page_size, 10)), 50) paginator Paginator(qs, page_size) page_obj paginator.get_page(page) return JsonResponse({ total: paginator.count, page: page_obj.number, list: [order.to_dict() for order in page_obj] })page_size用min()限制最大值 50是防止有人把参数改成 99999 一次拉走全表数据。order_by(-created_at)让最新订单排在前面用户更容易看到刚下的单。Paginator.count走的是SELECT COUNT(*)和取数据列表是两条 SQL在数据量超过十万的订单表里会有一点性能开销单店点餐规模完全不需要优化。to_dict()方法是手动在模型里写的序列化函数比在视图里拼 dict 更整洁也能被多个视图复用。5. 答辩前必须跑通的几个联调细节5.1 微信开发者工具的调试开关与顶部导航适配本地跑这段项目时必须打开微信开发者工具右上角的“详情-本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。如果不勾选小程序请求http://127.0.0.1:8000会被直接拦截控制台报错url not in domain list。这是遇到最多的第一个坑不是代码 bug是工具检查。首页顶部导航栏在 iPhone 上存在刘海屏适配问题常见做法是用wx.getMenuButtonBoundingClientRect()拿到胶囊按钮的位置再动态设置自定义导航栏高度。下面这段可以放在app.js的onLaunch里所有页面都能读取App({ onLaunch() { const menu wx.getMenuButtonBoundingClientRect(); wx.setStorageSync(menuRect, { top: menu.top, height: menu.height }); } });top是胶囊按钮距顶部的距离height是胶囊高度自定义导航栏的高度通常取menu.top - 状态栏高度 menu.height。如果不想折腾也可以直接使用系统默认导航把这个适配代码整段删掉不影响点餐主流程。5.2 高频报错与五步预检法把整个项目跑起来之前先按下面这张表格排查能省掉大部分联调时间报错现象可能原因修复方式url not in domain list开发者工具未关闭合法域名校验详情-本地设置-勾选不校验域名CSRF token missing登录接口缺失csrf_exempt视图加上装饰器或改用 JWTNo such table: menu_dish未执行migrate执行python manage.py migrateUnknown column数据库和模型不同步备份数据后重新makemigrations401 Unauthorizedtoken 过期或请求头写错检查Authorization: Token xxx格式图片不显示MEDIA_URL未配置settings.py配置媒体根目录最后还有一个小技巧演示前用python manage.py check检查项目配置再执行python manage.py collectstatic收集后台样式。check会报出未定义字段、URL 反向解析错误这类静态问题collectstatic把 Admin 自带的样式文件从环境目录复制到项目静态目录这两条命令跑完后台页面和 API 交互基本就稳了。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询