news 2026/9/8 11:11:58

微信小程序 + Django 在线点餐系统:从建表到部署的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序 + Django 在线点餐系统:从建表到部署的完整实践

简介:本资源是一套完整的本科毕业设计项目,面向计算机专业学生及Web全栈初学者,聚焦餐饮行业数字化需求,提供微信小程序前端与Python-Django后端协同开发的在线点餐系统实战案例。资源共2000个文件,含1474个Python后端逻辑与Django配置文件、201个HTML模板页、127个JavaScript交互脚本、30个CSS样式文件及13个WXML/WXSS小程序页面组件,完整覆盖用户端点餐、商家端管理、微信支付对接与订单状态同步等核心模块,压缩包大小为18.39MB。已有96人学习下载,适合用于毕设参考、前后端分离架构实践及小程序+Django集成开发学习。读者可直接部署运行,获取包含数据库迁移脚本、RESTful API接口文档、小程序项目源码、商家后台可视化界面及响应式UI样式(含Bootstrap、Font Awesome等预编译CSS)在内的全套工程化交付成果。

1. 你的毕设选题没毛病:微信小程序 + Django 做在线点餐系统

每年毕业季都能看到一批人在做"在线点餐""校园外卖""食堂预约"这类题目,原因很简单:业务场景完整、技术栈主流、工作量可控。你拿到这个.zip的时候,可能心里在打鼓——这东西拿到答辩台上,老师会不会觉得太普通了?我的看法是:普通不代表敷衍,关键是看你有没有把该做的细节做透。我在带毕设和带项目时见过太多"看代码没问题、一问原理就哑火"的案例,所以这篇内容不讲虚的,直接拆解这套"前端微信小程序 + 后端 Python Django"的在线点餐系统,把设计思路、数据表怎么建、接口怎么定、联调踩了什么坑,全部摊开讲。

1.1 这套系统到底要做什么?

在线点餐系统,核心业务闭环是:用户逛菜单 → 加购 → 下单 → 支付(可模拟) → 商家接单 → 出餐。听起来简单,但落到代码层面,一个能毕业的系统至少要包含这几部分:

  • 用户端小程序:菜品分类浏览、菜品详情、规格选择(比如大杯/中杯、加冰/去冰)、购物车、提交订单、订单状态跟踪、个人中心。
  • 商家管理端(可选,但强烈建议做):简易的订单管理、菜品上下架、销量统计,用 Django Admin 就能低成本实现,但也能体现你对"后台"概念的完整理解。
  • 后端服务:用户登录鉴权、菜品数据接口、订单创建与状态流转、购物车数据保存、联表查询。

如果只做前后端 CRUD,那和课设没什么区别;真正拉开差距的,是对"订单状态机""并发场景下的库存扣减""用户身份识别"这些细节的理解。后面我都会展开。

1.2 技术选型的三个关键词:微信小程序、Django、前后端分离

先说小程序端。微信小程序为什么是毕设首选?一是验证成本低,手机扫码就能预览;二是它天然贴近"点餐"的真实使用场景;三是面试时聊起来有话题——"你处理过小程序的登录态吗?"这种问题很常见。

再谈 Django。国内很多学校第一门 Web 框架课教的就是它,所以用 Django 做毕设最容易"自圆其说":MTV 思想、ORM 操作、Admin 后台、DRF(Django REST Framework)写接口,你能说出每一个组件存在的理由。这套组合的实际开发模式是"前后端分离":小程序端只通过 HTTP 请求与后端交互,后端不返回 HTML 页面,只返回 JSON 数据。

这种模式的好处有三个:

  1. 前后端可以并行开发,你自己一个人写的时候也方便先 Mock 数据联调。
  2. 后端只关心数据逻辑,前端只关心页面渲染,职责清晰,答辩时好讲。
  3. 将来如果要把商家端换成 Web 管理后台,后端接口无需改动,直接复用。

2. 动手之前先建数据模型:Django 模型设计的核心思路

