尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flask+Vue构建乡村生态旅游平台:从设计到部署的完整实践

Flask+Vue构建乡村生态旅游平台:从设计到部署的完整实践 1. 项目概述乡村生态旅游平台到底在做什么说实话光看标题“python基于flask的乡村生态旅游服务商平台-vue pycharm django”第一反应是这技术栈写得有点杂——flask和django同时出现前端又挂了vueIDE还单独占了个位置。但我做文旅类Web项目三四年了这类标题恰恰是很多入门全栈开发者最真实的项目状态技术调研没做完就动工写着写着发现需求比预期复杂最后flask做API、django模板那套没派上用场前端老老实实用vue扛下来了。这类乡村生态旅游服务商平台本质上是给“乡村本地服务商”和“城市游客”搭一座桥。服务商是农家乐老板、民宿房东、采摘园主、本地向导平台要让他们能上架旅游产品、管理订单、查看收入游客则要能浏览目的地、查攻略、下单预订、写评价。平台方还需要一个管理后台做服务商审核、内容审核、数据统计。为什么说这个项目非常适合当作Web全栈练手项目因为它麻雀虽小五脏俱全需要用户体系、商品体系、订单流程、文件上传、权限控制、前后端联调还要处理图片视频这类静态资源。这些恰恰是真实业务系统里最常见的模块组合学会了这套东西后面做电商、做教务、做物业系统核心套路基本是相通的。适合看这篇内容的人我大致分三类一是准备做毕业设计或课程设计的学生需要一个结构完整、能讲清楚“为什么这么做”的项目二是刚学完Python基础和Flask教程、想上一个完整全栈项目的转行者可以照着一步步搭起来三是给小型景区或村镇做信息化方案的从业者可以参考这个架构评估技术可行性。这篇文章不打算只贴代码而是把关键决策的逻辑讲清楚——为什么用Flask不用Django、为什么前后端分离、订单状态机怎么设计、文件上传放本地还是OSS、生产环境怎么用Waitress加Nginx顶住访问。按我自己的习惯先把整个项目拆解明白再一头扎进代码里效率是最高的。2. 整体设计与技术选型为什么是Flask加Vue这套组合2.1 业务需求拆解与角色模型开始写代码之前先把需求彻底捋清楚。我见过太多项目写着写着返工原因几乎都是角色边界没定义好。乡村生态旅游服务商平台至少有三类角色游客C端用户、服务商供给方、平台管理员运营方。有些系统还会拆出“财务审核员”“内容编辑”但初期没必要把角色切得太碎三个角色加一套RBAC权限控制就够了。游客端的核心诉求找得到、看得懂、订得上。找得到是靠目的地分类、搜索、地图定位后续可接入GIS看得懂是靠图文详情、视频宣传、真实评价订得上是从浏览到下单再到支付核销的流程足够顺畅。服务商端的核心诉求能上架、能接单、能算账。上架旅游产品民宿、路线、采摘、特产接收游客订单并标记核销状态查看已结算和待结算金额。平台管理端的核心诉求控准入、控内容、看数据。审核服务商入驻资质巡查产品内容是否有违规信息查看平台整体订单量和交易额。这三个角色对应三套前端界面但共用同一个后端API服务。这也是我推荐前后端分离的根本原因Flask后端只负责输出JSON数据Vue前端根据角色渲染不同页面。如果按照传统的服务端渲染方式三个角色各做一套模板代码重复率会非常高。2.2 Flask与Django的选择标题里的两个框架到底用哪个标题里同时写了flask和django这其实是个很有意思的信号。很多初学者在选框架时纠结很久最后项目描述里把两个都写上防止被问“你为什么不学那个”。基于这个项目的实际需求我的建议很明确后端用FlaskDjango的MTV模式可以作为理解参考但不必在项目里混用。Flask的逻辑非常直接一个路由对应一个Python函数函数返回JSON或页面。它的生命周期对初学者极其友好——请求进来匹配路由执行视图函数返回结果。调试时可以从头到尾看清楚每个环节发生了什么。而Django自带Admin后台适合内容管理很重的系统自带ORM、迁移、认证体系适合大而全的脚手架式开发。问题在于Django框架本身理念较重新手容易陷入“配置对了一套东西跑起来但不知道里面怎么工作”的状态。对乡村文旅平台这种中轻型业务用Flask可以充分掌控每个模块后续要加功能也不会被框架风格绑架。ORM方面Flask官方没有绑死ORM选型我习惯配SQLAlchemy。它的声明方式比Django ORM更灵活字段和模型的关系很直观。下面这段是旅游产品的模型定义可以看到一个产品关联了服务商、分类、图片集这就是业务的核心骨架from flask_sqlalchemy import SQLAlchemy from datetime import datetime db SQLAlchemy() class Product(db.Model): __tablename__ t_product id db.Column(db.Integer, primary_keyTrue) title db.Column(db.String(120), nullableFalse) category db.Column(db.String(20), indexTrue) # hotel/route/picking/gifts merchant_id db.Column(db.Integer, db.ForeignKey(t_merchant.id)) price db.Column(db.Numeric(10, 2), nullableFalse) stock db.Column(db.Integer, default0) cover_url db.Column(db.String(255)) detail db.Column(db.Text) status db.Column(db.Integer, default1) # 0下架 1上架 2待审核 created_at db.Column(db.DateTime, defaultdatetime.now) merchant db.relationship(Merchant, backrefproducts)2.3 Vue在项目中的角色定位Vue在这套体系里负责的是“外壳”——页面交互、组件复用、数据绑定。为什么不用JQuery加模板引擎因为平台有三个角色页面上有大量重复组件产品卡片、订单状态标签、图片轮播、评论列表。这套东西用Vue组件化以后一套组件三个端复用维护成本直线下降。版本我建议直接用Vue 3配Vite不要再碰Vue 2加Webpack那套老配置。Vue 3的组合式APIComposition API在写业务逻辑时比选项式API清爽太多——比如通用的登录状态检查抽成一个函数各个组件里直接调用即可。Vite的启动速度也比Webpack时代的配置型开发工具快好几倍改完代码保存后浏览器几乎是秒级刷新这套开发体验在调试前后端联调时特别重要。下面这个例子是游客端产品搜索组件输入关键词后调用后端接口返回列表。你会发现Vue 3的代码非常直白template结构简单script区就是一个搜索函数template div classsearch-bar input v-modelkeyword placeholder搜索民宿 / 路线 / 采摘园 keyup.entersearch / button clicksearch搜索/button /div /template script setup import { ref } from vue import { searchProducts } from /api/product const keyword ref() const emit defineEmits([search]) async function search() { const { data } await searchProducts({ keyword: keyword.value }) emit(search, data) } /script3. 后端核心模块实现Flask API服务与数据库设计3.1 项目目录结构与配置管理一个规范的Flask项目目录结构直接决定了后面扩展功能的成本。我不推荐新手把所有的路由全部写在app.py里然后一个文件几千行那撑不到第二个版本就要重构。标准化之后的结构应该是app目录放业务代码instance目录放环境配置migrations目录放数据库迁移脚本。project-root/ ├── app/ │ ├── __init__.py # 创建Flask应用注册蓝图 │ ├── extensions.py # db, jwt, cors 等扩展实例 │ ├── models/ # SQLAlchemy模型 │ │ ├── user.py │ │ ├── merchant.py │ │ ├── product.py │ │ └── order.py │ ├── api/ # 蓝图路由 │ │ ├── auth.py │ │ ├── product.py │ │ ├── order.py │ │ └── merchant.py │ └── utils/ │ ├── response.py # 统一返回值封装 │ └── upload.py # 文件上传处理 ├── instance/ │ └── config.py # 本地配置文件不提交仓库 ├── migrations/ # Flask-Migrate迁移脚本 ├── requirements.txt └── run.py # 入口文件配置文件里最需要注意的是区分“开发环境”和“生产环境”。开发环境下SQLite就够用生产环境必须切MySQL或PostgreSQL。SQLAlchemy的数据库连接串一换模型代码不需要改动这就是ORM带来的最大便利import os class Config: SECRET_KEY os.environ.get(SECRET_KEY, dev-secret-key) SQLALCHEMY_DATABASE_URI os.environ.get( DATABASE_URL, sqlite:///tourism.db ) SQLALCHEMY_TRACK_MODIFICATIONS False JWT_EXPIRATION_HOURS 72 MAX_CONTENT_LENGTH 16 * 1024 * 1024 # 上传文件最大16MB class ProductionConfig(Config): SQLALCHEMY_DATABASE_URI os.environ.get( DATABASE_URL, mysqlpymysql://user:passlocalhost/tourism_db )3.2 数据库表结构从用户到订单的完整链路数据库设计是整个项目最不能省的部分。我拆成五个核心表用户表t_user、服务商表t_merchant、产品表t_product、订单表t_order、评价表t_review。外加一些辅助表产品图片表、订单日志表、提现记录表。订单表设计是这个项目的关键。乡村文旅的订单形态比较特殊民宿按天计费、路线按人头计费、采摘按份计费、特产按件计费不同类型的计费方式不同但最终都要收敛到“一个订单对应一个总金额”的统一模型。我的做法是订单表存订单级信息明细表存每个产品的单价、数量、入住日期或出行日期。游客下单时传入产品ID、规格参数和数量后端算出总价并生成待支付订单。class Order(db.Model): __tablename__ t_order id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse) user_id db.Column(db.Integer, db.ForeignKey(t_user.id)) merchant_id db.Column(db.Integer, db.ForeignKey(t_merchant.id)) total_amount db.Column(db.Numeric(10, 2), nullableFalse) status db.Column(db.Integer, default0) # 0待支付 1已支付 2已核销 3已取消 4退款中 contact_name db.Column(db.String(30)) contact_phone db.Column(db.String(20)) remark db.Column(db.String(255)) created_at db.Column(db.DateTime, defaultdatetime.now) paid_at db.Column(db.DateTime) used_at db.Column(db.DateTime) class OrderItem(db.Model): __tablename__ t_order_item id db.Column(db.Integer, primary_keyTrue) order_id db.Column(db.Integer, db.ForeignKey(t_order.id)) product_id db.Column(db.Integer, db.ForeignKey(t_product.id)) product_title db.Column(db.String(120)) unit_price db.Column(db.Numeric(10, 2)) quantity db.Column(db.Integer, default1) travel_date db.Column(db.Date) # 出行日或入住日订单状态机这里值得多说几句。0到4这五个状态是单向推进为主退款操作会走单独的分支。设计时注意已支付订单在服务商未核销前游客发起退款要自动给服务商发送站内信提醒已核销订单原则上不再支持线上退款只能线下协商。这套逻辑虽然简单但在项目答辩或简历上写出来会显得你对业务闭环有完整思考。3.3 登录鉴权与RBAC权限控制Flask生态里JWTJSON Web Token方案相当成熟我用的是flask-jwt-extended这个扩展。游客注册、服务商注册、管理员登录都走同一个签发入口只是角色字段不同。Token里会放user_id和role两个核心信息前端拿到Token后存到localStorage每次请求在Authorization头里带上去from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity auth_bp.route(/login, methods[POST]) def login(): data request.get_json() user User.query.filter_by(phonedata.get(phone)).first() if not user or not check_password_hash(user.password_hash, data.get(password)): return jsonify(code400, message手机号或密码错误), 400 token create_access_token( identitystr(user.id), additional_claims{role: user.role} ) return jsonify(code0, data{token: token, role: user.role})有了JWT之后权限控制可以做成装饰器。比如“只有服务商才能上架产品”这个需求可以写一个require_role装饰器from functools import wraps from flask_jwt_extended import verify_jwt_in_request, get_jwt def require_role(*roles): def wrapper(fn): wraps(fn) jwt_required() def decorator(*args, **kwargs): claims get_jwt() if claims.get(role) not in roles: return jsonify(code403, message无权限访问), 403 return fn(*args, **kwargs) return decorator return wrapper3.4 图片上传与视频播放方案乡村文旅项目里图片和视频是重头戏——民宿环境图、采摘园实拍、宣传短视频都是游客决策的重要依据。上传接口用Flask的request.files接收文件简单处理后保存到本地uploads目录。需要注意两个坑一是文件名校验不能让用户随便传路径穿越字符二是图片压缩手机拍的照片动辄四五MB直接存服务器既占空间又影响页面加载速度。图片处理我用Pillow做常规压缩设置最大边长为1280像素质量压到80输出JPEG格式。这样一张原图可以从4MB压缩到300KB左右游客端页面加载速度会明显提升。视频处理更复杂一点通常需要FFmpeg转码和切片。项目初期我的建议是视频先直接用Flask的send_file做本地流式播放不追求切片和CDN。当并发量上来之后再接入点播服务不迟。游客端播放宣传视频时Flask后端可以用send_file带Range头实现断点续传Vue前端用video标签直接播放MP4。网上介绍的StreamingHttpResponse方案是Django的写法Flask里的等价物就是send_file。如果技术总监要求支持m3u8格式那说明项目已经到视频平台级别了单机Flask方案就得让位给专门的视频处理服务from flask import send_file product_bp.route(/video/int:product_id) def product_video(product_id): product Product.query.get_or_404(product_id) video_path os.path.join(current_app.config[UPLOAD_FOLDER], product.video_path) return send_file(video_path, mimetypevideo/mp4, conditionalTrue)4. 前端工程化Vue 3搭建三个端口的界面4.1 开发环境配置与依赖安装前端这边我默认读者已经装好了Node.js环境和npm包管理器。用Vite新建Vue 3项目时官方脚手架会问几个问题——选Vue、选TypeScript还是JavaScript。我的建议是如果项目偏毕设或个人练习优先JavaScript可以少处理一层类型报错如果用于正式商业项目就选TypeScript后面维护成本更低。npm create vitelatest tourism-web -- --template vue cd tourism-web npm install npm install axios vue-router4 pinia element-plus npm run devElement Plus是Vue 3生态里最成熟的组件库表格、表单、弹窗、分页、日期选择器这些后台管理页面高频组件都直接可用。游客端的风格可以自定义CSS覆盖默认样式而管理端直接用默认样式就能达到不错的效果。项目内的部分建议这样组织views目录按角色划分比如views/visitor存放游客端页面views/merchant存放服务商端页面views/admin存放管理后台页面。api目录按资源模块放接口封装文件utils目录放axios实例和通用方法。4.2 路由设计三个角色共用一套前端路由表是整个前端的骨架。游客端页面首页、产品列表、产品详情、购物车可选、订单确认、个人中心。服务商端产品管理、订单管理、收入统计。管理端服务商审核、内容巡查、数据看板。路由守卫是权限控制的第一道关卡。游客访问服务商端页面时检查store里的用户角色不匹配就重定向到登录页router.beforeEach((to, from, next) { const authStore useAuthStore() if (to.meta.requiresAuth !authStore.token) { next({ path: /login, query: { redirect: to.fullPath } }) } else if (to.meta.role authStore.role ! to.meta.role) { next({ path: /404 }) } else { next() } })4.3 axios封装与接口联调中的跨域问题axios封装是前后端联调的桥头堡。我会先在utils/request.js里创建一个axios实例设置baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器统一把Token加到请求头响应拦截器统一处理业务错误码比如登录过期跳转登录页接口异常弹出错误提示import axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { // 约定code为0表示成功 if (response.data.code ! 0) { ElMessage.error(response.data.message) return Promise.reject(new Error(response.data.message)) } return response.data }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } else { ElMessage.error(error.message) } return Promise.reject(error) } )这里涉及一个高频踩坑点跨域。开发环境Vite跑在5173端口Flask跑在5000端口两个端口不同必然产生跨域问题。有两种解决方法一是Flask端安装flask-cors用CORS(app)放行所有跨域请求二是Vite配置proxy代理把/api开头的请求转发到5000端口。我的推荐是两种都做——开发时用Vite代理生产时用Nginx反代Flask端的CORS只作为兜底方案// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })4.4 视频播放与响应式数据渲染游客端产品详情页里视频区域是吸引用户的重要手段。Vue 3里直接使用原生video标签数据源绑定后端返回的播放地址。如果后端返回的是一个支持Range请求的MP4地址浏览器默认就能拖拽进度。这里要提醒一下如果后端返回的不是视频流而是JSONvideo标签是无法播放的需要确认接口返回的是application/octet-stream或video/mp4类型。产品列表页面的渲染用v-for循环组件配合分页组件。我习惯把产品卡片单独抽成一个组件这样服务商端的产品管理列表也可以复用部分样式。图片懒加载直接使用Vue的懒加载指令或者Element Plus的图片组件优化首屏加载速度。电商类页面最烦人的是库存和价格的联动。比如民宿有单间和套间采摘有不同时段的票每个SKU的价格和库存都不同。前端需要在选择规格时通过emit事件向父组件传递所选规格父组件再根据规格更新价格。这个逻辑一开始没梳理好后面改起来会非常痛苦。5. 从开发到上线pycharm调试、Waitress部署与Nginx反代5.1 pycharm中的Flask运行配置PyCharm是我推荐给Python项目的主力IDE社区版就够用。调试Flask项目时创建一个Run ConfigurationScript path指向项目的run.pyEnvironment variables里设置FLASK_ENVdevelopment和DATABASE_URL等环境变量。这样点一下Debug按钮就能在视图函数里打断点看变量。社区版虽然没有专业的JavaScript调试面板但可以配合浏览器开发者工具完成Vue前端调试。PyCharm里主要干后端活写接口、调模型、跑迁移脚本。Run面板里的Console会实时展示SQLAlchemy执行的SQL语句这对于排查N1查询或者字段错误特别有用。虚拟环境的配置也必须在PyCharm里做对。新建项目时选择Virtualenv或Conda环境解释器指向Python 3.10或3.11然后在Terminal面板里pip install -r requirements.txt。如果这些依赖安装失败大概率是网络源问题建议配置清华镜像源pip install -i https://pypi.tuna.tsinghua.edu.cn/simple。5.2 生产环境部署Waitress替代Flask开发服务器Flask自带的开发服务器werkzeug.serving是单进程单线程模型性能有限而且会打印一大堆调试日志绝不能直接用于生产。Windows环境上部署我推荐用Waitress作为WSGI服务器它支持多线程能扛住小规模并发。Linux环境则优先Gunicorn配合多worker进程进一步提升并发。Windows上用Waitress部署其实简单到不像部署# run_prod.py from waitress import serve from app import create_app app create_app(production) if __name__ __main__: serve(app, host0.0.0.0, port5000, threads8)一个真实的乡村文旅平台高峰期同时在线也就几百人QPS可能在个位数到几十之间。Waitress的8个线程足够了真正影响体验的往往是数据库慢查询和图片加载而不是WSGI服务器本身。5.3 Nginx反代与静态资源托管生产环境的请求链路一般是这样用户的浏览器请求Nginx80或443端口Nginx把/api开头的请求转发给后端的Waitress5000端口把静态资源JS、CSS、图片直接由Nginx托管。这样既解决了跨域问题又让Nginx处理静态文件的性能优势发挥出来。一个基础配置片段server { listen 80; server_name tourism.example.com; location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { alias /var/www/tourism/uploads/; expires 30d; add_header Cache-Control public, max-age2592000; } location / { root /var/www/tourism-web/dist; try_files $uri $uri/ /index.html; } }注意location /下面那行try_files这是Vue Router的history模式和Nginx的配合要点。没有这行配置刷新Vue子路由页面时会直接404因为请求路径在后端没有对应的文件Nginx需要把请求重新指向index.html交给Vue Router处理。5.4 数据库迁移与数据备份Flask-Migrate和Alembic配合让数据库结构变更变得可追溯。第一次使用需要init和migrate之后每次修改模型字段执行flask db migrate生成新的迁移脚本再执行flask db upgrade应用变更flask db init flask db migrate -m create user and product tables flask db upgradeMySQL备份用mysqldump比较方便SQLite直接复制文件就行。我给几个做这类型项目的客户实施时都会配一个定时任务每天凌晨把数据库导出到指定目录保留最近7天的备份。数据是无价之宝这个习惯必须养成。6. 常见问题与排查技巧实录写代码过程中踩过的坑整理成速查表按出现频率排序每个问题都备注了排查思路和解决方案。现象原因解决方案前端请求接口报CORS错误未配置跨域或代理Vite配置proxy或Flask安装flask-cors上传图片刷新后404上传目录在项目临时目录重启丢失配置绝对路径的UPLOAD_FOLDER做好持久化JWT Token过期后跳登录页响应拦截器未处理401在axios拦截器里捕获401状态清除Token并重定向Vue路由刷新404Nginx未配置try_files添加try_files $uri $uri/ /index.html控制台报SyntaxError: Unexpected token后端返回了HTML而不是JSON检查Flask视图函数是否写错路由或未返回jsonifySocket连接失败或资源加载缓慢图片未压缩或视频未做流式处理图片压缩视频用Range请求或者转码切片数据库CharSet不一致导致中文乱码MySQL表未使用utf8mb4SQLAlchemy连接串加?charsetutf8mb4还有几个从业务角度发现的坑。第一个是时区问题。游客下单时间记录要统一用UTC存储展示时再按北京时间转换。如果前后端各存各的订单统计对不上账排查起来特别头疼。第二个是库存并发问题。当一个热门采摘园的产品瞬间被抢购时SQLAlchemy的普通查询更新会出现超卖现象。简单做法是在更新语句中加入库存条件result Product.query.filter( Product.id product_id, Product.stock quantity ).update({ Product.stock: Product.stock - quantity }) db.session.commit() if result 0: return jsonify(code400, message库存不足)第三个是订单号生成。直接用自增ID暴露给用户会泄露业务量我习惯生成一个带日期和随机数的订单号比如20250607103015xxxxxx前面是时间后面是随机部分再在数据库里加上唯一索引。7. 实操总结与后续扩展建议痛痛快快写完整个项目再看最开始那个混杂的技术栈标题其实可以给出更清晰的定义Flask负责API服务Vue负责所有前端页面的渲染和交互PyCharm是开发调试的IDEDjango可以作为学习参考但不建议在同一项目里同时使用。选择Flask加Vue这套组合最大的优势是技术栈清晰、职责分离、学习曲线缓和同时保留了充分的扩展空间。我自己的经验是这类项目做完第一版后后续扩展方向往往非常明确接入地图API展示周边景点和农家乐位置让游客能按地理位置筛选引入模拟支付微信或支付宝沙箱环境订单流程才算真正完整服务商端的结算体系可以做成按订单比例抽佣后台自动统计待结算金额如果服务商数量增多还可以增加站点公告和客服工单模块。这些功能不会改变整体架构都是在现有模型上增加些表和接口的活。最后分享一个实操心得乡村生态旅游类项目真正的难点完全不在技术而在于业务流程的细致理解。不同村落的服务商习惯不同——有些民宿老板习惯了电话接单对线上库存管理不熟悉那平台端就要设计简单好用的上架引导有些采摘园的产品按当季产量浮动定价那价格就不是固定的而需要服务商随时修改。开发者只有把业务规则想得越清楚代码写起来才会越顺手。技术选型最终是服务于业务场景的别为了炫技引入不必要的复杂度能把一个业务流程从头到尾跑通、跑稳比什么都重要。
返回列表