baseline: 批次A-D 成果 + membership 半成品(测试红)
This commit is contained in:
@@ -0,0 +1,190 @@
|
||||
"""电商与渠道服务层:
|
||||
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
|
||||
Reference in New Issue
Block a user