388 lines
15 KiB
Python
388 lines
15 KiB
Python
"""模块:订单管理器
|
|
SAAS 级订单编排器——子服务通过此管理器操作订单生命周期。
|
|
|
|
职责:
|
|
1. 基于模板快速创建订单(冻结模板快照)
|
|
2. 完整订单状态机流转(确认/支付/处理/完成/取消/退款/关闭/重开)
|
|
3. 订单查询(按租户/公司/买家/状态多维度)
|
|
4. 行项目批量管理
|
|
|
|
用法:
|
|
manager = OrderManager(tenant_uuid='xxx', company_uuid='yyy', user=admin_user)
|
|
|
|
# 从模板创建订单
|
|
order = manager.CreateFromTemplate(
|
|
template=tpl,
|
|
level=level,
|
|
buyer_user_uuid='user_001',
|
|
order_title='VIP 月卡',
|
|
)
|
|
|
|
# 状态流转
|
|
manager.Confirm(order.OrderUUID)
|
|
manager.Pay(order.OrderUUID, payment_transaction=tx)
|
|
manager.Complete(order.OrderUUID)
|
|
|
|
# 查询
|
|
orders = manager.ListOrders(status=Order.ORD_PAID)
|
|
"""
|
|
import uuid as _uuid
|
|
import logging
|
|
from decimal import Decimal
|
|
from typing import Optional, List
|
|
|
|
from django.db import transaction
|
|
from django.utils import timezone as _timezone
|
|
|
|
from gvsdsdk.order.models import (
|
|
OrderLevel,
|
|
OrderTemplate,
|
|
OrderTemplateItem,
|
|
Order,
|
|
OrderItem,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class OrderError(Exception):
|
|
"""订单业务异常"""
|
|
|
|
def __init__(self, message, order=None):
|
|
super().__init__(message)
|
|
self.Order = order
|
|
|
|
|
|
class OrderManager:
|
|
"""订单编排器——SAAS 主服务器核心入口
|
|
|
|
所有方法均按 tenant_uuid + company_uuid 隔离操作。
|
|
状态流转方法会自动校验当前状态是否允许该流转。
|
|
"""
|
|
|
|
# 状态流转规则:{ target_status: (allowed_from_statuses, timestamp_field) }
|
|
FLOW_RULES = {
|
|
Order.ORD_CONFIRMED: ({Order.ORD_PENDING}, 'ConfirmTime'),
|
|
Order.ORD_PAID: ({Order.ORD_CONFIRMED}, 'PayTime'),
|
|
Order.ORD_PROCESSING: ({Order.ORD_PAID}, None),
|
|
Order.ORD_COMPLETED: ({Order.ORD_PROCESSING}, 'CompleteTime'),
|
|
Order.ORD_CANCELLED: ({Order.ORD_PENDING, Order.ORD_CONFIRMED}, 'CancelTime'),
|
|
Order.ORD_REFUNDING: ({Order.ORD_PAID, Order.ORD_PROCESSING, Order.ORD_COMPLETED}, None),
|
|
Order.ORD_REFUNDED: ({Order.ORD_REFUNDING}, None),
|
|
Order.ORD_CLOSED: ({Order.ORD_CANCELLED, Order.ORD_REFUNDED, Order.ORD_COMPLETED}, None),
|
|
}
|
|
|
|
# Reopen 特殊处理:CLOSED → PENDING
|
|
REOPEN_FROM = Order.ORD_CLOSED
|
|
REOPEN_TO = Order.ORD_PENDING
|
|
|
|
def __init__(self, tenant_uuid, company_uuid=None, user=None):
|
|
self.TenantUUID = self._to_bytes(tenant_uuid)
|
|
self.CompanyUUID = self._to_bytes(company_uuid)
|
|
self.User = user
|
|
|
|
# ========== 查询方法 ==========
|
|
|
|
def GetOrder(self, order_uuid):
|
|
"""查询单个订单(按租户+公司隔离)"""
|
|
order_uuid_bytes = self._to_bytes(order_uuid)
|
|
if not order_uuid_bytes:
|
|
return None
|
|
qs = Order.objects.filter(TenantUUID=self.TenantUUID, OrderUUID=order_uuid_bytes)
|
|
if self.CompanyUUID:
|
|
qs = qs.filter(CompanyUUID=self.CompanyUUID)
|
|
return qs.first()
|
|
|
|
def ListOrders(self, status=None, buyer_user_uuid=None, order_type=None,
|
|
page=1, page_size=20):
|
|
"""列出订单(多维度过滤)"""
|
|
try:
|
|
page = max(int(page), 1)
|
|
page_size = min(max(int(page_size), 1), 200)
|
|
except (TypeError, ValueError):
|
|
page = 1
|
|
page_size = 20
|
|
qs = Order.objects.filter(TenantUUID=self.TenantUUID)
|
|
if self.CompanyUUID:
|
|
qs = qs.filter(CompanyUUID=self.CompanyUUID)
|
|
if status is not None:
|
|
qs = qs.filter(OrderStatus=status)
|
|
if buyer_user_uuid:
|
|
buyer_bytes = self._to_bytes(buyer_user_uuid)
|
|
if buyer_bytes:
|
|
qs = qs.filter(BuyerUserUUID=buyer_bytes)
|
|
if order_type:
|
|
qs = qs.filter(OrderType=order_type)
|
|
qs = qs.order_by('-CreateTime')
|
|
total = qs.count()
|
|
start = (page - 1) * page_size
|
|
records = list(qs[start:start + page_size])
|
|
return {'records': records, 'total': total, 'page': page, 'page_size': page_size}
|
|
|
|
def GetOrderItems(self, order_uuid):
|
|
"""获取订单行项目列表"""
|
|
order_uuid_bytes = self._to_bytes(order_uuid)
|
|
if not order_uuid_bytes:
|
|
return []
|
|
# 使用 Order_id 直接按 FK 主键过滤(与 create 中 Order=order 风格不同但语义一致:
|
|
# 过滤时无需加载 Order 对象,创建时已有 Order 实例直接赋值)
|
|
return list(OrderItem.objects.filter(Order_id=order_uuid_bytes))
|
|
|
|
# ========== 创建方法 ==========
|
|
|
|
@transaction.atomic
|
|
def CreateFromTemplate(self, template, buyer_user_uuid, level=None,
|
|
seller_user_uuid=None, order_title=None,
|
|
order_description='', order_type=None,
|
|
discount_amount=Decimal('0'), currency='CNY',
|
|
expire_time=None, remark='', items_override=None,
|
|
payment_config=None):
|
|
"""从模板创建订单——自动冻结模板/等级快照,复制模板项到订单项
|
|
|
|
Args:
|
|
template: OrderTemplate 实例
|
|
level: OrderLevel 实例(可选)
|
|
buyer_user_uuid: 买家用户 UUID
|
|
seller_user_uuid: 卖家用户 UUID(可选)
|
|
order_title: 订单标题(默认用模板名)
|
|
order_description: 订单描述
|
|
order_type: 订单类型(默认用模板类型)
|
|
discount_amount: 优惠金额
|
|
currency: 币种
|
|
expire_time: 过期时间
|
|
remark: 备注
|
|
items_override: 自定义行项目列表(覆盖模板项),每项为 dict:
|
|
{ItemCode, ItemName, ItemDescription, ItemType, UnitPrice, Quantity, SubTotal, ItemMetadata}
|
|
payment_config: PaymentGatewayConfig 实例(可选)
|
|
|
|
Returns:
|
|
Order 实例
|
|
"""
|
|
if not template:
|
|
raise OrderError('订单模板不能为空')
|
|
if not buyer_user_uuid:
|
|
raise OrderError('买家用户 UUID 不能为空')
|
|
|
|
buyer_bytes = self._to_bytes(buyer_user_uuid)
|
|
seller_bytes = self._to_bytes(seller_user_uuid) if seller_user_uuid else None
|
|
|
|
# 生成订单编号
|
|
order_no = self._generate_order_no()
|
|
|
|
# 冻结模板快照
|
|
template_snapshot = self._snapshot_template(template)
|
|
level_snapshot = self._snapshot_level(level) if level else {}
|
|
|
|
# 计算金额
|
|
total_amount = Decimal('0')
|
|
if level:
|
|
total_amount = level.Price or Decimal('0')
|
|
pay_amount = total_amount - (discount_amount or Decimal('0'))
|
|
if pay_amount < 0:
|
|
pay_amount = Decimal('0')
|
|
|
|
order = Order.objects.create(
|
|
OrderUUID=_uuid.uuid4().bytes,
|
|
TenantUUID=self.TenantUUID,
|
|
CompanyUUID=self.CompanyUUID,
|
|
OrderNO=order_no,
|
|
Template=template,
|
|
Level=level,
|
|
OrderTitle=order_title or template.TemplateName,
|
|
OrderDescription=order_description,
|
|
OrderType=order_type or template.TemplateType,
|
|
BuyerUserUUID=buyer_bytes,
|
|
SellerUserUUID=seller_bytes,
|
|
TotalAmount=total_amount,
|
|
DiscountAmount=discount_amount or Decimal('0'),
|
|
PayAmount=pay_amount,
|
|
Currency=currency,
|
|
OrderStatus=Order.ORD_PENDING,
|
|
PaymentConfig=payment_config,
|
|
ExpireTime=expire_time,
|
|
TemplateSnapshot=template_snapshot,
|
|
LevelSnapshot=level_snapshot,
|
|
OrderMetadata={},
|
|
Remark=remark,
|
|
)
|
|
|
|
# 创建订单项
|
|
item_list = []
|
|
if items_override:
|
|
for item_data in items_override:
|
|
item_list.append(OrderItem(
|
|
ItemUUID=_uuid.uuid4().bytes,
|
|
Order=order,
|
|
ItemCode=item_data.get('ItemCode', ''),
|
|
ItemName=item_data.get('ItemName', ''),
|
|
ItemDescription=item_data.get('ItemDescription', ''),
|
|
ItemType=item_data.get('ItemType', 'default'),
|
|
UnitPrice=item_data.get('UnitPrice', Decimal('0')),
|
|
Quantity=item_data.get('Quantity', 1),
|
|
Subtotal=item_data.get('SubTotal') or (
|
|
item_data.get('UnitPrice', Decimal('0')) * item_data.get('Quantity', 1)
|
|
),
|
|
ItemMetadata=item_data.get('ItemMetadata', {}),
|
|
))
|
|
else:
|
|
# 复制模板项
|
|
for tpl_item in template.Items.all():
|
|
item_list.append(OrderItem(
|
|
ItemUUID=_uuid.uuid4().bytes,
|
|
Order=order,
|
|
ItemCode=tpl_item.ItemCode,
|
|
ItemName=tpl_item.ItemName,
|
|
ItemDescription=tpl_item.ItemDescription,
|
|
ItemType=tpl_item.ItemType,
|
|
UnitPrice=tpl_item.UnitPrice,
|
|
Quantity=tpl_item.DefaultQuantity,
|
|
Subtotal=tpl_item.UnitPrice * tpl_item.DefaultQuantity,
|
|
ItemMetadata=tpl_item.ItemMetadata,
|
|
))
|
|
if item_list:
|
|
OrderItem.objects.bulk_create(item_list)
|
|
|
|
logger.info('[OrderManager] 创建订单: %s (template=%s, buyer=%s)',
|
|
order_no, template.TemplateName, buyer_user_uuid)
|
|
return order
|
|
|
|
# ========== 状态流转方法 ==========
|
|
|
|
def Confirm(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_CONFIRMED, 'Confirm')
|
|
|
|
def Pay(self, order_uuid, payment_transaction=None):
|
|
order = self._flow(order_uuid, Order.ORD_PAID, 'Pay')
|
|
if order and payment_transaction:
|
|
order.PaymentTransaction = payment_transaction
|
|
order.save(update_fields=['PaymentTransaction'])
|
|
return order
|
|
|
|
def Process(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_PROCESSING, 'Process')
|
|
|
|
def Complete(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_COMPLETED, 'Complete')
|
|
|
|
def Cancel(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_CANCELLED, 'Cancel')
|
|
|
|
def Refund(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_REFUNDING, 'Refund')
|
|
|
|
def Refunded(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_REFUNDED, 'Refunded')
|
|
|
|
def Close(self, order_uuid):
|
|
return self._flow(order_uuid, Order.ORD_CLOSED, 'Close')
|
|
|
|
def Reopen(self, order_uuid):
|
|
"""重新打开已关闭的订单(回到待确认状态)"""
|
|
order = self._get_order_or_raise(order_uuid)
|
|
if order.OrderStatus != self.REOPEN_FROM:
|
|
raise OrderError(
|
|
f'订单状态流转失败:当前状态为「{order.OrderStatus}」,仅允许从「{self.REOPEN_FROM}」重新打开',
|
|
order=order,
|
|
)
|
|
old_status = order.OrderStatus
|
|
order.OrderStatus = self.REOPEN_TO
|
|
# 清空所有流转时间戳
|
|
order.ConfirmTime = None
|
|
order.PayTime = None
|
|
order.CompleteTime = None
|
|
order.CancelTime = None
|
|
order.UpdateTime = _timezone.now()
|
|
order.save(update_fields=[
|
|
'OrderStatus', 'ConfirmTime', 'PayTime',
|
|
'CompleteTime', 'CancelTime', 'UpdateTime',
|
|
])
|
|
logger.info('[OrderManager] 订单重新打开: %s (%s → %s)',
|
|
order.OrderNO, old_status, self.REOPEN_TO)
|
|
return order
|
|
|
|
# ========== 内部工具方法 ==========
|
|
|
|
def _flow(self, order_uuid, target_status, action_label):
|
|
"""通用状态流转方法"""
|
|
order = self._get_order_or_raise(order_uuid)
|
|
allowed_from, timestamp_field = self.FLOW_RULES[target_status]
|
|
if order.OrderStatus not in allowed_from:
|
|
allowed_str = ', '.join(str(s) for s in sorted(allowed_from))
|
|
raise OrderError(
|
|
f'订单状态流转失败:当前状态为「{order.OrderStatus}」,仅允许从「{allowed_str}」流转到「{target_status}」',
|
|
order=order,
|
|
)
|
|
old_status = order.OrderStatus
|
|
order.OrderStatus = target_status
|
|
now = _timezone.now()
|
|
if timestamp_field:
|
|
setattr(order, timestamp_field, now)
|
|
order.UpdateTime = now
|
|
update_fields = ['OrderStatus', 'UpdateTime']
|
|
if timestamp_field:
|
|
update_fields.append(timestamp_field)
|
|
order.save(update_fields=update_fields)
|
|
logger.info('[OrderManager] 订单状态流转: %s %s (%s → %s)',
|
|
order.OrderNO, action_label, old_status, target_status)
|
|
return order
|
|
|
|
def _get_order_or_raise(self, order_uuid):
|
|
order = self.GetOrder(order_uuid)
|
|
if not order:
|
|
raise OrderError(f'订单不存在或无权访问: {order_uuid}')
|
|
return order
|
|
|
|
def _generate_order_no(self):
|
|
"""生成唯一订单编号: ORD-YYYYMMDD-XXXXXXXX"""
|
|
now = _timezone.now()
|
|
random_part = _uuid.uuid4().hex[:8].upper()
|
|
return f'ORD-{now.strftime("%Y%m%d")}-{random_part}'
|
|
|
|
def _snapshot_template(self, template):
|
|
"""冻结模板快照"""
|
|
return {
|
|
'TemplateUUID': self._to_str(template.pk),
|
|
'TemplateName': template.TemplateName,
|
|
'TemplateType': template.TemplateType,
|
|
'TemplateStatus': template.TemplateStatus,
|
|
}
|
|
|
|
def _snapshot_level(self, level):
|
|
"""冻结等级快照"""
|
|
return {
|
|
'LevelUUID': self._to_str(level.pk),
|
|
'LevelCode': level.LevelCode,
|
|
'LevelName': level.LevelName,
|
|
'Price': str(level.Price),
|
|
'OriginalPrice': str(level.OriginalPrice),
|
|
'Currency': level.Currency,
|
|
'DurationDays': level.DurationDays,
|
|
'DurationMonths': level.DurationMonths,
|
|
}
|
|
|
|
@staticmethod
|
|
def _to_bytes(value):
|
|
if value is None:
|
|
return None
|
|
if hasattr(value, 'bytes'):
|
|
return value.bytes
|
|
if isinstance(value, bytes):
|
|
return value
|
|
if isinstance(value, str):
|
|
try:
|
|
return _uuid.UUID(value).bytes
|
|
except ValueError:
|
|
return None
|
|
return None
|
|
|
|
@staticmethod
|
|
def _to_str(value):
|
|
if value is None:
|
|
return None
|
|
if isinstance(value, bytes) and len(value) == 16:
|
|
try:
|
|
return str(_uuid.UUID(bytes=value))
|
|
except ValueError:
|
|
return None
|
|
return str(value)
|