news 2026/11/5 3:49:36

Django+Vue3全栈实战:从零开发校园二手书交易平台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+Vue3全栈实战:从零开发校园二手书交易平台

先说个真实背景。我去年年底动手做这个二手图书交易系统,起因很朴素——学校课程群里每天都有学长学姐刷屏卖考研资料和教材,几十条消息翻下来,不是被一句"出了"截胡,就是根本分不清谁在卖哪本。图书这种低频买卖,用聊天流去做效率实在太低。于是我想自己做一个能搜、能筛、能下单的校园二手书平台,后端接口用 Django 写,前端页面用 Vue3 搭,前后端一分离,很多东西就顺了。

这个项目做完之后,我最大的感受是:它特别适合作为 Django + Vue3 全栈实战的入门项目。原因有三:一是业务流程完整,从注册登录、图书发布、列表搜索到订单状态流转,横跨了 Web 开发最典型的功能面;二是前后端边界清楚,Django 只出接口、Vue3 只做交互,每一层都能独立测试;三是坑足够典型,跨域、认证、图片上传、token 过期这些实战问题你都会挨个踩一遍。如果你正在学 Django 或 Vue3,想找一个能完整复现的练手项目,这篇应该能省你不少调试时间。

1. 项目定位与架构拆解:为什么是 Django + Vue3 的组合

1.1 校园二手书交易的核心业务流程

动手写代码之前,我先把业务流程画了一遍。二手书交易和普通电商有差异:书有具体版本、新旧程度、笔记量这些属性,交易双方通常都是同一个校园的人,买家更在意"这书是不是我要的那版"而不是"物流几天能到"。

所以核心流程落在五个环节上:

  • 用户注册登录,区分买家和卖家身份,但一个账号可以既是买家也是卖家;
  • 图书发布,卖家上传书名、作者、ISBN、原价、售价、新旧程度、实物照片;
  • 图书浏览与搜索,买家按书名、作者、ISBN 搜索,也可以按分类筛选;
  • 下单交易,买家对某本书下单,订单状态从"待处理"流转到"已成交"或"已取消";
  • 收藏管理,买家把感兴趣的书先收藏,方便回头再看。

这个流程不高大上,但每一个环节都对应数据库里的实体关系。我把表设计成五张核心表:用户表、图书表、订单表、收藏表、分类表。后面编码的所有逻辑,都是围绕这五张表的增删改查展开。

1.2 前后端分离架构的选型理由

有人可能会问:校园内部项目,用 Django 模板加 Bootstrap 一把梭不更简单吗?为什么非要拆成前后端两个工程?

我一开始也犹豫过,但实际对比后发现,前后端分离对这个项目的收益更大。

第一,Vue3 的组件化确实提升开发效率。图书列表、图书卡片、分页器、状态标签这些是可以复用的界面单元,组件化之后改一处样式全站生效。如果全用 Django 模板,每个页面都要单独写一份 HTML,后期调整很费劲。

第二,前后端分离给扩展留了后路。这个项目做完之后,如果想让用户通过小程序下单,前端只需要用 uni-app 重新写一套界面,后端 API 几乎不用动。一旦你打算做多端产品,接口层和后端逻辑的复用价值会指数级上升。

第三,分开部署之后职责更清晰。Django 处理业务逻辑和数据持久化,Vue3 只管渲染和交互,出了问题排查范围明显收窄。日志一翻就能知道是接口挂了还是页面逻辑挂了。

1.3 技术栈版本选择

版本选择上,我用了当时相对稳定的一套组合:

  • 后端:Python 3.11 + Django 4.2 + Django REST Framework 3.14
  • 前端:Node.js 18 + Vue3.4 + Vite 5 + Element Plus + Pinia + Vue Router
  • 数据库:SQLite(开发阶段) / PostgreSQL(生产环境)
  • 认证方案:Django REST Framework SimpleJWT(JSON Web Token)

这里有个经验供参考:Django 版本不必追新,4.2 是 LTS,官方支持时间长,生态兼容性也更好。Vue3 这边则建议直接用最新的稳定版,因为 Vue3 生态迭代很快,新项目没理由用旧版本。

