摘要钩子:想用Django开发一个真正的企业级电商系统,却不知从何下手?本文将带你从零开始,手把手构建一个功能完整的电商后台管理系统,涵盖商品SPU/SKU设计、订单状态机、库存防超卖、支付集成等核心模块。通过3000+行实际代码,你将掌握电商系统开发的全链路技能,告别“玩具项目”,打造真正能上线的商业应用!
开篇:从“学习项目”到“商业系统”的鸿沟
很多Django学习者都有这样的困惑:学完了基础教程,也能做出简单的博客、商城,但当面对真正的企业级需求时,却无从下手:
- 商品要支持多规格(颜色、尺寸),怎么设计数据库?
- 高并发下如何防止库存超卖?
- 订单状态流转复杂,如何避免逻辑混乱?
- 支付接口如何安全集成?
- 后台管理界面如何满足运营需求?
如果你正在为这些问题发愁,那么恭喜你,找到了正确的学习路径。本文将带你跨越这道鸿沟,通过一个完整的电商后台项目,掌握企业级开发的实战技能。
先来看一个真实的业务场景:某服装电商平台需要管理系统后台,商品要支持颜色、尺码等多种规格组合,日订单量超过1万,促销活动时并发订单达每秒1000+。作为开发者,你需要设计一个既能满足复杂业务需求,又能保证高性能、高可用的系统。
通过本文的学习,你将获得解决这类问题的完整方案。我们将从零开始,一步步构建一个功能完善的电商后台管理系统。
第一部分:项目架构与数据库设计
1.1 电商系统核心模块分析
一个完整的电商后台系统通常包含以下核心模块:
- 用户管理:用户注册、登录、权限控制
- 商品管理:分类、属性、SPU/SKU、库存
- 订单管理:购物车、订单生成、状态流转
- 支付管理:支付接口集成、对账处理
- 营销管理:优惠券、促销活动
- 数据统计:销售报表、用户行为分析
1.2 数据库设计:SPU与SKU分离
电商系统中最核心也最复杂的就是商品模型设计。传统的一维商品表无法满足多规格需求,我们必须采用SPU(Standard Product Unit) 和SKU(Stock Keeping Unit) 分离的设计思路。
核心概念解释:
- SPU:标准化产品单元,描述一类商品的共同属性。例如“iPhone 15”就是一个SPU
- SKU:库存量单位,具体到某个规格的商品。例如“iPhone 15 黑色 256GB”就是一个SKU
实体关系设计:
Product (SPU) 1:n SKU
Category 1:n Product
Attribute 1:n AttributeValue
SKU n:n AttributeValue (通过中间表)
1.3 项目目录结构
企业级项目需要有清晰、规范的目录结构:
ecommerce_backend/
├── config/ # 配置层
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py
│ │ ├── development.py
│ │ └── production.py
│ ├── urls.py
│ ├── asgi.py
│ └── wsgi.py
├── apps/ # 业务应用层
│ ├── user/ # 用户管理
│ ├── product/ # 商品管理
│ ├── order/ # 订单管理
│ ├── payment/ # 支付管理
│ └── analytics/ # 数据分析
├── infrastructure/ # 基础设施层
│ ├── persistence/
│ ├── caching/
│ └── messaging/
├── shared/ # 共享组件
│ ├── utils/
│ ├── constants/
│ └── exceptions/
└── manage.py
这种结构清晰分离了业务逻辑、基础设施和配置,便于团队协作和后期维护。
第二部分:核心代码实现
2.1 商品模型设计
首先,我们创建商品相关的模型。这是电商系统的基石。
apps/product/models.py:
from django.db import models
from django.contrib.auth import get_user_model
from django.utils import timezone
import uuid
User = get_user_model()
class Category(models.Model):
"""商品分类"""
name = models.CharField('分类名称', max_length=100)
parent = models.ForeignKey(
'self',
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='children',
verbose_name='父级分类'
)
level = models.IntegerField('层级', default=0)
sort_order = models.IntegerField('排序', default=0)
is_active = models.BooleanField('是否启用', default=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
verbose_name = '商品分类'
verbose_name_plural = verbose_name
ordering = ['level', 'sort_order', 'name']
indexes = [
models.Index(fields=['parent', 'is_active']),
models.Index(fields=['level', 'is_active']),
]
def __str__(self):
if self.parent:
return f"{self.parent.name} > {self.name}"
return self.name
def save(self, *args, **kwargs):
"""保存时自动计算层级"""
if self.parent:
self.level = self.parent.level + 1
else:
self.level = 0
super().save(*args, **kwargs)
class Product(models.Model):
"""商品SPU(标准化产品单元)"""
STATUS_CHOICES = [
('draft', '草稿'),
('pending', '待审核'),
('approved', '已上架'),
('rejected', '已驳回'),
('off_shelf', '已下架'),
]
name = models.CharField('商品名称', max_length=200)
category = models.ForeignKey(
Category,
on_delete=models.PROTECT,
related_name='products',
verbose_name='商品分类'
)
brand = models.CharField('品牌', max_length=100, blank=True)
main_image = models.ImageField('主图', upload_to='products/main/%Y/%m/')
description = models.TextField('商品描述')
spec_template = models.TextField('规格模板', help_text='JSON格式,定义商品规格')
status = models.CharField(
'状态',
max_length=20,
choices=STATUS_CHOICES,
default='draft'
)
base_price = models.DecimalField(
'基准价格',
max_digits=10,
decimal_places=2,
default=0
)
total_stock = models.IntegerField('总库存', default=0)
total_sales = models.IntegerField('总销量', default=0)
is_recommended = models.BooleanField('是否推荐', default=False)
is_hot = models.BooleanField('是否热销', default=False)
is_new = models.BooleanField('是否新品', default=True)
seo_title = models.CharField('SEO标题', max_length=200, blank=True)
seo_keywords = models.CharField('SEO关键词', max_length=300, blank=True)
seo_description = models.TextField('SEO描述', blank=True)
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
related_name='created_products',
verbose_name='创建人'
)
created_at = models.DateTimeField('创建时间', default=timezone.now)
updated_at = models.DateTimeField('更新时间', auto_now=True)
approved_at = models.DateTimeField('审核时间', null=True, blank=True)
off_shelf_at = models.DateTimeField('下架时间', null=True, blank=True)
class Meta:
verbose_name = '商品SPU'
verbose_name_plural = verbose_name
ordering = ['-created_at']
indexes = [
models.Index(fields=['category', 'status']),
models.Index(fields=['status', 'is_recommended', 'is_hot']),
models.Index(fields=['total_sales']),
]
def __str__(self):
return f"{self.name} ({self.get_status_display()})"
@property
def available_skus(self):
"""获取所有可售的SKU"""
return self.skus.filter(is_active=True, stock__gt=0)
def update_total_stock(self):
"""更新总库存"""
total = self.skus.filter(is_active=True).aggregate(
total=models.Sum('stock')
)['total'] or 0
self.total_stock = total
self.save(update_fields=['total_stock'])
class Attribute(models.Model):
"""商品属性(如颜色、尺寸)"""
name = models.CharField('属性名称', max_length=50)
code = models.CharField('属性编码', max_length=50, unique=True)
input_type = models.CharField(
'输入类型',
max_length=20,
choices=[
('text', '文本'),
('select', '下拉选择'),
('checkbox', '复选框'),
('radio', '单选框'),
],
default='select'
)
category = models.ForeignKey(
Category,
on_delete=models.CASCADE,
related_name='attributes',
verbose_name='所属分类'
)
is_required = models.BooleanField('是否必填', default=False)
sort_order = models.IntegerField('排序', default=0)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '商品属性'
verbose_name_plural = verbose_name
ordering = ['sort_order', 'name']
unique_together = ['name', 'category']
def __str__(self):
return f"{self.category.name} - {self.name}"
class AttributeValue(models.Model):
"""属性值(如红色、XL)"""
attribute = models.ForeignKey(
Attribute,
on_delete=models.CASCADE,
related_name='values',
verbose_name='属性'
)
value = models.CharField('属性值', max_length=100)
color_code = models.CharField('颜色代码', max_length=7, blank=True, help_text='十六进制颜色代码,如#FF0000')
image = models.ImageField('图片', upload_to='attributes/%Y/%m/', blank=True)
sort_order = models.IntegerField('排序', default=0)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '属性值'
verbose_name_plural = verbose_name
ordering = ['attribute', 'sort_order', 'value']
unique_together = ['attribute', 'value']
def __str__(self):
if self.color_code:
return f"{self.attribute.name}: {self.value} ({self.color_code})"
return f"{self.attribute.name}: {self.value}"
class SKU(models.Model):
"""商品SKU(库存量单位)"""
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='skus',
verbose_name='所属商品'
)
sku_code = models.CharField(
'SKU编码',
max_length=50,
unique=True,
default=uuid.uuid4
)
name = models.CharField('SKU名称', max_length=200)
price = models.DecimalField(
'销售价格',
max_digits=10,
decimal_places=2
)
cost_price = models.DecimalField(
'成本价格',
max_digits=10,
decimal_places=2,
null=True,
blank=True
)
stock = models.IntegerField('库存数量', default=0)
warning_stock = models.IntegerField('预警库存', default=10)
sales_count = models.IntegerField('销售数量', default=0)
view_count = models.IntegerField('浏览数量', default=0)
attribute_values = models.ManyToManyField(
AttributeValue,
related_name='skus',
verbose_name='属性值'
)
main_image = models.ImageField(
'主图',
upload_to='products/sku/%Y/%m/',
blank=True
)
is_active = models.BooleanField('是否启用', default=True)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
verbose_name = '商品SKU'
verbose_name_plural = verbose_name
ordering = ['product', 'sku_code']
indexes = [
models.Index(fields=['product', 'is_active']),
models.Index(fields=['stock', 'is_active']),
models.Index(fields=['sku_code']),
]
def __str__(self):
return f"{self.product.name} - {self.name} ({self.sku_code})"
@property
def is_in_stock(self):
"""是否有库存"""
return self.stock > 0
def increase_stock(self, quantity):
"""增加库存"""
self.stock += quantity
self.save(update_fields=['stock'])
self.product.update_total_stock()
def decrease_stock(self, quantity):
"""减少库存"""
if self.stock < quantity:
raise ValueError(f"库存不足: 当前库存{self.stock},需要{quantity}")
self.stock -= quantity
self.save(update_fields=['stock'])
self.product.update_total_stock()
def get_attribute_display(self):
"""获取属性展示文本"""
values = self.attribute_values.all()
return " | ".join([f"{v.attribute.name}: {v.value}" for v in values])
class ProductImage(models.Model):
"""商品图片"""
product = models.ForeignKey(
Product,
on_delete=models.CASCADE,
related_name='images',
verbose_name='所属商品'
)
image = models.ImageField('图片', upload_to='products/images/%Y/%m/')
alt_text = models.CharField('替代文本', max_length=200, blank=True)
sort_order = models.IntegerField('排序', default=0)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '商品图片'
verbose_name_plural = verbose_name
ordering = ['sort_order', '-created_at']
def __str__(self):
return f"{self.product.name} - 图片{self.sort_order}"
2.2 订单模型设计
接下来是订单系统的核心模型,包含购物车、订单、订单项等。
apps/order/models.py:
from django.db import models
from django.contrib.auth import get_user_model
from django.utils import timezone
import uuid
from decimal import Decimal
User = get_user_model()
class Cart(models.Model):
"""购物车"""
user = models.ForeignKey(
User,
on_delete=models.CASCADE,
related_name='carts',
verbose_name='用户'
)
session_key = models.CharField(
'会话标识',
max_length=40,
blank=True,
help_text='未登录用户的临时购物车标识'
)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
verbose_name = '购物车'
verbose_name_plural = verbose_name
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'created_at']),
]
def __str__(self):
if self.user:
return f"{self.user.username}的购物车"
return f"匿名购物车({self.session_key})"
@property
def total_quantity(self):
"""购物车总商品数量"""
return self.items.aggregate(
total=models.Sum('quantity')
)['total'] or 0
@property
def total_amount(self):
"""购物车总金额"""
total = Decimal('0.00')
for item in self.items.all():
total += item.subtotal
return total
def clear(self):
"""清空购物车"""
self.items.all().delete()
class CartItem(models.Model):
"""购物车商品项"""
cart = models.ForeignKey(
Cart,
on_delete=models.CASCADE,
related_name='items',
verbose_name='购物车'
)
sku = models.ForeignKey(
'product.SKU',
on_delete=models.CASCADE,
verbose_name='商品SKU'
)
quantity = models.IntegerField('数量', default=1)
selected = models.BooleanField('是否选中', default=True)
added_at = models.DateTimeField('添加时间', auto_now_add=True)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
verbose_name = '购物车商品项'
verbose_name_plural = verbose_name
ordering = ['-added_at']
unique_together = ['cart', 'sku']
indexes = [
models.Index(fields=['cart', 'sku']),
]
def __str__(self):
return f"{self.sku.product.name} x{self.quantity}"
@property
def subtotal(self):
"""小计金额"""
return self.sku.price * self.quantity
def increase_quantity(self, quantity=1):
"""增加数量"""
self.quantity += quantity
self.save(update_fields=['quantity'])
def decrease_quantity(self, quantity=1):
"""减少数量"""
if self.quantity <= quantity:
self.delete()
else:
self.quantity -= quantity
self.save(update_fields=['quantity'])
class Order(models.Model):
"""订单主表"""
STATUS_CHOICES = [
('pending', '待支付'),
('paid', '已支付'),
('shipped', '已发货'),
('delivered', '已收货'),
('completed', '已完成'),
('cancelled', '已取消'),
('refunded', '已退款'),
]
PAYMENT_METHOD_CHOICES = [
('alipay', '支付宝'),
('wechat', '微信支付'),
('bank', '银行转账'),
('cash', '货到付款'),
]
order_number = models.CharField(
'订单编号',
max_length=50,
unique=True,
default=lambda: f"ORD{timezone.now().strftime('%Y%m%d')}{uuid.uuid4().hex[:8].upper()}"
)
user = models.ForeignKey(
User,
on_delete=models.PROTECT,
related_name='orders',
verbose_name='用户'
)
status = models.CharField(
'订单状态',
max_length=20,
choices=STATUS_CHOICES,
default='pending'
)
payment_method = models.CharField(
'支付方式',
max_length=20,
choices=PAYMENT_METHOD_CHOICES,
null=True,
blank=True
)
payment_status = models.CharField(
'支付状态',
max_length=20,
choices=[
('unpaid', '未支付'),
('paid', '已支付'),
('failed', '支付失败'),
('refunded', '已退款'),
],
default='unpaid'
)
total_amount = models.DecimalField(
'订单总额',
max_digits=12,
decimal_places=2,
default=Decimal('0.00')
)
discount_amount = models.DecimalField(
'优惠金额',
max_digits=12,
decimal_places=2,
default=Decimal('0.00')
)
shipping_fee = models.DecimalField(
'运费',
max_digits=8,
decimal_places=2,
default=Decimal('0.00')
)
final_amount = models.DecimalField(
'实付金额',
max_digits=12,
decimal_places=2,
default=Decimal('0.00')
)
shipping_address = models.ForeignKey(
'user.ShippingAddress',
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name='收货地址'
)
consignee = models.CharField('收货人', max_length=50)
phone = models.CharField('联系电话', max_length=20)
address = models.TextField('详细地址')
note = models.TextField('订单备注', blank=True)
paid_at = models.DateTimeField('支付时间', null=True, blank=True)
shipped_at = models.DateTimeField('发货时间', null=True, blank=True)
delivered_at = models.DateTimeField('收货时间', null=True, blank=True)
cancelled_at = models.DateTimeField('取消时间', null=True, blank=True)
created_at = models.DateTimeField('创建时间', default=timezone.now)
updated_at = models.DateTimeField('更新时间', auto_now=True)
class Meta:
verbose_name = '订单'
verbose_name_plural = verbose_name
ordering = ['-created_at']
indexes = [
models.Index(fields=['user', 'status']),
models.Index(fields=['order_number']),
models.Index(fields=['created_at', 'status']),
]
def __str__(self):
return f"{self.order_number} ({self.get_status_display()})"
@property
def is_payable(self):
"""是否可支付"""
return self.status == 'pending'
@property
def is_cancellable(self):
"""是否可取消"""
return self.status in ['pending', 'paid']
def calculate_final_amount(self):
"""计算最终金额"""
self.final_amount = self.total_amount - self.discount_amount + self.shipping_fee
self.save(update_fields=['final_amount'])
def update_status(self, new_status, commit=True):
"""更新订单状态"""
if new_status != self.status:
self.status = new_status
# 记录状态变更时间
now = timezone.now()
if new_status == 'paid':
self.paid_at = now
elif new_status == 'shipped':
self.shipped_at = now
elif new_status == 'delivered':
self.delivered_at = now
elif new_status == 'cancelled':
self.cancelled_at = now
if commit:
self.save()
def cancel(self, reason='用户取消'):
"""取消订单"""
if not self.is_cancellable:
raise ValueError(f"订单状态{self.get_status_display()}不可取消")
self.update_status('cancelled')
# 恢复库存
for item in self.items.all():
try:
item.sku.increase_stock(item.quantity)
except Exception as e:
# 记录错误,但不阻止订单取消
print(f"恢复库存失败: {e}")
# 记录取消原因
OrderLog.objects.create(
order=self,
action='cancel',
details={'reason': reason}
)
class OrderItem(models.Model):
"""订单商品项"""
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='items',
verbose_name='订单'
)
sku = models.ForeignKey(
'product.SKU',
on_delete=models.PROTECT,
verbose_name='商品SKU'
)
product_name = models.CharField('商品名称', max_length=200)
sku_name = models.CharField('SKU名称', max_length=200)
price = models.DecimalField(
'单价',
max_digits=10,
decimal_places=2
)
quantity = models.IntegerField('数量', default=1)
subtotal = models.DecimalField(
'小计',
max_digits=12,
decimal_places=2,
default=Decimal('0.00')
)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '订单商品项'
verbose_name_plural = verbose_name
ordering = ['-created_at']
indexes = [
models.Index(fields=['order', 'sku']),
]
def __str__(self):
return f"{self.product_name} x{self.quantity}"
def save(self, *args, **kwargs):
"""保存时自动计算小计"""
self.subtotal = self.price * self.quantity
super().save(*args, **kwargs)
class OrderLog(models.Model):
"""订单操作日志"""
order = models.ForeignKey(
Order,
on_delete=models.CASCADE,
related_name='logs',
verbose_name='订单'
)
action = models.CharField('操作', max_length=50)
details = models.JSONField('详情', default=dict)
operator = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name='操作人'
)
created_at = models.DateTimeField('创建时间', auto_now_add=True)
class Meta:
verbose_name = '订单日志'
verbose_name_plural = verbose_name
ordering = ['-created_at']
indexes = [
models.Index(fields=['order', 'action']),
models.Index(fields=['created_at']),
]
def __str__(self):
return f"{self.order.order_number} - {self.action}"
2.3 防超卖:库存并发控制
电商系统的核心挑战之一就是库存并发控制。在高并发场景下,如何防止超卖是关键。
apps/order/services.py:
import redis
from django.db import transaction
from django.core.exceptions import ValidationError
from django.conf import settings
from decimal import Decimal
import time
import logging
from .models import Order, OrderItem, Cart, CartItem
from product.models import SKU
logger = logging.getLogger(__name__)
# Redis连接
redis_client = redis.Redis(
host=settings.REDIS_HOST,
port=settings.REDIS_PORT,
db=settings.REDIS_DB,
decode_responses=True
)
class InventoryService:
"""库存服务"""
@staticmethod
def check_stock(sku_id, quantity):
"""检查库存是否足够"""
try:
sku = SKU.objects.get(id=sku_id, is_active=True)
if sku.stock < quantity:
return False, f"商品库存不足,当前库存{sku.stock}"
return True, sku
except SKU.DoesNotExist:
return False, "商品不存在或已下架"
@staticmethod
def lock_stock(sku_id, quantity, timeout=10):
"""锁定库存(Redis分布式锁)"""
lock_key = f"stock_lock:{sku_id}"
order_key = f"stock_order:{sku_id}"
# 尝试获取锁
lock_acquired = redis_client.setnx(lock_key, 1)
if lock_acquired:
redis_client.expire(lock_key, timeout)
try:
# 检查并扣减库存
with transaction.atomic():
sku = SKU.objects.select_for_update().get(
id=sku_id,
is_active=True
)
if sku.stock < quantity:
return False, "库存不足"
# 预扣库存
sku.stock -= quantity
sku.save(update_fields=['stock'])
# 记录预扣订单
redis_client.hincrby(order_key, 'locked', quantity)
return True, sku
except Exception as e:
logger.error(f"库存锁定失败: {e}")
return False, str(e)
finally:
# 释放锁
redis_client.delete(lock_key)
else:
return False, "系统繁忙,请稍后重试"
@staticmethod
def release_stock(sku_id, quantity):
"""释放锁定的库存"""
order_key = f"stock_order:{sku_id}"
with transaction.atomic():
sku = SKU.objects.select_for_update().get(id=sku_id)
sku.stock += quantity
sku.save(update_fields=['stock'])
# 更新预扣记录
current_locked = int(redis_client.hget(order_key, 'locked') or 0)
new_locked = max(0, current_locked - quantity)
redis_client.hset(order_key, 'locked', new_locked)
@staticmethod
def confirm_stock(sku_id, quantity):
"""确认扣减库存(订单支付成功后)"""
order_key = f"stock_order:{sku_id}"
# 减少预扣数量
current_locked = int(redis_client.hget(order_key, 'locked') or 0)
new_locked = max(0, current_locked - quantity)
redis_client.hset(order_key, 'locked', new_locked)
# 更新实际库存(预扣时已扣减,这里只需要记录销售)
with transaction.atomic():
sku = SKU.objects.get(id=sku_id)
sku.sales_count += quantity
sku.save(update_fields=['sales_count'])
class OrderService:
"""订单服务"""
@staticmethod
def create_order_from_cart(user, cart_id, shipping_address_id=None, note=''):
"""从购物车创建订单"""
try:
# 获取购物车
cart = Cart.objects.get(id=cart_id, user=user)
# 检查购物车是否为空
if not cart.items.exists():
raise ValidationError("购物车为空")
# 获取收货地址
shipping_address = None
if shipping_address_id:
from user.models import ShippingAddress
shipping_address = ShippingAddress.objects.get(
id=shipping_address_id,
user=user,
is_active=True
)
with transaction.atomic():
# 创建订单
order = Order.objects.create(
user=user,
note=note,
shipping_address=shipping_address
)
if shipping_address:
order.consignee = shipping_address.consignee
order.phone = shipping_address.phone
order.address = shipping_address.full_address
total_amount = Decimal('0.00')
# 处理购物车商品
for cart_item in cart.items.filter(selected=True):
sku = cart_item.sku
quantity = cart_item.quantity
# 检查并锁定库存
success, result = InventoryService.lock_stock(
sku.id,
quantity
)
if not success:
raise ValidationError(f"商品{sku.product.name}库存不足: {result}")
# 创建订单项
order_item = OrderItem.objects.create(
order=order,
sku=sku,
product_name=sku.product.name,
sku_name=sku.name,
price=sku.price,
quantity=quantity
)
total_amount += order_item.subtotal
# 更新订单金额
order.total_amount = total_amount
order.calculate_final_amount()
order.save()
# 清空购物车
cart.items.filter(selected=True).delete()
return order, None
except Exception as e:
logger.error(f"创建订单失败: {e}")
return None, str(e)
@staticmethod
def cancel_order(order_id, user, reason='用户取消'):
"""取消订单"""
try:
order = Order.objects.get(id=order_id, user=user)
# 检查订单状态是否可以取消
if not order.is_cancellable:
raise ValidationError(f"订单状态{order.get_status_display()}不可取消")
with transaction.atomic():
# 恢复库存
for item in order.items.all():
InventoryService.release_stock(item.sku.id, item.quantity)
# 更新订单状态
order.update_status('cancelled')
# 记录日志
from .models import OrderLog
OrderLog.objects.create(
order=order,
action='cancel',
details={'reason': reason},
operator=user
)
return True, None
except Exception as e:
logger.error(f"取消订单失败: {e}")
return False, str(e)
@staticmethod
def pay_order(order_id, user, payment_method):
"""支付订单"""
try:
order = Order.objects.get(id=order_id, user=user)
# 检查订单状态是否可以支付
if not order.is_payable:
raise ValidationError(f"订单状态{order.get_status_display()}不可支付")
with transaction.atomic():
# 更新订单支付信息
order.payment_method = payment_method
order.payment_status = 'paid'
order.update_status('paid')
# 确认库存扣减
for item in order.items.all():
InventoryService.confirm_stock(item.sku.id, item.quantity)
# 记录日志
from .models import OrderLog
OrderLog.objects.create(
order=order,
action='pay',
details={
'payment_method': payment_method,
'amount': str(order.final_amount)
},
operator=user
)
return True, None
except Exception as e:
logger.error(f"支付订单失败: {e}")
return False, str(e)
class CartService:
"""购物车服务"""
@staticmethod
def get_or_create_cart(user, session_key=None):
"""获取或创建购物车"""
if user.is_authenticated:
# 已登录用户:获取用户购物车
cart, created = Cart.objects.get_or_create(user=user)
# 如果有session_key,合并匿名购物车
if session_key and not created:
CartService.merge_anonymous_cart(cart, session_key)
else:
# 未登录用户:使用session_key
if not session_key:
raise ValueError("未登录用户必须提供session_key")
cart, created = Cart.objects.get_or_create(
session_key=session_key,
user=None
)
return cart
@staticmethod
def merge_anonymous_cart(user_cart, session_key):
"""合并匿名购物车到用户购物车"""
try:
anonymous_cart = Cart.objects.get(
session_key=session_key,
user=None
)
with transaction.atomic():
for item in anonymous_cart.items.all():
# 检查是否已存在相同SKU
existing_item = user_cart.items.filter(sku=item.sku).first()
if existing_item:
# 合并数量
existing_item.quantity += item.quantity
existing_item.save()
else:
# 创建新项
CartItem.objects.create(
cart=user_cart,
sku=item.sku,
quantity=item.quantity
)
# 删除匿名购物车
anonymous_cart.delete()
except Cart.DoesNotExist:
pass
@staticmethod
def add_to_cart(cart, sku_id, quantity=1):
"""添加商品到购物车"""
try:
# 检查库存
success, result = InventoryService.check_stock(sku_id, quantity)
if not success:
return False, result
sku = result
# 检查是否已存在
item, created = CartItem.objects.get_or_create(
cart=cart,
sku=sku,
defaults={'quantity': quantity}
)
if not created:
# 更新数量
new_quantity = item.quantity + quantity
# 再次检查库存
if sku.stock < new_quantity:
return False, f"库存不足,当前库存{sku.stock}"
item.quantity = new_quantity
item.save()
return True, item
except Exception as e:
logger.error(f"添加购物车失败: {e}")
return False, str(e)
@staticmethod
def update_cart_item(cart_item_id, quantity):
"""更新购物车商品数量"""
try:
if quantity <= 0:
# 数量为0时删除
CartItem.objects.filter(id=cart_item_id).delete()
return True, "已删除"
cart_item = CartItem.objects.get(id=cart_item_id)
sku = cart_item.sku
# 检查库存
if sku.stock < quantity:
return False, f"库存不足,当前库存{sku.stock}"
cart_item.quantity = quantity
cart_item.save()
return True, cart_item
except CartItem.DoesNotExist:
return False, "购物车项不存在"
except Exception as e:
logger.error(f"更新购物车失败: {e}")
return False, str(e)
@staticmethod
def remove_from_cart(cart_item_id):
"""从购物车移除商品"""
try:
CartItem.objects.filter(id=cart_item_id).delete()
return True, None
except Exception as e:
logger.error(f"移除购物车失败: {e}")
return False, str(e)
2.4 后台管理界面定制
Django Admin后台是电商系统的管理核心,我们需要进行深度定制。
apps/product/admin.py:
from django.contrib import admin
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.urls import reverse
from django.contrib import messages
import json
from .models import (
Category, Product, SKU,
Attribute, AttributeValue, ProductImage
)
class CategoryAdmin(admin.ModelAdmin):
"""分类管理"""
list_display = ['name', 'parent', 'level', 'is_active', 'product_count']
list_filter = ['is_active', 'level']
search_fields = ['name']
ordering = ['level', 'sort_order', 'name']
list_per_page = 20
def product_count(self, obj):
"""商品数量"""
count = obj.products.count()
url = reverse('admin:product_product_changelist')
return format_html(
'<a href="{}?category__id__exact={}">{}</a>',
url, obj.id, count
)
product_count.short_description = '商品数量'
class ProductImageInline(admin.TabularInline):
"""商品图片内联"""
model = ProductImage
extra = 1
fields = ['image', 'alt_text', 'sort_order']
readonly_fields = ['image_preview']
def image_preview(self, obj):
"""图片预览"""
if obj.image:
return format_html(
'<img src="{}" style="max-height: 100px; max-width: 100px;" />',
obj.image.url
)
return "-"
image_preview.short_description = '预览'
class SKUInline(admin.TabularInline):
"""SKU内联"""
model = SKU
extra = 0
fields = ['sku_code', 'name', 'price', 'stock', 'is_active', 'attribute_display']
readonly_fields = ['attribute_display']
def attribute_display(self, obj):
"""属性展示"""
return obj.get_attribute_display()
attribute_display.short_description = '属性'
class ProductAdmin(admin.ModelAdmin):
"""商品SPU管理"""
list_display = [
'name', 'category', 'status',
'base_price', 'total_stock', 'total_sales',
'is_recommended', 'is_hot', 'is_new',
'created_at', 'actions'
]
list_filter = ['status', 'category', 'is_recommended', 'is_hot', 'is_new']
search_fields = ['name', 'description']
ordering = ['-created_at']
list_per_page = 20
inlines = [ProductImageInline, SKUInline]
fieldsets = (
('基本信息', {
'fields': (
'name', 'category', 'brand',
'main_image', 'image_preview',
'description', 'spec_template',
)
}),
('状态与属性', {
'fields': (
'status', 'base_price',
'is_recommended', 'is_hot', 'is_new',
'seo_title', 'seo_keywords', 'seo_description',
)
}),
('时间信息', {
'fields': (
'created_at', 'updated_at',
'approved_at', 'off_shelf_at',
),
'classes': ('collapse',)
}),
)
readonly_fields = [
'image_preview', 'created_at', 'updated_at',
'approved_at', 'off_shelf_at'
]
actions = ['approve_products', 'reject_products', 'off_shelf_products']
def image_preview(self, obj):
"""主图预览"""
if obj.main_image:
return format_html(
'<img src="{}" style="max-height: 200px; max-width: 200px;" />',
obj.main_image.url
)
return "未上传"
image_preview.short_description = '主图预览'
def actions(self, obj):
"""操作按钮"""
buttons = []
if obj.status == 'pending':
approve_url = reverse('admin:product_product_approve', args=[obj.id])
buttons.append(
f'<a href="{approve_url}" class="button">审核通过</a>'
)
reject_url = reverse('admin:product_product_reject', args=[obj.id])
buttons.append(
f'<a href="{reject_url}" class="button" style="background-color: #dc3545;">驳回</a>'
)
if obj.status == 'approved':
off_shelf_url = reverse('admin:product_product_off_shelf', args=[obj.id])
buttons.append(
f'<a href="{off_shelf_url}" class="button" style="background-color: #6c757d;">下架</a>'
)
if buttons:
return format_html(' '.join(buttons))
return "-"
actions.short_description = '操作'
def approve_products(self, request, queryset):
"""批量审核通过"""
updated = queryset.filter(status='pending').update(
status='approved',
approved_at=timezone.now()
)
self.message_user(
request,
f"成功审核通过 {updated} 个商品",
messages.SUCCESS
)
def reject_products(self, request, queryset):
"""批量驳回"""
updated = queryset.filter(status='pending').update(status='rejected')
self.message_user(
request,
f"成功驳回 {updated} 个商品",
messages.WARNING
)
def off_shelf_products(self, request, queryset):
"""批量下架"""
updated = queryset.filter(status='approved').update(
status='off_shelf',
off_shelf_at=timezone.now()
)
self.message_user(
request,
f"成功下架 {updated} 个商品",
messages.INFO
)
approve_products.short_description = "审核通过选中商品"
reject_products.short_description = "驳回选中商品"
off_shelf_products.short_description = "下架选中商品"
def get_urls(self):
"""自定义URL"""
from django.urls import path
urls = super().get_urls()
custom_urls = [
path(
'<int:product_id>/approve/',
self.admin_site.admin_view(self.approve_product),
name='product_product_approve'
),
path(
'<int:product_id>/reject/',
self.admin_site.admin_view(self.reject_product),
name='product_product_reject'
),
path(
'<int:product_id>/off_shelf/',
self.admin_site.admin_view(self.off_shelf_product),
name='product_product_off_shelf'
),
]
return custom_urls + urls
def approve_product(self, request, product_id):
"""审核通过单个商品"""
from django.shortcuts import get_object_or_404, redirect
product = get_object_or_404(Product, id=product_id)
product.status = 'approved'
product.approved_at = timezone.now()
product.save()
self.message_user(
request,
f"商品'{product.name}'已审核通过",
messages.SUCCESS
)
return redirect('admin:product_product_changelist')
def reject_product(self, request, product_id):
"""驳回单个商品"""
from django.shortcuts import get_object_or_404, redirect
product = get_object_or_404(Product, id=product_id)
product.status = 'rejected'
product.save()
self.message_user(
request,
f"商品'{product.name}'已驳回",
messages.WARNING
)
return redirect('admin:product_product_changelist')
def off_shelf_product(self, request, product_id):
"""下架单个商品"""
from django.shortcuts import get_object_or_404, redirect
product = get_object_or_404(Product, id=product_id)
product.status = 'off_shelf'
product.off_shelf_at = timezone.now()
product.save()
self.message_user(
request,
f"商品'{product.name}'已下架",
messages.INFO
)
return redirect('admin:product_product_changelist')
class AttributeAdmin(admin.ModelAdmin):
"""属性管理"""
list_display = ['name', 'code', 'category', 'input_type', 'is_required']
list_filter = ['category', 'input_type', 'is_required']
search_fields = ['name', 'code']
ordering = ['category', 'sort_order', 'name']
list_per_page = 20
class AttributeValueAdmin(admin.ModelAdmin):
"""属性值管理"""
list_display = ['attribute', 'value', 'color_code', 'image_preview']
list_filter = ['attribute', 'attribute__category']
search_fields = ['value', 'attribute__name']
ordering = ['attribute', 'sort_order', 'value']
list_per_page = 20
def image_preview(self, obj):
"""图片预览"""
if obj.image:
return format_html(
'<img src="{}" style="max-height: 50px; max-width: 50px;" />',
obj.image.url
)
return "-"
image_preview.short_description = '图片'
class SKUAdmin(admin.ModelAdmin):
"""SKU管理"""
list_display = [
'sku_code', 'product', 'name',
'price', 'stock', 'sales_count',
'is_active', 'attribute_display'
]
list_filter = ['is_active', 'product__category']
search_fields = ['sku_code', 'name', 'product__name']
ordering = ['product', 'sku_code']
list_per_page = 20
readonly_fields = ['attribute_display']
def attribute_display(self, obj):
"""属性展示"""
return obj.get_attribute_display()
attribute_display.short_description = '属性'
# 注册管理类
admin.site.register(Category, CategoryAdmin)
admin.site.register(Product, ProductAdmin)
admin.site.register(Attribute, AttributeAdmin)
admin.site.register(AttributeValue, AttributeValueAdmin)
admin.site.register(SKU, SKUAdmin)