ARTICLE DETAIL

资讯详情

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

基于Django+Flask+Vue的美妆免税商品选购系统开发与部署

基于Django+Flask+Vue的美妆免税商品选购系统开发与部署 做美妆类免税商品选购系统听起来就是个标准的前后端分离电商项目但真正落地的时候细节一点都不少。技术栈选的是Python系后端django/flask配合vue做前端这套组合在中小型业务系统里非常常见尤其是需要快速上线、后期好维护的项目几乎可以无脑用。这个系统要解决的痛点很明确免税商品的SKU多、批次有效期敏感、价格随汇率波动、库存和海关额度需要校验普通电商框架要么太重要么太死用django做主体业务、flask做轻量辅助服务、vue做交互层反而能各取所长。这篇文章我会把整个系统从选型、建模、接口实现到部署踩坑完整捋一遍适合正在做类似“python后端vue前端”项目的同学参考尤其是刚接触django/flask、想搞懂前后端怎么配合的新手以及准备把项目部署到windows服务器上的人。我会直接讲项目里真实会遇到的细节包括模型字段怎么定、ORM删除对象要注意什么、vue路由参数怎么传、static文件为什么加载不出来、waitressnginx部署的坑尽量把能提前避开的坑都写出来。1. 项目定位与技术栈选型为什么是django/flaskvue1.1 美妆免税选购的业务特殊性做美妆免税商品选购系统不能直接套普通电商的思路。普通电商关注的是SKU、价格、库存但免税美妆多了一层“监管批次”的属性。同一款雅诗兰黛小棕瓶不同批次的有效期不同采购价格也不同免税渠道还涉及每人每年的免税额度、限购件数这些都要在系统里控制。所以数据模型不能只建简单的商品表、订单表还得有批次库存、额度校验、汇率换算这些模块。我实际建模时把商品和SKU分开商品表存基础信息SKU表存规格、价格、库存批次。每个SKU关联一个或多个批次批次里有有效期和剩余数量下单时优先扣“临期批次”而不是随意扣。这个逻辑虽然初期实现多花了点功夫但后面盘库存、处理临期商品时省了巨大的麻烦。如果一开始图省事只做一张commodity表后面要加批次、加汇率就要频繁改表结构非常痛苦。1.2 django与flask如何分工标题里同时出现django和flask很多人第一反应是“二选一”但实际项目里两者完全可以共存。django自带ORM、Admin后台、迁移工具非常适合做主业务系统——商品管理、订单、用户、额度校验这些需要规范建模和事务处理的功能交给django很稳。flask更轻适合做独立的辅助服务比如汇率定时拉取服务、库存预警推送、或者暴露给移动端的轻量接口。我这边实际的分工是django负责所有核心业务接口包括商品列表、购物车、下单、支付回调、订单查询flask单独跑了一个“汇率与价格同步服务”每天定时去拉取最新的免税价格和汇率计算完写入Redis再通过一个简单的接口输出给django使用。这样做的好处是汇率同步逻辑挂了不影响主服务下单两个进程能独立重启、独立部署。如果硬把定时任务写进django的app里一旦同步逻辑出问题主进程就可能被拖垮。1.3 vue在项目里的角色前端用vue带来最大的改变是页面不再整页刷新。商品的筛选、排序、加入购物车、切换规格这些操作如果都走传统的form提交用户体验会卡顿尤其是在商品图片多、SKU选择器复杂的美妆商城场景下。vue组件化之后商品卡片、规格选择器、购物车列表都被拆成独立组件改一个组件不会影响到其他页面。组件化也有个容易被忽视的好处多端复用。同一套vue代码稍作调整就能打包成Web端和管理后台端甚至配合uni-app可以复用大部分逻辑到小程序。我当时没直接上nuxt做SSR核心原因是这个系统更偏内部业务工具商城对SEO要求不高纯SPA足够。如果你后续要做面向公网的内容推广那再考虑SSR也不迟初期不必过度设计。2. 后端核心模型与MTV模式落地2.1 三张核心数据表的设计一个电商选购系统最核心的数据表就是商品、库存/批次、订单外加用户和额度记录。我的设计是这样的Commodity商品表id、name、brand、category、image、description、status、created_atSku规格库存表id、commodity_id、spec、price、currency、quantity、batch_no、expiry_dateOrder订单表id、user_id、order_no、total_amount、status、created_atOrderItem订单明细表id、order_id、sku_id、quantity、priceUserQuota免税额度表user_id、total_quota、used_quota、year这里有个容易踩的坑金额字段不要用Float要用DecimalField。美妆免税价格可能带小数点Float在Python里会有精度问题比如0.10.2不等于0.3金额计算一旦出现这种偏差对账的时候会非常头疼。django的models.DecimalField(max_digits10, decimal_places2)能保证计算精度虽然ORM层面的性能稍微有点损耗但做金额计算完全值得。2.2 django创建app与模型迁移创建django项目的流程不复杂但新手容易在“该建几个app”这个问题上纠结。我的建议是按业务域拆不要一个app写到底。我当时拆成了goods商品、trade交易、users用户、quota额度四个app每个app只负责自己的模型和视图代码清晰后期维护成本低。执行python manage.py startapp goods之后把模型写进models.py然后两步走python manage.py makemigrations python manage.py migratemakemigrations是生成迁移文件migrate才是真正把表建到数据库里。很多新手只执行migrate不执行makemigrations系统会提示“No changes detected”然后就一脸懵。记住这个流程改模型 → makemigrations → migrate再改再重复。2.3 执行查询与删除对象ORM操作细节django的ORM让开发者不用写原生SQL但很多人对“查询对象”和“删除对象”的理解停留在表面实际用的时候容易出问题。先说查询。Model.objects.get(id1)返回的是单个对象如果查询结果不存在或存在多个会直接抛异常所以get适合用来查唯一记录比如按order_no查订单。而Model.objects.filter(status1)返回的是一个QuerySet即使结果只有一条返回的也是集合要用.first()才能拿到对象。我之前在项目里见过一个bug用filter取结果后直接访问字段报错QuerySet object has no attribute xxx就是这个原因。再说删除。删除对象有两种方式# 方式一删除单个对象 obj Commodity.objects.get(id1) obj.delete() # 方式二批量删除 Commodity.objects.filter(status0).delete()方式一适合删除前要做一些校验或日志记录的场景删除后会返回一个元组(1, {goods.Commodity: 1})第一个数字是删除的记录总数。方式二适合清理脏数据但要注意批量删除不会触发模型里重写的delete()方法如果被删除对象有外键关联且没有设置on_deletemodels.CASCADE会报完整性错误。所以删除前一定要想清楚外键关系尤其是订单删除了订单明细应该怎么办。2.4 用flask做轻量辅助接口flask在这个项目里承担的是辅助服务的角色。我单独建了一个price_sync_service目录里面是一个完整的flask应用结构很简单from flask import Flask, jsonify import redis app Flask(__name__) app.route(/api/price/latest) def latest_prices(): r redis.Redis(hostlocalhost, port6379, db0) data r.get(latest_prices) return jsonify({code: 0, data: data})有同学问“flask如何绑定到网页元素”其实flask后端不直接绑定网页元素它只负责提供接口网页元素是vue去绑定的。比如vue里的div clickaddToCart(skuId)点击后调用axios请求flask或django的API拿到返回数据后再更新页面。这个交互链路是DOM事件 - vue方法 - axios请求 - 后端接口 - 数据库 - 响应 - vue更新DOM把“绑定元素”理解成vue的click、:class这些指令就好。flask里还有一个实用技巧查看从客户端获取的变量数据类型。很多新手从request.args.get(id)拿到值后直接和int比较结果怎么都不对就是因为忘了它返回的是字符串。排查时可以这样from flask import request val request.args.get(id) print(type(val)) # class str拿到字符串之后记得int(val)转换或者用参数转换器app.route(/api/goods/int:goods_id)让flask自动帮你转类型。3. vue前端实现与联调要点3.1 环境准备vue安装及环境配置vue的环境配置说简单也简单说坑也坑。官方推荐用npm安装vue流程是npm install -g vue/cli vue create frontend cd frontend npm install npm run serve但很多人卡在第一步npm安装速度极慢或直接失败。原因多半是默认源在国外换成国内镜像就好npm config set registry https://registry.npmmirror.com还有个问题是node版本。vue3要求node版本在16以上如果本机node版本太老vue create会失败。建议直接用nvm管理node版本需要哪个切哪个避免版本冲突。创建项目之后进入src目录核心文件是main.js、App.vue和router/index.js。main.js负责挂载Vue实例router负责路由App.vue是根组件。第一次跑起来看到默认的HelloWorld页面说明环境没问题。3.2 商品列表页与路由参数传递商品列表页是商城系统的门面我用了vue-router做页面跳转商品详情页的URL设计成/goods/detail?skuId1001这种查询参数形式。从列表页跳详情页有两种写法我比较推荐用编程式导航this.$router.push({ path: /goods/detail, query: { skuId: this.currentSku.id } })在详情页接收参数const skuId this.$route.query.skuId这里有个很典型的坑用query传参刷新页面后参数还在因为参数在URL里但有人图省事用params传参且不写路径刷新后参数就丢了。所以传递商品ID这种需要刷新后依然有效的参数务必用query。另外从列表到详情性能优化上可以用router-link配合v-for但商品数量大时v-for里大量DOM节点会导致渲染卡顿这时候要做分页或虚拟滚动不要一次渲染一千个商品卡片。3.3 axios请求封装与接口对接vue项目里请求后端接口我习惯在src/utils/request.js里做一个axios实例封装。为什么要封装因为统一处理baseURL、超时时间、token注入、错误提示比在每个组件里重复写一遍要干净得多。import axios from axios const service axios.create({ baseURL: process.env.VUE_APP_BASE_URL || http://127.0.0.1:8000/api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Token token } return config }) service.interceptors.response.use( response response.data, error { console.error(接口请求失败:, error) return Promise.reject(error) } ) export default service接口对接时最常见的问题是跨域。django后端默认不允许别的端口访问vue跑在8080django跑在8000两者直接通信会报CORS错误。解决方式有两个一是后端装django-cors-headers配置允许的域名二是前端在vue.config.js里配proxy代理开发环境把请求转发到8000端口。我用的是后者因为生产环境会用nginx统一转发开发环境的proxy代理能模拟这个场景。3.4 产品视频播放的m3u8处理美妆商品详情页经常要放产品使用视频而且为了方便加载视频源经常是m3u8格式的流媒体文件。vue里直接放video标签播放m3u8是不行的需要借助hls.js这个库。安装npm install hls.js在组件里播放import Hls from hls.js export default { mounted() { const video this.$refs.video if (Hls.isSupported()) { const hls new Hls() hls.loadSource(https://cdn.example.com/goods/123.m3u8) hls.attachMedia(video) hls.on(Hls.Events.MANIFEST_PARSED, () video.play()) } } }这里有两个要点一是m3u8的URL需要后端动态生成或从接口获取不要硬编码到前端代码里二是m3u8通常涉及跨域视频所在的服务端需要配置允许跨域访问否则hls.js加载视频会失败。如果视频文件是自己的建议直接用mp4格式配video标签就行别为了“看起来专业”硬上m3u8流媒体协议适合长视频和直播场景短视频用mp4反而简单。3.5 vue样式与devtools调试vue的样式默认写在.vue文件的style scoped中加了scoped后样式只会作用于当前组件不会污染全局。但有一个坑如果用了子组件父组件里的scoped样式无法穿透到子组件内部需要用到deep选择器也就是:deep(.child-class)。我当时给商品详情页的规格选择器写样式折腾了半天最后发现是scoped隔离的问题换成:deep()就生效了。vue-devtools是调试神器可以在浏览器里直观看到组件的data、props、vuex状态。下载时要注意vue2和vue3的devtools已经不兼容了从chrome应用商店下载时要选对版本。装好后如果面板不显示检查是否开启了“允许访问文件网址”并且开发模式要用npm run serve而不是直接打开本地html文件。4. 核心功能全流程实现从商品浏览到下单结算4.1 商品列表与筛选API商品列表接口要支持分类筛选、关键词搜索、排序。django里配合django-filter可以快速实现但如果不引入额外库手写也不难。核心思路是接收request参数动态拼filter条件def goods_list(request): queryset Commodity.objects.filter(status1) category request.GET.get(category) keyword request.GET.get(keyword) sort request.GET.get(sort, default) if category: queryset queryset.filter(categorycategory) if keyword: queryset queryset.filter(name__icontainskeyword) if sort price_asc: queryset queryset.order_by(price) elif sort price_desc: queryset queryset.order_by(-price)要注意模糊查询用name__icontains而不是name__contains前者忽略大小写用户体验更好。分页用django自带的Paginator每页返回10条或20条同时把总条数、当前页、是否有下一页一起返回前端才好做分页组件。4.2 购物车与库存扣减购物车我用的方案是存数据库而不是存在localStorage。虽然localStorage实现简单但换设备后数据不同步而且无法在服务端做库存预校验。购物车表至少要有user_id、sku_id、quantity、selected这几个字段。加入购物车时后端要先查询SKU的库存如果库存小于请求数量直接返回“库存不足”。真正容易出问题的在下单时的库存扣减。高并发场景下如果先查库存再扣减两个用户同时下单就可能超卖。正确做法是先扣库存再创建订单并且扣减时用原子操作from django.db.models import F updated Sku.objects.filter(idsku_id, quantity__gteneed_qty).update(quantityF(quantity) - need_qty) if updated 0: return JsonResponse({code: 1, msg: 库存不足})F(quantity) - need_qty是在数据库层面执行减法避免了先select再update的时间差。filter(idsku_id, quantity__gteneed_qty)这个条件保证只有库存足够时才更新成功返回的行数updated为0就说明库存不够。这个写法对比“先查后改”的写法直接把超卖风险降为零。4.3 订单创建与支付回调订单创建要考虑事务。一个订单包含主表和明细表主表写入了明细表写入失败就会产生脏数据。django里可以用transaction.atomic()包裹from django.db import transaction with transaction.atomic(): order Order.objects.create(...) for item in cart_items: OrderItem.objects.create(orderorder, ...) cart_items.delete()订单状态我设计成几个值pending_payment待支付、paid已支付、shipped已发货、completed已完成、cancelled已取消。支付回调是另一个难点第三方支付接口回调时会通知你支付结果回调里要做三件事验签、更新订单状态、处理幂等。幂等是指同一笔订单的支付回调可能会收到多次处理方式是在更新状态前先检查订单当前状态如果已经是paid直接忽略本次回调。4.4 前后端联调怎么测前后端联调阶段我的经验是先定接口文档再用mock数据跑通前端页面最后接真实后端接口。没有接口文档联调就是灾难谁改什么都不说前端等后端后端等前端。推荐用apifox或swagger这类工具管理接口。django用drf-spectacular可以自动生成swagger文档flask可以用flasgger。接口文档里要写清楚请求地址、请求方法、请求参数、返回示例、错误码。联调时前端照着文档对接后端照着文档自测能省至少一半的沟通时间。5. 部署实战与问题排查5.1 windows下waitressnginx部署项目最后要部署到windows服务器上这个场景我踩过的坑不少。django自带的开发服务器runserver只能用于开发生产环境绝对不能直接用性能太差且不安全。在windows上我推荐用waitress它是纯Python的WSGI服务器安装简单性能也够用。pip install waitress waitress-serve --listen127.0.0.1:8000 myproject.wsgi:application注意这里监听的是127.0.0.1:8000也就是说waitress只在本机提供Web服务外部不能直接访问。外部请求统一由nginx转发nginx监听80端口匹配到/api/前缀就转发给waitress匹配到静态文件直接返回文件。nginx的配置差不多是这样server { listen 80; server_name your_domain.com; location /static/ { alias C:/path/to/project/static/; } location /media/ { alias C:/path/to/project/media/; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { root C:/path/to/vue_dist; index index.html; try_files $uri $uri/ /index.html; } }这里有个关键点vue项目要执行npm run build把打包出来的dist目录交给nginx托管。vue-router如果用的是history模式try_files $uri $uri/ /index.html这行必须写否则刷新路由时nginx会返回404。5.2 静态文件与附件路径的坑在django里加载图片很多人写过这样的代码img src{% static images/logo.png %} /然后图片就是不显示F12看到404。这种情况多半是静态文件目录配错了。django项目的settings里要确保STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static]STATIC_URL是URL访问前缀STATICFILES_DIRS是静态文件的实际存放路径。两个都配置正确{% static %}标签才能正常工作。开发环境下django会自动找STATICFILES_DIRS下的文件但生产环境下要执行python manage.py collectstatic把静态文件统一收集到一个目录再由nginx托管。附件路径的问题是另一类图片上传成功数据库里有路径但前端访问不到。原因往往是settings里MEDIA_ROOT和MEDIA_URL配错或者django没把media目录交给nginx。而且windows环境下路径分隔符是反斜杠URL里要用正斜杠处理时要统一替换。flask项目部署后附件路径错误和django类似基本都是因为app.config里的UPLOAD_FOLDER写死成了绝对路径换服务器后忘记改。我建议所有上传路径都用相对路径拼接部署时通过环境变量指定根目录不要硬编码。5.3 CORS跨域与csrf部署阶段最容易遇到的两个问题就是跨域和csrf。开发环境配了vue的proxy代理跨域问题不会暴露但一部署前端在http://your_domain.com接口也在同一个域名下的/api/路径理论上不走跨域。然而如果你非要把接口域名分离成api.your_domain.com跨域就来了nginx里要配add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods POST, GET, OPTIONS; add_header Access-Control-Allow-Headers Authorization, Content-Type;另外django默认开启了csrf防护POST请求不带csrf token会被拒绝返回403。前后端分离项目中最简单的方式是在django的settings里对API视图使用csrf_exempt装饰器或者设置MIDDLEWARE里注释掉CsrfViewMiddleware再用token做认证。更严谨的做法是前端从cookie里读csrf token在axios请求头里带X-CSRFToken。后一种更安全但配置麻烦一些。自己项目里可以根据安全等级来选如果是内部工具系统csrf_exempt也够用。5.4 常见问题速查表问题现象可能原因解决方式django模型改了但表没变化忘记执行makemigrations和migrate按顺序执行两个命令vue页面空白控制台报错路由history模式刷新404或JS报错nginx配置try_files检查console错误图片403或404静态文件路径配置错误或media未托管检查STATICFILES_DIRSnginx配置location /media/跨域报错前后端不同端口/域名开发环境用proxy代理生产用nginx统一域名或配CORS头CSRF验证失败django默认csrf防护拦截POST对接口使用csrf_exempt或配置X-CSRFToken下单提示库存不足但库存明明有数据库事务隔离级别或并发问题使用filterupdate原子操作扣库存上传文件后访问404附件存储路径错误检查MEDIA_ROOT统一路径分隔符waitress启动后外网访问不了waitress监听127.0.0.1改用0.0.0.0或由nginx转发这些坑都是实际项目里踩过的。有些问题可能你一周后才遇到提前收藏这篇到时候直接照着排查能省下不少熬夜时间。另外说个我个人的习惯每次部署前都会把前端build产物、后端代码、nginx配置、数据库备份四样东西分别打包标记版本号出问题能快速回滚。这个习惯救过我很多次尤其是在windows服务器上改错了配置有时候连备份都没有只能重装环境那才是真正的灾难。最后再分享一个小技巧django的Admin后台不要浪费商品录入、订单查询这种管理操作用Admin后台几分钟就能搭出来。我当时给运营同学配了只读权限他们自己查订单、改商品状态完全不用我写管理页面。这一点看起来不起眼但对一个系统的实际使用体验来说帮助真的非常大。
返回列表