2. 后端 Django 起步:从环境搭建到数据模型设计的实操细节

2.1 虚拟环境与依赖安装

后端的第一个坑就在环境上。我用python -m venv venv创建虚拟环境,激活后安装依赖。顺序很重要:先把虚拟环境激活再安装,不然包会装进全局环境,后续部署会乱。

安装依赖就四个核心包:

pip install django djangorestframework djangorestframework-simplejwt django-cors-headers Pillow

我说一下为什么是这几个包:

  • djangorestframework是提供 API 能力的核心库,序列化、路由、视图集都是它给的;
  • djangorestframework-simplejwt负责 JWT 登录认证,这样前端拿到 token 后放在请求头里即可;
  • django-cors-headers解决前后端分离开发时的跨域问题;
  • Pillow是 Django 处理图片上传的依赖,图书封面图必须有它。

如果下载慢,就换国内镜像源,pip install -i https://pypi.tuna.tsinghua.edu.cn/simple 包名,这个不是必须的选项,但能省很多等待时间。

2.2 项目与应用结构

我通常用django-admin startproject secondhand_book创建项目,再用python manage.py startapp books、startapp users、startapp orders创建三个应用。应用划分的逻辑是"按业务域切分":books 管图书、收藏、分类,orders 管订单,users 管用户相关逻辑。虽然默认的 User 模型可以满足基础需求,但我自定义了用户表,加上了phone、avatar等字段,方便扩展。

应用创建完,必须去settings.py的INSTALLED_APPS里注册,这一步漏掉的话,makemigrations时模型根本不生效。常见新手错误就是创建了 app 但忘了注册,然后一脸茫然地来问为什么数据库没有表。

2.3 核心模型设计:字段怎么定才合理

图书表是整个系统最核心的模型。我设计了这些字段:

class Book(models.Model): CONDITION_CHOICES = [ ('new', '全新'), ('like_new', '近全新'), ('good', '良好'), ('acceptable', '有笔记划线'), ] title = models.CharField(max_length=200, verbose_name='书名') author = models.CharField(max_length=100, verbose_name='作者') publisher = models.CharField(max_length=200, blank=True, verbose_name='出版社') isbn = models.CharField(max_length=20, blank=True, verbose_name='ISBN') price = models.DecimalField(max_digits=7, decimal_places=2, verbose_name='售价') original_price = models.DecimalField(max_digits=7, decimal_places=2, blank=True, null=True, verbose_name='原价') condition = models.CharField(max_length=20, choices=CONDITION_CHOICES, default='good', verbose_name='新旧程度') description = models.TextField(blank=True, verbose_name='描述') cover = models.ImageField(upload_to='covers/%Y/%m/', blank=True, verbose_name='封面图') seller = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='books', verbose_name='卖家') status = models.CharField(max_length=20, choices=[('on_sale', '在售'), ('sold', '已售出'), ('off_shelf', '下架')], default='on_sale') created_at = models.DateTimeField(auto_now_add=True)

字段设计有几个关键判断:

价格用DecimalField而不是FloatField,因为浮点数在计算机里是二进制的,会出现 0.1 + 0.2 不等于 0.3 的问题,涉及钱必须用定点数。

on_delete=models.CASCADE表示卖家账号删除时,他发布的图书也一起删除。但在订单表里我用了PROTECT,因为订单属于交易凭证,不能因为买家或卖家删号就悄悄消失。

related_name='books'是给反向查询用的,设置后可以用seller.books.all()查某人发布的全部图书,不用再写Book.objects.filter(seller=xxx),省事很多。

订单表里我加了一个order_no字段,用 UUID 生成唯一订单号。这个字段在人工对账的时候特别好用,因为自增主键很容易暴露订单量,而且不直观。

2.4 ORM 查询与删除对象:几个容易翻车的细节

关于"django 执行查询-删除对象",网上一搜能出来一堆问答,但我想分享几个自己真实踩过的坑。

第一个坑是get和filter的混用。Book.objects.get(id=xxx)在记录不存在时会抛DoesNotExist异常,如果不在视图里捕获,前端就会收到 500。更稳妥的写法是:

