作为一个长期从事Web开发的工程师,我最近完成了一个基于Flask+Vue.js的电商管理系统项目。这个系统从零开始构建,涵盖了用户管理、商品展示、订单处理等完整电商功能模块。选择Flask作为后端框架主要是看中它的轻量级特性和高度可定制化能力,而Vue.js的前端组件化开发模式则非常适合构建复杂的交互界面。
这个项目最有趣的地方在于,虽然核心使用Flask构建,但我也借鉴了Django的一些优秀设计理念,比如它的ORM模型和Admin后台。这种技术组合既保持了Flask的灵活性,又吸收了Django的高效开发模式。整个开发过程在PyCharm中完成,充分利用了它的代码提示和调试功能。
在项目初期,我对比了几种常见的技术组合。最终选择Flask+Vue.js主要基于以下几点考虑:
轻量级与灵活性:Flask作为微框架,没有Django那样的"约定优于配置"限制,可以自由选择数据库、模板引擎等组件。这对于需要高度定制化的电商系统非常重要。
开发效率:Vue.js的组件化开发模式与Flask的RESTful API风格天然契合。前端可以独立开发,通过API与后端交互,大大提升了并行开发效率。
学习曲线:相比Django的全栈式框架,Flask+Vue.js的组合虽然需要自己集成更多组件,但每个部分都更简单直观,特别适合中小型项目。
提示:对于刚接触Flask的开发者,建议先从Flask官方文档的"快速入门"部分开始,理解其核心概念如路由、视图函数和模板渲染。
整个系统采用前后端分离架构:
code复制├── 后端(Flask)
│ ├── app.py # Flask应用入口
│ ├── models.py # 数据模型定义
│ ├── routes/ # API路由
│ ├── utils/ # 工具函数
│ └── config.py # 配置文件
│
└── 前端(Vue.js)
├── public/ # 静态资源
├── src/
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── router/ # 路由配置
│ ├── store/ # Vuex状态管理
│ ├── views/ # 页面视图
│ └── App.vue # 根组件
└── package.json # 依赖配置
这种架构清晰分离了前后端职责,后端专注于数据处理和API提供,前端负责用户交互和界面展示。
用户认证是电商系统的核心功能之一。我选择了JWT(JSON Web Token)作为认证机制,相比传统的Session认证更适合RESTful API。
首先安装必要的扩展:
bash复制pip install flask-jwt-extended
然后在Flask应用中配置JWT:
python复制from flask_jwt_extended import JWTManager, create_access_token, jwt_required
app.config['JWT_SECRET_KEY'] = 'your-secret-key' # 生产环境应从环境变量获取
jwt = JWTManager(app)
# 用户登录路由
@app.route('/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = User.query.filter_by(username=username).first()
if not user or not user.check_password(password):
return jsonify({"msg": "Bad credentials"}), 401
access_token = create_access_token(identity=user.id)
return jsonify(access_token=access_token)
在Vue.js中,我们需要在登录后存储token,并在后续请求中携带:
javascript复制// src/utils/auth.js
import axios from 'axios'
const API = axios.create({
baseURL: process.env.VUE_APP_API_URL
})
// 请求拦截器
API.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
API.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
// token过期处理
store.dispatch('logout')
router.push('/login')
}
return Promise.reject(error)
}
)
export default API
注意:JWT token应存储在HttpOnly的cookie中以提高安全性,但在开发阶段可以先用localStorage简化流程。
商品是电商系统的核心实体,需要设计灵活的数据模型来支持各种商品属性。
使用Flask-SQLAlchemy定义商品模型:
python复制class Product(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
price = db.Column(db.Float, nullable=False)
stock = db.Column(db.Integer, default=0)
image_url = db.Column(db.String(255))
category_id = db.Column(db.Integer, db.ForeignKey('category.id'))
category = db.relationship('Category', backref='products')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
def serialize(self):
return {
'id': self.id,
'name': self.name,
'price': self.price,
'image_url': self.image_url,
'category': self.category.name if self.category else None
}
创建商品相关的RESTful API:
python复制from flask_restx import Resource, fields
product_ns = api.namespace('products', description='Product operations')
product_model = api.model('Product', {
'id': fields.Integer(readOnly=True),
'name': fields.String(required=True),
'price': fields.Float(required=True),
'stock': fields.Integer
})
@product_ns.route('/')
class ProductList(Resource):
@product_ns.marshal_list_with(product_model)
def get(self):
"""List all products"""
return Product.query.all()
@product_ns.expect(product_model)
@product_ns.marshal_with(product_model)
def post(self):
"""Create a new product"""
data = api.payload
product = Product(**data)
db.session.add(product)
db.session.commit()
return product, 201
在Vue.js中创建商品列表组件:
vue复制<template>
<div class="product-list">
<div v-for="product in products" :key="product.id" class="product-card">
<img :src="product.image_url" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">¥{{ product.price.toFixed(2) }}</p>
<button @click="addToCart(product)">加入购物车</button>
</div>
</div>
</template>
<script>
import API from '@/utils/auth'
export default {
data() {
return {
products: []
}
},
async created() {
const response = await API.get('/products')
this.products = response.data
},
methods: {
addToCart(product) {
this.$store.dispatch('addToCart', product)
}
}
}
</script>
购物车是电商系统的关键功能,需要考虑用户未登录和已登录两种状态。
python复制cart_ns = api.namespace('cart', description='Cart operations')
@cart_ns.route('/')
class CartResource(Resource):
@jwt_required()
def get(self):
"""Get user's cart"""
user_id = get_jwt_identity()
cart = Cart.query.filter_by(user_id=user_id).first()
if not cart:
cart = Cart(user_id=user_id)
db.session.add(cart)
db.session.commit()
return cart.serialize()
@jwt_required()
@cart_ns.expect(api.model('CartItem', {
'product_id': fields.Integer(required=True),
'quantity': fields.Integer(default=1)
}))
def post(self):
"""Add item to cart"""
user_id = get_jwt_identity()
data = api.payload
cart = Cart.query.filter_by(user_id=user_id).first_or_404()
# 检查商品是否存在
product = Product.query.get(data['product_id'])
if not product:
return {'message': 'Product not found'}, 404
# 添加或更新购物车项
item = CartItem.query.filter_by(
cart_id=cart.id,
product_id=product.id
).first()
if item:
item.quantity += data.get('quantity', 1)
else:
item = CartItem(
cart_id=cart.id,
product_id=product.id,
quantity=data.get('quantity', 1)
)
db.session.add(item)
db.session.commit()
return cart.serialize()
使用Vuex管理购物车状态:
javascript复制// src/store/modules/cart.js
const state = {
items: []
}
const mutations = {
ADD_TO_CART(state, { product, quantity }) {
const existingItem = state.items.find(item => item.product.id === product.id)
if (existingItem) {
existingItem.quantity += quantity
} else {
state.items.push({ product, quantity })
}
localStorage.setItem('cart', JSON.stringify(state.items))
}
}
const actions = {
addToCart({ commit }, { product, quantity = 1 }) {
commit('ADD_TO_CART', { product, quantity })
}
}
export default {
namespaced: true,
state,
mutations,
actions
}
订单处理是电商系统的核心业务流程,需要确保数据一致性和事务完整性。
python复制@order_ns.route('/')
class OrderList(Resource):
@jwt_required()
def post(self):
"""Create new order from cart"""
user_id = get_jwt_identity()
cart = Cart.query.filter_by(user_id=user_id).first_or_404()
if not cart.items:
return {'message': 'Cart is empty'}, 400
try:
# 开始事务
db.session.begin_nested()
# 创建订单
order = Order(user_id=user_id, status='pending')
db.session.add(order)
# 转换购物车项为订单项
for item in cart.items:
product = Product.query.get(item.product_id)
if product.stock < item.quantity:
raise ValueError(f'Insufficient stock for {product.name}')
order_item = OrderItem(
order=order,
product_id=product.id,
quantity=item.quantity,
price=product.price
)
db.session.add(order_item)
# 扣减库存
product.stock -= item.quantity
# 清空购物车
CartItem.query.filter_by(cart_id=cart.id).delete()
db.session.commit()
return order.serialize(), 201
except Exception as e:
db.session.rollback()
return {'message': str(e)}, 400
vue复制<template>
<div class="checkout">
<h2>确认订单</h2>
<div v-for="item in cartItems" :key="item.product.id" class="order-item">
<span>{{ item.product.name }}</span>
<span>×{{ item.quantity }}</span>
<span>¥{{ (item.product.price * item.quantity).toFixed(2) }}</span>
</div>
<button @click="submitOrder" :disabled="isSubmitting">
{{ isSubmitting ? '处理中...' : '提交订单' }}
</button>
</div>
</template>
<script>
import { mapGetters } from 'vuex'
export default {
data() {
return {
isSubmitting: false
}
},
computed: {
...mapGetters('cart', ['cartItems', 'totalPrice'])
},
methods: {
async submitOrder() {
this.isSubmitting = true
try {
const response = await API.post('/orders')
this.$router.push(`/orders/${response.data.id}`)
} catch (error) {
alert('下单失败: ' + error.response.data.message)
} finally {
this.isSubmitting = false
}
}
}
}
</script>
借鉴Django Admin的思路,为Flask构建一个简单的管理后台。
python复制admin_ns = api.namespace('admin', description='Admin operations')
@admin_ns.route('/dashboard')
class AdminDashboard(Resource):
@jwt_required()
@admin_required
def get(self):
"""Admin dashboard statistics"""
stats = {
'total_users': User.query.count(),
'total_products': Product.query.count(),
'total_orders': Order.query.count(),
'recent_orders': [
o.serialize() for o in
Order.query.order_by(Order.created_at.desc()).limit(5).all()
]
}
return stats
使用Vue Router的导航守卫保护管理路由:
javascript复制// src/router/index.js
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAdmin)) {
if (!store.getters.isAdmin) {
next('/login')
} else {
next()
}
} else {
next()
}
})
管理界面组件示例:
vue复制<template>
<div class="admin-dashboard">
<div class="stats">
<div class="stat-card" v-for="(value, key) in stats" :key="key">
<h3>{{ key }}</h3>
<p>{{ value }}</p>
</div>
</div>
<h3>最近订单</h3>
<order-table :orders="stats.recent_orders || []" />
</div>
</template>
<script>
import API from '@/utils/auth'
export default {
data() {
return {
stats: {}
}
},
async created() {
const response = await API.get('/admin/dashboard')
this.stats = response.data
}
}
</script>
使用Nginx + uWSGI部署Flask应用:
bash复制pip install uwsgi
uwsgi.ini:ini复制[uwsgi]
module = wsgi:app
master = true
processes = 4
socket = /tmp/ecommerce.sock
chmod-socket = 660
vacuum = true
die-on-term = true
nginx复制server {
listen 80;
server_name yourdomain.com;
location / {
include uwsgi_params;
uwsgi_pass unix:/tmp/ecommerce.sock;
}
location /static {
alias /path/to/your/static/files;
}
}
创建Dockerfile:
dockerfile复制FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["gunicorn", "--bind", "0.0.0.0:5000", "wsgi:app"]
使用docker-compose.yml编排服务:
yaml复制version: '3'
services:
web:
build: .
ports:
- "5000:5000"
environment:
- FLASK_ENV=production
- DATABASE_URL=postgresql://user:password@db:5432/ecommerce
depends_on:
- db
db:
image: postgres:13
environment:
- POSTGRES_USER=user
- POSTGRES_PASSWORD=password
- POSTGRES_DB=ecommerce
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
数据库优化:
lazy='dynamic'处理大型结果集缓存策略:
前端优化:
数据库迁移管理:
API版本控制:
/api/v1/products)错误处理:
跨域问题(CORS):
python复制from flask_cors import CORS
CORS(app, resources={r"/api/*": {"origins": "*"}})
数据库连接泄漏:
teardown_appcontext钩子性能瓶颈:
静态文件服务:
在开发过程中,我最大的体会是Flask虽然轻量,但要构建一个完整的电商系统,需要仔细选择和集成各种扩展。与Django相比,Flask提供了更大的灵活性,但也需要开发者自己做出更多架构决策。这种灵活性对于需要定制化功能的中小型项目特别有价值。