1. 项目背景与核心价值
作为一个常年混迹技术社区的老码农,我最近用Django+Vue3堆了个二手图书交易平台。这个项目最初源于一个很实际的需求——每次搬家时看着成箱的旧书实在头疼,挂闲鱼又太麻烦。市面上现有的二手书平台要么流程复杂,要么抽成太高,于是决定自己动手搞个轻量级解决方案。
这个全栈项目采用Django Rest Framework构建后端API服务,Vue3+Element Plus实现前端交互,整体架构清晰、扩展性强。特别适合想学习现代全栈开发模式的中级开发者,项目完整实现了用户认证、商品管理、订单系统等电商核心功能模块,代码量适中但技术栈全面,涵盖了Web开发中90%的典型场景。
2. 技术架构设计解析
2.1 前后端分离架构
系统采用经典的前后端分离设计,这种架构有几个明显优势:
- 后端专注数据逻辑,前端专注交互体验
- 便于团队分工协作
- 支持多端复用API(后续可轻松扩展小程序/App)
- 技术栈选择更灵活
我选择Django Rest Framework(DRF)作为后端框架,主要考虑:
- Django自带的ORM能快速建模图书交易这种典型关系型数据场景
- DRF的序列化器完美支持JSON API开发
- 内置的认证、权限系统省去重复造轮子
- 丰富的第三方插件生态(如django-filter)
前端选用Vue3+TypeScript的组合,相比Vue2:
- Composition API让代码组织更清晰
- 更好的TypeScript支持
- 性能提升显著(尤其虚拟DOM重写)
- 更小的打包体积
2.2 数据库设计要点
图书交易系统的核心是数据模型设计,主要包含这几个实体:
python复制class Book(models.Model):
title = models.CharField(max_length=200)
author = models.CharField(max_length=100)
isbn = models.CharField(max_length=13, unique=True)
cover = models.ImageField(upload_to='covers/')
price = models.DecimalField(max_digits=6, decimal_places=2)
condition_choices = [
('new', '全新'),
('like_new', '几乎全新'),
('good', '有轻微使用痕迹'),
('fair', '有明显使用痕迹')
]
condition = models.CharField(max_length=20, choices=condition_choices)
seller = models.ForeignKey(User, on_delete=models.CASCADE)
posted_at = models.DateTimeField(auto_now_add=True)
is_sold = models.BooleanField(default=False)
几个设计关键点:
- 图书状态使用choices限定可选值,避免自由输入导致数据混乱
- 价格字段使用Decimal而非Float,避免浮点精度问题
- 建立合理的索引(如isbn、seller_id)
- 图片存储采用单独media目录,方便后续扩展CDN
注意:Django的ImageField需要安装Pillow库,且生产环境务必配置好MEDIA_ROOT和MEDIA_URL
3. 核心功能实现细节
3.1 用户认证系统
采用JWT(JSON Web Token)实现无状态认证,相比Session有以下优势:
- 服务端不需要存储会话信息
- 天然支持跨域
- 更适合RESTful API
- 便于实现分布式系统
DRF中配置JWT认证:
python复制# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
# urls.py
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'),
]
前端处理JWT的典型流程:
typescript复制// auth.ts
const login = async (credentials: LoginForm) => {
const response = await axios.post('/api/token/', credentials)
localStorage.setItem('access_token', response.data.access)
localStorage.setItem('refresh_token', response.data.refresh)
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.access}`
}
// 请求拦截器处理token刷新
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
if (error.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
const refreshToken = localStorage.getItem('refresh_token')
const response = await axios.post('/api/token/refresh/', { refresh: refreshToken })
localStorage.setItem('access_token', response.data.access)
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.access}`
return axios(originalRequest)
}
return Promise.reject(error)
}
)
3.2 图书搜索与筛选
二手书平台的核心体验在于高效的图书发现机制,我们实现了:
- 全文搜索(使用Django的SearchVector)
- 多条件筛选
- 结果分页
后端API实现示例:
python复制# views.py
class BookListView(generics.ListAPIView):
serializer_class = BookSerializer
filter_backends = [filters.SearchFilter, DjangoFilterBackend]
search_fields = ['title', 'author', 'isbn']
filterset_fields = {
'price': ['gte', 'lte'],
'condition': ['exact'],
'seller__id': ['exact'],
}
pagination_class = PageNumberPagination
def get_queryset(self):
queryset = Book.objects.filter(is_sold=False)
if 'q' in self.request.GET:
queryset = queryset.annotate(
search=SearchVector('title', 'author', 'description')
).filter(search=self.request.GET['q'])
return queryset
前端使用Vue3的Composition API实现搜索组件:
vue复制<script setup>
import { ref, computed } from 'vue'
const searchQuery = ref('')
const filters = ref({
minPrice: null,
maxPrice: null,
condition: []
})
const { data: books, pending } = useAsyncData(
'books',
() => $fetch('/api/books/', {
params: {
search: searchQuery.value,
price__gte: filters.value.minPrice,
price__lte: filters.value.maxPrice,
condition: filters.value.condition.join(',')
}
})
)
const filteredBooks = computed(() => {
return books.value?.filter(book => {
// 客户端额外筛选逻辑
return true
})
})
</script>
4. 交易流程实现
4.1 订单系统设计
订单是交易的核心,主要涉及以下模型:
python复制class Order(models.Model):
STATUS_CHOICES = [
('pending', '待付款'),
('paid', '已付款'),
('shipped', '已发货'),
('completed', '已完成'),
('cancelled', '已取消')
]
buyer = models.ForeignKey(User, on_delete=models.PROTECT, related_name='orders_bought')
seller = models.ForeignKey(User, on_delete=models.PROTECT, related_name='orders_sold')
book = models.ForeignKey(Book, on_delete=models.PROTECT)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
price = models.DecimalField(max_digits=6, decimal_places=2)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
shipping_address = models.TextField()
tracking_number = models.CharField(max_length=50, blank=True)
关键业务逻辑:
- 创建订单时锁定图书状态
- 状态变更记录(可使用django-simple-history)
- 支付超时自动取消(Celery定时任务)
4.2 支付集成方案
考虑到二手交易的特性,我们选择了两种支付方式集成:
- 模拟支付(开发环境使用):
python复制@api_view(['POST'])
@permission_classes([IsAuthenticated])
def mock_payment(request):
order_id = request.data.get('order_id')
try:
order = Order.objects.get(pk=order_id, buyer=request.user)
if order.status != 'pending':
return Response({'error': '订单状态异常'}, status=400)
order.status = 'paid'
order.book.is_sold = True
order.save()
order.book.save()
return Response({'status': '支付成功'})
except Order.DoesNotExist:
return Response({'error': '订单不存在'}, status=404)
- 支付宝沙箱环境(生产环境替换):
python复制from alipay import AliPay
alipay = AliPay(
appid=settings.ALIPAY_APP_ID,
app_notify_url=None,
app_private_key_string=settings.APP_PRIVATE_KEY,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY,
sign_type="RSA2",
debug=True
)
def create_alipay_trade(order):
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order.id,
total_amount=str(order.price),
subject=f"二手书购买:{order.book.title}",
return_url=settings.ALIPAY_RETURN_URL,
notify_url=settings.ALIPAY_NOTIFY_URL
)
return f"{settings.ALIPAY_GATEWAY}?{order_string}"
5. 项目部署与优化
5.1 生产环境部署
推荐使用Docker Compose部署,典型配置:
dockerfile复制# backend/Dockerfile
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000"]
dockerfile复制# frontend/Dockerfile
FROM node:16 as build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build-stage /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
yaml复制# docker-compose.prod.yml
version: '3.8'
services:
db:
image: postgres:13
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: ${DB_USER}
POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_DB: ${DB_NAME}
backend:
build: ./backend
ports:
- "8000:8000"
env_file:
- .env.prod
depends_on:
- db
frontend:
build: ./frontend
ports:
- "80:80"
depends_on:
- backend
volumes:
postgres_data:
5.2 性能优化技巧
-
数据库优化:
- 使用select_related/prefetch_related减少查询次数
- 添加适当的数据库索引
- 考虑使用Redis缓存热门查询
-
前端优化:
- 实现图片懒加载
- 使用Vue的keep-alive缓存组件
- 按需加载第三方库
-
部署优化:
- 配置Gunicorn工作进程数(通常2-4 x CPU核心)
- 启用Nginx gzip压缩
- 设置适当的静态文件缓存头
6. 常见问题与解决方案
6.1 跨域问题处理
开发环境下需配置CORS:
python复制# settings.py
INSTALLED_APPS = [
...
'corsheaders',
]
MIDDLEWARE = [
...
'corsheaders.middleware.CorsMiddleware',
'django.middleware.common.CommonMiddleware',
...
]
CORS_ALLOWED_ORIGINS = [
"http://localhost:8080",
"http://127.0.0.1:8080",
]
生产环境建议通过Nginx反向代理解决跨域:
nginx复制server {
listen 80;
server_name api.yourdomain.com;
location / {
proxy_pass http://backend: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;
}
}
6.2 图片上传与处理
使用django-storages扩展云存储支持:
python复制# settings.py
DEFAULT_FILE_STORAGE = 'storages.backends.s3boto3.S3Boto3Storage'
AWS_ACCESS_KEY_ID = os.getenv('AWS_ACCESS_KEY_ID')
AWS_SECRET_ACCESS_KEY = os.getenv('AWS_SECRET_ACCESS_KEY')
AWS_STORAGE_BUCKET_NAME = os.getenv('AWS_STORAGE_BUCKET_NAME')
AWS_S3_REGION_NAME = os.getenv('AWS_S3_REGION_NAME')
AWS_S3_FILE_OVERWRITE = False
AWS_DEFAULT_ACL = 'public-read'
前端图片上传组件示例:
vue复制<template>
<div>
<input type="file" @change="handleUpload" accept="image/*">
<img v-if="previewUrl" :src="previewUrl" class="preview-image">
</div>
</template>
<script setup>
const previewUrl = ref('')
const emit = defineEmits(['uploaded'])
const handleUpload = async (e) => {
const file = e.target.files[0]
if (!file) return
// 本地预览
previewUrl.value = URL.createObjectURL(file)
// 实际上传
const formData = new FormData()
formData.append('cover', file)
try {
const response = await axios.post('/api/books/upload-cover/', formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
})
emit('uploaded', response.data.url)
} catch (error) {
console.error('上传失败:', error)
}
}
</script>
6.3 实时通知实现
使用WebSocket实现订单状态实时更新:
python复制# consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class OrderConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.order_id = self.scope['url_route']['kwargs']['order_id']
self.room_group_name = f'order_{self.order_id}'
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'order_message',
'message': message
}
)
async def order_message(self, event):
message = event['message']
await self.send(text_data=json.dumps({
'message': message
}))
前端使用:
typescript复制const setupOrderSocket = (orderId: string) => {
const socket = new WebSocket(`ws://yourdomain.com/ws/orders/${orderId}/`)
socket.onmessage = (e) => {
const data = JSON.parse(e.data)
console.log('收到订单更新:', data.message)
// 更新UI状态
}
socket.onclose = () => {
console.log('WebSocket连接关闭')
}
return socket
}
7. 项目扩展方向
这个基础版本完成后,可以考虑以下几个扩展方向:
- 推荐系统:基于用户浏览/购买历史实现协同过滤推荐
- 聊天功能:买家和卖家直接沟通
- 多租户支持:为学校/社区提供专属二手书市场
- 移动端适配:使用Capacitor打包成混合App
- 数据分析看板:使用Django-admin或Metabase构建
实现推荐系统的简单示例:
python复制# recommendations.py
from collections import defaultdict
from django.db.models import Count
def calculate_similarity(user1, user2):
# 获取两个用户都购买过的图书
common_books = set(user1.orders_bought.values_list('book', flat=True)) & \
set(user2.orders_bought.values_list('book', flat=True))
if not common_books:
return 0
# 简单相似度计算
return len(common_books) / min(
user1.orders_bought.count(),
user2.orders_bought.count()
)
def recommend_books(user):
# 找到相似用户
all_users = User.objects.exclude(id=user.id)
similarities = []
for other_user in all_users:
similarity = calculate_similarity(user, other_user)
if similarity > 0:
similarities.append((other_user, similarity))
# 按相似度排序
similarities.sort(key=lambda x: x[1], reverse=True)
# 获取推荐图书
recommended = defaultdict(float)
for other_user, similarity in similarities[:5]:
for book in other_user.orders_bought.exclude(
book__in=user.orders_bought.values_list('book', flat=True)
).values_list('book', flat=True):
recommended[book] += similarity
# 返回排序后的推荐
return sorted(recommended.items(), key=lambda x: x[1], reverse=True)[:10]
这个二手书交易平台项目从技术选型到实现细节都体现了现代Web开发的典型模式,涵盖了认证授权、API设计、状态管理、支付集成等核心知识点。我在开发过程中最大的体会是:合理的架构设计比过早优化更重要,特别是在初期要控制功能范围,先做出MVP再逐步迭代完善。