Appearance
章节2:核心模块开发
学习目标
完成本章学习后,你将能够:
- 实现完整的用户注册、登录与权限管理体系(JWT + RBAC)
- 开发商品管理与多级分类系统的 CRUD 接口
- 设计购物车和订单的全流程状态机
- 集成支付宝/微信支付回调处理
- 搭建后台管理系统,使用 ECharts 呈现数据统计
2.1 用户注册登录与权限
2.1.1 JWT 认证流程
定义:JWT(JSON Web Token)是一种无状态的认证方案,服务端签发 Token,客户端携带 Token 访问受保护资源,服务端无需存储会话信息。
客户端 服务端
│ │
│── POST /api/v1/auth/login ──→ │ 验证用户名密码
│←── { access_token, refresh }─│ 签发 JWT
│ │
│── GET /api/v1/user/profile │
│ Authorization: Bearer xxx ──→│ 验证 Token 签名
│←── { user data } ─────────── │ 返回用户信息2.1.2 用户注册实现
python
# backend/apps/user/views.py
from rest_framework import generics, status
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from .serializers import UserRegisterSerializer, UserLoginSerializer
class RegisterView(generics.CreateAPIView):
"""用户注册"""
serializer_class = UserRegisterSerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.save()
# 注册成功后自动签发 JWT
refresh = RefreshToken.for_user(user)
return Response({
"code": 201,
"message": "注册成功",
"data": {
"user_id": user.id,
"username": user.username,
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
}
}, status=status.HTTP_201_CREATED)
class LoginView(generics.GenericAPIView):
"""用户登录"""
serializer_class = UserLoginSerializer
def post(self, request):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data['user']
refresh = RefreshToken.for_user(user)
# 记录最后登录时间
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
return Response({
"code": 200,
"message": "登录成功",
"data": {
"access_token": str(refresh.access_token),
"refresh_token": str(refresh),
"user": UserBriefSerializer(user).data,
}
})2.1.3 权限控制体系(RBAC)
定义:RBAC(Role-Based Access Control)基于角色的访问控制,将权限授予角色,再将角色分配给用户,实现灵活的权限管理。
python
# backend/apps/user/permissions.py
from rest_framework.permissions import BasePermission
class IsAdminUser(BasePermission):
"""管理员权限"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated
and request.user.is_staff)
class IsOwnerOrAdmin(BasePermission):
"""用户本人或管理员可操作"""
def has_object_permission(self, request, view, obj):
if hasattr(obj, 'user'):
return obj.user == request.user or request.user.is_staff
return request.user.is_staff
# 使用示例
class OrderDetailView(generics.RetrieveAPIView):
serializer_class = OrderSerializer
permission_classes = [IsAuthenticated, IsOwnerOrAdmin]
def get_queryset(self):
# 管理员可查看所有订单,普通用户只能看自己的
if self.request.user.is_staff:
return Order.objects.all()
return Order.objects.filter(user=self.request.user)2.1.4 前端登录交互
vue
<!-- frontend/src/views/login/LoginView.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
import { login } from '@/api/user'
const router = useRouter()
const userStore = useUserStore()
const form = ref({ username: '', password: '' })
const loading = ref(false)
const handleLogin = async () => {
loading.value = true
try {
const res = await login(form.value)
userStore.setTokens(res.data.access_token, res.data.refresh_token)
userStore.setUser(res.data.user)
ElMessage.success('登录成功')
router.push('/')
} catch (err: any) {
ElMessage.error(err.response?.data?.message || '登录失败')
} finally {
loading.value = false
}
}
</script>
<template>
<el-card class="login-card">
<h2>用户登录</h2>
<el-form :model="form" @submit.prevent="handleLogin">
<el-form-item label="用户名">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="密码">
<el-input v-model="form.password" type="password" show-password />
</el-form-item>
<el-button type="primary" :loading="loading" @click="handleLogin">
登录
</el-button>
</el-form>
</el-card>
</template>2.2 商品管理与分类系统
2.2.1 多级分类实现
定义:商品分类通常采用树形结构,支持无限级嵌套,前端以级联选择器或树形菜单展示。
python
# backend/apps/product/views.py
from rest_framework import viewsets
from rest_framework.decorators import action
from rest_framework.response import Response
from .models import Category, Product
from .serializers import (
CategorySerializer, CategoryTreeSerializer,
ProductListSerializer, ProductDetailSerializer
)
class CategoryViewSet(viewsets.ModelViewSet):
queryset = Category.objects.filter(parent__isnull=True)
serializer_class = CategorySerializer
@action(detail=False, methods=['get'])
def tree(self, request):
"""返回完整分类树"""
categories = Category.objects.filter(parent__isnull=True).prefetch_related('children')
serializer = CategoryTreeSerializer(categories, many=True)
return Response(serializer.data)
# frontend/src/api/product.ts
export async function getCategoryTree(): Promise<CategoryNode[]> {
const res = await request.get('/api/v1/categories/tree/')
return res.data
}2.2.2 商品列表与搜索
python
# backend/apps/product/serializers.py
from rest_framework import serializers
from .models import Product
class ProductListSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
class Meta:
model = Product
fields = ['id', 'name', 'price', 'discount_price',
'sales_count', 'stock', 'images', 'category_name']
ref_name = 'ProductList'
# backend/apps/product/views.py —— 带筛选、排序、搜索
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import filters
class ProductViewSet(viewsets.ReadOnlyModelViewSet):
queryset = Product.objects.filter(is_active=True)
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
filterset_fields = ['category', 'category__parent_id']
search_fields = ['name', 'description']
ordering_fields = ['price', 'sales_count', 'created_at']
ordering = ['-sales_count']
def get_serializer_class(self):
if self.action == 'list':
return ProductListSerializer
return ProductDetailSerializervue
<!-- 前端商品列表组件 -->
<script setup lang="ts">
const products = ref<Product[]>([])
const loading = ref(false)
const pagination = reactive({ page: 1, pageSize: 20, total: 0 })
const filters = reactive({ categoryId: null, keyword: '', ordering: '-sales_count' })
const fetchProducts = async () => {
loading.value = true
try {
const res = await getProducts({
page: pagination.page,
page_size: pagination.pageSize,
category: filters.categoryId,
search: filters.keyword,
ordering: filters.ordering,
})
products.value = res.results
pagination.total = res.total
} finally {
loading.value = false
}
}
watch([() => filters.categoryId, () => filters.keyword], () => {
pagination.page = 1
fetchProducts()
})
</script>2.3 购物车与订单流程
2.3.1 购物车设计
定义:购物车是用户选购商品的临时容器,支持增删改查、数量调整、价格实时计算。
python
# backend/apps/cart/models.py
class CartItem(models.Model):
user = models.ForeignKey(
'user.User', on_delete=models.CASCADE, related_name='cart_items'
)
product = models.ForeignKey('product.Product', on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1, verbose_name="数量")
selected = models.BooleanField(default=True, verbose_name="选中状态")
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
db_table = 'cart_item'
unique_together = ('user', 'product') # 同一用户对同一商品只有一条记录
@property
def total_price(self):
price = self.product.discount_price or self.product.price
return price * self.quantitypython
# backend/apps/cart/views.py
class CartViewSet(viewsets.ModelViewSet):
serializer_class = CartItemSerializer
permission_classes = [IsAuthenticated]
def get_queryset(self):
return CartItem.objects.filter(user=self.request.user)\
.select_related('product')
def perform_create(self, serializer):
# 如果商品已在购物车,增加数量而非重复创建
product = serializer.validated_data['product']
existing = CartItem.objects.filter(
user=self.request.user, product=product
).first()
if existing:
existing.quantity += serializer.validated_data.get('quantity', 1)
existing.save()
return existing
serializer.save(user=self.request.user)2.3.2 订单状态机
定义:订单状态机定义订单从创建到完成的所有状态流转,确保业务逻辑的可追踪性和一致性。
待支付 ──→ 已支付 ──→ 已发货 ──→ 已完成
│ │
├──→ 已取消 └──→ 已退款
│
└──→ 超时关闭python
# backend/apps/order/models.py
from django.db import models
class Order(models.Model):
class Status(models.TextChoices):
PENDING = 'pending', '待支付'
PAID = 'paid', '已支付'
SHIPPED = 'shipped', '已发货'
DELIVERED = 'delivered', '已完成'
CANCELLED = 'cancelled', '已取消'
REFUNDED = 'refunded', '已退款'
CLOSED = 'closed', '超时关闭'
order_no = models.CharField(max_length=32, unique=True, verbose_name="订单号")
user = models.ForeignKey('user.User', on_delete=models.CASCADE, related_name='orders')
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PENDING)
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="总金额")
pay_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="实付金额")
address = models.JSONField(verbose_name="收货地址快照")
created_at = models.DateTimeField(auto_now_add=True)
paid_at = models.DateTimeField(null=True, blank=True)
class Meta:
db_table = 'order'
def can_cancel(self):
return self.status in (self.Status.PENDING,)
def cancel(self):
if not self.can_cancel():
raise ValueError("当前状态不可取消")
self.status = self.Status.CANCELLED
self.save()
# 恢复库存
for item in self.items.all():
item.product.stock += item.quantity
item.product.save()2.3.3 下单流程
python
# backend/apps/order/services.py
from django.db import transaction
from .models import Order, OrderItem
class OrderService:
@staticmethod
@transaction.atomic
def create_order(user, address_id, cart_ids):
"""创建订单(事务保证原子性)"""
# 1. 查询购物车商品
cart_items = CartItem.objects.filter(
id__in=cart_ids, user=user, selected=True
).select_related('product')
if not cart_items:
raise ValueError("购物车为空或未选中商品")
# 2. 检查库存并锁定
for item in cart_items:
if item.product.stock < item.quantity:
raise ValueError(f"商品 {item.product.name} 库存不足")
# 3. 生成订单号
order_no = f"ORD{timezone.now().strftime('%Y%m%d%H%M%S')}{user.id:04d}"
# 4. 创建订单主表
address = Address.objects.get(id=address_id, user=user)
order = Order.objects.create(
order_no=order_no,
user=user,
address={
'name': address.name,
'phone': address.phone,
'province': address.province,
'city': address.city,
'district': address.district,
'detail': address.detail,
},
total_amount=sum(
(item.product.discount_price or item.product.price) * item.quantity
for item in cart_items
),
pay_amount=sum(
(item.product.discount_price or item.product.price) * item.quantity
for item in cart_items
),
)
# 5. 创建订单明细 & 扣减库存
for item in cart_items:
OrderItem.objects.create(
order=order,
product=item.product,
product_name=item.product.name,
product_image=item.product.images[0] if item.product.images else '',
price=item.product.discount_price or item.product.price,
quantity=item.quantity,
)
Product.objects.filter(id=item.product.id).update(
stock=models.F('stock') - item.quantity,
sales_count=models.F('sales_count') + item.quantity,
)
# 6. 清空购物车
cart_items.delete()
return order2.4 支付集成与回调处理
2.4.1 支付宝支付集成
定义:支付集成是指接入第三方支付平台(支付宝/微信),实现用户在线付款、异步回调通知、订单状态同步的全流程。
python
# backend/apps/payment/services/alipay.py
from alipay import AliPay
from django.conf import settings
class AlipayService:
"""支付宝支付服务"""
def __init__(self):
self.alipay = AliPay(
appid=settings.ALIPAY_APP_ID,
app_notify_url=settings.ALIPAY_NOTIFY_URL,
app_private_key_string=open(settings.ALIPAY_PRIVATE_KEY_PATH).read(),
alipay_public_key_string=open(settings.ALIPAY_PUBLIC_KEY_PATH).read(),
sign_type="RSA2",
debug=settings.DEBUG,
)
def get_pay_url(self, order_no: str, amount: float, subject: str) -> str:
"""获取支付宝支付页面 URL"""
order_string = self.alipay.api_alipay_trade_page_pay(
out_trade_no=order_no,
total_amount=amount,
subject=subject,
return_url=settings.ALIPAY_RETURN_URL,
notify_url=settings.ALIPAY_NOTIFY_URL,
)
return f"{settings.ALIPAY_GATEWAY}?{order_string}"
def verify_notify(self, data: dict) -> bool:
"""验证异步通知签名"""
signature = data.pop('sign', None)
return self.alipay.verify(data, signature)2.4.2 支付回调处理
python
# backend/apps/payment/views.py
from django.views.decorators.csrf import csrf_exempt
from .services.alipay import AlipayService
class AlipayNotifyView(View):
"""支付宝异步通知回调(无需 CSRF 验证)"""
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request):
alipay = AlipayService()
# 1. 验证签名
data = request.POST.dict()
if not alipay.verify_notify(data):
return HttpResponse('failure')
# 2. 获取订单号与交易状态
order_no = data.get('out_trade_no')
trade_status = data.get('trade_status')
trade_no = data.get('trade_no') # 支付宝交易号
# 3. 幂等处理(防止重复通知)
payment, created = Payment.objects.get_or_create(
order__order_no=order_no,
defaults={
'order': Order.objects.get(order_no=order_no),
'trade_no': trade_no,
'total_amount': data.get('total_amount'),
'pay_time': data.get('gmt_payment'),
}
)
# 4. 更新订单状态
if trade_status == 'TRADE_SUCCESS' and payment.status == Payment.Status.UNPAID:
payment.status = Payment.Status.PAID
payment.save()
payment.order.status = Order.Status.PAID
payment.order.paid_at = timezone.now()
payment.order.save()
return HttpResponse('success')2.4.3 微信支付集成要点
python
# backend/apps/payment/services/wechat.py
import requests
import hashlib
import xmltodict
from wechatpy.pay import WeChatPay
class WechatPayService:
"""微信支付服务"""
def __init__(self):
self.wxpay = WeChatPay(
appid=settings.WECHAT_APP_ID,
mch_id=settings.WECHAT_MCH_ID,
api_key=settings.WECHAT_API_KEY,
mch_cert=settings.WECHAT_CERT_PATH,
)
def create_jsapi_order(self, openid, order_no, amount, description):
"""创建 JSAPI 支付订单(公众号/小程序)"""
result = self.wxpay.order.create(
trade_type='JSAPI',
body=description,
out_trade_no=order_no,
total_fee=int(amount * 100), # 微信以"分"为单位
spbill_create_ip='127.0.0.1',
notify_url=settings.WECHAT_NOTIFY_URL,
openid=openid,
)
# 返回前端调起支付的参数
return self.wxpay.pay.get_jsapi_params(
prepay_id=result['prepay_id']
)
def verify_notify(self, xml_data):
"""验证回调签名"""
return self.wxpay.pay.handle_result(xml_data)2.5 后台管理与数据统计
2.5.1 Django Admin 自定义
python
# backend/apps/product/admin.py
from django.contrib import admin
from .models import Category, Product
@admin.register(Category)
class CategoryAdmin(admin.ModelAdmin):
list_display = ['name', 'level', 'parent', 'sort_order']
list_editable = ['sort_order']
list_filter = ['level']
search_fields = ['name']
@admin.register(Product)
class ProductAdmin(admin.ModelAdmin):
list_display = ['name', 'category', 'price', 'stock', 'sales_count', 'status']
list_filter = ['category', 'is_active', 'created_at']
search_fields = ['name', 'description']
readonly_fields = ['sales_count', 'created_at', 'updated_at']
fieldsets = [
('基本信息', {'fields': ['name', 'category', 'description']}),
('价格库存', {'fields': ['price', 'discount_price', 'stock']}),
('状态管理', {'fields': ['is_active', 'sales_count']}),
('媒体信息', {'fields': ['images']}),
]2.5.2 ECharts 数据统计
vue
<!-- frontend/src/views/admin/StatsView.vue -->
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import * as echarts from 'echarts'
import { getSalesStats, getCategoryStats } from '@/api/admin'
const salesChartRef = ref<HTMLDivElement>()
const categoryChartRef = ref<HTMLDivElement>()
onMounted(async () => {
// ===== 销售趋势折线图 =====
const salesData = await getSalesStats({ days: 30 })
const salesChart = echarts.init(salesChartRef.value!)
salesChart.setOption({
title: { text: '近30天销售趋势' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: salesData.dates },
yAxis: { type: 'value', name: '销售额(元)' },
series: [{
name: '销售额',
type: 'line',
data: salesData.amounts,
smooth: true,
areaStyle: { opacity: 0.3 },
}],
})
// ===== 分类占比饼图 =====
const categoryData = await getCategoryStats()
const categoryChart = echarts.init(categoryChartRef.value!)
categoryChart.setOption({
title: { text: '商品分类销售额占比' },
tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: categoryData.map((d: any) => ({
name: d.name,
value: d.total_sales,
})),
emphasis: {
itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.3)' },
},
}],
})
})
</script>
<template>
<div class="stats-container">
<el-row :gutter="20">
<el-col :span="14">
<el-card><div ref="salesChartRef" style="height: 400px"></div></el-card>
</el-col>
<el-col :span="10">
<el-card><div ref="categoryChartRef" style="height: 400px"></div></el-card>
</el-col>
</el-row>
</div>
</template>2.5.3 后端统计 API
python
# backend/apps/admin/views.py
from rest_framework.views import APIView
from rest_framework.permissions import IsAdminUser
from django.db.models import Sum, Count
from django.db.models.functions import TruncDate
class SalesStatsView(APIView):
"""销售统计"""
permission_classes = [IsAdminUser]
def get(self, request):
days = int(request.query_params.get('days', 30))
start_date = timezone.now() - timedelta(days=days)
stats = (
OrderItem.objects
.filter(order__status=Order.Status.PAID,
order__paid_at__gte=start_date)
.annotate(date=TruncDate('order__paid_at'))
.values('date')
.annotate(
total_amount=Sum(models.F('price') * models.F('quantity')),
total_orders=Count('order', distinct=True)
)
.order_by('date')
)
return Response({
"dates": [s['date'].strftime('%Y-%m-%d') for s in stats],
"amounts": [float(s['total_amount']) for s in stats],
"orders": [s['total_orders'] for s in stats],
})小结
知识回顾
- 用户认证:JWT 无状态认证 + RBAC 权限模型,前后端分离的标准方案
- 商品系统:多级分类树形结构 + 商品搜索/排序/筛选,django-filter 简化实现
- 购物车与订单:购物车 CRUD → 事务下单(库存扣减)→ 订单状态机流转
- 支付集成:支付宝/微信支付的签名验证与异步回调处理,注意幂等性
- 后台管理:Django Admin 快速搭建 + ECharts 可视化数据呈现
练习与思考
- 代码实现:实现一个
ResetPasswordView,支持通过手机验证码重置密码 - 购物车优化:给购物车添加"失效商品"标识(商品下架或库存不足时自动标记)
- 订单状态机:编写单元测试,覆盖订单状态的所有合法流转和非法流转
- 支付集成:模拟支付宝异步通知,编写回调处理的单元测试用例
- 思考题:在高并发场景下,如何防止"超卖"?谈谈乐观锁与悲观锁的区别和适用场景