很多同学一上来就写业务代码,结果写到订单那块发现表结构不合理,推倒重来,特别浪费时间。我在做这类系统时,第一步永远是建模。数据模型设计好了,后面所有接口都顺。在线点餐系统里,核心表就这几张:

2.1 从菜品到订单:核心表结构拆解

Category(菜品分类表)

from django.db import models class Category(models.Model): name = models.CharField('分类名称', max_length=50, unique=True) sort_order = models.IntegerField('排序', default=0) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '菜品分类' verbose_name_plural = verbose_name def __str__(self): return self.name

Dish(菜品表)

class Dish(models.Model): name = models.CharField('菜品名称', max_length=100) category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='dishes', verbose_name='所属分类') price = models.DecimalField('价格', max_digits=8, decimal_places=2) image = models.ImageField('菜品图片', upload_to='dishes/', blank=True, null=True) description = models.TextField('描述', blank=True) sales = models.IntegerField('月售', default=0) status = models.BooleanField('是否上架', default=True) created_at = models.DateTimeField(auto_now_add=True)

这里我加了一个status字段,表示菜品是否在售。为什么要这个字段?因为点餐场景里"今日售罄"是常态,你总不能让商品从数据库里消失吧?用 status 控制软下架是最常见的做法。

CartItem(购物车)和 OrderItem(订单项)

购物车有两种实现思路:存后端 or 只存前端。如果只存前端(小程序本地 Storage),实现简单,但换设备购物车就丢了,也没有真正体现后端的价值;如果存后端,需要一张购物车表。我建议毕设阶段做成后端存储,理由很简单:答辩时你能多讲一个"购物车数据的持久化设计"。

class CartItem(models.Model): user = models.ForeignKey('WeChatUser', on_delete=models.CASCADE, related_name='cart_items') dish = models.ForeignKey(Dish, on_delete=models.CASCADE) quantity = models.PositiveIntegerField('数量', default=1) updated_at = models.DateTimeField(auto_now=True)

订单部分就更有讲究了。很多人只会建一张订单表,再建一张订单明细表,然后下单时主表写一条、从表写几条。这没错,但至少要理解"为什么要拆两张表":订单表存的是整单维度信息(总价、状态、下单时间、桌号/收货信息),订单项表存的是菜品维度信息——每一道菜买了几个、多少钱、有没有备注。如果不拆,一张表里反复存订单号,冗余不说,后续查"这个订单里都有什么菜"都得 LIKE 匹配,性能差到没法看。

class Order(models.Model): ORDER_STATUS = [ (1, '待支付'), (2, '已支付/备餐中'), (3, '配送中/待取餐'), (4, '已完成'), (5, '已取消'), ] order_no = models.CharField('订单号', max_length=64, unique=True) user = models.ForeignKey('WeChatUser', on_delete=models.CASCADE, related_name='orders') total_amount = models.DecimalField('总金额', max_digits=10, decimal_places=2) status = models.IntegerField('订单状态', choices=ORDER_STATUS, default=1) remark = models.CharField('备注', max_length=200, blank=True) address = models.CharField('配送地址', max_length=200, blank=True) contact = models.CharField('联系电话', max_length=20, blank=True) created_at = models.DateTimeField(auto_now_add=True)

订单号为什么单独用一个order_no?因为主键 id 会自增,容易被猜到,如果你做了"查询订单详情"的接口,直接遍历 id 就能爬到别人的订单信息,不安全。用时间戳+随机数的组合生成 order_no,能体现你对数据安全的意识。

2.2 用户表设计:微信小程序登录后存什么?

小程序端拿到的用户信息,和 Web 端不一样。你没有密码,也没有邮箱,核心是openid——它是微信用户在当前小程序下的唯一标识。所以用户表长这样:

class WeChatUser(models.Model): openid = models.CharField('微信OpenID', max_length=64, unique=True) nickname = models.CharField('昵称', max_length=100, blank=True) avatar_url = models.CharField('头像', max_length=500, blank=True) phone = models.CharField('手机号', max_length=20, blank=True) created_at = models.DateTimeField(auto_now_add=True) last_login = models.DateTimeField('最近登录', auto_now=True)

