Skip to content

章节2:核心模块开发


学习目标

完成本章学习后,你将能够:

  1. 实现完整的用户注册、登录与权限管理体系(JWT + RBAC)
  2. 开发商品管理与多级分类系统的 CRUD 接口
  3. 设计购物车和订单的全流程状态机
  4. 集成支付宝/微信支付回调处理
  5. 搭建后台管理系统,使用 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 ProductDetailSerializer
vue
<!-- 前端商品列表组件 -->
<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.quantity
python
# 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 order

2.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],
        })

小结

知识回顾

  1. 用户认证:JWT 无状态认证 + RBAC 权限模型,前后端分离的标准方案
  2. 商品系统:多级分类树形结构 + 商品搜索/排序/筛选,django-filter 简化实现
  3. 购物车与订单:购物车 CRUD → 事务下单(库存扣减)→ 订单状态机流转
  4. 支付集成:支付宝/微信支付的签名验证与异步回调处理,注意幂等性
  5. 后台管理:Django Admin 快速搭建 + ECharts 可视化数据呈现

练习与思考

  1. 代码实现:实现一个 ResetPasswordView,支持通过手机验证码重置密码
  2. 购物车优化:给购物车添加"失效商品"标识(商品下架或库存不足时自动标记)
  3. 订单状态机:编写单元测试,覆盖订单状态的所有合法流转和非法流转
  4. 支付集成:模拟支付宝异步通知,编写回调处理的单元测试用例
  5. 思考题:在高并发场景下,如何防止"超卖"?谈谈乐观锁与悲观锁的区别和适用场景

Python 学习资料