前阵子接了个小项目,要给一家线下婴幼儿用品店做一个在线销售网站。客户需求不算复杂:商品展示、注册登录、加购物车、下单,最好还能有个后台管理商品。技术栈我最终选了Python+Vue,开发环境用的Pycharm。这个项目做下来大概花了三周,中间踩了不少坑,也积累了一些经验。今天就把完整的实现过程拆开讲讲,包括我在Django和Flask之间到底怎么选、前后端怎么对接、遇到问题怎么排查。如果你也想做类似的全栈电商网站,这篇文章应该能帮你省不少时间。
很多人一上来就问“Django和Flask哪个好”,其实这个问题没有标准答案,关键看你的项目边界和团队习惯。这次的婴幼儿用品销售网站,核心业务是标准的CRUD,还要有用户体系、后台管理、图片上传,我最后选了Django。为什么?后面会详细对比说明。前端用Vue,是因为Vue模板语法直观、组件化开发效率高,而且社区里现成的UI库特别多,能快速把页面搭漂亮。整个过程用Pycharm做开发调试,它的调试器、数据库面板、Git集成确实比别的IDE顺手。
1. 项目整体设计与技术选型
1.1 为什么用Python+Vue这套组合
先说说技术选型的整体逻辑。做这种中小企业网站,首先考虑的是开发效率和维护成本。Python的语法简洁,写业务逻辑非常快,这一点在CRUD密集型项目上体现得尤其明显。配合Django这种“全家桶”框架,ORM、认证、后台管理、表单处理全部内置,不需要到处找第三方库拼凑,版本兼容性问题也少。
前端用Vue而不是React,我的理由有几点:第一,Vue的中文文档和社区资源丰富,学习曲线平缓,团队里其他成员也更容易接手;第二,Vue的双向绑定和指令系统在处理表单、列表渲染时非常顺手;第三,配合Element Plus这类组件库,几个小时就能把后台管理界面搭出个雏形。而React虽然生态更大,但JSX语法和状态管理对初学者不算友好,在这个项目规模下反而增加沟通成本。
Pycharm作为IDE,专注Python开发十余年,智能提示和代码导航做得最好。尤其是调试Django的请求链路时,可以轻松在框架内部代码处打断点,对理解Django的请求处理流程特别有帮助。Pycharm的社区版免费且足够用,我项目里就用社区版完成了全部开发。
1.2 Django还是Flask?最终还是选了Django
这个对比是项目里最值得展开的地方。标题里同时出现了django和flask,说明很多人在这两个框架之间纠结。我一开始其实想用Flask,因为Flask足够轻,一个文件就能跑起来,感觉上手更快。但深入想了一下,这个项目需要的功能点非常明确:
- 用户注册和登录(会话、密码加密、token认证)
- 商品分类和商品信息管理
- 购物车和订单流转
- 后台管理界面(至少能编辑商品)
如果用Flask,以上每块都要自己选择并集成相应的库,比如登录认证需要Flask-Login,ORM需要SQLAlchemy,表单需要WTForms,后台管理需要一个Flask-Admin。这些库虽然都能用,但彼此之间的版本兼容需要花时间调试,而且数据库迁移的工具链也相对复杂。Django则完全不同:自带的admin后台直接就能管理商品数据,auth模块提供完整的用户认证,ORM自带migration机制,改模型后一条命令自动同步数据库。
那Flask适合什么场景?我认为是小型API服务、微服务、或者需要高度自定义架构的项目。如果你的项目就三五个接口,用Flask很爽。但像这种电商网站,涉及十几个数据表、七八个模块,Django的“生态一体”优势非常明显。
我用一个表格来总结:
| 对比项 | Django | Flask | FastAPI |
|---|---|---|---|
| 项目结构 | 固定模式,自带app划分 | 自由定义 | 自由定义 |
| ORM | 内置,功能强大 | 需自行选择SQLAlchemy等 | 需自行选择,支持异步 |
| 后台管理 | 自带admin | 需要Flask-Admin | 无 |
| 用户认证 | 内置auth和权限 | 需Flask-Login等 | 需自行实现 |
| 适合项目 | 电商、内容管理、后台系统 | 轻量API、微服务 | 高性能API、前后端分离 |
| 学习曲线 | 中等,概念多但规范 | 低,但后续要学生态 | 中,要求有类型理解 |
FastAPI我在这里提一下,它性能好,自动生成OpenAPI文档,但项目生态相对新,遇到问题时可参考的成熟方案少一些。如果做高并发API,可以考虑,但传统电商网站还是Django稳妥。
1.3 数据库与整体架构设计
婴幼儿用品销售网站的数据库,核心是商品相关的表。我设计了这几张表:用户表(User)、分类表(Category)、商品表(Product)、订单表(Order)、订单详情表(OrderItem)。考虑到用户地址和购物车,也可以加地址表(Address)和购物车表(Cart),但为控制复杂度,购物车我用前端localStorage存储,下单时直接提交商品ID和数量到后端,这样服务端少两张表,逻辑也更简单。
整体架构是前后端分离:Django后端提供RESTful API,Vue前端通过axios调用API渲染页面。前端路由用Vue Router,状态管理用简单的provide/inject或直接本地存储,不需要Vuex。开发时前端跑在localhost:8080,后端跑在localhost:8000,通过代理转发解决跨域。
2. 后端开发环境搭建与项目初始化
2.1 Python环境与Pycharm配置
在正式创建项目前,我先把Python环境准备好。推荐用Python 3.9以上版本,直接去官网下载安装包。这里有个经验:安装时记得勾选“Add Python to PATH”,否则后面在命令行里找不到python命令,还得去改环境变量。
然后在Pycharm里新建项目,选择虚拟环境。Pycharm的“New environment”会自动帮我们创建venv,这个虚拟环境会把项目依赖隔离,避免和系统Python包冲突。创建完成后,打开Terminal,确认激活了虚拟环境,看到命令提示符前面有(venv)就对了。
接下来安装基础依赖。我先习惯性地装上两个全局工具:pip升级、wheel。然后创建requirements.txt文件,把项目依赖逐步写进去。这个项目最终核心依赖如下:
Django>=4.2 djangorestframework>=3.14 django-cors-headers>=3.13 djangorestframework-simplejwt>=5.2 Pillow>=10.0 mysqlclient>=2.2如果只是本地测试,可以先用SQLite,等要部署再切MySQL。但我这里直接用了MySQL,因为客户本地已有的数据库就是MySQL,且mysqlclient驱动性能更好。Pycharm的数据库面板可以直接连上MySQL,查看表结构和数据非常方便,这也是我选Pycharm的原因之一。
2.2 创建Django项目和app
一切准备好后,在Pycharm终端执行:
django-admin startproject baby_mall cd baby_mall python manage.py startapp goods python manage.py startapp users python manage.py startapp orders这里解释一下为什么分成多个app:Django的app是一种模块化组织方式,把不同功能块拆开,代码结构更清晰。goods负责商品,users负责用户,orders负责订单。以后如果还要加评论功能,就新建一个comments app,不会污染现有的模块。
创建完app后,在settings.py的INSTALLED_APPS里注册它们:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'rest_framework', 'corsheaders', 'goods', 'users', 'orders', ]顺便把corsheaders.middleware.CorsMiddleware加到中间件第一行,后面解决跨域要用。
设置好语言和时区,避免后面时间错乱:
LANGUAGE_CODE = 'zh-hans' TIME_ZONE = 'Asia/Shanghai' USE_TZ = True2.3 数据模型设计与迁移
在www.goods/models.py里定义商品相关模型。为了让文章清晰,我贴出关键代码:
from django.db import models class Category(models.Model): name = models.CharField('分类名称', max_length=50) parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='父级分类') class Meta: verbose_name = '商品分类' verbose_name_plural = verbose_name def __str__(self): return self.name class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products', verbose_name='所属分类') name = models.CharField('商品名称', max_length=150) price = models.DecimalField('价格', max_digits=8, decimal_places=2) stock = models.PositiveIntegerField('库存') image = models.ImageField('商品图片', upload_to='products/%Y/%m/') description = models.TextField('商品详情', blank=True) sales = models.PositiveIntegerField('销量', default=0) status = models.BooleanField('上架状态', default=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = '商品' verbose_name_plural = verbose_name ordering = ['-created_at'] def __str__(self): return self.name这里的upload_to路径按年月分目录,方便管理图片。ImageField依赖于Pillow库,所以前面必须安装。
用户表继承Django自带的AbstractUser,加两个字段:
from django.contrib.auth.models import AbstractUser class User(AbstractUser): phone = models.CharField('手机号', max_length=11, blank=True) address = models.CharField('收货地址', max_length=255, blank=True)订单表放在orders/models.py:
class Order(models.Model): STATUS_CHOICES = ( ('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ) user = models.ForeignKey(from users.models import User, on_delete=models.CASCADE, verbose_name='用户') total = models.DecimalField('订单金额', max_digits=10, decimal_places=2) status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='pending') address = models.CharField('收货地址', max_length=255) created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.PROTECT) quantity = models.PositiveIntegerField() price = models.DecimalField('下单时价格', max_digits=8, decimal_places=2)定义好模型后,执行迁移:
python manage.py makemigrations python manage.py migrate然后创建超级管理员,方便用Django admin管理数据:
python manage.py createsuperuser也可以在admin.py里注册模型,这样后台就能直接增删改查商品了。把这个基础功能做好,客户自己就能在后台维护商品,不用每次都麻烦我。
3. 后端API实现:商品、登录、购物车与订单
3.1 商品列表与详情API
现在开始提供API。我使用了Django REST framework(DRF),不用自己写序列化逻辑,效率很高。在goods/serializers.py里写:
from rest_framework import serializers from .models import Product class ProductSerializer(serializers.ModelSerializer): category_name = serializers.CharField(source='category.name', read_only=True) class Meta: model = Product fields = ['id', 'name', 'price', 'image', 'description', 'category_name', 'sales']然后在goods/views.py里用ViewSet:
from rest_framework import viewsets from rest_framework.permissions import AllowAny from .models import Product from .serializers import ProductSerializer class ProductViewSet(viewsets.ReadOnlyModelViewSet): queryset = Product.objects.filter(status=True) serializer_class = ProductSerializer permission_classes = [AllowAny]Router注册:
# baby_mall/urls.py from rest_framework.routers import DefaultRouter from goods.views import ProductViewSet router = DefaultRouter() router.register('products', ProductViewSet) urlpatterns = [ path('api/v1/', include(router.urls)), ]这样商品列表和详情都有了:GET /api/v1/products/返回列表,GET /api/v1/products/1/返回单个商品详情。不需要额外写视图函数,ViewSet已经把增删改查的路由都做好了,这里只开放了只读,修改交给后台admin。
3.2 用户注册登录与JWT
用户认证我选择了SimpleJWT。安装后,在settings.py里配置:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], }然后在users/views.py里写注册接口:
from rest_framework import generics, permissions from rest_framework.response import Response from rest_framework_simplejwt.tokens import RefreshToken from .serializers import UserSerializer class RegisterView(generics.CreateAPIView): serializer_class = UserSerializer permission_classes = [permissions.AllowAny] def post(self, request, *args, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) user = serializer.save() refresh = RefreshToken.for_user(user) return Response({ 'user': UserSerializer(user).data, 'refresh': str(refresh), 'access': str(refresh.access_token), })UserSerializer里需要覆写create方法,把密码哈希化:
class UserSerializer(serializers.ModelSerializer): password = serializers.CharField(write_only=True, min_length=6) class Meta: model = User fields = ['id', 'username', 'password', 'phone'] def create(self, validated_data): password = validated_data.pop('password') user = User(**validated_data) user.set_password(password) user.save() return user登录接口可以直接用SimpleJWT提供的TokenObtainPairView,注册路由:
from rest_framework_simplejwt.views import TokenObtainPairView urlpatterns += [ path('api/v1/login/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/v1/register/', RegisterView.as_view(), name='register'), ]前端保存access token到localStorage,每次请求带在Authorization头:Bearer <token>。
3.3 购物车与订单接口
购物车我没做服务端存储,原因之前提过:数据量小,localStorage足够,还能减轻服务器压力。前端把商品ID、数量、名称、价格存到localStorage,下单时把这些数据整体提交给订单接口。
订单接口在orders/views.py:
from rest_framework import generics, permissions from rest_framework.response import Response from .models import Order, OrderItem from .serializers import OrderSerializer class CreateOrderView(generics.CreateAPIView): serializer_class = OrderSerializer permission_classes = [permissions.IsAuthenticated] def perform_create(self, serializer): order = serializer.save(user=self.request.user) items_data = self.request.data.get('items', []) total = 0 for item in items_data: product_id = item['product_id'] quantity = item['quantity'] product = Product.objects.get(id=product_id) total += product.price * quantity OrderItem.objects.create(order=order, product=product, quantity=quantity, price=product.price) product.stock -= quantity product.sales += quantity product.save() order.total = total order.save()这里最核心的一点是在下单时同步扣库存、增加销量,并且把下单时的商品价格记录到OrderItem里,避免之后改价影响历史订单。订单列表和详情也可以做出来,但原理类似,不再赘述。
4. Vue前端开发与核心页面实现
4.1 Vue项目创建与环境配置
前端我用了Vite,比vue-cli快很多。安装Node.js后,命令行执行:
npm create vite@latest baby_mall_front -- --template vue cd baby_mall_front npm install npm install vue-router@4 axios element-plus开发过程中,我把Vite的服务器代理配置到后端,避免跨域。在vite.config.js里:
export default defineConfig({ plugins: [vue()], server: { host: '0.0.0.0', port: 8080, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, } } } })这样前端请求/api/v1/products时,Vite开发服务器会自动把请求转发到Django的8000端口。生产时用Nginx做反向代理,效果一样。
4.2 路由与页面组件
项目页面包括:首页(展示分类和热销商品)、商品列表页、商品详情页、登录注册页、购物车页、订单确认页、订单成功页。我用vue-router配置如下:
const routes = [ { path: '/', component: Home }, { path: '/products', component: ProductList }, { path: '/product/:id', component: ProductDetail }, { path: '/login', component: Login }, { path: '/register', component: Register }, { path: '/cart', component: Cart }, { path: '/checkout', component: Checkout }, { path: '/order-success', component: OrderSuccess }, ]需要注意的一点是,History模式下的路由在部署到Nginx时需要配置try_files $uri $uri/ /index.html;,否则刷新页面会404。这个坑我在部署时踩过,后面会细说。
4.3 调用后端API与数据展示
我封装了一个axios实例,统一处理token和错误提示:
import axios from 'axios' import { ElMessage } from 'element-plus' const request = axios.create({ baseURL: '/api/v1', timeout: 5000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { ElMessage.error(error.response?.data?.detail || '请求失败') return Promise.reject(error) } ) export default request商品列表页就是调用request.get('/products'),把返回的数组渲染成卡片。用Element Plus的el-card和el-row组件,几分钟就能搭好一个像样的商品展示页面。
商品详情页有个小技巧:通过路由参数this.$route.params.id取到商品ID,然后请求/products/${id}。加入购物车则是把商品数据存到localStorage的一个数组里,同时提示用户“已加入购物车”。
购物车页面读取localStorage里的数据,展示商品明细和总价。点击“去结算”时,把购物车数据通过request.post('/orders/', { items })提交到后端。下单成功后清空本地购物车,跳到订单成功页。
5. 前后端联调与部署中的常见坑
5.1 跨域问题的处理
开发时如果不用Vite代理,直接让前端请求http://localhost:8000/api/...,浏览器会报CORS错误。我一开始就这么干的,后来才反应过来。解决方案有两种:一是后端开启跨域,二是前端代理。
我两者都做了:后端配置django-cors-headers,允许从前端开发地址访问:
CORS_ALLOWED_ORIGINS = [ "http://localhost:8080", ]如果生产环境前后端同域名,可以不用开CORS,交给Nginx代理更安全。跨域配置好后,登录、商品接口都能正常访问了。
5.2 图片上传与媒体文件配置
商品图片上传是个容易出问题的地方。Django的ImageField会把文件存到MEDIA_ROOT目录,URL前缀是MEDIA_URL。开发时在urls.py里加:
from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)同时在settings.py设置:
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'前端在展示商品图片时,URL要拼成http://localhost:8000/media/xxx.jpg。我用Vite代理后,可以直接用/media/前缀,因为代理把/media也转发了。这个代理配置记得加上。部署生产时,Nginx需要单独配一个location/media给Django的媒体目录,否则图片全部404。
5.3 时区与时间显示的坑
Django开启了USE_TZ = True,数据库存的时间是UTC,而前端在浏览器显示的时间默认是本地时区。我一开始直接返回created_at,前端显示的时间比北京时间晚了8小时,给测试的客户看得一头雾水。
解决方法很简单:前端拿到ISO字符串后,用new Date()解析,再用toLocaleString()转换成本地时间;或者后端在序列化器里把时间字段格式化好:
class OrderSerializer(serializers.ModelSerializer): create_time = serializers.DateTimeField(format='%Y-%m-%d %H:%M:%S', read_only=True)后端直接返回格式化后的北京时间,前端没那么多处理。
5.4 Pycharm调试与数据库连接技巧
Pycharm的调试器很强大,但有个坑:Django开发服务器默认开启自动重载,导致调试时想断点有延迟。我的做法是在运行配置里勾选“Run with Python Console”,并且把“Disable background run”的选项调好,这样可以在调试状态下稳定打断点。
另外,如果用Pycharm的Database面板连接MySQL,连接时一定要选对时区参数,否则时间查询会出错。连接字符串里加上serverTimezone=Asia/Shanghai。
6. 项目实操总结与个人体会
这个项目本身不算复杂,但完整走下来,从技术选型到部署上线,每一环都有值得复盘的地方。我最大的体会是:在框架选型上不能只看谁更“流行”或谁“更快”,而要综合考虑项目体量、团队熟悉度、生态成熟度。Django虽然“重”,但在这个电商网站场景里,它内置的后台和认证功能直接帮我省了两三天时间。Flask对我来说反而需要花更多时间集成第三方库,并不像名字那么“轻”。
另一个体会是前后端分离项目里,联调和部署的细节往往比写业务代码更耗时。比如跨域、代理、媒体文件路径、时区,每一个都能让人卡半天。我建议做类似项目的朋友,在项目一开始就把前后端的代理和媒体文件服务配置好,不要等写了好几天接口再去调。
最后分享一个小技巧:因为客户需要随时改商品信息,我用Django admin做了简单的后台。但admin的界面不够好看,客户还是希望有个更直观的商品管理页。所以后来我用Vue又做了一个简易的后台页面,只包含商品列表、编辑、删除、新增四个功能,通过JWT认证访问。这样前后端全是自己写的,想怎么改都方便。如果你也要给这类项目加后台管理,可以先从Django admin顶一阵子,稳住数据管理需求后再按需开发专属后台,这样节奏会更从容。