关于头像和昵称,这里要提醒一句:微信在 2022 年后调整了用户信息授权策略,现在前端调wx.getUserProfile也会受到一定限制。所以毕设里别一上来就弹窗要用户授权,可以让用户先浏览、先加购,等到下单时再引导完善信息。这个设计细节,答辩时提出来会非常加分。

3. 小程序前端:页面怎么划分、API 怎么调

小程序端不能老想着"一把梭",动手写代码前把页面结构梳理清楚,后面工作量能省一半。我的做法是先画一下页面导航图和数据流图,确定好每个页面需要什么数据,再写一个统一的request.js封装。

3.1 页面规划与 tabBar 设计

一个小程序最直观的入口是底部 tabBar。在线点餐系统通常安排三个 tab:

  • 首页 / 点餐:菜单分类 + 菜品列表 + 顶部搜索
  • 订单:订单列表
  • 我的:用户信息 + 地址管理 + 设置

购物车不做 tab,用右下角悬浮球,点开是半屏抽屉,这个细节比单纯把购物车堆在页面底部高级很多。

首页的点餐页是核心,布局为左侧分类栏,右侧菜品列表。实现时用scroll-view做双列联动滚动,左侧分类点击时右侧对应滚到指定位置,右侧滚动时左侧分类高亮切换。这个交互用原生小程序写也不难,但要注意性能——右侧每个菜品项渲染图片时,尽量给image标签加lazy-load属性。

3.2 登录与请求封装

小程序的登录流程要理解透:wx.login只能拿到一个临时的code,后端拿这个 code 去微信服务器换openidsession_key(需要小程序 appid 和 secret)。换到 openid 后,你在自己的后端创建/查找用户,并签发一个自定义登录态。最简单的做法是后端生成一个token,返回给前端,前端存到wx.setStorageSync('token'),之后所有请求都在 header 里带上。

请求封装是所有前端的必修课:

// utils/request.js const BASE_URL = 'http://127.0.0.1:8000/api' function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token') wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Token ' + token : '' }, success(res) { if (res.statusCode === 200) { resolve(res.data) } else if (res.statusCode === 401) { // 登录态过期 wx.navigateTo({ url: '/pages/login/login' }) reject(new Error('登录过期')) } else { reject(res) } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) } module.exports = { request }

注意我用的是Token而不是JWT。如果你的毕设时间紧,Django 的rest_framework.authtoken就能用;如果想要更完整、更好的答辩点,djangorestframework-simplejwt也值得上。两者差异后面细说。

3.3 购物车状态管理:全局还是页面级?

小程序原生写法里,每个页面是独立的Page实例,页面间的数据共享需要靠getApp().globalData或 Storage。我建议用globalData+ 手动同步的方式来实现购物车数据:

// app.js App({ globalData: { cart: [], // [{ dishId, name, price, quantity }] totalCount: 0, totalPrice: 0 } })

每次加购/减购时,更新getApp().globalData.cart并立即调接口同步到后端,这样即使小程序切后台,数据也不会丢。页面渲染购物车时,从 globalData 读取,不要每次从接口拉,减少请求量,页面切换也更跟手。

4. Django 后端:接口怎么写、鉴权怎么做

后端部分我默认你已经装好 Django 和 DRF,版本建议 Django 4.x + djangorestframework 3.14+,Python 3.10 以上。如果学校机器装的是 Windows 且版本偏老,用 Django 3.2 也行,语法差别不大,但 model 的on_delete必须显式声明。

4.1 用 REST Framework 写接口

先配置 settings:

# settings.py INSTALLED_APPS = [ # ... 'rest_framework', 'rest_framework.authtoken', 'corsheaders', 'users', 'dishes', 'orders', ] REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework.authentication.TokenAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.AllowAny', ], }

然后写菜品接口,直接用 ViewSet + Router 的方式:

