前阵子接了个小项目要给一家线下婴幼儿用品店做一个在线销售网站。客户需求不算复杂商品展示、注册登录、加购物车、下单最好还能有个后台管理商品。技术栈我最终选了PythonVue开发环境用的Pycharm。这个项目做下来大概花了三周中间踩了不少坑也积累了一些经验。今天就把完整的实现过程拆开讲讲包括我在Django和Flask之间到底怎么选、前后端怎么对接、遇到问题怎么排查。如果你也想做类似的全栈电商网站这篇文章应该能帮你省不少时间。很多人一上来就问“Django和Flask哪个好”其实这个问题没有标准答案关键看你的项目边界和团队习惯。这次的婴幼儿用品销售网站核心业务是标准的CRUD还要有用户体系、后台管理、图片上传我最后选了Django。为什么后面会详细对比说明。前端用Vue是因为Vue模板语法直观、组件化开发效率高而且社区里现成的UI库特别多能快速把页面搭漂亮。整个过程用Pycharm做开发调试它的调试器、数据库面板、Git集成确实比别的IDE顺手。1. 项目整体设计与技术选型1.1 为什么用PythonVue这套组合先说说技术选型的整体逻辑。做这种中小企业网站首先考虑的是开发效率和维护成本。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-LoginORM需要SQLAlchemy表单需要WTForms后台管理需要一个Flask-Admin。这些库虽然都能用但彼此之间的版本兼容需要花时间调试而且数据库迁移的工具链也相对复杂。Django则完全不同自带的admin后台直接就能管理商品数据auth模块提供完整的用户认证ORM自带migration机制改模型后一条命令自动同步数据库。那Flask适合什么场景我认为是小型API服务、微服务、或者需要高度自定义架构的项目。如果你的项目就三五个接口用Flask很爽。但像这种电商网站涉及十几个数据表、七八个模块Django的“生态一体”优势非常明显。我用一个表格来总结对比项DjangoFlaskFastAPI项目结构固定模式自带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 APIVue前端通过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文件把项目依赖逐步写进去。这个项目最终核心依赖如下Django4.2 djangorestframework3.14 django-cors-headers3.13 djangorestframework-simplejwt5.2 Pillow10.0 mysqlclient2.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这里解释一下为什么分成多个appDjango的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_length50) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.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_deletemodels.CASCADE, related_nameproducts, verbose_name所属分类) name models.CharField(商品名称, max_length150) price models.DecimalField(价格, max_digits8, decimal_places2) stock models.PositiveIntegerField(库存) image models.ImageField(商品图片, upload_toproducts/%Y/%m/) description models.TextField(商品详情, blankTrue) sales models.PositiveIntegerField(销量, default0) status models.BooleanField(上架状态, defaultTrue) created_at models.DateTimeField(auto_now_addTrue) 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_length11, blankTrue) address models.CharField(收货地址, max_length255, blankTrue)订单表放在orders/models.pyclass Order(models.Model): STATUS_CHOICES ( (pending, 待支付), (paid, 已支付), (shipped, 已发货), (completed, 已完成), (cancelled, 已取消), ) user models.ForeignKey(from users.models import User, on_deletemodels.CASCADE, verbose_name用户) total models.DecimalField(订单金额, max_digits10, decimal_places2) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultpending) address models.CharField(收货地址, max_length255) created_at models.DateTimeField(auto_now_addTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) product models.ForeignKey(Product, on_deletemodels.PROTECT) quantity models.PositiveIntegerField() price models.DecimalField(下单时价格, max_digits8, decimal_places2)定义好模型后执行迁移python manage.py makemigrations python manage.py migrate然后创建超级管理员方便用Django admin管理数据python manage.py createsuperuser也可以在admin.py里注册模型这样后台就能直接增删改查商品了。把这个基础功能做好客户自己就能在后台维护商品不用每次都麻烦我。3. 后端API实现商品、登录、购物车与订单3.1 商品列表与详情API现在开始提供API。我使用了Django REST frameworkDRF不用自己写序列化逻辑效率很高。在goods/serializers.py里写from rest_framework import serializers from .models import Product class ProductSerializer(serializers.ModelSerializer): category_name serializers.CharField(sourcecategory.name, read_onlyTrue) class Meta: model Product fields [id, name, price, image, description, category_name, sales]然后在goods/views.py里用ViewSetfrom 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(statusTrue) 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(datarequest.data) serializer.is_valid(raise_exceptionTrue) 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_onlyTrue, min_length6) 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(), nametoken_obtain_pair), path(api/v1/register/, RegisterView.as_view(), nameregister), ]前端保存access token到localStorage每次请求带在Authorization头Bearer token。3.3 购物车与订单接口购物车我没做服务端存储原因之前提过数据量小localStorage足够还能减轻服务器压力。前端把商品ID、数量、名称、价格存到localStorage下单时把这些数据整体提交给订单接口。订单接口在orders/views.pyfrom 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(userself.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(idproduct_id) total product.price * quantity OrderItem.objects.create(orderorder, productproduct, quantityquantity, priceproduct.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 vitelatest baby_mall_front -- --template vue cd baby_mall_front npm install npm install vue-router4 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_rootsettings.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_onlyTrue)后端直接返回格式化后的北京时间前端没那么多处理。5.4 Pycharm调试与数据库连接技巧Pycharm的调试器很强大但有个坑Django开发服务器默认开启自动重载导致调试时想断点有延迟。我的做法是在运行配置里勾选“Run with Python Console”并且把“Disable background run”的选项调好这样可以在调试状态下稳定打断点。另外如果用Pycharm的Database面板连接MySQL连接时一定要选对时区参数否则时间查询会出错。连接字符串里加上serverTimezoneAsia/Shanghai。6. 项目实操总结与个人体会这个项目本身不算复杂但完整走下来从技术选型到部署上线每一环都有值得复盘的地方。我最大的体会是在框架选型上不能只看谁更“流行”或谁“更快”而要综合考虑项目体量、团队熟悉度、生态成熟度。Django虽然“重”但在这个电商网站场景里它内置的后台和认证功能直接帮我省了两三天时间。Flask对我来说反而需要花更多时间集成第三方库并不像名字那么“轻”。另一个体会是前后端分离项目里联调和部署的细节往往比写业务代码更耗时。比如跨域、代理、媒体文件路径、时区每一个都能让人卡半天。我建议做类似项目的朋友在项目一开始就把前后端的代理和媒体文件服务配置好不要等写了好几天接口再去调。最后分享一个小技巧因为客户需要随时改商品信息我用Django admin做了简单的后台。但admin的界面不够好看客户还是希望有个更直观的商品管理页。所以后来我用Vue又做了一个简易的后台页面只包含商品列表、编辑、删除、新增四个功能通过JWT认证访问。这样前后端全是自己写的想怎么改都方便。如果你也要给这类项目加后台管理可以先从Django admin顶一阵子稳住数据管理需求后再按需开发专属后台这样节奏会更从容。