book = Book.objects.filter(id=book_id).first() if not book: return Response({'detail': '图书不存在'}, status=status.HTTP_404_NOT_FOUND)

filter().first()找不到时返回None,不会抛异常,适合"找不到就返回 404"的场景。而当你确定记录一定存在、且数据库有唯一约束时,用get更直接,但外面的try/except不能省。

第二个坑是delete()的级联行为。Django 的CASCADE是很方便的,但也容易误伤。比如你删掉一本书,所有跟这本书关联的收藏记录、订单记录会一并删除。如果订单表的外键用的也是CASCADE,那意味着"卖家下架图书"和"删除图书"是两个完全不同的操作——前者只是改status,后者是物理删除且连带清空交易记录。所以我在订单表里用PROTECT来挡住误删:只要有订单引用了这本书,删除操作就会被拦截并抛ProtectedError。

第三个坑是delete()的返回值。很多人写book.delete()之后就走了,没想过确认是否真的删干净了。其实delete()会返回一个元组:(被删除的总数, {'app_label.Model': 数量})。调试时把返回值打出来,能清晰看到级联删了哪些表的数据,强烈建议在删除敏感数据时看一眼。

数据迁移方面,python manage.py makemigrations和python manage.py migrate是每天的固定动作。要注意一点:迁移文件生成后不要急着删改,特别是在多人协作时,迁移文件就是数据库版本的提交记录,随便删掉会造成各环境之间无法同步。

3. 用 DRF 把业务闭环变成可用的 API

3.1 序列化器设计:嵌套展示与写入校验

模型建好之后,接下来是用 Django REST Framework 把它变成 API。我理解的 DRF 核心价值其实就一句话:把模型数据转换成 JSON 给前端用,同时把前端传回的 JSON 校验后写进数据库。

图书序列化器我这样写的:

class BookSerializer(serializers.ModelSerializer): seller_name = serializers.CharField(source='seller.username', read_only=True) condition_display = serializers.CharField(source='get_condition_display', read_only=True) class Meta: model = Book fields = ['id', 'title', 'author', 'publisher', 'isbn', 'price', 'original_price', 'condition', 'condition_display', 'description', 'cover', 'seller', 'seller_name', 'status', 'created_at'] read_only_fields = ['seller', 'status', 'created_at']

两个关键点:

一是seller_name。图书列表页需要展示卖家的用户名,如果只返回seller这个外键 id,前端还得再请求一次用户接口才能拿到名字。用source='seller.username'直接在序列化的时候取关联字段,前端拿到的数据就是完整的。

二是read_only_fields。seller字段不能在创建时由前端随意指定,否则用户可以伪造卖家身份,必须由后端从当前登录用户中自动填入。status也不该由用户传,上架默认是'on_sale'。

创建图书时,我在 ViewSet 的perform_create里写:

def perform_create(self, serializer): serializer.save(seller=self.request.user)

这样卖家身份始终来自 token,而不是来自请求体。

3.2 ViewSet 与路由:用最少的代码覆盖最多的接口

DRF 的ModelViewSet是效率利器。一套完整的图书增删改查接口,用普通视图至少要写 8 个函数,用 ViewSet 只需要继承出来然后注册即可:

from rest_framework.viewsets import ModelViewSet class BookViewSet(ModelViewSet): queryset = Book.objects.filter(status='on_sale').select_related('seller') serializer_class = BookSerializer def get_queryset(self): if self.request.user.is_staff: return Book.objects.all() return self.queryset

路由注册也很简单:

router = DefaultRouter() router.register(r'books', BookViewSet, basename='book') urlpatterns += router.urls

然后GET /api/books/、GET /api/books/1/、POST /api/books/、PUT /api/books/1/、DELETE /api/books/1/这些接口就全部自动可用了。

但这里有个隐藏问题:默认的get_queryset直接写在类属性上,普通用户访问的时候,会把已售出的书也查出来。所以get_queryset里做了区分——非管理员只能看到"在售"状态的图书,管理员可以看全部。这个逻辑如果漏掉,就会出现大量已成交的书挂在列表上,界面很乱。

3.3 认证与权限:JWT 方案在 Web 项目里的实际做法

