"""电商与渠道服务层: 1. sync_channel_orders: 从外部平台拉取订单并入库 2. convert_channel_order_to_sales_order: 自动转内部销售订单 3. handle_inbound_webhook: 接收外部 Webhook 并分发 """ from decimal import Decimal from datetime import date from django.db import transaction from apps.sales.models import SalesOrder, SalesOrderLine from apps.catalog.models import Product from apps.partner.models import Customer from .models import ChannelAccount, ChannelOrder, WebhookEvent from .adapters import get_adapter_for_account @transaction.atomic def convert_channel_order_to_sales_order(channel_order: ChannelOrder) -> SalesOrder: """将渠道拉取的订单转为内部标准的销售订单(SalesOrder)。""" if channel_order.sync_status == "converted" and channel_order.sales_order: return channel_order.sales_order tenant = channel_order.tenant account = channel_order.channel_account # 客户解析:优先使用店铺配置的默认客户,不存在则动态查找或创建 customer = account.default_customer if not customer: customer, _ = Customer.objects.get_or_create( tenant=tenant, code=f"C_CH_{account.platform}_{account.shop_id}", defaults={ "name": f"{account.shop_name}客户(挂账)", "phone": channel_order.buyer_phone or "13800000000", "address": channel_order.receiver_address, }, ) # 仓库解析:优先店铺默认仓库,不存在则取租户首个可用仓库 from apps.inventory.models import Warehouse warehouse = account.default_warehouse or Warehouse.objects.filter(tenant=tenant).first() if not warehouse: warehouse = Warehouse.objects.create(tenant=tenant, code="WH_DEFAULT", name="默认仓库") items = channel_order.raw_payload.get("items", []) if not items: channel_order.sync_status = "failed" channel_order.sync_error = "外部订单缺少明细商品 (items)" channel_order.save(update_fields=["sync_status", "sync_error", "updated_at"]) raise ValueError("外部订单明细不能为空") # 创建 SalesOrder today_str = date.today().strftime("%Y%m%d") from apps.core.services import next_bill_no so_no = next_bill_no(tenant, "SO", SalesOrder) sales_order = SalesOrder.objects.create( tenant=tenant, bill_no=so_no, customer=customer, warehouse=warehouse, bill_date=date.today(), total_amount=Decimal("0"), state="confirmed", # 电商已付订单默认直接转为 confirmed 待开单发货 remark=f"来自电商渠道 {account.shop_name},外部单号: {channel_order.external_order_id},买家: {channel_order.buyer_name}", source_channel=account.platform, ) total_amount = Decimal("0") for item in items: p_code = item.get("product_code") product = Product.objects.filter(tenant=tenant, code=p_code, status="active").first() if not product: product = Product.objects.filter(tenant=tenant, code=p_code).first() if not product: channel_order.sync_status = "failed" channel_order.sync_error = f"商品编码 {p_code} 在系统商品中心不存在" channel_order.save(update_fields=["sync_status", "sync_error", "updated_at"]) raise ValueError(f"商品编码 {p_code} 不存在") qty = Decimal(str(item.get("quantity", 1))) price = Decimal(str(item.get("unit_price", product.sale_price))) amount = qty * price SalesOrderLine.objects.create( tenant=tenant, order=sales_order, product=product, quantity=qty, unit_price=price, amount=amount, ) total_amount += amount sales_order.total_amount = total_amount sales_order.save(update_fields=["total_amount", "updated_at"]) channel_order.sales_order = sales_order channel_order.sync_status = "converted" channel_order.sync_error = "" channel_order.save(update_fields=["sales_order", "sync_status", "sync_error", "updated_at"]) return sales_order @transaction.atomic def sync_channel_orders(account: ChannelAccount, auto_convert: bool = True) -> dict: """从渠道拉取订单并记录,可自动转为内部销售单。""" adapter = get_adapter_for_account(account) raw_orders = adapter.fetch_orders(account) pulled_count = 0 converted_count = 0 failed_count = 0 for ro in raw_orders: ext_id = ro["external_order_id"] corder, was_created = ChannelOrder.objects.get_or_create( tenant=account.tenant, channel_account=account, external_order_id=ext_id, defaults={ "order_status": ro.get("order_status", "PAID"), "order_amount": Decimal(str(ro.get("order_amount", 0))), "buyer_name": ro.get("buyer_name", ""), "buyer_phone": ro.get("buyer_phone", ""), "receiver_address": ro.get("receiver_address", ""), "raw_payload": ro, "sync_status": "pending", }, ) if was_created: pulled_count += 1 if auto_convert and corder.sync_status == "pending": try: convert_channel_order_to_sales_order(corder) converted_count += 1 except Exception: failed_count += 1 return { "shop_id": account.shop_id, "pulled_count": pulled_count, "converted_count": converted_count, "failed_count": failed_count, } def handle_inbound_webhook(tenant, platform: str, event_type: str, payload: dict) -> WebhookEvent: """处理外部 Webhook 回调。""" event = WebhookEvent.objects.create( tenant=tenant, direction="inbound", source=platform, event_type=event_type, payload=payload, status="received", ) try: # 如果是订单支付事件,且包含外部单号与店铺ID,则自动建单 if event_type == "order.paid": shop_id = payload.get("shop_id") account = ChannelAccount.objects.filter(tenant=tenant, platform=platform, shop_id=shop_id).first() if account and payload.get("external_order_id"): corder, _ = ChannelOrder.objects.get_or_create( tenant=tenant, channel_account=account, external_order_id=payload["external_order_id"], defaults={ "order_status": "PAID", "order_amount": Decimal(str(payload.get("order_amount", 0))), "buyer_name": payload.get("buyer_name", ""), "buyer_phone": payload.get("buyer_phone", ""), "receiver_address": payload.get("receiver_address", ""), "raw_payload": payload, }, ) convert_channel_order_to_sales_order(corder) event.status = "processed" event.save(update_fields=["status", "updated_at"]) except Exception as e: event.status = "failed" event.error_message = str(e) event.save(update_fields=["status", "error_message", "updated_at"]) return event