1. 技术栈分析
核心技术栈
- 后端框架: Django 4.x (Python 3.8+)
- API层: GraphQL (Graphene-Django)
- 数据库: PostgreSQL (主数据库)
- 缓存: Redis (缓存和会话存储)
- 消息队列: Celery + Redis (异步任务处理)
- 搜索引擎: Elasticsearch (可选,产品搜索)
- 文件存储: AWS S3 / Google Cloud Storage / 本地存储
- 前端: React + TypeScript (Dashboard)
- 移动端: React Native (可选)
依赖库详情
python
# 核心依赖
Django>=4.2
graphene-django>=3.0
celery>=5.2
redis>=4.0
psycopg2-binary>=2.9
Pillow>=9.0
# 支付集成
stripe
braintree
adyen
# 搜索和分析
elasticsearch-dsl>=7.0
django-elasticsearch-dsl
# 国际化和本地化
django-babel
django-countries
django-phonenumber-field
# 安全和认证
django-oauth-toolkit
cryptography
PyJWT
# 图像处理
Pillow
django-versatileimagefield
# 监控和日志
sentry-sdk
structlog
2. 架构设计分析
2.1 Headless架构优势
Saleor采用API-first、headless架构,GraphQL作为唯一的API接口,实现技术无关性和高度解耦
核心组件分离:
- Backend API: Django + GraphQL核心
- Dashboard: React管理界面(独立部署)
- Storefront: 可定制前端(Next.js示例)
- Mobile Apps: React Native或原生应用
2.2 微服务友好设计
python
# 应用模块化结构
saleor/
├── account/ # 用户账户管理
├── attribute/ # 产品属性系统
├── checkout/ # 结账流程
├── core/ # 核心工具和基础组件
├── discount/ # 促销折扣系统
├── giftcard/ # 礼品卡功能
├── graphql/ # GraphQL schema和resolvers
├── order/ # 订单管理
├── page/ # CMS页面管理
├── payment/ # 支付网关集成
├── product/ # 产品目录管理
├── shipping/ # 物流配送
├── site/ # 站点配置
├── warehouse/ # 仓库库存管理
└── webhook/ # Webhook系统
3. 优势分析
3.1 技术优势
- 现代技术栈: 基于Python、Django和React的现代技术栈,提供更好的性能、易扩展性和开发灵活性
- GraphQL原生: GraphQL提供灵活的查询语言,客户端只请求需要的数据,提升性能和用户体验
- 多渠道支持: 原生支持多渠道销售,每个渠道可独立控制价格、货币、库存和产品
- API优先设计: 所有功能通过API提供,便于集成和扩展
- 容器化部署: Docker支持,云原生设计
3.2 业务优势
- 企业级功能: 多仓库、多货币、多语言、复杂促销规则
- 灵活的产品模型: 支持复杂的产品变体和属性系统
- 强大的订单管理: 支持分单发货、退换货、部分退款
- 完整的支付系统: 支持多种支付网关和支付流程
- SEO友好: Headless架构提供无限的SEO优化空间
3.3 开发者体验
- 丰富的文档: 完整的API文档和开发指南
- 活跃社区: 开源社区支持和贡献
- 插件系统: 通过Apps和Webhooks扩展功能
- TypeScript支持: 前端完全使用TypeScript开发
4. 劣势分析
4.1 技术挑战
- 学习曲线: 需要掌握Django、GraphQL、React等多种技术
- 复杂性: Headless架构对小团队来说可能过于复杂
- Python性能: 相比Node.js等,Python在高并发场景下性能有限
- 内存占用: Django应用内存占用相对较高
4.2 部署和维护
- 基础设施要求: 需要PostgreSQL、Redis、Celery等多个组件
- 运维复杂度: 相比单体应用,微服务架构运维更复杂
- 版本升级: 主要版本升级可能涉及数据迁移和API变更
- 资源消耗: 开发和部署环境资源需求较高
4.3 生态系统
- 第三方集成: 相比成熟平台如Shopify,第三方应用生态较小
- 即用性: 需要较多定制化开发才能投入生产使用
- 人才要求: 需要具备全栈开发能力的团队
5. 主要使用场景
5.1 企业级电商
- 大型B2C平台: 需要高性能、可扩展的电商解决方案
- 多品牌集团: 统一平台管理多个品牌和渠道
- 国际化业务: 多语言、多货币、多地区运营
- 高流量场景: 需要处理大量并发订单和用户
5.2 特定行业解决方案
- 时尚服装: 复杂的产品变体(尺码、颜色、材质)
- 数字产品: 软件、课程、数字内容销售
- B2B批发: 企业采购、批量定价、账期管理
- 预订制商品: 个性化定制、预订销售
5.3 创新应用场景
- Omnichannel零售: 线上线下一体化
- 移动商务: PWA或原生App商城
- 社交电商: 集成社交媒体销售
- 订阅商务: 订阅盒子、会员制销售
6. 代码结构深度分析
6.1 Django应用架构
python
# 典型的Saleor Django应用结构
saleor/product/
├── __init__.py
├── migrations/ # 数据库迁移文件
├── fixtures/ # 测试数据
├── models.py # 数据模型定义
├── managers.py # 自定义查询管理器
├── validators.py # 数据验证器
├── utils.py # 工具函数
├── tasks.py # Celery异步任务
├── search.py # 搜索相关功能
└── tests/ # 单元测试
├── __init__.py
├── test_models.py
├── test_utils.py
└── test_tasks.py
6.2 GraphQL Schema组织
python
# saleor/graphql/product/schema.py
import graphene
from ...product import models
from .types import Product, ProductVariant
from .mutations import ProductCreate, ProductUpdate
class ProductQueries(graphene.ObjectType):
product = graphene.Field(
Product,
id=graphene.Argument(graphene.ID, required=True),
description="查找单个产品"
)
products = DjangoFilterConnectionField(
Product,
filterset_class=ProductFilter,
description="产品列表查询"
)
class ProductMutations(graphene.ObjectType):
product_create = ProductCreate.Field()
product_update = ProductUpdate.Field()
product_delete = ProductDelete.Field()
6.3 模型设计模式
python
# saleor/product/models.py
from django.db import models
from ..core.models import SortableModel, PublishableModel
class Product(SortableModel, PublishableModel):
"""产品主模型"""
name = models.CharField(max_length=250)
slug = models.SlugField(max_length=255, unique=True)
description = JSONField(blank=True, default=dict)
# 使用JSONField存储富文本和多语言内容
# 支持版本化和国际化
category = models.ForeignKey(
"Category",
related_name="products",
on_delete=models.CASCADE
)
# 软删除模式
objects = models.Manager()
published = PublishedProductManager()
class Meta:
ordering = ("sort_order", "name")
permissions = (
("manage_products", "Manage products"),
)
7. 主要执行步骤分析
7.1 产品查询流程
- GraphQL查询接收: 客户端发送GraphQL查询
- 权限验证: 检查用户权限和访问控制
- 查询解析: GraphQL resolver解析查询字段
- 数据库查询: Django ORM执行优化的数据库查询
- 数据序列化: 将数据库结果转换为GraphQL响应
- 缓存处理: Redis缓存热门查询结果
- 响应返回: 返回JSON格式的GraphQL响应
7.2 订单处理流程
- 购物车管理: 前端管理购物车状态
- 结账初始化: 创建Checkout对象
- 地址验证: 验证配送和账单地址
- 运费计算: 根据商品和地址计算运费
- 税费计算: 计算适用的税费
- 支付处理: 调用支付网关处理付款
- 订单创建: 支付成功后创建Order对象
- 库存扣减: 扣减相应的产品库存
- 异步任务: 发送确认邮件、更新分析数据
- Webhook通知: 通知外部系统订单状态
8. 时序图
8.1 用户注册登录流程
8.2 产品搜索流程
8.3 订单创建流程
mermaid
9. 开发示例
9.1 自定义产品属性
python
# 扩展产品模型以支持自定义属性
from saleor.attribute.models import Attribute, AttributeValue
from saleor.product.models import Product, ProductType
from django.db import models
class CustomProductManager:
"""自定义产品管理器"""
def __init__(self):
pass
def create_product_with_attributes(self, product_data, attributes_data):
"""创建带有自定义属性的产品"""
# 创建产品类型
product_type = ProductType.objects.create(
name=product_data['type_name'],
slug=product_data['type_slug'],
has_variants=product_data.get('has_variants', False)
)
# 创建产品
product = Product.objects.create(
name=product_data['name'],
slug=product_data['slug'],
product_type=product_type,
description=product_data.get('description', {}),
weight=product_data.get('weight', 0)
)
# 添加自定义属性
for attr_data in attributes_data:
attribute, created = Attribute.objects.get_or_create(
name=attr_data['name'],
slug=attr_data['slug'],
defaults={
'input_type': attr_data.get('input_type', 'dropdown'),
'value_required': attr_data.get('required', False)
}
)
# 添加属性到产品类型
product_type.product_attributes.add(attribute)
# 为产品设置属性值
if 'value' in attr_data:
attr_value, created = AttributeValue.objects.get_or_create(
attribute=attribute,
name=attr_data['value'],
defaults={'slug': attr_data['value'].lower()}
)
# 关联产品和属性值
from saleor.attribute.models import AssignedProductAttribute
AssignedProductAttribute.objects.create(
product=product,
assignment__attribute=attribute,
values=[attr_value]
)
return product
# 使用示例
manager = CustomProductManager()
product = manager.create_product_with_attributes(
product_data={
'name': '定制T恤',
'slug': 'custom-tshirt',
'type_name': '服装',
'type_slug': 'clothing',
'has_variants': True,
'description': {'content': '100%棉质定制T恤'},
'weight': 0.2
},
attributes_data=[
{'name': '尺码', 'slug': 'size', 'value': 'L', 'input_type': 'dropdown'},
{'name': '颜色', 'slug': 'color', 'value': '蓝色', 'input_type': 'dropdown'},
{'name': '材质', 'slug': 'material', 'value': '棉', 'input_type': 'dropdown'}
]
)
9.2 自定义GraphQL Mutation
python
# saleor/graphql/custom/mutations.py
import graphene
from graphql import GraphQLError
from saleor.core.permissions import ProductPermissions
from saleor.product import models
from saleor.core.mutations import BaseMutation
class ProductBulkUpdateInput(graphene.InputObjectType):
ids = graphene.List(graphene.ID, required=True)
category_id = graphene.ID()
is_published = graphene.Boolean()
visible_in_listings = graphene.Boolean()
class ProductBulkUpdate(BaseMutation):
"""批量更新产品"""
class Arguments:
input = ProductBulkUpdateInput(required=True)
class Meta:
description = "批量更新多个产品的通用属性"
permissions = (ProductPermissions.MANAGE_PRODUCTS,)
count = graphene.Int(description="更新的产品数量")
@classmethod
def perform_mutation(cls, _root, info, **data):
input_data = data.get("input")
ids = input_data.get("ids", [])
if not ids:
raise GraphQLError("至少需要提供一个产品ID")
# 获取要更新的产品
products = models.Product.objects.filter(
id__in=ids
).select_related("category")
if not products.exists():
raise GraphQLError("未找到匹配的产品")
# 准备更新数据
update_data = {}
if "category_id" in input_data:
try:
category = models.Category.objects.get(
id=input_data["category_id"]
)
update_data["category"] = category
except models.Category.DoesNotExist:
raise GraphQLError("指定的分类不存在")
if "is_published" in input_data:
update_data["is_published"] = input_data["is_published"]
if "visible_in_listings" in input_data:
update_data["visible_in_listings"] = input_data["visible_in_listings"]
# 执行批量更新
updated_count = products.update(**update_data)
# 清理相关缓存
from saleor.core.utils import flush_cache_for_products
flush_cache_for_products(list(products.values_list("id", flat=True)))
return ProductBulkUpdate(count=updated_count)
# 注册自定义支付网关
def get_payment_gateway(gateway_name: str, config: GatewayConfig) -> PaymentGateway:
"""获取支付网关实例"""
if gateway_name == 'custom_gateway':
return CustomPaymentGateway(config)
# 其他网关...
raise ValueError(f"不支持的支付网关: {gateway_name}")
9.5 自定义库存管理
python
# saleor/warehouse/custom_inventory.py
from typing import Dict, List, Optional
from decimal import Decimal
from django.db import transaction
from saleor.warehouse.models import Stock, Warehouse, Allocation
from saleor.product.models import ProductVariant
from saleor.order.models import OrderLine
import logging
logger = logging.getLogger(__name__)
class AdvancedInventoryManager:
"""高级库存管理器"""
def __init__(self):
pass
def allocate_inventory_optimized(
self,
order_lines: List[OrderLine],
shipping_address: Optional[Dict] = None
) -> Dict[str, Any]:
"""优化的库存分配算法"""
allocation_results = []
failed_allocations = []
with transaction.atomic():
for line in order_lines:
variant = line.variant
quantity_needed = line.quantity
# 获取可用库存,按距离排序
available_stocks = self.get_available_stocks_by_distance(
variant, shipping_address
)
allocated_quantity = 0
line_allocations = []
for stock in available_stocks:
if allocated_quantity >= quantity_needed:
break
available_qty = stock.quantity - stock.allocated_quantity
if available_qty <= 0:
continue
# 计算当前库存可分配的数量
allocate_qty = min(
quantity_needed - allocated_quantity,
available_qty
)
if allocate_qty > 0:
# 创建分配记录
allocation = Allocation.objects.create(
order_line=line,
stock=stock,
quantity_allocated=allocate_qty
)
# 更新库存分配数量
stock.allocated_quantity += allocate_qty
stock.save(update_fields=['allocated_quantity'])
line_allocations.append({
'warehouse': stock.warehouse.name,
'allocated_quantity': allocate_qty,
'allocation_id': allocation.id
})
allocated_quantity += allocate_qty
if allocated_quantity >= quantity_needed:
allocation_results.append({
'order_line_id': line.id,
'variant_sku': variant.sku,
'requested_quantity': quantity_needed,
'allocated_quantity': allocated_quantity,
'allocations': line_allocations,
'status': 'success'
})
else:
# 库存不足
failed_allocations.append({
'order_line_id': line.id,
'variant_sku': variant.sku,
'requested_quantity': quantity_needed,
'allocated_quantity': allocated_quantity,
'shortage': quantity_needed - allocated_quantity,
'status': 'insufficient_stock'
})
return {
'success': len(failed_allocations) == 0,
'successful_allocations': allocation_results,
'failed_allocations': failed_allocations,
'total_lines': len(order_lines)
}
def get_available_stocks_by_distance(
self,
variant: ProductVariant,
shipping_address: Optional[Dict] = None
) -> List[Stock]:
"""根据距离获取可用库存"""
stocks = Stock.objects.filter(
product_variant=variant,
quantity__gt=0
).select_related('warehouse')
if not shipping_address:
# 如果没有配送地址,按库存数量排序
return list(stocks.order_by('-quantity'))
# 根据地理位置计算距离并排序
stocks_with_distance = []
shipping_lat = shipping_address.get('latitude')
shipping_lng = shipping_address.get('longitude')
for stock in stocks:
warehouse = stock.warehouse
if warehouse.address and warehouse.address.latitude and warehouse.address.longitude:
distance = self.calculate_distance(
shipping_lat, shipping_lng,
warehouse.address.latitude, warehouse.address.longitude
)
stocks_with_distance.append((stock, distance))
else:
# 没有地理位置信息的仓库放到最后
stocks_with_distance.append((stock, float('inf')))
# 按距离排序
stocks_with_distance.sort(key=lambda x: x[1])
return [stock for stock, _ in stocks_with_distance]
def calculate_distance(self, lat1: float, lon1: float, lat2: float, lon2: float) -> float:
"""计算两点间距离(简化的球面距离公式)"""
import math
# 地球半径(公里)
R = 6371
# 转换为弧度
lat1_rad = math.radians(lat1)
lon1_rad = math.radians(lon1)
lat2_rad = math.radians(lat2)
lon2_rad = math.radians(lon2)
# 计算差值
dlat = lat2_rad - lat1_rad
dlon = lon2_rad - lon1_rad
# 球面距离公式
a = (math.sin(dlat/2)**2 +
math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon/2)**2)
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1-a))
distance = R * c
return distance
def rebalance_inventory(self, warehouse_from_id: int, warehouse_to_id: int,
rebalance_rules: Dict[str, Any]) -> Dict[str, Any]:
"""库存平衡调拨"""
try:
warehouse_from = Warehouse.objects.get(id=warehouse_from_id)
warehouse_to = Warehouse.objects.get(id=warehouse_to_id)
except Warehouse.DoesNotExist:
return {'success': False, 'error': '仓库不存在'}
transfer_records = []
with transaction.atomic():
# 获取需要平衡的库存
stocks_from = Stock.objects.filter(
warehouse=warehouse_from,
quantity__gt=rebalance_rules.get('min_stock_threshold', 10)
).select_related('product_variant')
for stock_from in stocks_from:
variant = stock_from.product_variant
# 获取目标仓库库存
stock_to, created = Stock.objects.get_or_create(
warehouse=warehouse_to,
product_variant=variant,
defaults={'quantity': 0}
)
# 计算调拨数量
max_transfer = rebalance_rules.get('max_transfer_percentage', 0.3)
transfer_qty = int(stock_from.quantity * max_transfer)
# 检查目标仓库是否需要补货
target_min = rebalance_rules.get('target_min_stock', 5)
if stock_to.quantity >= target_min:
continue
needed_qty = target_min - stock_to.quantity
actual_transfer = min(transfer_qty, needed_qty, stock_from.quantity)
if actual_transfer > 0:
# 执行调拨
stock_from.quantity -= actual_transfer
stock_to.quantity += actual_transfer
stock_from.save(update_fields=['quantity'])
stock_to.save(update_fields=['quantity'])
# 记录调拨记录(需要自定义模型)
transfer_records.append({
'variant_sku': variant.sku,
'from_warehouse': warehouse_from.name,
'to_warehouse': warehouse_to.name,
'quantity': actual_transfer,
'timestamp': timezone.now()
})
return {
'success': True,
'transfers_count': len(transfer_records),
'transfer_records': transfer_records
}
# 使用示例
inventory_manager = AdvancedInventoryManager()
# 订单库存分配
allocation_result = inventory_manager.allocate_inventory_optimized(
order_lines=order.lines.all(),
shipping_address={
'latitude': 31.2304,
'longitude': 121.4737
}
)
10. 二次开发建议
10.1 架构优化方向
10.1.1 性能优化
python
# 数据库查询优化
class OptimizedProductQueries:
"""优化的产品查询"""
@staticmethod
def get_products_with_minimal_queries(category_id: Optional[int] = None):
"""使用select_related和prefetch_related优化查询"""
queryset = Product.objects.select_related(
'category',
'product_type'
).prefetch_related(
'variants__stocks',
'attributes__values',
'images',
'collections'
)
if category_id:
queryset = queryset.filter(category_id=category_id)
return queryset.published()
@staticmethod
def get_product_variants_with_availability():
"""获取产品变体及库存信息"""
from django.db.models import Sum, F
return ProductVariant.objects.select_related(
'product',
'product__category'
).annotate(
total_stock=Sum('stocks__quantity'),
available_stock=Sum('stocks__quantity') - Sum('stocks__allocated_quantity')
).filter(
total_stock__gt=0
)
# 缓存策略
from django.core.cache import cache
from django.utils.cache import make_template_fragment_key
class ProductCacheManager:
"""产品缓存管理器"""
CACHE_TIMEOUT = 60 * 15 # 15分钟
@classmethod
def get_cached_product(cls, product_id: int) -> Optional[Product]:
"""获取缓存的产品信息"""
cache_key = f"product:{product_id}"
product = cache.get(cache_key)
if product is None:
try:
product = Product.objects.select_related('category').get(id=product_id)
cache.set(cache_key, product, cls.CACHE_TIMEOUT)
except Product.DoesNotExist:
return None
return product
@classmethod
def invalidate_product_cache(cls, product_id: int):
"""清除产品缓存"""
cache_keys = [
f"product:{product_id}",
f"product_variants:{product_id}",
f"product_availability:{product_id}"
]
cache.delete_many(cache_keys)
10.1.2 微服务架构改造
python
# 服务分离建议
"""
建议的微服务拆分:
1. 用户服务 (User Service)
- 用户注册、登录、认证
- 用户配置文件管理
- 权限控制
2. 产品目录服务 (Catalog Service)
- 产品信息管理
- 分类和属性
- 搜索和筛选
3. 库存服务 (Inventory Service)
- 库存管理
- 仓库管理
- 库存分配和调拨
4. 订单服务 (Order Service)
- 购物车管理
- 订单创建和处理
- 订单状态跟踪
5. 支付服务 (Payment Service)
- 支付网关集成
- 支付流程管理
- 退款处理
6. 通知服务 (Notification Service)
- 邮件发送
- 短信通知
- 推送通知
7. 分析服务 (Analytics Service)
- 数据收集
- 报表生成
- 业务分析
"""
# 服务间通信示例
class ServiceClient:
"""微服务客户端基类"""
def __init__(self, service_url: str, api_key: str):
self.service_url = service_url
self.api_key = api_key
self.session = requests.Session()
self.session.headers.update({
'Authorization': f'Bearer {api_key}',
'Content-Type': 'application/json'
})
def make_request(self, method: str, endpoint: str, **kwargs):
"""发送HTTP请求"""
url = f"{self.service_url.rstrip('/')}/{endpoint.lstrip('/')}"
response = self.session.request(method, url, **kwargs)
response.raise_for_status()
return response.json()
class InventoryServiceClient(ServiceClient):
"""库存服务客户端"""
def check_availability(self, variant_id: int, quantity: int) -> bool:
"""检查库存可用性"""
data = self.make_request('POST', '/inventory/check', json={
'variant_id': variant_id,
'quantity': quantity
})
return data.get('available', False)
def allocate_inventory(self, order_lines: List[Dict]) -> Dict:
"""分配库存"""
return self.make_request('POST', '/inventory/allocate', json={
'order_lines': order_lines
})
10.2 功能扩展建议
10.2.1 多租户支持
python
# 多租户架构实现
from django.db import models
from django_tenants.models import TenantMixin, DomainMixin
class Client(TenantMixin):
"""租户模型"""
name = models.CharField(max_length=100)
created_on = models.DateField(auto_now_add=True)
# 默认字段
auto_create_schema = True
class Domain(DomainMixin):
"""域名模型"""
pass
# 租户特定的产品模型
class TenantProduct(Product):
"""租户特定的产品"""
class Meta:
proxy = True
def save(self, *args, **kwargs):
# 确保产品属于当前租户
from django_tenants.utils import tenant_context
with tenant_context(self.tenant):
super().save(*args, **kwargs)
# 租户中间件
class TenantMiddleware:
"""租户识别中间件"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# 从域名或子域名识别租户
hostname = request.get_host().split(':')[0].lower()
try:
tenant = Client.objects.get(domains__domain=hostname)
request.tenant = tenant
except Client.DoesNotExist:
# 使用默认租户或返回404
request.tenant = None
response = self.get_response(request)
return response
10.2.2 高级促销引擎
python
# 复杂促销规则引擎
from typing import List, Dict, Any
from decimal import Decimal
from enum import Enum
class PromotionType(Enum):
PERCENTAGE_DISCOUNT = "percentage"
FIXED_DISCOUNT = "fixed"
BUY_X_GET_Y = "buy_x_get_y"
SHIPPING_DISCOUNT = "shipping"
TIERED_DISCOUNT = "tiered"
class PromotionCondition:
"""促销条件基类"""
def evaluate(self, context: Dict[str, Any]) -> bool:
raise NotImplementedError
class MinimumOrderAmountCondition(PromotionCondition):
"""最小订单金额条件"""
def __init__(self, minimum_amount: Decimal):
self.minimum_amount = minimum_amount
def evaluate(self, context: Dict[str, Any]) -> bool:
order_total = context.get('order_total', Decimal('0'))
return order_total >= self.minimum_amount
class ProductCategoryCondition(PromotionCondition):
"""产品分类条件"""
def __init__(self, category_ids: List[int]):
self.category_ids = category_ids
def evaluate(self, context: Dict[str, Any]) -> bool:
cart_items = context.get('cart_items', [])
return any(
item.get('category_id') in self.category_ids
for item in cart_items
)
class AdvancedPromotionEngine:
"""高级促销引擎"""
def __init__(self):
self.active_promotions = []
def add_promotion(self, promotion: Dict[str, Any]):
"""添加促销活动"""
self.active_promotions.append(promotion)
def calculate_discounts(self, order_context: Dict[str, Any]) -> List[Dict[str, Any]]:
"""计算可用的折扣"""
applicable_discounts = []
for promotion in self.active_promotions:
if self.is_promotion_applicable(promotion, order_context):
discount = self.calculate_promotion_discount(promotion, order_context)
if discount['amount'] > 0:
applicable_discounts.append(discount)
# 按折扣金额排序,返回最优惠的
applicable_discounts.sort(key=lambda x: x['amount'], reverse=True)
return applicable_discounts
def is_promotion_applicable(self, promotion: Dict[str, Any],
context: Dict[str, Any]) -> bool:
"""检查促销是否适用"""
conditions = promotion.get('conditions', [])
for condition in conditions:
if not condition.evaluate(context):
return False
return True
def calculate_promotion_discount(self, promotion: Dict[str, Any],
context: Dict[str, Any]) -> Dict[str, Any]:
"""计算促销折扣"""
promotion_type = promotion.get('type')
if promotion_type == PromotionType.PERCENTAGE_DISCOUNT.value:
return self.calculate_percentage_discount(promotion, context)
elif promotion_type == PromotionType.FIXED_DISCOUNT.value:
return self.calculate_fixed_discount(promotion, context)
elif promotion_type == PromotionType.BUY_X_GET_Y.value:
return self.calculate_buy_x_get_y_discount(promotion, context)
return {'amount': Decimal('0'), 'description': ''}
def calculate_percentage_discount(self, promotion: Dict[str, Any],
context: Dict[str, Any]) -> Dict[str, Any]:
"""计算百分比折扣"""
percentage = Decimal(str(promotion.get('percentage', 0))) / 100
order_total = context.get('order_total', Decimal('0'))
max_discount = promotion.get('max_discount')
discount_amount = order_total * percentage
if max_discount and discount_amount > Decimal(str(max_discount)):
discount_amount = Decimal(str(max_discount))
return {
'amount': discount_amount,
'description': f"{promotion.get('percentage')}% 折扣",
'promotion_id': promotion.get('id')
}
10.3 部署和运维建议
10.3.1 容器化部署
yaml
# docker-compose.yml
version: '3.8'
services:
api:
build:
context: .
dockerfile: Dockerfile
environment:
- DATABASE_URL=postgresql://saleor:saleor@db:5432/saleor
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/1
depends_on:
- db
- redis
volumes:
- ./media:/app/media
ports:
- "8000:8000"
worker:
build:
context: .
dockerfile: Dockerfile
command: celery -A saleor worker --loglevel=info
environment:
- DATABASE_URL=postgresql://saleor:saleor@db:5432/saleor
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/1
depends_on:
- db
- redis
volumes:
- ./media:/app/media
beat:
build:
context: .
dockerfile: Dockerfile
command: celery -A saleor beat --loglevel=info
environment:
- DATABASE_URL=postgresql://saleor:saleor@db:5432/saleor
- REDIS_URL=redis://redis:6379/0
- CELERY_BROKER_URL=redis://redis:6379/1
depends_on:
- db
- redis
db:
image: postgres:13
environment:
- POSTGRES_DB=saleor
- POSTGRES_USER=saleor
- POSTGRES_PASSWORD=saleor
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:6-alpine
ports:
- "6379:6379"
volumes:
postgres_data:
10.3.2 Kubernetes部署配置
yaml
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: saleor-api
labels:
app: saleor-api
spec:
replicas: 3
selector:
matchLabels:
app: saleor-api
template:
metadata:
labels:
app: saleor-api
spec:
containers:
- name: saleor-api
image: saleor/saleor:3.15
ports:
- containerPort: 8000
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: saleor-secrets
key: database-url
- name: REDIS_URL
value: "redis://redis-service:6379/0"
- name: SECRET_KEY
valueFrom:
secretKeyRef:
name: saleor-secrets
key: secret-key
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
readinessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 30
periodSeconds: 10
livenessProbe:
httpGet:
path: /health/
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
---
apiVersion: v1
kind: Service
metadata:
name: saleor-api-service
spec:
selector:
app: saleor-api
ports:
- protocol: TCP
port: 80
targetPort: 8000
type: ClusterIP
10.3.3 监控和日志配置
python
# monitoring/metrics.py
from prometheus_client import Counter, Histogram, Gauge
import time
from functools import wraps
# 定义指标
REQUEST_COUNT = Counter('saleor_requests_total', 'Total requests', ['method', 'endpoint', 'status'])
REQUEST_LATENCY = Histogram('saleor_request_duration_seconds', 'Request latency')
ACTIVE_ORDERS = Gauge('saleor_active_orders', 'Number of active orders')
INVENTORY_LEVELS = Gauge('saleor_inventory_level', 'Inventory levels', ['sku'])
def track_metrics(func):
"""请求指标装饰器"""
@wraps(func)
def wrapper(*args, **kwargs):
start_time = time.time()
try:
result = func(*args, **kwargs)
REQUEST_COUNT.labels(
method=kwargs.get('method', 'unknown'),
endpoint=kwargs.get('endpoint', 'unknown'),
status='success'
).inc()
return result
except Exception as e:
REQUEST_COUNT.labels(
method=kwargs.get('method', 'unknown'),
endpoint=kwargs.get('endpoint', 'unknown'),
status='error'
).inc()
raise
finally:
REQUEST_LATENCY.observe(time.time() - start_time)
return wrapper
# 日志配置
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'formatters': {
'json': {
'format': '{"time": "%(asctime)s", "level": "%(levelname)s", "name": "%(name)s", "message": "%(message)s"}',
'datefmt': '%Y-%m-%d %H:%M:%S'
}
},
'handlers': {
'console': {
'class': 'logging.StreamHandler',
'formatter': 'json'
},
'file': {
'class': 'logging.handlers.RotatingFileHandler',
'filename': '/app/logs/saleor.log',
'maxBytes': 1024*1024*10, # 10MB
'backupCount': 5,
'formatter': 'json'
}
},
'loggers': {
'saleor': {
'handlers': ['console', 'file'],
'level': 'INFO',
'propagate': False
}
}
}
10.4 安全性增强建议
python
# security/enhanced_auth.py
from django.contrib.auth.backends import BaseBackend
from django.contrib.auth import get_user_model
from django.utils import timezone
from datetime import timedelta
import bcrypt
import pyotp
User = get_user_model()
class EnhancedAuthenticationBackend(BaseBackend):
"""增强的认证后端"""
def authenticate(self, request, email=None, password=None, totp_token=None, **kwargs):
if email is None or password is None:
return None
try:
user = User.objects.get(email=email)
except User.DoesNotExist:
return None
# 检查用户状态
if not user.is_active:
return None
# 检查密码
if not user.check_password(password):
# 记录失败尝试
self.log_failed_attempt(user, request)
return None
# 检查账户锁定
if self.is_account_locked(user):
return None
# 检查2FA(如果启用)
if user.is_2fa_enabled:
if not totp_token or not self.verify_totp(user, totp_token):
return None
# 更新最后登录时间
user.last_login = timezone.now()
user.save(update_fields=['last_login'])
return user
def verify_totp(self, user, token):
"""验证TOTP令牌"""
totp = pyotp.TOTP(user.totp_secret)
return totp.verify(token, valid_window=1)
def is_account_locked(self, user):
"""检查账户是否被锁定"""
from .models import LoginAttempt
recent_attempts = LoginAttempt.objects.filter(
user=user,
timestamp__gte=timezone.now() - timedelta(minutes=30),
success=False
).count()
return recent_attempts >= 5
def log_failed_attempt(self, user, request):
"""记录登录失败尝试"""
from .models import LoginAttempt
LoginAttempt.objects.create(
user=user,
ip_address=self.get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
success=False
)
def get_client_ip(self, request):
"""获取客户端IP"""
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
return ip
这份详尽的分析涵盖了Saleor项目的核心技术架构、优劣势分析、实际应用场景以及深入的代码示例。Saleor作为现代化的headless电商平台,具有高度的可扩展性和定制性,特别适合需要复杂业务逻辑和高性能要求的企业级电商应用。
通过提供的开发示例和二次开发建议,您可以根据具体业务需求对Saleor进行深度定制,构建出符合企业特定需求的电商解决方案。册到schema class CustomMutations(graphene.ObjectType): product_bulk_update = ProductBulkUpdate.Field()
### 9.3 自定义Webhook处理
```python
# saleor/webhook/custom_handlers.py
from typing import Any, Dict
from django.http import HttpRequest
from saleor.webhook.event_types import WebhookEventAsyncType
from saleor.core.models import EventDelivery
import requests
import logging
logger = logging.getLogger(__name__)
class CustomWebhookHandler:
"""自定义Webhook处理器"""
def __init__(self):
self.supported_events = {
WebhookEventAsyncType.ORDER_CREATED: self.handle_order_created,
WebhookEventAsyncType.ORDER_CONFIRMED: self.handle_order_confirmed,
WebhookEventAsyncType.PRODUCT_UPDATED: self.handle_product_updated,
}
def handle_order_created(self, payload: Dict[str, Any], event_delivery: EventDelivery):
"""处理订单创建事件"""
order_data = payload.get('order', {})
# 发送到外部系统(如ERP)
external_system_payload = {
'order_id': order_data.get('id'),
'customer_email': order_data.get('user_email'),
'total_amount': order_data.get('total', {}).get('gross', {}).get('amount'),
'currency': order_data.get('total', {}).get('gross', {}).get('currency'),
'items': [
{
'sku': line.get('variant', {}).get('sku'),
'quantity': line.get('quantity'),
'price': line.get('unit_price', {}).get('gross', {}).get('amount')
}
for line in order_data.get('lines', [])
]
}
try:
response = requests.post(
'https://external-system.com/api/orders',
json=external_system_payload,
timeout=30
)
response.raise_for_status()
logger.info(f"订单 {order_data.get('id')} 成功同步到外部系统")
except requests.RequestException as e:
logger.error(f"同步订单到外部系统失败: {e}")
# 可以选择重新排队或记录错误
def handle_product_updated(self, payload: Dict[str, Any], event_delivery: EventDelivery):
"""处理产品更新事件"""
product_data = payload.get('product', {})
# 更新搜索索引
from saleor.search.utils import update_product_search_index
update_product_search_index(product_data.get('id'))
# 清理CDN缓存
self.clear_cdn_cache_for_product(product_data.get('slug'))
def clear_cdn_cache_for_product(self, product_slug: str):
"""清理产品相关的CDN缓存"""
cache_urls = [
f"https://cdn.example.com/products/{product_slug}/",
f"https://cdn.example.com/api/products/{product_slug}/",
]
for url in cache_urls:
try:
requests.post(
'https://cdn-api.example.com/purge',
json={'url': url},
timeout=10
)
except requests.RequestException:
logger.warning(f"清理CDN缓存失败: {url}")
# 在tasks.py中使用
from celery import shared_task
@shared_task(bind=True, max_retries=3)
def process_webhook_event(self, event_delivery_id: int):
"""处理Webhook事件的Celery任务"""
try:
event_delivery = EventDelivery.objects.get(id=event_delivery_id)
handler = CustomWebhookHandler()
if event_delivery.event_type in handler.supported_events:
handler.supported_events[event_delivery.event_type](
event_delivery.payload,
event_delivery
)
except Exception as exc:
logger.error(f"处理Webhook事件失败: {exc}")
self.retry(countdown=60 * (self.request.retries + 1))
9.4 自定义支付网关
python
# saleor/payment/gateways/custom_gateway.py
from decimal import Decimal
from typing import Dict, Any, Optional
from saleor.payment.interface import PaymentGateway, GatewayConfig
from saleor.payment.models import Payment
from saleor.core.payments import PaymentError
import requests
class CustomPaymentGateway(PaymentGateway):
"""自定义支付网关实现"""
def __init__(self, config: GatewayConfig):
self.config = config
self.api_key = config.connection_params.get('api_key')
self.endpoint = config.connection_params.get('endpoint')
self.merchant_id = config.connection_params.get('merchant_id')
def authorize(
self,
payment_information: Dict[str, Any],
config: GatewayConfig,
**kwargs
) -> Dict[str, Any]:
"""授权支付"""
try:
# 构建支付请求
payload = {
'merchant_id': self.merchant_id,
'amount': str(payment_information['amount']),
'currency': payment_information['currency'],
'order_id': payment_information.get('order_id'),
'customer_email': payment_information.get('customer_email'),
'payment_method': payment_information.get('payment_method_token'),
'return_url': payment_information.get('return_url'),
'action': 'authorize'
}
# 发送到支付网关
response = requests.post(
f"{self.endpoint}/payments/authorize",
json=payload,
headers={'Authorization': f'Bearer {self.api_key}'},
timeout=30
)
response_data = response.json()
if response.status_code == 200 and response_data.get('status') == 'success':
return {
'is_success': True,
'transaction_id': response_data.get('transaction_id'),
'amount': Decimal(response_data.get('amount', 0)),
'currency': response_data.get('currency'),
'gateway_response': response_data
}
else:
return {
'is_success': False,
'error': response_data.get('error_message', '支付授权失败'),
'gateway_response': response_data
}
except requests.RequestException as e:
return {
'is_success': False,
'error': f'网关连接错误: {str(e)}',
'gateway_response': {}
}
def capture(
self,
payment_information: Dict[str, Any],
config: GatewayConfig,
**kwargs
) -> Dict[str, Any]:
"""捕获已授权的支付"""
transaction_id = payment_information.get('transaction_id')
if not transaction_id:
return {
'is_success': False,
'error': '缺少交易ID',
'gateway_response': {}
}
try:
payload = {
'merchant_id': self.merchant_id,
'transaction_id': transaction_id,
'amount': str(payment_information['amount']),
'action': 'capture'
}
response = requests.post(
f"{self.endpoint}/payments/capture",
json=payload,
headers={'Authorization': f'Bearer {self.api_key}'},
timeout=30
)
response_data = response.json()
return {
'is_success': response.status_code == 200,
'transaction_id': response_data.get('transaction_id', transaction_id),
'amount': Decimal(response_data.get('amount', 0)),
'gateway_response': response_data
}
except requests.RequestException as e:
return {
'is_success': False,
'error': f'网关连接错误: {str(e)}',
'gateway_response': {}
}
def refund(
self,
payment_information: Dict[str, Any],
config: GatewayConfig,
**kwargs
) -> Dict[str, Any]:
"""退款处理"""
transaction_id = payment_information.get('transaction_id')
refund_amount = payment_information.get('amount')
try:
payload = {
'merchant_id': self.merchant_id,
'transaction_id': transaction_id,
'amount': str(refund_amount),
'reason': payment_information.get('reason', '客户申请退款'),
'action': 'refund'
}
response = requests.post(
f"{self.endpoint}/payments/refund",
json=payload,
headers={'Authorization': f'Bearer {self.api_key}'},
timeout=30
)
response_data = response.json()
return {
'is_success': response.status_code == 200,
'transaction_id': response_data.get('refund_id'),
'amount': Decimal(response_data.get('refund_amount', 0)),
'gateway_response': response_data
}
except requests.RequestException as e:
return {
'is_success': False,
'error': f'退款处理失败: {str(e)}',
'gateway_response': {}
}
# 注
核心亮点总结:
技术架构优势:
- Saleor采用API-first、headless架构,使用GraphQL作为唯一API接口,实现了技术无关性和高度解耦 GitHub - facebookresearch/segment-anything: The repository provides code for running inference with the SegmentAnything Model (SAM), links for downloading the trained model checkpoints, and example notebooks that show how to use the model.
- 基于Django + PostgreSQL + Redis + Celery的现代化技术栈,提供企业级的性能和可扩展性 GitHub - facebookresearch/segment-anything: The repository provides code for running inference with the SegmentAnything Model (SAM), links for downloading the trained model checkpoints, and example notebooks that show how to use the model.
- 原生支持多渠道、多货币、多语言,每个渠道可独立控制价格、货币、库存和产品 GitHub - facebookresearch/segment-anything: The repository provides code for running inference with the SegmentAnything Model (SAM), links for downloading the trained model checkpoints, and example notebooks that show how to use the model.
特别适合的场景:
- 企业级B2C/B2B电商:需要复杂的产品变体、多仓库管理、高级促销规则
- 国际化业务:多语言、多货币、跨境电商
- 定制化需求强:通过Apps和Webhooks实现深度定制
- 高并发场景:微服务友好的架构设计
开发建议重点:
- 性能优化:我提供了数据库查询优化、缓存策略、微服务改造的具体实现
- 功能扩展:包含多租户支持、高级促销引擎、自定义支付网关等实用示例
- 运维部署:提供了完整的Docker、Kubernetes配置和监控方案
- 安全增强:包括增强认证、2FA、账户锁定等安全机制
技术挑战:
- 学习曲线较陡峭,需要掌握Django、GraphQL、React等多种技术
- 对小团队来说可能过于复杂,更适合有一定技术实力的团队
- 相比现成的SaaS方案,需要更多的定制开发工作
Saleor是一个非常强大的现代化电商平台,特别适合那些需要高度定制化、具备技术开发能力、面向企业级应用的场景。如果您的项目符合这些特点,Saleor将是一个出色的选择。
您对哪个方面特别感兴趣,需要我进一步详细说明吗?比如具体的实施步骤、某个技术点的深入分析,或者针对您的具体业务场景的定制建议?
Retry
Claude can make mistakes.
Please double-check responses.
Sonnet 4