# dishes/views.py from rest_framework import viewsets from .models import Category, Dish from .serializers import CategorySerializer, DishSerializer class CategoryViewSet(viewsets.ReadOnlyModelViewSet): queryset = Category.objects.all() serializer_class = CategorySerializer class DishViewSet(viewsets.ReadOnlyModelViewSet): queryset = Dish.objects.filter(status=True) serializer_class = DishSerializer def get_queryset(self): qs = super().get_queryset() category_id = self.request.query_params.get('category') keyword = self.request.query_params.get('keyword') if category_id: qs = qs.filter(category_id=category_id) if keyword: qs = qs.filter(name__icontains=keyword) return qs

注意这些细节:

  • 只暴露ReadOnlyModelViewSet。菜品数据的修改是商家后台的事,用户端没有 POST/PUT 权限,这既是安全考虑,也是"最小权限原则"的体现。
  • get_queryset里做了筛选?category=1?keyword=牛肉是前端最常用的查询方式,用icontains做模糊搜索,比contains更能兼容中英文大小写场景。
  • Django 的query_params是只读的,你不能在这里执行self.request.data之类操作。

Serializer 定义:

# dishes/serializers.py from rest_framework import serializers from .models import Category, Dish class CategorySerializer(serializers.ModelSerializer): class Meta: model = Category fields = ['id', 'name'] class DishSerializer(serializers.ModelSerializer): category = serializers.CharField(source='category.name', read_only=True) class Meta: model = Dish fields = ['id', 'name', 'category', 'price', 'image', 'description', 'sales']

category显示出分类名而不是 id,前端拿到的数据更友好。这里用source实现嵌套读取,也是面试高频考点。

4.2 订单创建的幂等与状态校验

创建订单是最容易出 Bug 的地方,我从两个角度讲自己的做法。

第一步:请求体校验。前端传过来的购物车数据,后端必须重新校验,不能轻信。不能说前端算了总价是 100,后端就存 100。正确做法是后端根据菜品 id 重新从库里查价格,算出真正的总价:

# orders/views.py from rest_framework.decorators import api_view, permission_classes from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated @api_view(['POST']) @permission_classes([IsAuthenticated]) def create_order(request): items_data = request.data.get('items', []) remark = request.data.get('remark', '') if not items_data: return Response({'error': '购物车为空'}, status=400) order_no = format(now().timestamp(), '.0f') + str(random.randint(1000, 9999)) order = Order.objects.create( order_no=order_no, user=request.user, total_amount=0, remark=remark ) total = Decimal('0.00') for item in items_data: dish = Dish.objects.filter(id=item.get('dish_id'), status=True).first() if not dish: return Response({'error': '菜品不存在或已下架'}, status=400) quantity = int(item.get('quantity', 1)) if quantity <= 0: return Response({'error': '数量不合法'}, status=400) OrderItem.objects.create( order=order, dish=dish, price=dish.price, quantity=quantity ) total += dish.price * quantity order.total_amount = total order.save() return Response({'order_id': order.id, 'order_no': order.order_no, 'total_amount': str(total)})

这个函数有几个关键点:

  1. 订单状态初始为待支付。支付回调是后话,毕设里可以用"模拟支付"按钮,点击后置为已支付。
  2. 价格快照OrderItem里存的price是下单那一刻的菜品价格,不是从 Dish 表实时 join 出来的。为什么?因为如果商家之后改了菜品价格,历史订单的金额不应该跟着变。这在电商系统里叫"价格快照",答辩时说这个点很加分。
  3. 金额用 Decimal。Django 里价格字段是DecimalField,计算时也必须用Decimal('0.00')初始化,不能用 float,否则出现 0.1 + 0.2 = 0.30000000000000004 这种问题,到时候对账对不上。

第二步:状态流转限制。订单状态不能随意跳变。比如"已完成"不能变回"备餐中","已取消"不能变回"已支付"。我建议在 Model 上加一个状态流转函数:

