package service import ( "context" "encoding/json" "errors" "fmt" "strings" "affiliate_dash/internal/model" "affiliate_dash/internal/pkg/lewan" ) // LewanProvider 实现乐玩开放平台履约逻辑 type LewanProvider struct { client *lewan.Client } func NewLewanProvider(client *lewan.Client) *LewanProvider { return &LewanProvider{client: client} } func (p *LewanProvider) Name() string { return ProviderLewan } func (p *LewanProvider) PrepareDelivery(ctx context.Context, order *model.FulfillmentOrder, openOrder *OpenOrderQuery) (*DeliveryOrderState, error) { if p.client == nil { return nil, errors.New("乐玩平台客户端未初始化") } sku := "" configStr := "" if order != nil { sku = order.ProductSKU if order.MerchantProduct != nil { configStr = order.MerchantProduct.FulfillmentConfig } } cfg := ParseProductRoutingConfig(configStr, sku) if cfg.LewanProductID == "" { return &DeliveryOrderState{ CanShip: false, CannotShipReason: "未配置乐玩商品ID", Provider: ProviderLewan, }, nil } lewanProd, err := p.client.GetProduct(ctx, cfg.LewanProductID) if err != nil { return &DeliveryOrderState{ CanShip: false, CannotShipReason: fmt.Sprintf("获取乐玩商品失败: %v", err), Provider: ProviderLewan, }, nil } if lewanProd.AccessMode != "PURCHASE" { return &DeliveryOrderState{ CanShip: false, CannotShipReason: "乐玩商品当前不可购买 (accessMode != PURCHASE)", Provider: ProviderLewan, }, nil } if lewanProd.Stock == 0 { return &DeliveryOrderState{ CanShip: false, CannotShipReason: "乐玩商品当前无库存", Provider: ProviderLewan, }, nil } deliveryProd := &DeliveryProduct{ Name: lewanProd.Name, SKU: sku, Game: lewanProd.CategoryName, Image: lewanProd.ThumbnailUrl, } if openOrder != nil && openOrder.Product != nil && openOrder.Product.Name != "" { deliveryProd.Name = openOrder.Product.Name } goodMap := map[string]interface{}{ "id": lewanProd.ID, "name": lewanProd.Name, "category_name": lewanProd.CategoryName, "stock": lewanProd.Stock, "delivery_type": lewanProd.DeliveryType, "price": lewanProd.EffectiveUnitPrice, } return &DeliveryOrderState{ CanShip: true, Product: deliveryProd, Good: goodMap, Provider: ProviderLewan, }, nil } func (p *LewanProvider) Bind(ctx context.Context, order *model.FulfillmentOrder, gameAccount string) (*ProviderBindResult, error) { if p.client == nil { return nil, errors.New("乐玩平台客户端未初始化") } sku := order.ProductSKU configStr := "" if order.MerchantProduct != nil { configStr = order.MerchantProduct.FulfillmentConfig } cfg := ParseProductRoutingConfig(configStr, sku) if cfg.LewanProductID == "" { return nil, errors.New("该商品未配置乐玩商品ID") } // 1. 检查或获取现有乐玩单号 lewanOrderID := resultDataString(order.ResultData, "lewan_order_id") var ( lewanOrder *lewan.LewanOrder bindingAction *lewan.BindingAction err error ) if lewanOrderID != "" { // 已创建过乐玩订单,查单或恢复动作 lewanOrder, err = p.client.GetOrder(ctx, lewanOrderID) if err == nil && lewanOrder != nil { bindingAction = lewanOrder.BindingAction if bindingAction != nil && bindingAction.BindingUrlExpired { restored, restoreErr := p.client.RestoreBindingAction(ctx, lewanOrderID) if restoreErr == nil && restored != nil { bindingAction = restored.BindingAction } } } } // 2. 若尚未建单,调乐玩下单 if lewanOrder == nil { quantity := order.Quantity if quantity <= 0 { quantity = 1 } createRes, createErr := p.client.CreateOrder(ctx, lewan.CreateOrderReq{ CallerOrderID: order.OrderNo, RoleID: gameAccount, Items: []lewan.OrderItem{ { ProductID: cfg.LewanProductID, Quantity: quantity, }, }, }) if createErr != nil { // 如果提示 callerOrderId 已存在,尝试按 callerOrderId 查单 if strings.Contains(createErr.Error(), "ALREADY_EXISTS") || strings.Contains(createErr.Error(), "DUPLICATE") { byCaller, getErr := p.client.GetOrderByCallerOrder(ctx, order.OrderNo) if getErr == nil && byCaller != nil { lewanOrder = byCaller bindingAction = byCaller.BindingAction } else { return nil, createErr } } else { return nil, createErr } } else if createRes != nil { lewanOrder = createRes.Order bindingAction = createRes.BindingAction } } if lewanOrder == nil { return nil, errors.New("乐玩创建订单未返回有效订单数据") } lewanOrderID = lewanOrder.ID patch := map[string]interface{}{ "provider": ProviderLewan, "lewan_order_id": lewanOrderID, "lewan_order_no": lewanOrder.OrderNo, "lewan_status": lewanOrder.Status, } // 3. 判断是否需要绑定 if bindingAction != nil && !bindingAction.BindingRequired { // 角色已在乐玩绑定过,免扫码直接已绑定;尽量补齐角色名/区服,便于领取页展示。 roleName, gameChannel, boundRoleID := extractLewanRoleDisplay(lewanOrder, nil) if refreshRes, refreshErr := p.client.RefreshBinding(ctx, lewanOrderID); refreshErr == nil { rn, gc, rid := extractLewanRoleDisplay(nil, refreshRes) if rn != "" { roleName = rn } if gc != "" { gameChannel = gc } if rid != "" { boundRoleID = rid } } if roleName != "" { patch["role_name"] = roleName } if gameChannel != "" { patch["game_channel"] = gameChannel } bindURL := strings.TrimSpace(bindingAction.BindingUrl) qrURL := "" if bindURL != "" { qrURL = qrCodeURL(bindURL) } return &ProviderBindResult{ BindUUID: lewanOrderID, BindURL: bindURL, QRURL: qrURL, Bound: true, GameAccount: firstNonEmpty(boundRoleID, gameAccount), RoleName: roleName, GameChannel: gameChannel, Message: "账号已在平台授权,免扫码直接进入发货", Provider: ProviderLewan, PatchResultData: patch, }, nil } bindURL := "" if bindingAction != nil { bindURL = bindingAction.BindingUrl } qrURL := "" if bindURL != "" { qrURL = qrCodeURL(bindURL) } return &ProviderBindResult{ BindUUID: lewanOrderID, BindURL: bindURL, QRURL: qrURL, Bound: false, GameAccount: gameAccount, Message: "请扫码或打开链接完成授权绑定", Provider: ProviderLewan, PatchResultData: patch, }, nil } func (p *LewanProvider) CheckBind(ctx context.Context, order *model.FulfillmentOrder, bindUUID string) (*ProviderBindResult, error) { if p.client == nil { return nil, errors.New("乐玩平台客户端未初始化") } lewanOrderID := bindUUID if lewanOrderID == "" { lewanOrderID = resultDataString(order.ResultData, "lewan_order_id") } if lewanOrderID == "" { return nil, errors.New("缺少乐玩订单编号") } refreshRes, err := p.client.RefreshBinding(ctx, lewanOrderID) if err != nil { // 查询订单兜底判断 orderDetail, getErr := p.client.GetOrder(ctx, lewanOrderID) if getErr == nil && orderDetail != nil { if orderDetail.Status != "WAITING_BINDING" { roleName, gameChannel, boundRoleID := extractLewanRoleDisplay(orderDetail, nil) return &ProviderBindResult{ BindUUID: lewanOrderID, Bound: true, GameAccount: firstNonEmpty(boundRoleID, orderDetail.RoleID), RoleName: roleName, GameChannel: gameChannel, Message: "绑定已完成", Provider: ProviderLewan, }, nil } } return &ProviderBindResult{ BindUUID: bindUUID, Bound: false, Message: "尚未完成绑定,请继续扫码操作", Provider: ProviderLewan, }, nil } bound := false gameAccount := "" roleName := "" gameChannel := "" if refreshRes != nil && refreshRes.Binding != nil { bound = refreshRes.Binding.Matched gameAccount = refreshRes.Binding.RoleID roleName = refreshRes.Binding.RoleName gameChannel = firstNonEmpty(refreshRes.Binding.RoleArea, refreshRes.Binding.ServerName) } if refreshRes != nil && refreshRes.Order != nil { if refreshRes.Order.Status != "WAITING_BINDING" { bound = true } rn, gc, rid := extractLewanRoleDisplay(refreshRes.Order, nil) if roleName == "" { roleName = rn } if gameChannel == "" { gameChannel = gc } if gameAccount == "" { gameAccount = rid } } msg := "绑定成功" if !bound { msg = "尚未完成扫码绑定或角色未匹配" } return &ProviderBindResult{ BindUUID: lewanOrderID, Bound: bound, GameAccount: gameAccount, RoleName: roleName, GameChannel: gameChannel, Message: msg, Provider: ProviderLewan, }, nil } func (p *LewanProvider) Submit(ctx context.Context, order *model.FulfillmentOrder, gameAccount, bindUUID string) (*ProviderSubmitResult, error) { if p.client == nil { return nil, errors.New("乐玩平台客户端未初始化") } lewanOrderID := bindUUID if lewanOrderID == "" { lewanOrderID = resultDataString(order.ResultData, "lewan_order_id") } if lewanOrderID == "" { return nil, errors.New("缺少乐玩订单编号") } // 刷新绑定状态并推动队列 refreshRes, err := p.client.RefreshBinding(ctx, lewanOrderID) var lewanOrder *lewan.LewanOrder if refreshRes != nil && refreshRes.Order != nil { lewanOrder = refreshRes.Order } else { lewanOrder, _ = p.client.GetOrder(ctx, lewanOrderID) } if lewanOrder == nil { if err != nil { return nil, fmt.Errorf("提交乐玩发货失败: %w", err) } return nil, errors.New("未获取到乐玩订单") } if lewanOrder.Status == "WAITING_BINDING" { return nil, errors.New("角色尚未绑定完成,请先完成扫码绑定后再提交发货") } providerOrderNo := lewanOrder.OrderNo if providerOrderNo == "" { providerOrderNo = lewanOrder.ID } nextStatus := model.OrderStatusDelivering message := "已提交乐玩平台发货,等待发货结果" if lewanOrder.Status == "SUCCEEDED" { nextStatus = model.OrderStatusDelivered message = "乐玩平台已发货完成" } else if lewanOrder.Status == "FAILED" { nextStatus = model.OrderStatusShipFailed message = "乐玩平台发货失败" } roleName := "" gameChannel := "" if refreshRes != nil && refreshRes.Binding != nil { roleName = refreshRes.Binding.RoleName gameChannel = firstNonEmpty(refreshRes.Binding.RoleArea, refreshRes.Binding.ServerName) } if roleName == "" || gameChannel == "" { rn, gc, _ := extractLewanRoleDisplay(lewanOrder, nil) if roleName == "" { roleName = rn } if gameChannel == "" { gameChannel = gc } } return &ProviderSubmitResult{ ProviderOrderNo: providerOrderNo, NextStatus: nextStatus, Message: message, GameAccount: map[string]interface{}{ "game_account": gameAccount, "role_name": roleName, "game_channel": gameChannel, }, UpstreamOrder: lewanOrder, PatchResultData: map[string]interface{}{ "provider": ProviderLewan, "provider_order_stage": deliveryStageSubmitted, "provider_order_no": providerOrderNo, "lewan_order_id": lewanOrder.ID, "lewan_order_no": lewanOrder.OrderNo, "lewan_status": lewanOrder.Status, "game_uid": gameAccount, "role_name": roleName, "game_channel": gameChannel, "delivery": lewanOrder, }, }, nil } func (p *LewanProvider) Cancel(ctx context.Context, order *model.FulfillmentOrder) error { if p.client == nil { return nil } lewanOrderID := resultDataString(order.ResultData, "lewan_order_id") if lewanOrderID == "" { return nil } _, err := p.client.CancelOrder(ctx, lewanOrderID) return err } func (p *LewanProvider) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) { if p.client == nil || order == nil { return nil, nil } callerOrderID := order.OrderNo lewanOrderID := resultDataString(order.ResultData, "lewan_order_id") var ( lewanOrder *lewan.LewanOrder err error ) if callerOrderID != "" { lewanOrder, err = p.client.GetOrderByCallerOrder(ctx, callerOrderID) } if lewanOrder == nil && lewanOrderID != "" { lewanOrder, err = p.client.GetOrder(ctx, lewanOrderID) } if err != nil || lewanOrder == nil { return nil, err } roleName := "" roleArea := "" gameUID := lewanOrder.RoleID if lewanOrder.Binding != nil { if lewanOrder.Binding.RoleID != "" { gameUID = lewanOrder.Binding.RoleID } roleName = lewanOrder.Binding.RoleName roleArea = lewanOrder.Binding.RoleArea } if gameUID == "" { gameUID = resultDataString(order.ResultData, "game_uid") } raw, _ := json.Marshal(lewanOrder) providerOrderNo := lewanOrder.OrderNo if providerOrderNo == "" { providerOrderNo = lewanOrder.ID } switch lewanOrder.Status { case "SUCCEEDED": return &ProviderSyncResult{ Status: "success", ProviderOrderNo: providerOrderNo, GameUID: gameUID, RoleName: roleName, RoleArea: roleArea, RawPayload: string(raw), }, nil case "FAILED": reason := strings.Join(lewanOrder.FailureCodes, "; ") if reason == "" { reason = "乐玩平台发货失败" } return &ProviderSyncResult{ Status: "failed", ProviderOrderNo: providerOrderNo, FailReason: reason, GameUID: gameUID, RoleName: roleName, RoleArea: roleArea, RawPayload: string(raw), }, nil case "PARTIAL_SUCCESS": if lewanOrder.SucceededQuantity > 0 { return &ProviderSyncResult{ Status: "success", ProviderOrderNo: providerOrderNo, GameUID: gameUID, RoleName: roleName, RoleArea: roleArea, RawPayload: string(raw), }, nil } reason := strings.Join(lewanOrder.FailureCodes, "; ") if reason == "" { reason = "乐玩部分单件被停止或释放" } return &ProviderSyncResult{ Status: "failed", ProviderOrderNo: providerOrderNo, FailReason: reason, GameUID: gameUID, RoleName: roleName, RoleArea: roleArea, RawPayload: string(raw), }, nil case "ORDER_CANCELLED", "BINDING_TIMEOUT": return &ProviderSyncResult{ Status: "failed", ProviderOrderNo: providerOrderNo, FailReason: "乐玩订单超时或已取消", GameUID: gameUID, RoleName: roleName, RoleArea: roleArea, RawPayload: string(raw), }, nil default: return &ProviderSyncResult{ Status: "delivering", ProviderOrderNo: providerOrderNo, GameUID: gameUID, RoleName: roleName, RoleArea: roleArea, RawPayload: string(raw), }, nil } } // extractLewanRoleDisplay 从乐玩订单/刷新结果提取角色名、区服与 RoleID,供领取页展示。 func extractLewanRoleDisplay( order *lewan.LewanOrder, refresh *lewan.RefreshBindingResult, ) (roleName, gameChannel, roleID string) { if refresh != nil && refresh.Binding != nil { roleName = strings.TrimSpace(refresh.Binding.RoleName) gameChannel = firstNonEmpty(refresh.Binding.RoleArea, refresh.Binding.ServerName) roleID = strings.TrimSpace(refresh.Binding.RoleID) } if refresh != nil && refresh.Order != nil && order == nil { order = refresh.Order } if order != nil { if roleID == "" { roleID = strings.TrimSpace(order.RoleID) } if order.Binding != nil { if roleName == "" { roleName = strings.TrimSpace(order.Binding.RoleName) } if gameChannel == "" { gameChannel = firstNonEmpty(order.Binding.RoleArea, order.Binding.ServerName) } if roleID == "" { roleID = strings.TrimSpace(order.Binding.RoleID) } } } return roleName, gameChannel, roleID }