登录认证我选了 JWT。相比 Django Session,JWT 的好处是无状态,后端不需要存 session 记录,前端把 token 放在Authorization: Bearer xxx头里即可。这个项目后续如果要接小程序,JWT 可以无缝复用。

配置分三步:

第一步,在settings.py里加:

REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'rest_framework_simplejwt.authentication.JWTAuthentication', ], 'DEFAULT_PERMISSION_CLASSES': [ 'rest_framework.permissions.IsAuthenticatedOrReadOnly', ], 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', 'PAGE_SIZE': 12, }

第二步,路由注册登录接口:

from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView urlpatterns = [ path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), ]

第三步,在需要登录才能操作的接口上加权限。比如发布图书的 ViewSet,我在get_permissions里做了区分:列表和详情允许匿名访问,创建、修改、删除必须登录:

def get_permissions(self): if self.action in ['create', 'update', 'partial_update', 'destroy']: return [IsAuthenticated()] return [AllowAny()]

这样做的好处是,游客可以逛书城,但想卖书就得登录。逻辑用起来很顺手,前端也只需要在axios拦截器里统一带上 token,不需要为每个接口单独处理。

3.4 搜索、过滤与自定义动作:接口如何贴近页面需求

图书列表页最需要的三个能力,我用 DRF 的过滤组件实现了:

from rest_framework import filters class BookViewSet(ModelViewSet): filter_backends = [filters.SearchFilter, filters.OrderingFilter] search_fields = ['title', 'author', 'isbn'] ordering_fields = ['price', 'created_at']

search_fields一配,前端请求GET /api/books/?search=考研数学,Django 就会在书名、作者、ISBN 三个字段里做模糊匹配,完全不用自己写Q查询。

除了基础的 CRUD,业务里还有几个特殊动作,比如"我发布的图书"和"我收藏的图书"。这两个接口用@action装饰器在 ViewSet 里扩展,比单独写视图更内聚:

from rest_framework.decorators import action from rest_framework.response import Response class BookViewSet(ModelViewSet): @action(detail=False, methods=['get'], permission_classes=[IsAuthenticated]) def mine(self, request): books = Book.objects.filter(seller=request.user) page = self.paginate_queryset(books) serializer = BookSerializer(page, many=True) return self.get_paginated_response(serializer.data) @action(detail=True, methods=['get']) def similar(self, request, pk=None): book = self.get_object() similar_books = Book.objects.filter(category=book.category, status='on_sale').exclude(id=book.id)[:6] serializer = BookSerializer(similar_books, many=True) return Response(serializer.data)

这样一来,前端要的数据接口就齐了。我还配了一个"收藏"功能,前端只需要调收藏列表接口和切换收藏状态的接口即可,详情页右上角的星标按钮就靠它工作。

4. Vue3 前端:从创建项目到核心页面实现

4.1 Composition API 和 Options API 怎么选

后端接口齐了之后,我开始写 Vue3 前端。这时第一个绕不开的问题,就是 Composition API 和 Options API 选哪个。

热搜词里"vue3 composition api 和 option api"排在前面是有原因的,这是每个 Vue3 初学者都要面对的第一个选择题。我的结论是:新项目直接用 Composition API,尤其是<script setup>语法。

为什么?因为这个项目的页面逻辑并不简单——图书列表页需要请求数据、处理分页、处理搜索;详情页需要请求图书详情、判断登录状态、切换收藏;订单中心需要按状态切换列表。如果用 Options API,每个逻辑单元的数据、方法、生命周期钩子会被拆到不同的代码块里,页面一复杂,跳来跳去很难受。

用 Composition API 的<script setup>,同一段业务逻辑可以聚在一起:

<script setup> import { ref, onMounted } from 'vue' import { getBookDetail } from '@/api/book' const book = ref(null) const loading = ref(false) const fetchDetail = async () => { loading.value = true try { book.value = await getBookDetail(props.id) } finally { loading.value = false } } onMounted(fetchDetail) </script>

数据、加载状态、请求逻辑都在同一块,阅读和修改的成本都低。而且数据刷新逻辑可以自然定义为fetchDetail函数,需要重新加载时直接调用一次即可,不需要像 Options API 那样绕方法名。

4.2 ref 和 reactive 的正确使用姿势

写 Vue3 时"ref 万能对象"这个说法很流行,但我实际用下来发现,它在引用场景下确实很方便——ref内部本身就是用reactive实现的,你传一个对象进去,它还是会被转换成响应式代理对象。不过这不代表ref可以无脑替代reactive。

我的选择标准很简单:

  • 基础类型、需要模板里用v-model绑定的值、需要整个替换的值,用ref。
  • 深层嵌套的对象、一组相互关联的字段,用reactive。

比如图书详情页,一份图书数据有书名、价格、作者、描述等十几个字段,我会直接写:

<script setup> import { reactive } from 'vue' const form = reactive({ title: '', author: '', publisher: '', price: 0, original_price: 0, condition: 'good', description: '' })

这样在模板里写form.title、form.author,数据结构跟后端字段一一对应,语义清晰。如果这里用ref,模板里写form.title尚且可以,但一旦你写form.value.title,代码就会很啰嗦,还容易漏掉.value。

列表页的分页参数则用ref:

const page = ref(1) const total = ref(0) const bookList = ref([])

翻页时直接给page.value++赋值,简洁且不会出错。用reactive的话,会出现state.page++,区别不大,但多包一层总是更绕。

一个容易踩的小坑:用reactive赋值整个对象时,Object.assign会丢失响应性吗?不会,但要小心直接替换引用,比如state = newState,那会丢掉响应式代理。正确写法是展开合并:

Object.assign(state, newState)

4.3 接口层封装与 axios 拦截器

Vue 页面里不要到处请求接口。我习惯建一个src/api/目录,每个业务模块一个文件,比如book.js、order.js、user.js。这样页面只调函数,不直接碰 axios,管理起来非常清楚。

axios 实例的封装这点很重要,我贴一下核心代码:

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?.status === 401) { localStorage.removeItem('access_token') router.push('/login') } ElMessage.error(error.response?.data?.detail || '请求失败') return Promise.reject(error) } )

baseURL: '/api'配合 Vite 代理,开发环境请求/api/books会被自动转发到 Django 的 8000 端口,避免跨域问题的同时,还可以在请求头里开心地带上 cookie。生产环境部署时,再由 Nginx 做同样的事。

401 拦截的逻辑很关键:token 过期后,后端的响应状态码是 401,axios 拦截器统一清理本地 token 并跳回登录页。这样前端不用在每个请求方法里写重复的过期处理。

4.4 核心页面拆解:列表、详情、发布、订单中心

图书列表页是这个项目的前端核心页面。结构上,顶部一个搜索框,下面一列图书卡片,底部一个分页器。搜索逻辑要防抖,不能每次敲一个字母就发一次请求。我用了一个简单的 300ms 防抖函数:

import { ref, watch } from 'vue' const searchText = ref('') const bookList = ref([]) watch(searchText, debounce(() => { fetchBooks() }, 300))

这个防抖很必要,不然用户输入"考研数学"四个字的过程中,会触发四次不必要的请求。

详情页的数据加载要注意竞态:用户快速在列表页点击,进入详情页后接口还没返回,又返回了列表页,这时响应回来可能已经污染了页面。我用了AbortController或者简单地用"详情 id 是否改变"来判断是否应用这次响应,从根本上避免旧请求覆盖新页面。

发布图书页的重点是表单校验。价格输入我用InputNumber,限制最小值为 0.01;封面图上传用el-upload,上传前设置auto-upload: false,等用户点提交时再把文件放进FormData一起 POST。这样用户可以先预览填好的信息,不会因为选一张图就立刻上传,省了很多"传完后悔"的流量浪费。

订单中心用的则是 Tab 切换方式,按状态分待处理、已成交、已取消三个页签。每个 Tab 对应一个订单列表接口,切换时重新拉数据。这里要注意,订单列表接口要同时展示买家身份或卖家身份的相关字段,所以我前端的订单卡片上同时显示了书名、价格、交易对象用户名、下单时间,方便买卖双方确认。

4.5 Element Plus 按需引入

