去年接手了一个乡村综合服务平台的项目,客户要求既要展示乡村特色文化,又要整合预约、电商等实用功能。经过多方考量,最终选择了Flask+Vue.js的技术栈组合,这里分享一下我的完整开发历程和踩坑经验。
Flask作为Python轻量级框架,非常适合快速构建RESTful API。它的微内核设计让我们可以按需添加扩展,不像Django那样"全家桶"式强制捆绑。不过项目中我们仍然保留了集成Django Admin的可能性,毕竟它的后台管理确实省时省力。前端选用Vue.js 2.x版本(客户预算有限),配合Element UI组件库,三周就搭出了像样的管理界面。
开发工具方面,PyCharm Professional版对Flask的路由调试、模板渲染支持非常友好,特别是它的Database工具直接可视化操作MySQL,省去了写大量SQL的麻烦。不过团队里有成员习惯VSCode,所以我们约定前端代码统一用VSCode开发,避免格式冲突。
采用经典的三层架构,但针对乡村服务特点做了调整:
code复制flask-backend/
├── app/
│ ├── __init__.py
│ ├── models/ # 数据模型层
│ │ ├── user.py # 包含农户、游客、管理员三种角色
│ │ └── village.py # 乡村特色数据模型
│ ├── services/ # 业务逻辑层
│ │ ├── auth.py # JWT认证服务
│ │ └── booking.py # 农家乐预约服务
│ └── routes/ # 路由控制层
│ ├── api_v1.py # RESTful API入口
│ └── admin.py # 管理后台路由
├── config.py # 多环境配置
└── requirements.txt # 依赖清单
特别注意:Flask的蓝本(Blueprint)一定要按功能拆分,我们最初把所有路由写在单个文件里,结果到后期改个参数都得翻半天代码。
使用Vue CLI 4创建项目时,特别注意:
bash复制vue create village-web --preset default # 不要选TS,乡村项目迭代慢
cd village-web
vue add element # 按需引入组件
vue add router # 启用history模式
重要配置项:
javascript复制// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:5000', // Flask默认端口
changeOrigin: true,
pathRewrite: {'^/api': ''}
}
}
}
}
踩坑提醒:开发环境一定要配好proxy,否则CORS问题能折腾你一整天。生产环境记得关掉devServer,直接用Nginx反向代理。
数据库设计采用JSON字段存储动态属性:
python复制# models/village.py
class Village(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
location = db.Column(db.String(100)) # 经纬度,格式"lat,lng"
features = db.Column(db.JSON) # 存储特产、民俗等动态属性
前端通过axios获取数据时,建议封装通用请求方法:
javascript复制// src/utils/request.js
import axios from 'axios'
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API,
timeout: 5000
})
// 请求拦截器
service.interceptors.request.use(
config => {
if (store.getters.token) {
config.headers['X-Token'] = getToken()
}
return config
},
error => {
return Promise.reject(error)
}
)
农家乐预约的核心在于时间冲突检测,我们采用时间段校验算法:
python复制# services/booking.py
def check_availability(room_id, start_time, end_time):
existing = Booking.query.filter(
Booking.room_id == room_id,
Booking.status.notin_(['cancelled', 'rejected']),
or_(
and_(Booking.start_time <= start_time, Booking.end_time > start_time),
and_(Booking.start_time < end_time, Booking.end_time >= end_time),
and_(Booking.start_time >= start_time, Booking.end_time <= end_time)
)
).count()
return existing == 0
前端日期选择推荐使用vue2-datepicker:
vue复制<template>
<date-picker
v-model="timeRange"
type="datetime"
range
:disabled-date="disableDates"
:disabled-hours="disableHours"
/>
</template>
<script>
export default {
methods: {
disableDates(date) {
return date < new Date() // 禁用过去日期
},
disableHours() {
return [0, 1, 2, 3, 4, 5, 22, 23] // 农家乐营业时间限制
}
}
}
</script>
推荐使用2核4G的云服务器,系统选Ubuntu 20.04 LTS。必须做的安全配置:
bash复制# 修改SSH端口
sudo sed -i 's/#Port 22/Port 你的端口号/' /etc/ssh/sshd_config
sudo systemctl restart sshd
# 配置防火墙
sudo ufw allow 你的端口号/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
Flask应用用Gunicorn+Supervisor托管:
ini复制; /etc/supervisor/conf.d/village.conf
[program:village]
command=/home/ubuntu/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app
directory=/home/ubuntu/village-backend
user=ubuntu
autostart=true
autorestart=true
stderr_logfile=/var/log/village.err.log
stdout_logfile=/var/log/village.out.log
Nginx关键配置:
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
root /home/ubuntu/village-web/dist;
try_files $uri $uri/ /index.html;
}
location /api {
include proxy_params;
proxy_pass http://127.0.0.1:8000;
}
location /static {
alias /home/ubuntu/village-backend/app/static;
}
}
血泪教训:静态文件一定要分开处理,我们最初把所有静态资源都扔到Flask里,结果图片加载慢得怀疑人生。
集成高德地图的实战代码:
javascript复制// src/components/Map.vue
export default {
mounted() {
const key = '你的高德密钥'
const script = document.createElement('script')
script.src = `https://webapi.amap.com/maps?v=1.4.15&key=${key}&plugin=AMap.MarkerClusterer`
script.onload = () => {
this.map = new AMap.Map('map-container', {
zoom: 10,
center: [116.397428, 39.90923]
})
this.loadVillages()
}
document.head.appendChild(script)
},
methods: {
async loadVillages() {
const { data } = await getVillages()
const markers = data.map(v => {
const [lng, lat] = v.location.split(',')
return new AMap.Marker({
position: [parseFloat(lng), parseFloat(lat)],
title: v.name,
content: `<div class="village-marker">${v.name}</div>`
})
})
this.map.add(markers)
}
}
}
支付对接微信支付的踩坑点:
核心支付流程:
python复制# services/payment.py
def create_wxpay_order(order_id, total_fee, client_ip):
unified_order = {
'appid': APPID,
'mch_id': MCH_ID,
'nonce_str': generate_nonce_str(),
'body': '农产品订单',
'out_trade_no': order_id,
'total_fee': total_fee,
'spbill_create_ip': client_ip,
'notify_url': 'https://yourdomain.com/api/pay/notify',
'trade_type': 'JSAPI',
'openid': get_openid()
}
unified_order['sign'] = generate_sign(unified_order)
xml_data = dict_to_xml(unified_order)
response = requests.post(
'https://api.mch.weixin.qq.com/pay/unifiedorder',
data=xml_data,
headers={'Content-Type': 'application/xml'}
)
return xml_to_dict(response.text)
Flask-SQLAlchemy的常见性能陷阱:
python复制# 错误示范 - N+1查询问题
villages = Village.query.all()
for v in villages:
print(v.owner.name) # 每次循环都查询数据库
# 正确做法 - 使用joinedload
from sqlalchemy.orm import joinedload
villages = Village.query.options(joinedload(Village.owner)).all()
Vue的异步组件实现:
javascript复制// router.js
const VillageDetail = () => import('./views/VillageDetail.vue')
const routes = [
{
path: '/village/:id',
component: VillageDetail,
meta: { requiresAuth: true }
}
]
图片懒加载配置:
html复制<img
v-lazy="village.imageUrl"
alt="乡村图片"
style="width: 100%; height: auto;">
javascript复制// main.js
import VueLazyload from 'vue-lazyload'
Vue.use(VueLazyload, {
preLoad: 1.3,
error: require('./assets/error-image.png'),
loading: require('./assets/loading-spinner.gif'),
attempt: 3
})
经过三个月的开发和迭代,这个乡村服务平台已经稳定运行半年多。几点深刻体会:
Flask的灵活是把双刃剑 - 初期开发快,但后期需要严格规范代码结构,我们中期进行了两次大的重构
乡村场景的网络条件普遍较差,前端必须做好加载优化,我们最终把首屏资源控制在200KB以内
农户用户对复杂操作接受度低,所有交互必须简化再简化,最后我们甚至为高龄用户做了语音导航功能
如果重新设计这个项目,我会考虑:
这个项目的完整源码已经整理成带详细注释的版本,需要参考的朋友可以联系我获取。特别提醒:数据库设计部分一定要仔细看,我们改了五版才最终定型。