Files
affiliate_dash/backend/internal/service/provider_lewan.go
T
yml2213andCursor 848d3da809 fix(delivery): 绑定失败回退 DLC,并补齐乐玩角色名展示
乐玩写接口失败时清除渠道锁定改试 DLC;免扫码/查绑/提交路径回传 role_name 与 game_channel。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 12:39:53 +08:00

566 lines
16 KiB
Go

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
}