ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Django+Flask+Vue全栈实战:咖啡点单系统从零搭建到联调

Django+Flask+Vue全栈实战:咖啡点单系统从零搭建到联调 前段时间帮一个学弟调他的咖啡点单系统前端用Vue写得挺漂亮后端选了Django做接口PyCharm里跑得也很欢结果卡在前后端联调那一步跨域问题、静态文件路径、购物车数据格式各种小坑轮着来。后来我们把整套流程捋了一遍从环境搭建到数据库设计再到下单接口的事务处理全部走通之后他才松了口气。这个项目本身算是一个很典型的“python Vue”全栈练手项目技术栈覆盖了PyCharm开发环境、Django/Flask后端框架、Vue前端工程化做完之后不管你是交毕业设计还是想在自己的作品集里放一个完整的前后端分离项目都非常合适。下面这篇文章我按实际开发顺序来写先讲清楚技术选型怎么定再教你从零把工程跑起来然后逐步拆解数据库设计和点单接口最后落到Vue页面实现和联调踩坑。里面所有代码都是我这套项目里实际跑过的你可以直接照着抄。如果你还在纠结用Django还是Flask或者担心Vue环境配不起来这篇文章应该能帮你把大部分疑虑解决掉。1. 技术选型Django、Flask、Vue、PyCharm之间怎么分工1.1 Django和Flask不是二选一而是两种开发节奏很多新手拿到“咖啡点单系统”这种题目第一反应就是Django和Flask到底选哪个。我个人的看法是如果你要做的是一个功能完整、带后台管理、带数据库模型关联的系统优先选Django如果只是想快速写几个接口演示一下下单流程Flask更轻。标题里同时出现“django flask”其实代表了很多初学者会纠结的点我的方案是主用Django同时把Flask的轻量整改方案也留在最后方便你向导师或面试官展示你两种框架都了解。Django的优势在于“全家桶”ORM、Admin后台、表单处理、认证系统全都内置了。咖啡点单这种业务天然有用户、商品、订单、订单明细这几个实体它们之间的外键关系正是Django ORM最擅长处理的。你不需要自己拼SQL也不用自己写后台管理页面Django Admin能直接给你一个可以增删改查的管理界面这对于快速交差或者做演示来说太重要了。Flask则更适合“小步快跑”没有复杂的项目结构一个app.py文件就能跑起来。如果你只想做商品列表和提交订单两个核心接口Flask加上Flask-SQLAlchemy三天就能出活。坏处是随着功能变多你要自己决定项目结构、自己处理数据库迁移、自己搭Admin这些在Django里都是现成的。所以我的结论是完整项目用Django想玩花样或赶时间再用Flask。1.2 Vue负责界面交互Django负责业务逻辑PyCharm负责把两件事组织好这种架构就是典型的前后端分离Django只写API接口返回JSON数据Vue负责页面渲染和用户操作通过axios请求后端接口拿数据。好处是职责非常清晰前端改样式不会碰后端代码后端改逻辑不会动页面结构。这里得重点夸一下PyCharm尤其是专业版。很多人以为PyCharm只是用来写Python的但专业版内置了Vue、HTML、JavaScript的插件支持你可以在同一个IDE里同时编辑Django的views.py和Vue的.vue文件。更关键的是PyCharm的调试器可以同时调试前后端请求链路前端在浏览器里发请求后端断点直接命中这种体验是分开用VS Code和IDEA做不到的。如果你用的是社区版也没关系社区版对Python和Django的支持完全够用前端部分我用VS Code或者直接在PyCharm社区版里写Vue也没有大问题只是没有专业版那么顺滑。整个项目的运行流程是这样用户在Vue页面浏览咖啡菜单加入购物车点结算按钮Vue把订单数据组装成JSON通过HTTP请求发给DjangoDjango校验库存、计算价格、生成订单记录再把结果返回给前端。PyCharm在整个过程中扮演的就是“控制台”角色写代码、跑命令、看日志、调接口全在一个窗口里完成。2. 环境准备与工程初始化这一步稳了后面就顺了2.1 Python版本选择和PyCharm配置的几个关键点先说版本咖啡点单系统用Django的话Python建议用3.10或3.11别为了尝鲜直接上3.13。Django的第三方依赖兼容速度往往比你想象得慢3.12和3.13在某些库上会遇到编译问题。装好Python之后打开PyCharm创建新项目一定要用虚拟环境Virtualenv而不是用全局Python。虚拟环境的意义是把你这个项目的依赖隔离起来不然你以后做第二个项目装的包版本冲突了会非常头疼。PyCharm里创建Django项目有两种方式一种是在新建项目时选择“Django”模板它会自动生成manage.py和项目结构另一种是先创建一个普通Python项目然后在终端里手动执行django-admin startproject命令。我更习惯后者因为这样你能更清楚Django的工程结构是怎么来的。接着是pip源的问题国内安装依赖慢到怀疑人生不是网络问题是默认源的问题。首次创建Django项目之前建议先执行一次换源操作pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple换成清华源之后再执行pip install django djangorestframework django-cors-headers这几个包是核心django是后端框架djangorestframework是写API的利器django-cors-headers是解决前后端跨域问题的后面联调时你会感谢这个包的。2.2 从零搭建Django后端骨架在PyCharm终端里执行下面的命令创建项目和应用django-admin startproject coffee_shop cd coffee_shop python manage.py startapp api项目名coffee_shop就是整个后端工程的名字api是我们自己写的应用里面放咖啡商品、订单相关的模型和接口。创建完之后目录结构大概是这样的coffee_shop/ ├── manage.py ├── coffee_shop/ │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py └── api/ ├── __init__.py ├── admin.py ├── models.py ├── views.py └── ...接着去coffee_shop/settings.py里做三件必做的事把api应用加进INSTALLED_APPS列表把corsheaders加进INSTALLED_APPS同时把corsheaders.middleware.CorsMiddleware加进MIDDLEWARE在文件末尾添加跨域配置CORS_ALLOW_ALL_ORIGINS True开发阶段直接允许所有跨域请求上线之前再收紧。2.3 初始化Vue前端工程Vue这边建议直接用Vite而不是老旧的Webpack模板创建命令很简单npm create vuelatest frontendVite创建项目的时候会问你装不装Router、Pinia、ESLint这些咖啡点单项目里Router是必须的状态管理可以用Pinia也可以用简单的ref对象为了减少学习成本我建议先不装Pinia直接用组件内ref和props传值就够了。项目创建完之后进入目录、安装依赖cd frontend npm install npm install axios依赖装好之后在vite.config.js里配置开发服务器代理这一步极其关键作用是让你前端发请求时把/api开头的地址转发到Django后端从而绕开跨域限制import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true } } } })这样你在Vue代码里请求/api/coffees/Vite开发服务器会帮你转发到http://127.0.0.1:8000/api/coffees/浏览器里不存在跨域问题。我强烈建议前端请求路径统一用/api前缀这样生产环境部署时只要加一层Nginx转发就能平滑切换不用改前端代码。2.4 前后端联调的第一步先确认Base URL我发现很多人项目跑不起来不是代码写错而是前端请求的地址写错了。你如果用Vite代理axios的baseURL应该写成/api而不是http://127.0.0.1:8000/api。两者的区别在于前者走Vite代理浏览器请求的是前端自己的地址不存在跨域后者是浏览器直接请求后端地址就必须依赖CORS配置才能放行。我自己调试时会在axios封装文件里做一个简单的环境切换import axios from axios const http axios.create({ baseURL: import.meta.env.DEV ? /api : https://你的线上域名/api, timeout: 5000 }) export default http开发环境走代理生产环境换成真实域名两个环境都不会出问题。这一步虽然不起眼但能给你省下大量排查时间。3. 后端核心实现数据模型与点单接口3.1 咖啡、购物车、订单的数据模型设计Django最值得称道的就是ORM模型设计把数据库表结构直接写在models.py里。咖啡点单系统核心表就三张咖啡商品表、订单表、订单明细表。购物车我建议先不做独立表而是由前端Vue用变量维护后端只在下单时接收最终的明细数据这样能减少不少工作量。咖啡商品表的模型可以这样设计from django.db import models class Category(models.Model): name models.CharField(max_length50) # 比如经典咖啡、拿铁、冷萃 class Coffee(models.Model): name models.CharField(max_length100) category models.ForeignKey(Category, on_deletemodels.CASCADE, related_namecoffees) price models.DecimalField(max_digits6, decimal_places2) stock models.IntegerField(default0) image_url models.URLField(blankTrue) is_active models.BooleanField(defaultTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) coffee models.ForeignKey(Coffee, on_deletemodels.CASCADE) quantity models.IntegerField() price models.DecimalField(max_digits6, decimal_places2) # 下单时单价快照 class Order(models.Model): STATUS_CHOICES ( (pending, 待支付), (paid, 已支付), (making, 制作中), (done, 已完成), ) user_name models.CharField(max_length50) # 简化版不做登录用 total_price models.DecimalField(max_digits8, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue)几个容易踩坑的细节金额字段一定要用DecimalField不能用FloatField因为浮点数在价格计算上会莫名其妙多出0.01元OrderItem里要记录下单时的单价快照因为咖啡价格以后涨价了历史订单里的价格也不能跟着变stock库存字段后面下单扣减时要用select_for_update加行锁防止并发超卖。Django还自带一个Admin后台你把模型注册进api/admin.pyfrom django.contrib import admin from .models import Category, Coffee, Order, OrderItem admin.site.register(Category) admin.site.register(Coffee) admin.site.register(Order) admin.site.register(OrderItem)然后创建管理员账号、启动服务python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver 8000浏览器访问http://127.0.0.1:8000/admin你就能直接往咖啡表里添加数据不必手动写SQL这也是Django对比Flask最明显的生产力优势。3.2 商品展示接口DRF让你少写一半代码Django REST Framework简称DRF是用来把模型转成JSON接口的库核心是序列化器。给Coffee模型写一个Serializer然后写一个ViewSet路由一配增删改查接口就全出来了from rest_framework import serializers, viewsets from .models import Category, Coffee class CoffeeSerializer(serializers.ModelSerializer): category_name serializers.CharField(sourcecategory.name, read_onlyTrue) class Meta: model Coffee fields [id, name, category_name, price, stock, image_url] class CoffeeViewSet(viewsets.ReadOnlyModelViewSet): queryset Coffee.objects.filter(is_activeTrue) serializer_class CoffeeSerializer路由配置from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import CoffeeViewSet router DefaultRouter() router.register(coffees, CoffeeViewSet, basenamecoffee) urlpatterns [ path(, include(router.urls)), ]前端请求GET /api/coffees/就能拿到所有上架商品的JSON列表字段名、价格、库存一目了然。这里有个细节CoffeeSerializer里用了source参数把外键id变成category_name字符串前端渲染菜单分类时就省得再根据category_id去查分类表了这种“为前端接口做数据整形”的习惯联调起来会非常舒服。3.3 下单与库存扣减事务和锁必须同时上下单是咖啡点单系统里最核心也最容易出错的接口。用户提交来的数据是一个订单项数组每一项包含咖啡id和数量后端要做的动作有根据id查咖啡、检查库存够不够、计算总价、创建订单、扣减库存。这个过程必须用事务包裹中间任何一步出错所有操作都要回滚否则会出现“订单生成了但库存没减”这种脏数据。同时库存扣减必须用select_for_update加行锁否则两个用户同时下单同一杯咖啡可能会超卖。完整代码如下from django.db import transaction from rest_framework.response import Response from rest_framework.decorators import api_view from .models import Coffee, Order, OrderItem api_view([POST]) transaction.atomic def create_order(request): items_data request.data.get(items, []) user_name request.data.get(user_name, ) if not items_data: return Response({error: 订单不能为空}, status400) total 0 order Order.objects.create(user_nameuser_name, total_price0, statuspending) for item in items_data: coffee Coffee.objects.select_for_update().get(pkitem[coffee_id]) qty int(item[quantity]) if qty 0: raise Exception(数量不合法) if coffee.stock qty: raise Exception(f{coffee.name}库存不足) total coffee.price * qty OrderItem.objects.create( orderorder, coffeecoffee, quantityqty, pricecoffee.price ) coffee.stock - qty coffee.save() order.total_price total order.save(update_fields[total_price]) return Response({order_id: order.id, total_price: total})关键点在于事务和锁的配合事务保证“要么全成功要么全失败”行锁保证“同一时间只有一个人能改同一杯咖啡的库存”。我在第一次写这个接口时只加了事务没加锁结果用两个脚本同时下单测试库存就出现了超卖后来加上select_for_update才解决。这个并发问题平时看不出来一旦真有人同时点单就会出事属于典型的“理论要懂、实战更要懂”的知识点。4. 前端Vue实现从菜单列表到下单结算4.1 路由规划和页面骨架前端Vue项目里我把页面拆成了四个核心视图首页菜单展示、购物车、结算页、订单列表。路由配置如下import { createRouter, createWebHistory } from vue-router import MenuView from /views/MenuView.vue import CartView from /views/CartView.vue import OrderView from /views/OrderView.vue import OrdersView from /views/OrdersView.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: menu, component: MenuView }, { path: /cart, name: cart, component: CartView }, { path: /checkout, name: checkout, component: OrderView }, { path: /orders, name: orders, component: OrdersView } ] }) export default router这个路由设计遵循一个原则单页应用里页面跳转全靠路由完成不刷新浏览器所以路由命名要直观。菜单页是入口购物车和结算页是核心操作区订单列表页方便用户查看历史记录。如果你想加一个咖啡详情页可以再用动态路由比如path: /coffee/:id在组件里用route.params.id去查接口这块Vue Router官方文档写得很清楚照着学就行。4.2 菜单展示与购物车组件化实现菜单页的核心是列表渲染用Vue的v-for把从后端拿到的咖啡数组渲染成卡片每个卡片上有加购按钮。这里有个非常实用的Vue特性computed计算属性。购物车总价就是典型的computed属性因为它依赖购物车中每一项的单价和数量只要购物车变化总价自动更新不需要手动监听和重新赋值。下面这个组件实现了一个简单的购物车面板script setup import { ref, computed } from vue import { getCoffees } from /api/coffee const coffeeList ref([]) const cart ref([]) async function loadCoffees() { const res await getCoffees() coffeeList.value res.data } function addToCart(coffee) { const found cart.value.find(item item.coffee.id coffee.id) if (found) { found.quantity 1 } else { cart.value.push({ coffee, quantity: 1 }) } } const totalPrice computed(() { return cart.value.reduce((sum, item) sum item.coffee.price * item.quantity, 0) }) function submitOrder() { // 组装订单数据调用后端下单接口 } /script购物车用数组对象引用的方式维护比Map更直观适合新手理解。computed属性会缓存计算结果只有依赖的响应式数据变化时才会重新计算所以在性能上也没有问题。Vue的另一个常用特性是插槽slot它允许父组件往子组件里塞内容。做咖啡卡片时你可以把“加入购物车”按钮通过插槽传给CoffeeCard组件这样同一个卡片组件既可以在菜单页显示也可以在推荐位显示只是按钮不同这属于组件复用层面的进阶玩法初学阶段稍微了解一下即可。4.3 axios封装与统一状态处理请求后端接口时建议把axios封装成统一模块别在每个组件里都写一遍axios.get。我在项目里这样封装import axios from axios const http axios.create({ baseURL: /api, timeout: 8000 }) // 响应拦截器统一处理错误 http.interceptors.response.use( response response, error { if (error.response error.response.status 500) { alert(服务器开小差了请稍后再试) } return Promise.reject(error) } ) export default http然后针对咖啡点单的接口单独建一个api/coffee.js文件import http from /utils/http export function getCoffees() { return http.get(/coffees/) } export function createOrder(data) { return http.post(/orders/create/, data) }这样做的好处是接口路径都放在一个地方管后端如果改了路径你只改一个文件就行不用满项目找。对于订单状态码的处理我建议在拦截器里先做统一处理比如401跳登录、500弹出提示这样业务代码里只需要关心成功逻辑。4.4 结算页面与订单提交结算页面拿到购物车数据后要做两件事展示最终订单明细让用户确认然后调用createOrder接口。一个关键细节前端显示的总价和后端计算的总价可能不一致因为前端展示取决于本地购物车数据后端计算则基于数据库当前价格。这没问题正确做法是以后端返回的总价为准前端展示“以结算页金额为准”之类的提示并在下单成功后用后端返回的值去渲染结果页。提交订单时用loading状态防止用户重复点击const submitting ref(false) async function handleSubmit() { if (cart.value.length 0) return submitting.value true try { const payload { user_name: userName.value, items: cart.value.map(item ({ coffee_id: item.coffee.id, quantity: item.quantity })) } const res await createOrder(payload) // 跳转到订单成功页同时清空购物车 cart.value [] } finally { submitting.value false } }把submitting.value true放在前面按钮立刻变成禁用状态同时显示“提交中...”可以有效防止连点下单产生重复订单。这个细节我在真实项目里被坑过用户狂点按钮后端一下子进来十几条相同订单后来就养成了给提交按钮加loading的习惯。5. 常见问题排查与避坑实录5.1 新手最容易翻车的五个问题跨域报错浏览器控制台出现Access-Control-Allow-Origin之类的错误绝大多数是因为没有装django-cors-headers或者装了但忘记在MIDDLEWARE里注册中间件。另一个可能是你直接用axios访问http://127.0.0.1:8000/api而不是走Vite代理。解决办法开发环境优先用代理后端CORS配置作为兜底。数据中文字符乱码Django返回的JSON中文变成\uXXXX弯弯绕绕是正常显示不是乱码这是因为DRF默认按ASCII转义中文。想让它直接显示中文在settings.py里加一条配置就行JSON_ASCII_ENCODE False或者直接忽略反正前端拿到JSON会自动解码。真正的中文乱码通常发生在数据库连接配置上确认MySQL里charset设置成utf8mb4即可。迁移报错执行python manage.py makemigrations提示No changes detected多半是你创建了app却忘记在INSTALLED_APPS里注册。这个问题看起来低级但实战中见得太多了。端口被占用runserver 8000时报Address already in use直接换成别的端口比如python manage.py runserver 8001或者用lsof -i:8000找到进程杀掉。Vue项目npm install卡住不动换npm镜像源执行npm config set registry https://registry.npmmirror.com速度立刻起飞。5.2 性能和安全方面的几条实用建议咖啡点单这种小系统谈高并发有点远了但基本的安全和性能意识还是要有的。数据库层最重要的就是N1查询问题Coffee模块里嵌套了Category外键如果你在Vue里循环调用接口查询每个分类那接口请求次数就会爆炸。解决方法是Django的select_related查咖啡的时候直接把关联的Category一并查出来queryset Coffee.objects.select_related(category).filter(is_activeTrue)安全层面DRF默认的JSON序列化不会把密码等敏感字段暴露出来只要你别在Serializer里把字段列表写全了用户密码如果要做登录功能一定要用Django自带的make_password和check_password去做哈希千万不要存明文。前端方面对接口返回的HTML字符串永远不要用v-html渲染万一数据被注入脚本就麻烦了。5.3 如果导师说“用Flask就行”怎么快速切换Flask实现同样的接口会轻量很多核心逻辑用一个文件就能表达from flask import Flask, jsonify, request from flask_sqlalchemy import SQLAlchemy app Flask(__name__) app.config[SQLALCHEMY_DATABASE_URI] sqlite:///coffee.db db SQLAlchemy(app) class Coffee(db.Model): id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(100)) price db.Column(db.Float) app.route(/api/coffees/) def list_coffees(): coffees Coffee.query.all() return jsonify([{id: c.id, name: c.name, price: c.price} for c in coffees]) app.route(/api/orders/, methods[POST]) def create_order(): data request.get_json() # 订单创建逻辑 return jsonify({order_id: 1, status: ok})从Django迁到Flask核心的业务逻辑库存扣减、事务处理完全一样只是换了一批写法和库名。Flask要做事务用with db.session.begin()包裹要做行锁用with_for_update()。你只要理解了我们上面设计的模型和接口逻辑在Flask里照搬就能跑起来。真要说Django和Flask哪个好没有定论但如果你时间紧任务重Flask可以让你快速演示如果你想系统地构建一个包含后台管理的完整系统Django仍然是最佳选择。最后再分享一点我个人的体会这个咖啡点单项目我第一次完整跑通前后端联调用了大概两周其中有三天卡在跨域代理配置上后来发现就是vite.config.js里target地址少写了一个斜杠。这种项目真正的价值不在于技术有多深而在于你完整经历了一次“设计数据库接口—开发后端API—搭建前端页面—联调排错”的闭环之后再去做任何前后端分离的项目心里都有底。尤其是Django的ORM和DRF这套组合学会了以后做管理后台、信息管理系统的效率会高很多这也是我力推Django的原因。如果你正在做类似的项目不妨按我上面写的步骤一步步来遇到具体问题欢迎对照着排查。
返回列表