乐玩写接口失败时清除渠道锁定改试 DLC;免扫码/查绑/提交路径回传 role_name 与 game_channel。 Co-authored-by: Cursor <cursoragent@cursor.com>
566 lines
16 KiB
Go
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
|
|
}
|