def transition_to(self, new_status): allowed = { 2: [1], # 已支付 ← 待支付 3: [2], # 配送中 ← 已支付 4: [3], # 已完成 ← 配送中 5: [1, 2], # 已取消 ← 待支付 / 已支付 } if new_status not in allowed or self.status not in allowed[new_status]: raise ValueError(f'非法状态流转:{self.status} -> {new_status}') self.status = new_status self.save()

虽然毕设项目大概率没人会乱改状态,但写了这个逻辑,代码的鲁棒性立刻上一个档次。

4.3 登录接口:code 换 openid 与 token

Django 端的登录接口是前端调用wx.login拿到 code 后调用的。这里需要用到requests库向微信服务器发起请求。

# users/views.py import requests, hashlib from django.conf import settings from rest_framework.decorators import api_view from rest_framework.response import Response from rest_framework.authtoken.models import Token from .models import WeChatUser @api_view(['POST']) def wx_login(request): code = request.data.get('code') if not code: return Response({'error': '缺少code'}, status=400) resp = requests.get( 'https://api.weixin.qq.com/sns/jscode2session', params={ 'appid': settings.WX_APPID, 'secret': settings.WX_SECRET, 'js_code': code, 'grant_type': 'authorization_code' } ) data = resp.json() openid = data.get('openid') if not openid: return Response({'error': '微信登录失败', 'detail': data}, status=400) user, _ = WeChatUser.objects.get_or_create(openid=openid) token, _ = Token.objects.get_or_create(user=user) return Response({ 'token': token.key, 'user': { 'id': user.id, 'nickname': user.nickname, 'avatar_url': user.avatar_url } })

get_or_create是这里最优雅的写法——老用户直接返回已有 token,新用户创建账号再返回 token,一次接口完成两种情况。Token 用 DRF 自带的还是 JWT 的?我的建议是毕设用authtoken就够,因为配置简单,不用处理刷新逻辑;如果你还有余力,可以升级到simplejwt,用 access token + refresh token 双 token 模式,答辩更有的聊。

5. 前后端联调的实战经验与避坑指南

做完前后端各自的开发,联调阶段才是真正考验人的地方。这里我把去年带学生做类似项目时遇到的典型问题整理一下,每一个都是真实踩过的坑。

5.1 小程序开发者工具里的 "request 合法域名" 问题

小程序正式上线时,请求域名必须是 HTTPS 且在小程序后台配置过白名单。但你做毕设,本地是http://127.0.0.1:8000,怎么调?

  • 开发环境:在微信开发者工具右上角"详情"里勾选"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书"。
  • 如果要用真机预览:本地后端跑在电脑上,手机和电脑连同一 Wi-Fi,把请求地址改成电脑的局域网 IP(比如http://192.168.1.101:8000/api),然后照样勾选"不校验合法域名"。

这里有个细节:Django 默认只允许本机访问,用局域网 IP 访问需要在启动时指定:

python manage.py runserver 0.0.0.0:8000

同时,因为小程序发的是跨域请求,后端要配 CORS。最简单的办法是装django-cors-headers

pip install django-cors-headers
# settings.py INSTALLED_APPS = [ # ... 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # ... ] CORS_ALLOW_ALL_ORIGINS = True

毕设阶段CORS_ALLOW_ALL_ORIGINS = True没关系,开发调起来方便。但如果后续要部署上线,一定要收紧为白名单。

5.2 Django 的 CSRF 问题

Vue/React 这类 SPA 项目调 Django REST Framework 时通常不开 CSRF,因为 DRF 默认只用 SessionAuthentication 和 TokenAuthentication,Token 认证本身不依赖 CSRF Token。但如果你在settings.py里用的是默认的SessionAuthentication,且前端又没带 CSRF Token,POST 请求会报 403。

建议统一使用TokenAuthentication,并在前端请求头带上Authorization: Token xxx。这样既避开了 CSRF 的坑,也符合小程序从wx.request发请求的实际场景。

5.3 图片上传与静态文件访问

菜品图片一般由商家后台录入,小程序的菜品展示读取图片地址。开发阶段可以用 Django 自带的 media 服务:

# urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns = [ # ... ] if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

图片上传接口用 DRF 的FileUploadParser或者直接在 ViewSet 里增加一个 action:

class DishViewSet(viewsets.ModelViewSet): # ... @action(detail=True, methods=['post'], permission_classes=[IsAdminUser]) def upload_image(self, request, pk=None): dish = self.get_object() dish.image = request.FILES.get('file') dish.save() return Response({'image_url': dish.image.url})

注意settings.py里要配MEDIA_ROOTMEDIA_URL,否则上传的文件不知道写哪里。

5.4 "图片 404" 的排查思路

小程序端图片不显示,90% 是这么几个原因:

  1. Django 静态文件服务没配好,访问/media/xxx.jpg返回 404。
  2. 小程序 image 组件加载不了 http 地址(本地调试时可以,真机上不行,需要域名白名单)。
  3. 图片路径拼接错误,比如把相对路径dishes/a.jpg直接塞给src,没加域名前缀。

排查时先在后端浏览器里直接访问图片 URL,确定后端没问题,再看小程序端的网络请求是否能通。这个排查顺序能帮你省大量时间。

5.5 多人协作时的代码同步问题

如果你不是一个人做毕设,而是组队(一个前端一个后端),建议用 Git 管理代码。后端同学提交两份文件我可以给个模板:

__pycache__/ *.py[cod] db.sqlite3 /media/ /static/ .env .venv/

注意.env一定要忽略掉,里面是你的WX_APPIDWX_SECRET,上传到公开仓库等于泄漏密钥。

6. 关于"微信小程序 + Django"这几个热搜词的扩展思考

我不太建议直接在博文标题里堆"python人狗大作战"或者"前端面试题2026"这种词,除非你写内容时确实能挂靠上。但这几个词背后透露的信号值得说一说——前端的就业市场上,"小程序开发"能力已经慢慢变成标配,而不是加分项;Django 因为"会的人多、岗位多、逻辑清晰",依然是后端方向比较稳妥的入门选择。

6.1 Django 在国内到底用得多不多?

总有学生担心学了 Django 是不是"过时工具"。客观讲,国内一线大厂 Java 系是主流,但 Python Web 方向除了 Django 还有 Flask、FastAPI,Django 因为"全家桶"的特性,在小团队、外包、中小型数字化项目里还是有很稳定的使用比例。你拿 Django 做毕设,核心不是证明"我学会了这个框架",而是证明"我理解 Web 开发的整个链路":HTTP 协议、数据库设计、认证鉴权、API 设计、前后端联调、部署上线。这些能力,换任何语言都一样适用。

6.2 从"毕设项目"到"面试项目"还差多少步?

一个完成度不错的毕设,在面试时可以把差距放在两个地方:一是可部署性,你能不能在服务器上真正跑起来并让别人通过外网访问?二是可测试性,你有没有写过哪怕二十个后端接口的单元测试?这两个点看似简单,但大多数应届生都没做。我给的建议是:毕设做完后,花两天时间做这两件事,项目含金量立刻不一样。

7. 部署上线:把毕设从本地搬到服务器

你可以不部署,但如果你做了,这会是答辩时最能"挺直腰杆"讲的部分。市面上可选的方案很多,我用经典的三板斧方案,nginx + uwsgi + sqlite3 跑一个小型应用足够了。

7.1 服务器环境准备与项目迁移

买一台最便宜的学生云服务器(Linux 系统即可),然后按顺序执行:

# 更新系统软件源 sudo apt update && sudo apt upgrade -y # 安装 Python 开发环境 sudo apt install python3 python3-pip python3-venv nginx # 克隆你的项目 git clone https://your-git-repo/your-project.git cd your-project # 创建虚拟环境并安装依赖 python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt pip install gunicorn

依赖清单里至少要有:

Django==4.2.7 djangorestframework==3.14.0 django-cors-headers==4.3.1 requests==2.31.0 gunicorn==21.2.0

注意requirements.txt是后端运行必需的,写完后别漏掉版本锁定的习惯——不同版本之间差异可能非常大。

7.2 迁移数据库与收集静态文件

python manage.py makemigrations python manage.py migrate python manage.py collectstatic python manage.py createsuperuser

collectstatic这一步是把 Django Admin 等静态文件收集到指定目录,方便 nginx 直接服务。如果你不执行,页面会出现"样式全丢"的裸奔现场。

7.3 gunicorn + nginx 配置

后端用 gunicorn 跑:

gunicorn myproject.wsgi:application --bind 0.0.0.0:8000 --workers 3 --daemon

nginx 配置反向代理,把 80 端口转发到 8000,并把静态文件和媒体文件路径指到项目目录:

server { listen 80; server_name your_domain_or_ip; location /static/ { alias /path/to/your/project/static/; } location /media/ { alias /path/to/your/project/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

配置完记得sudo nginx -t检查语法,再sudo systemctl reload nginx生效。

部署时最容易翻车的点是:记住执行 collectstatic 并在日志里检查有没有报错。有的人装上 nginx 后,HTML 是出来了,但 CSS 和图片全挂,问题就是静态文件目录配置错。这里给一个排查技巧:直接在浏览器访问http://你的IP/static/admin/css/base.css,如果 404,说明 alias 路径不对;如果 200 但页面还是没样式,那就是代理层缓存的问题。

8. 写在最后的避坑经验

做完整套项目,我最想提醒你"避免重复造轮子"的同时,"也不要错过该学的东西"。几个关键点:

  • 科目三定理:如果某一环不会,先最小化跑通,比如先写纯 HTML 页面测试后端接口,再接入小程序。别"小步慢走"的时候卡死在 idea 上——你要的是先把链路打通。
  • 数据安全:小程序端小程序密钥不要放前端代码,任何WX_APPIDWX_SECRET都只能出现在后端配置文件里,并且.env要加进.gitignore
  • 状态码:用 400、401、403、404、500 这些标准状态码表达错误,别一报错就 200 +{'status': 'fail'},这在小程序开发中能省一大堆排查时间。
  • 多看服务端日志:小程序端报错时,第一件事不是看前端控制台,而是看后端的日志输出。Django 开发服务器在终端里会打印完整的栈追踪,多数错误一两分钟就能定位。

在线点餐这个课题本身不算难,但"不难"不等于"没前途"。把基础功能做扎实,再在数据模型设计、订单状态流转、部署上线这些细节上花心思,整个项目的完成度和答辩说服力都会高出不少。希望这篇拆解能帮你把毕设做得明明白白。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 11:11:41

中微CMS79FT738/736触摸单片机官方样例程序详解与移植指南

简介&#xff1a;面向中微CMS79FT738/736单片机的官方原版样例程序包&#xff0c;适合正在选型国产8位微控制器、需要快速掌握芯片初始化与外围模块开发的软硬件工程师。整套代码源自官方演示工程&#xff0c;按功能模块划分&#xff0c;覆盖LCD液晶显示、ADC模数转换、USART串…

作者头像 李华
网站建设 2026/9/8 11:10:02

从零实现SHA256:C语言哈希算法详解与嵌入式实践

简介&#xff1a;这是一套SHA256哈希算法的C语言实现源码&#xff0c;适合需要理解或使用SHA-2算法的开发者&#xff0c;常见于数字签名、SSL/TLS协议、文件校验等场景。代码完整实现了SHA256核心流程&#xff1a;从宏定义和常量声明开始&#xff0c;对任意长度输入进行填充&am…

作者头像 李华
网站建设 2026/9/8 11:11:41

跨上下文窗口拆解:AI Coding 工程化落地的关键实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 10:48:52

山特SK2000UPS实战指南:为NAS与办公设备提供稳定电力保护

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/4 13:27:12

本地音视频AI处理工具实战:从环境部署到批量生产指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/6 10:07:44

搭建Leiolai式算力共享系统:从设备注册到任务调度实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华