资讯动态

二手车交易系统全栈开发实战:Django+Vue前后端分离从设计到联调

发布时间:2026/10/20 14:00:46 来源:尧图企业网站定制
二手车交易系统这个题目我前后见了不下几十个版本Python后端配Vue前端的组合出现频率最高django、flask、pycharm这套关键词都快成标配了。带过的同学也好、接过的定制项目也好绝大多数人卡住的点其实不在“写代码”而在“不知道怎么把业务拆成表结构”“前后端联调报错看不懂”“功能做出来但稀碎”。这篇文章我就把这套二手车交易系统从选型、设计、编码到联调的整体思路完整拆一遍该给代码给代码该给配置给配置保证你拿到之后能直接照着落地。这套系统的定位很清晰前台给用户看车、搜车、收藏、预约后台给管理员做车辆审核和内容管理。适合在校生做毕业设计、课程设计也适合刚入门的开发者拿来做全栈练手项目。不管你是老师指定了django还是flask核心思路都一样差别只在后端框架的写法上。1. 这题目说白了在做什么业务需求与功能拆解1.1 一套能用的二手车交易系统要包含哪些功能很多人拿到题目就开始建django工程结果建完不知道先写什么。我建议反过来先列功能清单再建工程。一套比较完整的二手车交易系统功能至少分成几个面用户端注册登录手机号或用户名注册密码加密存储登录后拿到token。车辆展示首页展示推荐车辆、最新上架车辆车辆列表页支持分页浏览。条件筛选按品牌、价格区间、车龄、变速箱类型、排放标准等组合过滤。车辆详情多张图片轮播、车辆参数表、车况描述、车主信息展示。收藏车辆登录用户可以收藏或取消收藏我的收藏页面能看到列表。预约看车用户填写联系方式、预约时间后台能收到预约记录。个人中心修改个人信息、查看我发布的车辆、查看预约记录。管理端车辆管理审核用户发布的车辆或者管理员自己录入车辆支持上下架操作。用户管理查看注册用户列表可以禁用账号。预约管理查看预约看车记录标记已联系或已完成。公告管理发布平台公告前台展示公告列表。功能不建议再贪多了。我见过有同学非要加在线支付、聊天系统、贷款计算器结果每个都没做深答辩一追问就露馅。把上面这套做扎实已经能撑起一个完整的全栈项目了。1.2 两种常见业务模式对设计的影响同样是二手车交易系统业务模式不同数据库设计差别很大。第一种是平台自营模式B2C车辆都由平台或商家发布普通用户只能浏览和预约。这种模式下不需要“用户发布车辆”功能后台管理车辆是核心角色只需要分管理员和普通用户。第二种是个人车主直卖模式C2C用户既能买车也能卖车自己发布车辆信息平台负责审核。这种模式需要给用户表加身份字段车辆表要加审核状态字段车辆发布要带图片上传功能。大多数课程设计、毕业设计选C2C模式的更多因为功能更完整、展示效果更好、答辩时有东西讲。我在下面写的设计和代码都以C2C模式为基础同时会标出哪些是纯卖家功能。2. 技术选型Django还是Flask这是个先想清楚的问题2.1 两个框架跑一个真实项目时的差别标题里同时写了django和flask说明你还没定或者老师在两个里面选一个。两个框架我都实际开发过项目说说在这个系统上的差别。Django最突出的优势是“全家桶”自带Admin后台、ORM、认证系统、表单校验。尤其是Admin后台做管理端几乎零成本。二手车交易系统天然需要管理端来审核车辆、管理用户Django的Admin在这类项目上特别省事。ORM的数据库迁移机制也稳改完模型跑一条makemigrations命令就能同步表结构。Flask的特点是轻量、灵活框架本身只提供路由和请求处理ORM、表单、鉴权都要自己集成典型组合是Flask SQLAlchemy Flask-JWT-Extended。好处是你对框架的掌控感更强代码结构更透明坏处是很多模块要自己拼时间成本高一些。如果你只是为了完成项目而选Flask后面很容易在和不在框架内置的东西上花大量时间。2.2 这个场景我为什么不推荐“纯Flask裸写”我见过不少Flask项目写着写着就变成一个超大的app.py路由堆了上千行模型散落各处没有工程分层。为什么因为Flask没给你强制约束规划不好就全堆一起。如果你确实要用Flask我的建议是至少按这个结构来组织car_project/ ├── app.py # 应用入口 ├── config.py # 配置 ├── extensions.py # 扩展对象db, jwt ├── models/ │ ├── __init__.py │ ├── user.py │ └── car.py ├── api/ │ ├── __init__.py # 蓝图注册 │ ├── auth.py │ ├── cars.py │ └── favorites.py └── utils/ └── response.py这样做的好处是后期加功能、改逻辑不会把所有代码搅成一团。Flask SQLAlchemy JWT做这套系统是完全可行的只是要自己把“工程规范”立起来。但从效率和稳定性角度这类业务明确、管理端占大头、需要CRUD很多表的系统我个人更推荐Django。省下的时间可以拿去调前端、写文档、准备答辩。2.3 用PyCharm管理前后端工程的方式PyCharm在标题里出现不是没原因的很多同学就是用它写Python然后前端Vue又不知道放哪。我的做法是一个根目录放两个子工程结构长这样second_hand_car/ ├── backend/ # Django 或 Flask 后端 │ ├── manage.py │ ├── venv/ │ └── ... ├── frontend/ # Vue 前端 │ ├── package.json │ ├── vite.config.js │ └── src/ │ ├── api/ │ ├── views/ │ └── components/在PyCharm里打开根目录把backend的虚拟环境配置为项目的Python解释器前端直接用Terminal窗口跑npm命令。Django的服务启动可以配置一个Run Configuration类型选Python脚本填manage.py参数填runserver。这样点一下就能启动后端调试断点也方便。下一节先说数据库设计这是我反复提醒大家要最先做扎实的一步。3. 先设计好数据库后面能少改一半接口3.1 核心表结构与字段设计拿C2C模式为例Django里的模型主要就这几张表用户表。直接用Django自带的AbstractUser扩展加手机号和身份标记from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): phone models.CharField(max_length11, blankTrue) # 0-普通用户 1-管理员 role models.IntegerField(default0)车辆表是整个系统的核心字段要覆盖展示和筛选需求class Car(models.Model): STATUS_CHOICES ( (0, 待审核), (1, 在售), (2, 已售), (3, 下架), ) brand models.CharField(max_length32, verbose_name品牌) model_name models.CharField(max_length64, verbose_name车系/车型) price models.DecimalField(max_digits10, decimal_places2, verbose_name价格(万元)) guide_price models.DecimalField(max_digits10, decimal_places2, verbose_name新车指导价(万元)) mileage models.IntegerField(verbose_name表显里程(万公里)) car_age models.CharField(max_length16, verbose_name车龄) gearbox models.CharField(max_length16, verbose_name变速箱) fuel_type models.CharField(max_length16, verbose_name燃料类型) emission_standard models.CharField(max_length16, verbose_name排放标准) location models.CharField(max_length64, verbose_name所在地) register_date models.DateField(verbose_name上牌时间) description models.TextField(verbose_name车况描述) cover_image models.ImageField(upload_tocars/, verbose_name封面图) status models.IntegerField(choicesSTATUS_CHOICES, default0, verbose_name状态) view_count models.IntegerField(default0, verbose_name浏览次数) favor_count models.IntegerField(default0, verbose_name收藏次数) seller models.ForeignKey(User, on_deletemodels.CASCADE, related_namecars, verbose_name卖家) created_at models.DateTimeField(auto_now_addTrue)再设计收藏表和预约表。这两个是典型的多对多关系表。class Favorite(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE) car models.ForeignKey(Car, on_deletemodels.CASCADE) created_at models.DateTimeField(auto_now_addTrue) class Meta: # 防止同一用户重复收藏同一辆车 unique_together (user, car) class Appointment(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name预约人) car models.ForeignKey(Car, on_deletemodels.CASCADE, verbose_name车辆) name models.CharField(max_length32, verbose_name联系人) phone models.CharField(max_length11, verbose_name联系电话) appoint_time models.DateTimeField(verbose_name预约看车时间) remark models.TextField(blankTrue, verbose_name备注) status models.IntegerField(default0, verbose_name处理状态) created_at models.DateTimeField(auto_now_addTrue)这套表设计覆盖了前台浏览到预约的完整闭环管理端在这几张表上做操作也够用。3.2 三个容易踩坑的字段设计细节价格字段必须用DecimalField不能用FloatField。我在MySQL里跑过浮点价格字段显示19.99实际存成19.989999999999998列表页一展示全是超长小数很尴尬。DecimalField前端拿过去是字符串格式序列化的时候记得转一下Django REST Framework默认会做转换问题不大。图片上传字段的处理。ImageField需要配置MEDIA_ROOT和MEDIA_URL不然上传的文件不知道存哪。settings里要加MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media然后在根URL里加static和media的serve配置。开发环境下这样才能在浏览器里直接访问上传的图片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)车辆状态字段不要用布尔值。待审核、在售、已售、下架是四个状态用IntegerField加choices最直观。很多同学刚开始只用is_published布尔字段后面要加已售状态就傻眼了。4. 后端接口实现鉴权、检索、收藏与预约4.1 注册登录与JWT鉴权处理接口层用Django REST Framework来写认证方案直接选JWT。为什么不用Django默认Session因为前端是Vue走的是前后端分离接口。Session要管cookies、CSRF联调时处处不舒服。JWT一个token放请求头里就能搞定前端方便后端也省事。安装和相关配置pip install djangorestframework djangorestframework-simplejwt在settings里配置REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticatedOrReadOnly, ] }注册登录接口自己写一个视图。注册的核心是创建用户时用create_user而不是create这样密码才会加密from rest_framework.views import APIView from rest_framework.response import Response from rest_framework import status from .models import User class RegisterView(APIView): permission_classes [] def post(self, request): username request.data.get(username) password request.data.get(password) if not username or not password: return Response({msg: 用户名和密码不能为空}, statusstatus.HTTP_400_BAD_REQUEST) if User.objects.filter(usernameusername).exists(): return Response({msg: 用户名已存在}, statusstatus.HTTP_400_BAD_REQUEST) user User.objects.create_user(usernameusername, passwordpassword) return Response({msg: 注册成功, user_id: user.id}, statusstatus.HTTP_201_CREATED)登录直接用simplejwt提供的TokenObtainPairView或者自己写一个然后手动签发token。自己写可以顺带返回用户信息前端一次请求就能拿到需要的数据from rest_framework_simplejwt.tokens import RefreshToken from django.contrib.auth import authenticate class LoginView(APIView): permission_classes [] def post(self, request): username request.data.get(username) password request.data.get(password) user authenticate(usernameusername, passwordpassword) if not user: return Response({msg: 用户名或密码错误}, statusstatus.HTTP_400_BAD_REQUEST) refresh RefreshToken.for_user(user) return Response({ access: str(refresh.access_token), refresh: str(refresh), user: {id: user.id, username: user.username} })前端拿到access token后后续请求在请求头里带Authorization: Bearer 。这里有个常见问题后面排查部分再细说。4.2 车辆列表与多条件筛选接口筛选是二手车系统的核心功能做不好用户体验差很多。接口设计上我推荐用DRF的GenericAPIView加filter_queryset手动过滤比堆一大坨if判断更清晰。筛选参数通常包括brand、price_min、price_max、gearbox、mileage_max、status。车辆状态至少要加个过滤条件在售状态是1不然待审核的车也显示出来了。from rest_framework.generics import ListAPIView from rest_framework import filters from django_filters.rest_framework import DjangoFilterBackend from .models import Car from .serializers import CarListSerializer class CarListView(ListAPIView): queryset Car.objects.filter(status1) serializer_class CarListSerializer filter_backends [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter] filterset_fields [brand, gearbox, fuel_type] search_fields [brand, model_name, location] ordering_fields [price, mileage, created_at, view_count]价格区间和里程上限这种区间过滤用django-filter最简单import django_filters from .models import Car class CarFilter(django_filters.FilterSet): price_min django_filters.NumberFilter(field_nameprice, lookup_exprgte) price_max django_filters.NumberFilter(field_nameprice, lookup_exprlte) mileage_max django_filters.NumberFilter(field_namemileage, lookup_exprlte) class Meta: model Car fields [brand, gearbox, fuel_type, price_min, price_max, mileage_max]如果在Django里不想装django-filter也可以直接在list视图里重写get_querysetdef get_queryset(self): queryset Car.objects.filter(status1) brand self.request.query_params.get(brand) price_min self.request.query_params.get(price_min) price_max self.request.query_params.get(price_max) if brand: queryset queryset.filter(brandbrand) if price_min: queryset queryset.filter(price__gteprice_min) if price_max: queryset queryset.filter(price__lteprice_max) return queryset我实际开发时更推荐手写get_queryset因为检索逻辑一目了然答辩的时候讲起来也方便。接口开发好了之后前端用query参数拼URL就行例如/api/cars/?brand大众price_min8price_max20gearbox自动分页用DRF内置的分页类设置每页数量REST_FRAMEWORK { PAGE_SIZE: 12, }前端传page参数获取第几页响应里自带count、next、previous、results列表页的数据结构很舒服。4.3 车辆详情、收藏与预约的实现要点车辆详情接口要注意两点一是浏览量累加二是前端需要知道当前用户是否已经收藏了这辆车。浏览量累加有个并发坑点。用Car.objects.filter(idid).update(view_countF(view_count) 1)让数据库去做原子操作比先查出来加一再保存要安全。在高并发下后者会丢失更新。收藏接口from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db import IntegrityError from .models import Favorite, Car class FavoriteView(APIView): permission_classes [IsAuthenticated] def post(self, request, car_id): try: car Car.objects.get(idcar_id) except Car.DoesNotExist: return Response({msg: 车辆不存在}, status404) # 重复点击收藏时直接返回已收藏 if Favorite.objects.filter(userrequest.user, carcar).exists(): return Response({msg: 已收藏}) try: Favorite.objects.create(userrequest.user, carcar) Car.objects.filter(idcar_id).update(favor_countF(favor_count) 1) return Response({msg: 收藏成功}) except IntegrityError: return Response({msg: 已收藏}) def delete(self, request, car_id): deleted, _ Favorite.objects.filter(userrequest.user, car_idcar_id).delete() if deleted: Car.objects.filter(idcar_id).update(favor_countF(favor_count) - 1) return Response({msg: 已取消收藏})这里不用多余的if判断挡并发直接靠数据库的唯一约束兜底。另外注意收藏计数不要用遍历rownum再update这种方式F表达式一个SQL就搞定。预约接口比较直接验证用户已登录把车辆、联系人、电话、预约时间保存下来就行。后端要校验预约时间不能是过去时间from datetime import datetime class AppointmentView(APIView): permission_classes [IsAuthenticated] def post(self, request, car_id): car Car.objects.filter(idcar_id, status1).first() if not car: return Response({msg: 车辆不存在或已下架}, status400) appoint_time request.data.get(appoint_time) name request.data.get(name) phone request.data.get(phone) try: appoint_dt datetime.fromisoformat(appoint_time) except (ValueError, TypeError): return Response({msg: 预约时间格式不正确}, status400) if appoint_dt datetime.now(): return Response({msg: 预约时间不能早于当前时间}, status400) Appointment.objects.create( userrequest.user, carcar, namename, phonephone, appoint_timeappoint_dt, ) return Response({msg: 预约成功})在售状态判断要写否则下架的车也能被预约管理端会很混乱。4.4 后台管理与图片上传后台管理我直接用了Django自带的Admin给Car、Appointment、User这几个模型注册进去from django.contrib import admin from .models import Car, Appointment class CarAdmin(admin.ModelAdmin): list_display (id, brand, model_name, price, mileage, status, seller, created_at) list_filter (status, brand) search_fields (brand, model_name) list_editable (status,) admin.site.register(Car, CarAdmin) admin.site.register(Appointment)list_editable里放status管理员在列表页直接下拉改审核状态、上下架状态不用点进详情页。这个细节对管理效率提升很明显。图片上传接口在C2C模式下必须提供给卖家使用。前端通过multipart/form-data提交图片文件后端接收后保存from rest_framework.parsers import MultiPartParser class CarImageUploadView(APIView): permission_classes [IsAuthenticated] parser_classes [MultiPartParser] def post(self, request): cover_image request.FILES.get(file) if not cover_image: return Response({msg: 请选择文件}, status400) if cover_image.size 5 * 1024 * 1024: return Response({msg: 图片不能超过5M}, status400) # 生成新的文件名避免中文名或特殊字符出问题 import uuid ext os.path.splitext(cover_image.name)[1] cover_image.name f{uuid.uuid4().hex}{ext} car Car.objects.create( sellerrequest.user, cover_imagecover_image, ... ) return Response({car_id: car.id})多图上传的策略是主图走Car表里的cover_image字段更多图片可以建一张CarImage表关联Car一对一或者一对多都行。简单项目一张封面图就够答辩演示效果也能撑住。5. 前端Vue开发页面结构、数据联调与细节处理5.1 页面路由与组件划分前端用Vue 3加Vite初始化工程路由用vue-routerUI组件库用Element Plus。项目推荐开发命令npm create vitelatest frontend -- --template vue cd frontend npm install npm install vue-router4 axios element-plus pinia路由划分直接对照后端接口// src/router/index.js import { createRouter, createWebHistory } from vue-router const routes [ { path: /, name: Home, component: () import(../views/Home.vue) }, { path: /cars, name: CarList, component: () import(../views/CarList.vue) }, { path: /cars/:id, name: CarDetail, component: () import(../views/CarDetail.vue) }, { path: /login, name: Login, component: () import(../views/Login.vue) }, { path: /register, name: Register, component: () import(../views/Register.vue) }, { path: /favorites, name: Favorites, component: () import(../views/Favorites.vue), meta: { requiresAuth: true } }, { path: /profile, name: Profile, component: () import(../views/Profile.vue), meta: { requiresAuth: true } }, ] const router createRouter({ history: createWebHistory(), routes, }) router.beforeEach((to, from, next) { const token localStorage.getItem(access_token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } }) export default routerrequiresAuth配合路由守卫做登录拦截比每个页面里自己判断是否登录要整洁得多。组件层面复用CarCard、FilterBar、PaginationBar这类公共组件。5.2 axios请求封装与token携带axios封装是前端联调的基础。我通常在src/api/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 }) // 响应拦截器统一处理错误 request.interceptors.response.use( response response.data, error { if (error.response) { if (error.response.status 401) { ElMessage.error(登录已过期请重新登录) localStorage.removeItem(access_token) router.push(/login) } else { ElMessage.error(error.response.data?.msg || 请求失败) } } else { ElMessage.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default request局部刷新token的功能我没写进来因为很多项目可能没有用refresh token。但实际部署时建议加上401时尝试拿refresh token换新的access token换成功则重放原请求这样用户使用体验会好很多。5.3 跨域与本地代理配置前后端分离开发时前端跑在5173端口后端跑在8000端口直接请求会跨域。开发阶段我不建议在后端开CORS然后前端写绝对地址而是用Vite的代理让前端发请求时走同源。配好后浏览器里看网络请求全是相对路径后端也无所谓CORS配置很多麻烦直接消失。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://127.0.0.1:8000, changeOrigin: true, } } } })后端Django的URL里把接口统一挂在/api前缀下代理配置就干干净净。注意target地址写成127.0.0.1不要写成localhost我遇到过因为本机DNS解析关系导致代理偶尔不生效的怪问题换成127.0.0.1就稳定了。如果后端的接口路由没有/api前缀可以加一个配置项from django.urls import path, include urlpatterns [ path(api/, include(cars.urls)), path(api/, include(users.urls)), ]这样不仅前端代理简单未来部署到nginx时也能省很多事。5.4 几个前端交互细节车辆列表页的筛选联动用户先选品牌再选价格区间点击搜索后重新请求列表接口页码重置为1。这里要注意重置页码不然从第5页搜索出来的结果可能为空。价格区间统一定义为数组前端用el-slider或el-select做选择参数名要和后端约定好。车辆详情页的收藏按钮状态回显依赖详情接口返回is_favorite字段。序列化器里这样处理class CarDetailSerializer(serializers.ModelSerializer): is_favorite serializers.SerializerMethodField() class Meta: model Car fields __all__ extra_kwargs {description: {required: False}} def get_is_favorite(self, obj): user self.context[request].user if not user.is_authenticated: return False return Favorite.objects.filter(useruser, carobj).exists()有登录状态就查没登录默认false游客访问详情页不会报错。价格格式化显示JS端统一处理export function formatPrice(price) { return Number(price).toFixed(2) }后端返回的Decimal到了前端可能是字符串直接展示会带多余的0顺手格式化一下界面观感好很多。6. PyCharm环境下从零跑通整个项目6.1 创建虚拟环境并安装后端依赖后端依赖管理我推荐requirements.txt在PyCharm终端里操作cd backend python -m venv venv # Windows venv\Scripts\activate # macOS / Linux source venv/bin/activate pip install django djangorestframework djangorestframework-simplejwt django-cors-headers pillow pip freeze requirements.txtvenv虚拟环境必须创建不然依赖会装进全局Python换项目就打架。PyCharm里配置解释器时选择Existing environment指到backend/venv目录Terminal也会自动激活。pillow必须装Django的ImageField依赖它处理图片忘装的话migrate阶段就会报错这是新手高频问题之一。6.2 初始化Django工程与数据库配置创建项目和应用django-admin startproject config . python manage.py startapp users python manage.py startapp cars这里我把项目配置目录命名为config而不是项目名是习惯了短命名输入命令更省事。其实都行关键是settings里的INSTALLED_APPS要注册appINSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, users, cars, ]数据库方面本地演示用SQLite就够提交到GitHub还是演示给别人看都不需要额外安装数据库软件。如果你想用MySQLsettings改成DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: second_car, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, }, } }用MySQL记得先建库并指定utf8mb4不然前端展示中文可能乱码CREATE DATABASE second_car DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;迁移和创建管理员python manage.py makemigrations python manage.py migrate python manage.py createsuperuser python manage.py runserver6.3 前端工程初始化与联调前端放在frontend目录cd frontend npm install npm run devVite默认打开5173端口PyCharm里再开一个Terminal标签跑后端两个进程互不干扰。改Django代码后runserver自动热重载前端代码改动Vite也自动刷新。一个容易忽略的点package.json的scripts里要检查一下dev和build命令是否存在用Vite模板初始化的话默认都有。如果你在Windows上npm安装很慢可以临时使用国内镜像源npm config set registry https://registry.npmmirror.com装完再恢复官方源也来得及或者直接保持镜像源个人开发问题不大。7. 看图填坑常见问题与排查实录7.1 高频问题速查表这些是我在实际开发、指导同学时反复遇到的问题整理成表方便你对照排查。问题现象可能原因处理方式前端请求成功但控制台报跨域错误使用绝对地址访问后端Vite代理未生效改为相对路径/api配好proxy登录接口返回账号密码正确但401token没带或格式不对请求头必须为Authorization: Bearer xxxDjango后台进不去createsuperuser没有执行执行之后访问/admin上传图片后访问图片URL 404MEDIA_URL没配置或没有serve mediasettings加MEDIA_URL/MEDIA_ROOT根URL加载static中文在MySQL里显示乱码库或表字符集不是utf8mb4建库时指定utf8mb4表已建议就ALTER价格显示很多位小数字段用了FloatField改用DecimalField历史数据需要重新迁移列表页没有数据车辆状态不等于1被状态过滤掉了确认status1或给管理员保留status过滤参数前端路由刷新后404用history模式但部署环境没配置本地开发用createWebHistory没有问题部署时nginx配置try_files7.2 我在接项目时最常提醒的三个坑第一个坑是后台管理权限完全没做。很多人的管理端只靠Django Admin但Admin的登录和前端用户体系是两套。如果管理员和普通用户都是同一个User表那用普通用户账号访问不了Admin。最简单的处理是给用户role字段加判断自定义一个staff权限函数或者在创建超管时再单独create一个管理员账号给管理端用。答辩演示时管理员账号和用户账号分开老师看起来也清晰。第二个坑是图片上传的路径和文件命名。直接拿用户上传的原始文件名保存很容易遇到中文文件名和路径分隔符问题。我用uuid重命名文件之后再存储稳妥很多。前端回显图片时用Django序列化返回的完整URL不要直接拼字符串因为MEDIA_URL可能会变。第三个坑是车辆关联卖家时外键的序列化。前端需要展示卖家昵称或联系方式但Car里只有seller_id序列化器里如果直接返回id前端还得再查一次用户接口。建议在Car序列化器里加seller_name字段class CarDetailSerializer(serializers.ModelSerializer): seller_name serializers.CharField(sourceseller.username, read_onlyTrue) class Meta: model Car fields (id, brand, model_name, price, mileage, seller_name, ...)这样前端一次请求拿到所有展示数据减少一次接口往返页面加载也更快。最后再分享一个我个人的习惯在动手写代码之前先把所有前端页面的路由、后端接口的URL、每个接口的返回字段列一张表贴在自己项目目录的README里。前后端约定好字段名联调阶段能省掉大量来回沟通的时间。做完这个二手车系统你如果还想扩展可以加一个“车辆对比”功能、或者“按销量排序的推荐位”核心骨架不变后面都是加表加接口的事。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