Element Plus 是 Vue3 生态里最好上手的组件库,但我一开始全量引入,打包体积直接超过 1MB,首屏加载很吃力。后来改成按需引入,用官方推荐的unplugin-vue-components和unplugin-auto-import自动按需加载,配置是在vite.config.js里加插件,然后组件就可以在模板里直接写,不用手动 import,体积也降到了 400KB 左右。这个优化对提升用户体验很明显,建议做正式项目时直接用按需引入。

5. 前后端联调中的实战问题:跨域、图片、token 过期与部署检查

5.1 CORS 配置:开发环境的真实误区

前后端分离开发时,跨域是第一个绕不开的问题。很多新手直接给django-cors-headers全放行:

CORS_ALLOW_ALL_ORIGINS = True

然后发现确实能通了,就放着不管。但这样配置在生产环境等于裸奔,任何人都可以用其他域名来请求你的接口。我的做法是开发环境明确指定 Vite 的地址,生产环境指定线上域名:

CORS_ALLOWED_ORIGINS = [ 'http://localhost:5173', 'http://127.0.0.1:5173', 'https://yourdomain.com', ]

另外,CORS_ALLOW_CREDENTIALS = True这个配置要看情况开。如果你用 JWT 存 localStorage,其实不需要启用 cookies 跨域,这个开关可以关掉。开了它反而会遇到"不允许携带凭据的跨域请求"之类的困扰。

还有一个小坑:django-cors-headers必须在MIDDLEWARE里放在尽量靠前的位置,而且要放在CommonMiddleware前面,否则某些请求头根本不会被处理到。我的MIDDLEWARE里把corsheaders.middleware.CorsMiddleware放到了最顶上。

5.2 图片上传与静态文件:开发与生产两条路

图片上传在后端需要两处配置:

MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'

然后在项目的urls.py里,开发阶段用 Django 自带的静态服务:

from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

这个配置只适合 DEBUG 模式。生产环境我用了 Nginx 直接处理/media/路径下的文件,Django 只负责接收上传、写入磁盘、把 URL 存数据库,Nginx 负责把图片文件吐给浏览器。这样做的原因是,Django 处理静态资源的能力本来就很弱,并发一高就卡,没必要让它干这个活。

前台上传图片时还有个费时间的坑:如果用 axios 默认的Content-Type: application/json,后端收到的request.FILES一定是空的。必须自己组装FormData,并且不要手动设置 Content-Type,让浏览器自动带上 boundary:

const formData = new FormData() formData.append('cover', file) formData.append('title', form.title) // 其他字段 const response = await request.post('/books/', formData)

如果手动设置Content-Type: application/json,文件流就废了。

5.3 路由守卫与登录态:前端如何配合 JWT

订单中心、发布图书页这些需要登录才能访问。我在 Vue Router 的meta里加了requiresAuth标记,然后在全局前置守卫里统一判断:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('access_token') if (to.matched.some(record => record.meta.requiresAuth) && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })

登录成功之后,跳回redirect参数指定的地址,用户不会因为登录就丢失原本想去的页面,这个细节体验提升很大。

还要注意的一点是:localStorage.getItem('access_token')不能作为判断登录的唯一依据,因为 token 可能在过期后还躺在本地。所以需要在前端拦截器里配合 401 处理逻辑:请求返回 401 时清掉 token 并跳转登录页,这样即使用户本地有 token,但已经失效了,也会被正确引导到重新登录。

5.4 上线部署前的检查清单

项目全部做完后,部署上线前我踩着坑整理了一份检查清单,在这里直接分享:

  • DEBUG必须改成False,不然错误信息会直接暴露给访问者,里面可能包含服务器路径和数据库信息;
  • 执行python manage.py collectstatic,把静态文件收集到指定目录,否则 Django 管理后台样式会丢失;
  • 数据库备份:python manage.py dumpdata或者直接用 PostgreSQL 的pg_dump,上线前备份一次,防止中途出问题;
  • 跨域配置改成生产域名,CORS_ALLOW_ALL_ORIGINS一定要关;
  • 用gunicorn跑 Django 服务而不是runserver,runserver是开发服务器,性能和并发处理能力都不够;Nginx 反向代理到 gunicorn,同时处理/media/静态文件;
  • 前端npm run build打包后,把dist目录放到 Nginx 静态目录下,并配置proxy_pass把/api/请求转发到 gunicorn。

