资讯详情

资讯详情

从需求到联调:Python Django + Vue3构建茶叶电商系统实战解析

做茶叶生意的人这两年都在琢磨一件事怎么把线下的客户挪到线上。前阵子我帮一位做茶叶零售的朋友搭了一套在线茶叶销售系统——后端用Python的Django前端用Vue 3整个开发调试都在Pycharm里完成。系统做出来后商品的分类筛选、购物车、下单、后台管理都能跑通朋友还挺满意。我想了想这个项目虽然不大但从技术选型到前后端联调几乎把PythonVue做业务系统常见的问题都过了一遍值得把过程完整记录下来。如果你正好也在用Python做电商类项目在Django和Flask之间犹豫或者正准备学Vue和Django怎么配合这篇文章应该能帮上忙。1. 项目整体思路与核心技术选型1.1 需求定位卖茶的电商系统核心模块有哪些首先把话说透这个系统不是什么大型平台就是一个中小茶叶品牌自用的商城。朋友的需求很明确前台要能看商品、筛商品、加购物车、下订单后台要能录茶叶、调库存、看订单。不需要支付通道、营销活动、会员积分先跑通业务再说。我按这个需求拆解系统就分成了前端展示层和后端服务层。前端负责所有与用户交互的页面和状态管理后端提供商品、购物车、订单的接口外加Django Admin做内容维护。为什么要用前后端分离而不是Django自带的模板渲染原因很简单我朋友后面可能会做小程序还会在公众号里放网页版。前后端分离之后接口是通用的换前端只是换一套壳子。茶叶商品图片多、交互状态多筛选、分页、购物车数量用Vue做响应式页面比服务端模板舒服得多。1.2 Django、Flask、FastAPI怎么选这个选择题几乎每个做Python后端的人都会遇到。我把三个框架放在一起对比过给项目做决策时列了张表对比维度DjangoFlaskFastAPI本身定位重量级全栈框架轻量微框架异步Web框架自带能力ORM、Admin后台、认证、表单、迁移只有基础路由/请求处理只提供API基础依赖额外组件适合项目业务系统、后台管理、电商小接口、微服务、快速原型高并发API、实时服务、机器学习推理ORM/数据库内置ORM迁移工具成熟选SQLAlchemy等选SQLAlchemy/SQLModel学习曲线相对陡但组件齐全平缓平缓异步有门槛社区成熟度高高增长快最终我选了Django理由很实在。第一我需要一个能用的后台。Django Admin做商品维护是现成的注册一下模型就能增删改查。如果用Flask后台得自己写一套页面那又是好几天的事。第二ORM和迁移省心。商品、订单这些模型定义好之后几行命令就把表建好了后面字段有改动makemigrations和migrate就能平滑升级。第三用户认证模块是现成的。订单必须绑定用户Django自带的User模型加权限系统可以直接用Flask要自己设计用户表和会话逻辑。我也不是完全排斥Flask。事实上在这个系统上线后我打算用Flask单独做一个报告服务统计每天订单量、爆款商品那个服务只有三四个只读接口没必要把Django那套重量级组件全部引进去。至于FastAPI它的性能优势和自动生成接口文档确实香但适合从零设计API服务。这个项目里交互模式都是标准的请求-响应且全工程用同步ORM操作数据库硬上异步化反而是给自己找麻烦。简单说就是用什么框架要看项目形态而不是看谁的star多。1.3 前端技术与开发工具的选择前端选择了Vue 3 Vite Vue Router Axios。Vue 3的script setup语法写起来很爽Composition API把商品筛选、购物车状态管理这类逻辑封装得清楚。Vite做开发服务器启动快热更新也快改一行代码浏览器马上刷新体验比老一代打包器强太多。可能有人问为什么不用React不是React不行是这个团队和我自己的技术栈里Vue更顺手。而且茶叶销售这类页面交互不算特别复杂Vue的单文件组件在布局上很直观模板里可以直接写v-for、v-if。工具方面整个开发都在Pycharm里进行。Pycharm专业版对Django的支持确实好新建项目时可以直接选Django模板能直接用图形界面运行manage.py命令写模板文件有补全调试Django接口时可以断点调试Python代码。Vue部分虽然Pycharm不是最强但2021版本之后对Vue 3的语法支持也够日常使用。前端有时候需要看组件渲染效果或者检查网络请求我会临时打开浏览器的开发者工具再切回Pycharm改代码。安装配置的细节放到后面实操章节讲这里先把选型理由讲透工具不一定要最新但一定要能减少你切换上下文的时间。2. 数据库设计与后端核心模块实现2.1 从需求到表结构不要过度设计拿到需求之后我第一件事是画表结构。这个阶段最容易犯的错误就是想着把所有功能提前做结果表建了一大堆用不上。我给自己定的标准是只建当前业务必须用到的表宁可后续加字段也不要一开始就建冗余。最终的表结构是这样的Category分类表id、name品类名比如绿茶、红茶、白茶、parent上级分类支持二级、sort_order排序Product商品表id、category外键、name、origin产地比如西湖、安溪、grade等级、year年份、price单价、stock库存、image图片、description详情、is_active是否上架、created_atCart购物车表id、user外键、product外键、quantity数量、selected勾选状态Order订单表id、order_no、user、total_amount、status待付款/已付款/已发货/已完成/已取消、receiver_name、receiver_phone、receiver_address、created_atOrderItem订单明细表id、order外键、product外键、product_name、price、quantity商品放一个表而不是分成多个表是因为茶叶SKU不复杂不需要像服装那样搞多规格属性所以用字段直接描述就好。分类做层级是因为朋友店里可能有绿茶/红茶/白茶大分类绿茶下面又分龙井、毛尖二级就够不用无限级。这里有一个细节很多人会忽略OrderItem里为什么要冗余product_name和price因为商品价格和名称是会变的订单一旦生成它记录的是当时成交的快照。如果后面商品改价了、改名了历史订单里如果只有product外键那查出来的就是错误的历史信息。电商系统里这类历史快照字段非常常见宁可冗余也不能让历史记录失真。2.2 Django项目创建、ORM查询与删除对象表设计好之后开始建Django项目。命令不复杂但每一步我都建议在Pycharm的终端里执行因为Pycharm会自动识别Django运行配置django-admin startproject tea_shop cd tea_shop python manage.py startapp goods python manage.py startapp ordersstartapp之后把两个app注册到settings.py的INSTALLED_APPS里。用Django的时候要养成一个好习惯所有模型字段要仔细设default不然makemigrations会一直让你补默认值。商品模型的写法字段和上一节表结构一一对应class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name品类) name models.CharField(max_length100, verbose_name商品名称) origin models.CharField(max_length50, verbose_name产地) grade models.CharField(max_length20, verbose_name等级) year models.IntegerField(verbose_name年份) price models.DecimalField(max_digits8, decimal_places2, verbose_name单价) stock models.IntegerField(verbose_name库存) image models.ImageField(upload_toproducts/, blankTrue, verbose_name商品图片) description models.TextField(blankTrue, verbose_name详情) is_active models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-created_at] def __str__(self): return self.name外键的on_delete参数这里我特别用了PROTECT而不是CASCADE。CASCADE的意思是一旦分类被删分类下的所有商品会被连带删除这个太危险了。PROTECT的意思是如果分类下还有商品分类就删不掉必须先处理掉商品。对于一个后台管理场景这个设计合理得多。数据库操作方面Django的ORM写起来真的很省事。比如前端传一个品类id后端要查出该品类所有上架商品products Product.objects.filter( category_idrequested_category_id, is_activeTrue, stock__gt0 ).select_related(category)要删除某个对象可以直接product Product.objects.get(idpk) product.delete()也可以按条件批量删除这在清空购物车时非常常用Cart.objects.filter(useruser, selectedTrue).delete()注意批量delete不会触发模型实例内的delete方法如果需要做一些级联操作要谨慎。这个坑我在早期项目里踩过后面会专门讲。2.3 订单生成的核心逻辑与并发安全订单流程看起来简单把购物车的商品汇总、算总价、生成订单、扣库存。但这里面有几个坑。第一个坑是价格计算必须用服务器端的价格不能用前端传过来的价格。用户完全可以改浏览器的请求把单价改成一毛钱。所以下单接口里所有金额都要重新根据商品表里的price计算。前端传过来的只有哪个商品、多少件。第二个坑是库存扣减与订单生成的原子性。如果先扣库存再写订单中间突然断电、报错库存就莫名其妙少了。如果先写订单再扣库存可能出现订单建了但库存不够。正确做法是把这些操作放在同一个数据库事务里任何一个步骤失败就全部回滚。Django里用transaction.atomic()from django.db import transaction from decimal import Decimal transaction.atomic def create_order(request): cart_items Cart.objects.filter(userrequest.user, selectedTrue).select_related(product) if not cart_items.exists(): return JsonResponse({error: 购物车为空}, status400) total Decimal(0.00) order_items_data [] for item in cart_items: product item.product if product.stock item.quantity: return JsonResponse({error: f{product.name} 库存不足}, status400) total product.price * item.quantity order_items_data.append({ product: product, product_name: product.name, price: product.price, quantity: item.quantity, }) product.stock - item.quantity product.save() order Order.objects.create( order_nouuid.uuid4().hex[:16].upper(), userrequest.user, total_amounttotal, receiver_namerequest.data.get(receiver_name), receiver_phonerequest.data.get(receiver_phone), receiver_addressrequest.data.get(receiver_address), ) OrderItem.objects.bulk_create( OrderItem(orderorder, **item_data) for item_data in order_items_data ) cart_items.delete() return JsonResponse({order_no: order.order_no, total_amount: str(total)}, status201)第三个坑是并发。两个用户同时买最后一件茶都读到stock1然后都减库存最后库存变成-1。解决思路有两个一个是Django的F()表达式把读取-修改-写回变成条件更新另一个是配合select_for_update()做行锁。我在项目里用的方案是先判断stock__gtequantity再用F表达式扣减再检查受影响行数如果受影响行数是0就说明库存被抢走了抛异常回滚from django.db.models import F updated Product.objects.filter(idproduct.id, stock__gteitem.quantity).update( stockF(stock) - item.quantity ) if updated 0: raise RuntimeError(库存不足下单失败)这几个方案在实际开发里足够用了。3. 前端页面的落地与前后端联调3.1 Vue环境和项目初始化前端这头的第一步是搭Vue工程。我用的是Vite创建Vue 3项目。先把Node环境准备好命令很简单node -v npm -v npm create vitelatest tea_web -- --template vue cd tea_web npm install接着装需要用到的依赖npm install axios vue-router4这一步如果网络慢可以用国内镜像npm config set registry https://registry.npmmirror.com装完依赖后开发服务器跑起来npm run devVite默认端口5173浏览器打开就能看到Vue默认页面。Pycharm里如果直接打开tea_web目录可以通过设置里的运行/调试配置添加一个npm配置运行dev脚本。这样在Pycharm里直接点绿色按钮就能启动前端不用每次切到终端敲命令。3.2 路由和页面结构在src/router/index.js里配置路由。项目页面不多主要就四个首页商品列表、商品详情、购物车、订单结算。import { createRouter, createWebHistory } from vue-router import ProductList from ../views/ProductList.vue import ProductDetail from ../views/ProductDetail.vue import Cart from ../views/Cart.vue import Checkout from ../views/Checkout.vue const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: list, component: ProductList }, { path: /product/:id, name: detail, component: ProductDetail, props: true }, { path: /cart, name: cart, component: Cart }, { path: /checkout, name: checkout, component: Checkout }, ], }) export default router路由的好处是把页面和URL对应起来刷新后还能回到正确的页面状态。这个在电商场景特别重要用户把商品详情页分享给别人对方点开链接直接进到对应商品。商品列表页是最核心的组件。用Vue 3的组合式API来写逻辑很清晰script setup import { ref, onMounted } from vue import api from ../api/request const products ref([]) const keyword ref() const categoryId ref() async function loadProducts() { const params {} if (keyword.value) params.keyword keyword.value if (categoryId.value) params.category categoryId.value products.value await api.get(/products/, { params }) } onMounted(loadProducts) /script模板部分用v-for渲染商品卡片。这里我遇到一个比较有意思的点卡片布局在购物车页面也想复用这时就用Vue的插槽slot来处理。把商品卡片抽成一个BaseCard组件默认内容显示商品标题、产地、价格插槽里可以自由塞按钮。template div classgoods-card img :srcproduct.image :altproduct.name div classinfo slot namebody :productproduct h3{{ product.name }}/h3 p{{ product.origin }} · {{ product.year }}/p span classprice¥{{ product.price }}/span /slot /div slot nameaction :productproduct/slot /div /template父组件里这样用GoodsCard v-foritem in products :keyitem.id :productitem template #action{ product } button clickaddToCart(product)加入购物车/button /template /GoodsCard插槽让卡片结构可以复用action区域不同页面放不同按钮。这个设计比每个页面复制一遍卡片代码强太多。3.3 axios封装与接口对接前端所有接口请求我统一封装在一个模块里不直接在每个组件里写axios.get。原因很简单一旦要加token、处理错误、统一超时时间改一个文件就行而不是改十几个文件。import axios from axios import { ElMessage } from element-plus const api axios.create({ baseURL: /api, timeout: 8000, }) api.interceptors.response.use( (res) res.data, (error) { const msg error.response?.data?.error || 请求失败 ElMessage.error(msg) return Promise.reject(error) } ) export default api开发环境的跨域问题我推荐用Vite的代理在vite.config.js里配置export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, }, }, }, })这样浏览器里请求/api/products/实际会被代理转发到http://127.0.0.1:8000/api/products/前端代码里看不到完整的后端地址也不会有跨域报错。但Django那端我仍然装了django-cors-headers原因是将来前端可能会部署到另外的域名比如公众号网页版服务端必须允许跨域请求现在只靠开发代理是不够的。两边都配好等于埋了一条后路。购物车总价计算可以放在一个computed里勾选变化后总价联动刷新这在Vue里是天然优势const totalPrice computed(() { return cartItems.value .filter(item item.selected) .reduce((sum, item) sum parseFloat(item.product.price) * item.quantity, 0) })注意订单结算时前端展示的价格只是参考后端下单接口拿到商品id和数量后会重新算价。前端传price过去是不被信任的这个安全问题在2.3节已经说过。3.4 图片上传与Media配置茶叶销售系统的商品图非常多Django处理图片上传有一套标准做法。settings.py里配置MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media项目的urls.py里在开发环境直接把media目录映射出来from django.conf import settings from django.conf.urls.static import static urlpatterns [...] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这样商品图片上传后可以通过/media/products/xxx.jpg访问。部署到云服务器时media目录要用Nginx单独映射而不是让Django频繁处理静态文件这个属于部署话题。4. 高频问题与排查技巧实录4.1 前后端联调时的跨域和Cookie问题联调第一天就遇到跨域报错浏览器控制台一片红No Access-Control-Allow-Origin header。这个问题的本质是浏览器为了安全默认不允许前端页面调用不同源协议、域名、端口任意一个不同就算跨域的接口。Vite开发服务器端口是5173Django端口是8000端口都不一样跨域是必然的。排查步骤很简单先确认浏览器请求确实到了Django看Django终端有没有访问日志再确认corsheaders中间件有没有正确配置。我当时的配置INSTALLED_APPS [ corsheaders, # ... ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # ... ]中间件的位置有讲究CorsMiddleware要尽量放在中间件列表的前面这样后面所有中间件拿到的已经是处理过跨域的请求。settings.py里加上允许的来源CORS_ALLOWED_ORIGINS [ http://localhost:5173, http://127.0.0.1:5173, ]开发阶段图省事也可以直接CORS_ALLOW_ALL_ORIGINS True但上线前一定要收窄成具体域名。如果后续要带Cookie认证还要加CORS_ALLOW_CREDENTIALS True前端axios实例要设置withCredentials: true。两个都要配缺一个浏览器就会拦截。4.2 数据库迁移和字段改动翻车开发中期加了一个商品规格字段执行python manage.py makemigrations一切正常但migrate报错提示表goods_product已经有column了。原因是之前手动在数据库里加过同名字段Django的迁移记录里没有这个migration两边状态对不上。解决方案是不要直接在数据库工具里改表结构永远通过Django migration来改。如果已经手动改了处理方法是python manage.py makemigrations goods python manage.py migrate goods --fake--fake会告诉Django这个迁移我已经假装执行过了只更新迁移记录不实际执行SQL。但这个方法只能用在数据库已经和迁移结果一致的情况如果你不确定一致性千万别乱用否则后面所有migrate都会乱套。更稳妥的通用流程是改models.py → makemigrations → 检查迁移文件里的内容是否正确 → migrate。每次迁移前把数据备份一下毕竟客户数据丢不起。4.3 Vue项目依赖和TS配置报错前端项目经常会遇到failed to load tsconfig vue/tsconfig/tsconfig.web.json这个报错。排查下来通常是两个原因一是npm install没跑完或者安装过程中断依赖不完整二是package.json里devDependencies声明的vue/tsconfig和实际安装的版本不一致。解决办法一般就三步rm -rf node_modules package-lock.json npm install如果还报错检查package.json里有没有vue/tsconfig没有就装一下npm install -D vue/tsconfig另外vue-tsc的版本和Vue版本不匹配也会导致类型检查报错。如果是纯JavaScript项目建议直接把package.json里build脚本中的vue-tsc去掉省得每次build都卡在类型检查上。这个技巧适合不想上TypeScript、只想快速看到效果的情况。4.4 Pycharm导入Django项目的几个常见问题很多人拿到别人给的Django项目源码用Pycharm打开后运行报错ModuleNotFoundError。大部分时候是Python解释器没有选对。Pycharm的操作路径是File → Settings → Project → Python Interpreter在这里选择项目对应的解释器最好是虚拟环境venv里的那个。虚拟环境创建也有坑。Pycharm在新建项目时如果要创建venv默认Python版本可能和你机器上装的Python 3.8不一样。项目如果要求Python 3.8就要在新建解释器界面手动指定Python路径。导入别人项目还有一个问题对方用的依赖版本和你本地的不同。送项目文件的时候一定要把requirements.txt一起发过去接收方执行pip install -r requirements.txt这样能解决九成的代码明明一样但跑不起来问题。剩下的一成是Python版本、操作系统差异只能在项目文档里写清楚。还有一点Django项目的启动配置不要直接用Python方式运行manage.py。Pycharm里推荐用Django Server类型的运行配置它能正确设置DJANGO_SETTINGS_MODULE环境变量调试时也能直接进入模板渲染、ORM查询的调试链路。4.5 中文编码和乱码的一揽子处理茶叶商品有大量中文名称数据库用MySQL时最容易出乱码。我总结的三步方案第一步建库的时候指定utf8mb4字符集CREATE DATABASE tea_shop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;第二步Django的数据库配置里也要指定DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: tea_shop, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: {charset: utf8mb4}, } }第三步API返回内容乱码一般不是数据库问题而是HTTP响应头里的charset不对。DRF默认响应的Content-Type是application/json一般不会乱码如果自己用HttpResponse返回字符串要显式加上content_typeapplication/json; charsetutf-8。另外提醒一句Windows环境下Pycharm的默认编码可能是GBK。如果代码文件里写中文出现乱码检查Settings里的File Encodings把Global Encoding、Project Encoding、Default encoding for properties files都设置成UTF-8。这个不改代码里所有中文注释都可能变成乱码非常影响开发体验。5. 项目做完后最想说的几句经验项目跑通那一刻我把所有页面点了一遍从商品列表筛选到加入购物车再到下单流程顺畅心情还是有点兴奋的。静下来复盘有几点经验特别想分享。第一选型时不要被热门框架牵着走。Django和Flask都是好工具我最后选Django是因为我需要现成的后台管理、认证、ORM迁移而不是因为它比Flask更高级。两个框架不需要分出高下只需要按项目形态选择。第二涉及金额和库存的逻辑一切以服务端数据为准。前端传过来的任何价格、数量都要怀疑服务端重新计算并且把下单和扣库存放在同一事务里。这个意识可以说是我这几年做Web项目最大的收获之一。第三联调节奏比什么都重要。不要花两周把前端写完再对接后端。正确做法是后端先把一个最简单的列表接口跑通前端同时把axios封装和路由搭好后面接口是流水线式添加联调就是自然发生的事情。第四这套系统的后端接口将来可以直接复用给小程序端或公众号H5端前端换套壳子就行。当初坚持前后端分离现在看是省下了一大笔重构成本。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →