十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django+Vue校园二手交易系统全栈实战:从设计到部署

Django+Vue校园二手交易系统全栈实战:从设计到部署 说说这个校园二手交易项目。Django Vue这套组合在前后端分离的项目里算是非常经典的技术栈搭配而校园二手交易系统又是一个业务边界清晰、用户场景明确、能覆盖“注册登录、商品发布、下单购买、订单管理”完整链路的练手项目。它适合正在学完Python基础想实战Django的人也适合准备毕业设计的学生以及想彻底搞明白前后端分离项目如何联调、如何部署的开发者。这篇文章我会把整个系统的功能设计、后端实现、前端实现、联调部署踩坑过程完整过一遍把我实际操作中遇到的经验和教训也一并写出来照着做基本能复刻出一套可运行的项目。1. 项目整体设计与技术选型思路1.1 为什么是校园二手场景为什么要用Django Vue校园里的二手交易需求一直存在——毕业生离校要处理书本和宿舍用品新生入学要买教材和自行车平时还有数码产品、音乐器材、小家电之类的闲置。但校园里的信息传播一直很分散要么靠QQ群刷屏要么靠朋友圈转发要么在宿舍楼下贴纸条。这套系统的核心价值就是把分散的闲置信息收拢到一个统一的平台上让学生能快速发布、浏览、搜索和购买自己需要的东西。技术选型上我最终定了Django Vue前后端分离的方案这里面有几个实际考量。后端选Django是因为它的ORM非常省事学生用户、商品、订单、留言这些表结构在Django里用一个model类就能直接映射到数据库自动生成表不用手写SQL。Django自带的admin后台在调试阶段也特别有用商品数据、用户数据能用图形界面直接看、直接改这对前期联调帮助很大。另外一个关键点是DRFDjango REST Framework它把序列化、路由、视图封装得比较完整写API接口的效率比Flask手写一堆路由要高不少。前端选Vue看重的是它的组件化思路和响应式数据绑定。二手交易页面里有很多重复的UI单元比如商品卡片、订单列表、留言列表用Vue组件可以只写一次到处复用。而且Vue的生态成熟配合Vue Router做页面跳转、Pinia或Vuex做状态管理一个完整的前端SPA单页应用很快就能搭起来。1.2 功能模块划分与核心流程这套二手交易系统从业务上切割成四个核心模块用户模块注册、登录、个人信息维护、我发布的商品、我购买的商品商品模块商品发布、商品列表、商品详情、商品搜索、商品下架订单模块创建订单购买、订单状态流转待付款/已付款/已完成/已取消互动模块商品收藏、商品留言从用户视角走一遍核心流程学生A注册登录后在发布页填写商品标题、描述、价格、图片点击发布商品就进入数据库并出现在首页列表。学生B浏览首页点击进入商品详情看到留言和卖家信息如果想买就点击“立即购买”系统生成一条订单订单状态是待付款。后续卖家可以在后台订单列表里看到这笔订单双方线下交易后订单状态改已完成。这个流程看起来简单但每个环节都有不少技术细节值得展开说。下面我会按后端、前端、部署三个阶段逐一拆解。2. 后端Django核心模块实现2.1 环境准备与项目初始化后端开发我推荐用虚拟环境隔离依赖。命令行操作大致是这样# 创建项目目录并进入 mkdir campus_secondhand cd campus_secondhand # 创建Python虚拟环境 python3 -m venv venv # 激活虚拟环境Windows/Linux/macOS命令有差异 # Windows: venv\Scripts\activate # Linux/macOS: source venv/bin/activate # 安装Django和DRF及相关依赖 pip install django pip install djangorestframework pip install django-cors-headers pip install pillow pip install mysqlclient这里我重点说两个坑。第一mysqlclient在Windows上经常装不上因为它需要MySQL的C客户端库。如果你不想折腾这个依赖开发阶段直接用SQLite完全够用Django对SQLite的支持是开箱即用的等要上生产再切MySQL。第二Pillow这个库必须装Django处理ImageField上传的图片时依赖它不装的话一上传图片就报错。项目初始化命令django-admin startproject config . python manage.py startapp goods python manage.py startapp users python manage.py startapp orders python manage.py startapp chat我个人习惯把项目配置文件目录叫config然后根据业务模块拆不同的app。用.参数让manage.py生成在当前目录这样后续路径处理会方便一些。然后在config/settings.py里注册app并做基础配置INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, rest_framework_simplejwt, corsheaders, users, goods, orders, chat, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # 跨域中间件尽量放前面 django.middleware.security.SecurityMiddleware, ... ] # 数据库配置先用SQLite DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, } } # 媒体文件配置 MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media # DRF配置 REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], } # 跨域配置 CORS_ALLOW_ALL_ORIGINS True # 开发阶段先全放行2.2 数据模型设计用户、商品、订单、留言数据模型是整个系统的地基。我在设计表结构时重点关注字段的实用性和关联关系。用户模型我用的方案是继承Django的AbstractUser扩展字段from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): avatar models.ImageField(upload_toavatars/, nullTrue, blankTrue) phone models.CharField(max_length11, nullTrue, blankTrue) student_id models.CharField(max_length20, nullTrue, blankTrue) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.username然后在settings.py里指定AUTH_USER_MODEL users.User这里有个非常重要的点自定义用户模型最好在一开始就做。如果你已经跑过migrate生成了默认的用户表再改成自定义用户模型会非常痛苦因为数据库里的django内置auth相关的表已经建好了切换起来很容易报外键关联错误。所以新建项目第一步就是把自定义用户模型配好。商品模型from django.conf import settings from django.db import models class Goods(models.Model): STATUS_CHOICES [ (on_sale, 在售), (sold, 已售), (off_shelf, 已下架), ] CONDITION_CHOICES [ (new, 全新), (like_new, 几乎全新), (good, 轻微使用), (acceptable, 有明显磨损), ] title models.CharField(max_length100, verbose_name商品标题) description models.TextField(verbose_name商品描述) price models.DecimalField(max_digits8, decimal_places2, verbose_name价格) original_price models.DecimalField(max_digits8, decimal_places2, nullTrue, blankTrue, verbose_name原价) condition models.CharField(max_length20, choicesCONDITION_CHOICES, defaultgood, verbose_name成色) image models.ImageField(upload_togoods/, nullTrue, blankTrue, verbose_name商品图片) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaulton_sale, verbose_name状态) seller models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namepublished_goods, verbose_name卖家) views_count models.IntegerField(default0, verbose_name浏览量) created_at models.DateTimeField(auto_now_addTrue, verbose_name发布时间) class Meta: ordering [-created_at] def __str__(self): return self.title订单模型class Order(models.Model): ORDER_STATUS [ (pending, 待付款), (paid, 已付款), (completed, 已完成), (cancelled, 已取消), ] order_no models.CharField(max_length32, uniqueTrue, verbose_name订单号) goods models.ForeignKey(Goods, on_deletemodels.CASCADE, related_nameorders, verbose_name商品) buyer models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namebuy_orders, verbose_name买家) seller models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_namesell_orders, verbose_name卖家) amount models.DecimalField(max_digits8, decimal_places2, verbose_name订单金额) status models.CharField(max_length20, choicesORDER_STATUS, defaultpending, verbose_name订单状态) created_at models.DateTimeField(auto_now_addTrue, verbose_name下单时间)订单号我一般用时间戳加随机数生成格式类似202502141030581234保证唯一性。留言模型class Message(models.Model): goods models.ForeignKey(Goods, on_deletemodels.CASCADE, related_namemessages, verbose_name商品) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, verbose_name留言用户) content models.TextField(verbose_name留言内容) created_at models.DateTimeField(auto_now_addTrue, verbose_name留言时间) class Meta: ordering [created_at]表结构设计好之后执行python manage.py makemigrations python manage.py migrate2.3 用DRF写API接口Django REST Framework写接口的思路是序列化器Serializer负责数据格式转换和校验视图集ViewSet处理HTTP请求路由器Router自动生成URL。以商品序列化器为例from rest_framework import serializers from .models import Goods class GoodsSerializer(serializers.ModelSerializer): seller_name serializers.CharField(sourceseller.username, read_onlyTrue) seller_id serializers.IntegerField(sourceseller.id, read_onlyTrue) class Meta: model Goods fields [id, title, description, price, original_price, condition, image, status, seller_name, seller_id, views_count, created_at] read_only_fields [id, status, views_count, created_at]商品视图集from rest_framework import viewsets, permissions, status from rest_framework.decorators import action from rest_framework.response import Response from .models import Goods from .serializers import GoodsSerializer class GoodsViewSet(viewsets.ModelViewSet): queryset Goods.objects.all() serializer_class GoodsSerializer def get_permissions(self): if self.action in [create, update, partial_update, destroy]: return [permissions.IsAuthenticated()] return [permissions.AllowAny()] def perform_create(self, serializer): serializer.save(sellerself.request.user) def list(self, request, *args, **kwargs): queryset self.get_queryset() # 按关键词搜索 keyword request.query_params.get(keyword, ) if keyword: queryset queryset.filter(title__icontainskeyword) # 按价格排序 sort request.query_params.get(sort, newest) if sort price_asc: queryset queryset.order_by(price) elif sort price_desc: queryset queryset.order_by(-price) else: queryset queryset.order_by(-created_at) if not request.user.is_authenticated: queryset queryset.filter(statuson_sale) serializer self.get_serializer(queryset, manyTrue) return Response(serializer.data) action(detailTrue, methods[post]) def favorite(self, request, pkNone): goods self.get_object() # 收藏逻辑 fav, created Favorite.objects.get_or_create(userrequest.user, goodsgoods) if not created: fav.delete() return Response({favorited: False}) return Response({favorited: True})这里我遇到了一个比较典型的业务场景问题未登录用户能不能浏览商品我当时的设计是允许游客浏览在售商品列表和详情但发布、下单、留言、收藏这些操作必须要登录。所以get_permissions里对不同action做了不同权限控制然后在list方法里根据登录状态对查询集做了过滤。用户认证部分我用的是SimpleJWT。登录接口配置# users/views.py from rest_framework_simplejwt.views import TokenObtainPairView from rest_framework_simplejwt.serializers import TokenObtainPairSerializer class CustomTokenObtainPairSerializer(TokenObtainPairSerializer): classmethod def get_token(cls, user): token super().get_token(user) token[username] user.username token[user_id] user.id return token class CustomTokenObtainPairView(TokenObtainPairView): serializer_class CustomTokenObtainPairSerializer配置URL路由from django.urls import path, include from rest_framework.routers import DefaultRouter from rest_framework_simplejwt.views import TokenRefreshView router DefaultRouter() router.register(rgoods, GoodsViewSet, basenamegoods) urlpatterns [ path(api/, include(router.urls)), path(api/auth/login/, CustomTokenObtainPairView.as_view(), nametoken_obtain_pair), path(api/auth/refresh/, TokenRefreshView.as_view(), nametoken_refresh), path(api/auth/register/, RegisterView.as_view(), nameregister), ]登录后前端拿到access_token和refresh_token。access_token的有效期我一般设置成30分钟refresh_token设置成7天这样用户在一次会话里操作比较顺畅也不会因为token长期有效带来太多安全隐患。2.4 图片上传与admin后界面商品图片上传是二手交易系统的刚需Django处理起来很简单。ImageField字段配好MEDIA_URL和MEDIA_ROOT之后开发阶段只需要在config/urls.py里加一行from django.conf import settings from django.conf.urls.static import static urlpatterns [ ... ] if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这样开发服务器就会自动服务media目录下的文件。但要注意这只适合开发环境生产环境必须由Nginx来处理图片的访问否则性能会很差。这个后面部署部分会讲到。Django自带admin后台对我们开发调试帮助太大了。默认情况下admin界面比较简陋我做了几个改进让商品、订单、用户这些模型注册进admin并通过list_display控制字段展示用search_fields开搜索用list_filter开按状态筛选。# goods/admin.py from django.contrib import admin from .models import Goods admin.register(Goods) class GoodsAdmin(admin.ModelAdmin): list_display [id, title, price, status, seller, created_at] search_fields [title, seller__username] list_filter [status, condition] list_editable [status]admin后台表面上是管理工具但在我开发联调阶段它比任何数据库管理工具都直观。我在模拟前端请求时发现某条数据有问题直接进admin改掉重新测效率高得多。3. 前端Vue核心功能实现3.1 Vue项目创建与依赖安装前端开发环境需要Node.js。我建议用LTS版本版本太低会出现依赖解析失败太高也偶尔会遇到兼容性警告。创建Vue项目我推荐用官方脚手架Vite# 安装并创建项目 npm create vuelatest # 或传统方式 npm create vitelatest campus_secondhand_web -- --template vue创建过程会询问是否安装Vue Router、Pinia、ESLint等这些可以根据需要选。然后进入项目目录安装依赖cd campus_secondhand_web npm install # 安装额外依赖 npm install axios npm install element-plus npm install element-plus/icons-vue npm install sass -DElement Plus在校园管理后台或者商品管理页面很好用表格、表单、分页、消息提示这些组件开箱即用能省下不少样式的时间。但要注意如果项目对UI定制要求很高全部引入Element Plus会有点重可以考虑按需引入。3.2 路由设计与页面结构前端页面的结构我按业务场景划分/首页商品瀑布流列表 搜索框 分类筛选/goods/:id商品详情/goods/publish发布商品需要登录/orders我的订单需要登录/favorites我的收藏需要登录/profile个人中心需要登录/login登录/register注册Vue Router配置// src/router/index.js import { createRouter, createWebHistory } from vue-router const router createRouter({ history: createWebHistory(), routes: [ { path: /, name: home, component: () import(/views/HomeView.vue) }, { path: /goods/:id, name: goods-detail, component: () import(/views/GoodsDetailView.vue) }, { path: /goods/publish, name: goods-publish, component: () import(/views/GoodsPublishView.vue), meta: { requiresAuth: true } }, { path: /orders, name: orders, component: () import(/views/OrderListView.vue), meta: { requiresAuth: true } }, { path: /login, name: login, component: () import(/views/LoginView.vue) }, { path: /register, name: register, component: () import(/views/RegisterView.vue) }, { path: /profile, name: profile, component: () import(/views/ProfileView.vue), meta: { requiresAuth: true } }, ], }) // 全局路由守卫 router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) if (to.meta.requiresAuth !token) { next({ name: login, query: { redirect: to.fullPath } }) } else { next() } }) export default router路由懒加载我这里全用了动态导入这样首屏只加载首页需要的代码商品详情、订单这些页面等用户真正访问时才加载首屏性能会好一些。3.3 Axios封装与接口对接前端和后端通信我用Axios做请求库。如果不做封装每个组件里直接调axios.get(/api/goods/)也能跑但一旦要统一处理token过期、错误提示、请求前缀就会非常麻烦。所以我在src/utils/request.js里做了一层封装// src/utils/request.js import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000, }) // 请求拦截器自动附带token request.interceptors.request.use( (config) { const token localStorage.getItem(access_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, (error) Promise.reject(error) ) // 响应拦截器统一处理错误 request.interceptors.response.use( (response) response.data, (error) { if (error.response) { const status error.response.status if (status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(access_token) localStorage.removeItem(refresh_token) router.push({ name: login }) } else if (status 403) { ElMessage.error(没有权限执行此操作) } else if (status 500) { ElMessage.error(服务器开小差了请稍后重试) } else { const detail error.response.data.detail || error.response.data.message || 请求失败 ElMessage.error(detail) } } else { ElMessage.error(网络异常请检查连接) } return Promise.reject(error) } ) export default request首页调用商品列表的接口大概是这样的写法// src/views/HomeView.vue import { ref, onMounted } from vue import request from /utils/request const goodsList ref([]) const loading ref(false) const keyword ref() const fetchGoods async () { loading.value true try { const data await request.get(/goods/, { params: { keyword: keyword.value } }) goodsList.value data } finally { loading.value false } } onMounted(() { fetchGoods() })首页商品卡片我抽成了一个独立的GoodsCard.vue组件接收一个商品对象作为props展示图片、标题、价格、发布时间。因为首页列表、商品搜索结果、我的发布列表都要用同一套卡片样式抽成组件后只需要维护一份代码。3.4 关键页面实现商品发布、商品详情、订单流程商品发布页的难点在于图片上传。Element Plus上传组件本身支持action属性直接指定上传URL但为了统一走我们封装的Axios实例、附带认证token我采用手动上传的方式template div classpublish-page el-form :modelform label-width80px el-form-item label商品标题 el-input v-modelform.title placeholder请输入商品标题 / /el-form-item el-form-item label商品描述 el-input v-modelform.description typetextarea :rows4 / /el-form-item el-form-item label价格 el-input-number v-modelform.price :min0 :precision2 / /el-form-item el-form-item label成色 el-select v-modelform.condition el-option label全新 valuenew / el-option label几乎全新 valuelike_new / el-option label轻微使用 valuegood / el-option label有明显磨损 valueacceptable / /el-select /el-form-item el-form-item label商品图片 el-upload :show-file-listfalse :before-uploadhandleBeforeUpload img v-ifform.image :srcform.image classupload-preview / el-icon v-elsePlus //el-icon /el-upload /el-form-item el-button typeprimary clicksubmitGoods发布/el-button /el-form /div /template script setup import { reactive } from vue import { ElMessage } from element-plus import request from /utils/request const form reactive({ title: , description: , price: 0, condition: good, image: , }) const handleBeforeUpload async (file) { const formData new FormData() formData.append(file, file) const data await request.post(/upload/image/, formData, { headers: { Content-Type: multipart/form-data } }) form.image data.url return false // 阻止默认上传行为 } const submitGoods async () { if (!form.title || !form.description) { ElMessage.warning(请填写商品标题和描述) return } await request.post(/goods/, form) ElMessage.success(发布成功) // 跳转到首页或商品列表 } /script因为Content-Type要设成multipart/form-data我单独写了一个图片上传接口放在Django后端。上传接口返回图片的URL这个URL直接存到表单的image字段提交商品时随其他字段一起传给后端。这个小设计让我在发布商品时不必再单独处理图片与商品的绑定关系。商品详情页的核心交互是展示商品信息、查看留言、发送留言、收藏、立即购买。详情页的数据结构大概是const goods ref(null) const messages ref([]) const isFavorited ref(false) const fetchDetail async () { const data await request.get(/goods/${route.params.id}/) goods.value data }详情页的“立即购买”按钮点击后调用创建订单接口传商品ID。后端在创建订单时会校验商品是否在售状态避免已经卖掉的商品被重复下单。订单列表页则分两个Tab我买到的、我卖出的。接口可以通过?rolebuyer和?roleseller参数区分后端返回不同视角的订单数据。前端展示订单对应的商品缩略图、标题、金额、状态以及状态操作按钮。4. 前后端联调、跨域与常见问题排查4.1 dev模式下如何用Vite代理解决跨域前后端分离开发时前端跑在localhost:5173后端跑在localhost:8000端口不同就意味着跨域。解决的方案有两种思路后端开CORS或前端用代理。开发阶段我推荐前端开Vite代理这样浏览器看到的请求是同源的最干净。在vite.config.js里配置// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: 0.0.0.0, port: 5173, proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, })这样我在前端代码里请求/api/goods/Vite开发服务器会把请求转发到http://localhost:8000/api/goods/。浏览器端看到的是从localhost:5173/api/goods/返回的响应不存在跨域问题。生产环境部署后这个代理就不生效了需要依赖Nginx做反向代理把/api前缀的请求转发到Django。这个后面部署部分会详细说。4.2 Django后端的CORS配置虽然开发阶段用了Vite代理但有时候直接拿Postman或者用其他域名调试时还是需要后端开启CORS。django-cors-headers这个库配置很简单# 安装后 # pip install django-cors-headers # settings.py中 INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, # 必须放在CommonMiddleware之前 ... ] # 开发阶段可以直接全放行 CORS_ALLOW_ALL_ORIGINS True # 生产环境应该限定具体域名 # CORS_ALLOWED_ORIGINS [ # https://your-frontend-domain.com, # ]这里有个容易踩的坑CorsMiddleware的位置很重要官方文档明确要求它尽量放在靠前的位置最好是放在CommonMiddleware之前。如果放错位置偶尔会出现请求跨域失败但后端日志又看不到任何报错的诡异情况排查起来很浪费时间。4.3 Nginx Gunicorn 宝塔部署Django部署阶段的方案我想重点说宝塔面板。宝塔在国内环境下的普及度很高图形化界面直接管理Nginx、MySQL、Python环境确实能省不少事情。Python项目的部署思路是用Gunicorn作为WSGI服务器启动Django应用Nginx作为反向代理处理静态文件和图片的访问以及把API请求转发给Gunicorn。先在服务器上安装好Python环境和依赖# 进入项目目录创建虚拟环境 cd /www/wwwroot/campus_secondhand_backend python3 -m venv venv source venv/bin/activate # 安装依赖 pip install -r requirements.txt # 收集静态文件 python manage.py collectstatic用Gunicorn启动Djangogunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 4这里用127.0.0.1:8000而不是0.0.0.0:8000是安全考虑。因为Nginx和Gunicorn在同一个服务器上Gunicorn没必要监听公网端口只需要接受Nginx转发的请求即可。Nginx配置server { listen 80; server_name your-domain.com; # 前端Vue打包后的静态文件 root /www/wwwroot/campus_secondhand_web/dist; index index.html; # 图片等媒体文件 location /media/ { alias /www/wwwroot/campus_secondhand_backend/media/; } # 静态文件 location /static/ { alias /www/wwwroot/campus_secondhand_backend/static/; } # API请求转发 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # Vue路由配置history模式刷新后不404 location / { try_files $uri $uri/ /index.html; } }前端打包npm run build输出的dist目录上传到服务器Nginx的root指向它即可。4.4 常见问题与排查技巧实录我把自己在开发和部署过程中遇到的高频问题整理成了一张速查表供参考问题现象可能原因解决方案mysqlclient安装失败Windows缺少MySQL C客户端库开发环境用SQLite或用pymysql替代Vue刷新404Nginx没有配置try_files增加try_files $uri $uri/ /index.html;图片上传后访问404media路由或Nginx配置缺失开发环境加static()生产配location /media/前端请求401频繁access_token过期时间太短调长有效期或增加token自动刷新逻辑collectstatic后样式丢失没有配STATIC_ROOTSTATIC_ROOT BASE_DIR / static再执行collectstatic页面能打开但接口502Gunicorn没启动或端口不对检查Gunicorn进程和Nginx代理端口商品图片上传成功后不显示CORS或MEDIA_URL配置不对检查图片URL前缀是否匹配MEDIA_URL与Nginx aliasVue打包后布局异常静态资源引用了绝对路径base配置改为./或Nginx配置正确base这里挑几个典型问题详细说说。第一个是Vue部署后刷新404。Vue Router用history模式时刷新/goods/123这样的路由Nginx会去磁盘找/goods/123这个文件找不到就返回404。解决办法就是Nginx的try_files指令让所有找不到文件的请求都回退到/index.html由前端路由接管。第二个是图片上传后访问404。开发阶段最常见的原因是MEDIA_ROOT和MEDIA_URL配置不匹配或者忘了加static()辅助函数。生产环境则要注意Django只在DEBUG模式才主动服务media文件生产环境必须把media目录交给Nginx。我在部署时因为Nginx配置里少写了一个location /media/图片全是404排查了将近一个小时才搞清楚。第三个是JWT过期导致用户操作突然失败。我在设计时把access_token设成30分钟用户发布商品写到一半token过期请求就401了。实际体验不太好。后续优化方案是前端在响应拦截器里检测到401时自动用refresh_token刷新token然后重放请求。这个逻辑会稍微复杂一点但体验会好很多。如果只是毕设或内部系统使用也可以直接把access_token有效期调长到24小时省事很多。4.5 部署之外上线前还要做哪些事系统跑通之后上线前还有一些细节不能忽视。第一数据库要从SQLite切换到MySQL或PostgreSQL。SQLite在单用户开发调试时非常方便但并发一上来就明显吃力。在宝塔面板上可以一键安装MySQL然后在Django的settings里改数据库配置重新执行migrate即可。注意SQLite里已有的数据需要导出迁移或者直接清空重新录入因为表结构转换比较麻烦。第二Django的DEBUG必须设为False同时配置好ALLOWED_HOSTS。如果不改这两项不仅会暴露详细的报错堆栈信息而且访问时会直接报DisallowedHost错误。通常在settings里用环境变量注入import os DEBUG os.getenv(DJANGO_DEBUG, False) True ALLOWED_HOSTS os.getenv(DJANGO_ALLOWED_HOSTS, your-domain.com).split(,)第三安全方面至少要保证用户密码的哈希存储、HTTPS访问。Django默认密码存储用的是PBKDF2算法这已经够了但如果在公网上用HTTP明文传输用户的密码和token都有被中间人截获的风险。有条件的话Nginx里配置一个免费的SSL证书把HTTP强制跳转到HTTPS。第四订单流程中的防重复下单逻辑。我在后端创建订单的接口里做了这样一个校验如果商品状态不是on_sale直接返回400如果同一个买家已经对同一个商品创建过一个待付款的订单也直接返回一个友好提示。这些逻辑不复杂但在实际使用中能避免很多bug。安全这块我再多说一句前后端分离项目的身份认证依赖token而token一旦被偷等于账号被偷。我在前端没有使用localStorage存用户敏感信息只存了access_token和refresh_token和用户基本资料。同时Django REST Framework里面我统一设置了权限类列表和详情允许匿名访问但是创建、修改、删除操作一律需要认证。发布商品接口还要额外校验request.user.is_authenticated防止未登录用户伪造请求。5. 项目后续功能扩展建议系统跑通之后如果再想往上加东西有几个方向性价比很高。第一个是消息通知。当买家下单时给卖家推送一条消息当留言有新回复时通知原留言人。Django这边可以建一个Notification模型前端在导航栏里用一个红点展示未读数量点进去查看通知列表。这个功能对交易体验的提升非常明显。第二个是商品多图片支持。目前商品只有一个主图但实际二手交易中买家往往想看多角度照片。我在商品详情页预留了GoodsImage关联表的空间如果要做就创建一个GoodsImage模型用外键关联Goods前端在上传组件里支持多图preview。第三个是数据统计。统计每个商品的浏览量、收藏量、成交转化率。Django的views_count字段已经有了浏览量累计逻辑收藏字段也有了再往后可以加一个简单的数据看板用Chart.js或者ECharts展示“本周发布量”“成交额Top10商品”这类维度不管是毕设答辩还是真实运营都很加分。第四个是聊天系统。二手交易中买卖双方沟通是刚需但做站内实时聊天对技术要求一下上升不少——需要WebSocket、在线状态管理、离线消息推送。如果不想上WebSocket也可以做一个留言式的“站内信”功能逻辑跟留言板类似用户给用户发消息对方下次登录时看到。这个折中方案在毕设阶段完全够用。我在做这个系统时的真实感受是技术选型再花哨不如把一个核心业务链路彻底跑通。Django和Vue之所以能成为很多项目的前后端标配不是因为它们多新潮而是因为它们的生态和范式足够成熟——Django帮你把数据库、管理员后台、认证这些后端基础事都安排得明明白白Vue帮你把页面交互组织得顺畅自然。你在做这类项目时如果遇到卡壳不要急着怀疑框架先看看是不是某个配置没配对、某个依赖版本不兼容这些才是前后端分离项目里最常见的坑。
返回列表