这份清单在我后面的好几个项目里都还在用,每次部署前过一遍,能少踩很多不必要的坑。

回到最开始的问题:为什么我说这个项目值得完整做一遍?因为二手图书交易系统本质上是一套"业务 + 认证 + 静态资源 + 部署"的全链路闭环。你做的虽然是图书,但登录、发布、订单、状态流转这些逻辑,换一个电商场景依然适用。而且 Django 和 Vue3 都是现在 Web 开发里需求很大的技术栈,做完一个能跑通的项目,比刷一百道题都有用。

最后分享一个小技巧:开发这种前后端分离项目时,前端先把所有接口注释掉,用 mock 数据把页面和交互调通,再接真接口。这样你能快速分清一个 bug 到底是前端问题还是后端问题,联调阶段会轻松非常多。我在这个项目里就是用这个方式,把联调时间压缩到了两天。如果你的项目时间紧,强烈建议也试试这个办法。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/30 9:57:51

VS Code 1.110 升级:Usages与Rename让代码重构更安全高效

前一周我刚刚经历了一次让我非常狼狈的线上事故。起因是一句看起来再普通不过的“全局替换”&#xff1a;把一个给外部 SDK 调用的公共方法从旧名字批量改成新名字。我本以为 CtrlShiftF 搜索后逐个替换已经足够谨慎&#xff0c;结果字符串拼接、序列化字段、测试夹具里的旧名字…

作者头像 李华
网站建设 2026/11/4 3:44:18

Photon Fusion 2自定义身份验证实战:从匿名连接到账号体系

去年在做一款基于 Fusion 2 的多人联机项目时&#xff0c;我在自定义身份验证&#xff08;Custom Authentication&#xff09;上整整折腾了两天。官方 Demo 默认是匿名连接&#xff0c;拿个 AppId 就能进房间&#xff0c;但真要对接账号体系、做封号处理、同步玩家数据&#xf…

作者头像 李华
网站建设 2026/10/29 21:19:31

发那科机器人二次开发:C#读写数据与点位信息获取实战

简介&#xff1a;这份资源面向从事工业自动化与机器人二次开发的C#开发者&#xff0c;聚焦发那科&#xff08;FANUC&#xff09;机器人的数据读写与点位信息获取。通过调用发那科机器人控制SDK中的API&#xff0c;开发者可读取机器人当前位置、速度、加速度等关键参数&#xff…

作者头像 李华
网站建设 2026/10/27 22:43:34

飞行轨迹预测实战:双向LSTM与GRU选型及数据切分避坑

简介&#xff1a;面向本科及以上学历、有一定MATLAB基础的开发者&#xff0c;提供一套完整的飞行轨迹预测实验方案&#xff0c;涵盖双向LSTM与GRU两种神经网络的建模、训练、评估与可视化。压缩包共18个文件&#xff0c;大小约319KB&#xff1b;核心为6个.m脚本&#xff0c;包括…

作者头像 李华
网站建设 2026/10/27 9:41:52

双NPN三极管恒流源设计:低成本高精度方案与温漂补偿实战

恒流源这个东西&#xff0c;说简单也简单&#xff0c;说讲究也是真讲究。一个运放加一个MOS管能搭&#xff0c;一个基准源加一个三极管也能搭&#xff0c;但真要把成本压到几毛钱、温漂控制在可接受范围内、还要在宽负载范围内稳定工作&#xff0c;双NPN三极管方案就值得拿出来…

作者头像 李华
网站建设 2026/10/23 6:04:56

Redis 生产环境最佳实践:7 大维度 43 条规范全详解

去年帮一家电商团队排查线上故障&#xff0c;现象很经典&#xff1a;Redis 内存告警、慢查询刷屏、数据库连接被打满。我打开他们的 Redis 一看——大 key、永不过期的 key、还有大量用 String 存的复杂结构&#xff0c;遍地都是雷。这几乎是每个没定过使用规范的团队&#xff…

作者头像 李华