package service import ( "context" "encoding/json" "net/url" "strings" "affiliate_dash/internal/model" ) const ( ProviderDLC = "dlc" ProviderLewan = "lewan" ProviderRock = "rock" RoutingModePriority = "priority" RoutingModeLewan = "lewan" RoutingModeDLC = "dlc" RoutingModeRock = "rock" ) // ProductRoutingConfig 商品履约配置 type ProductRoutingConfig struct { RoutingMode string `json:"routing_mode"` // priority | lewan | dlc Priority []string `json:"priority"` // ["lewan", "dlc"] 或 ["dlc", "lewan"] DLCGoodID string `json:"dlc_good_id"` // 第一平台 GoodID LewanProductID string `json:"lewan_product_id"` // 乐玩商品 UUID LewanName string `json:"lewan_name,omitempty"` DeliveryType string `json:"delivery_type,omitempty"` // LINK_BINDING / EMAIL_BINDING } // ParseProductRoutingConfig 解析商品履约配置,若未配置则根据 SKU 默认走 DLC 兜底。 func ParseProductRoutingConfig(fulfillmentConfig string, sku string) ProductRoutingConfig { var cfg ProductRoutingConfig if strings.TrimSpace(fulfillmentConfig) != "" { _ = json.Unmarshal([]byte(fulfillmentConfig), &cfg) } // 兼容老配置:只抽取字段,不提前固化 routing_mode, // 避免仅有 lewan_product_id 时在补齐内置 DLC 前被锁成 lewan-only。 legacyProvider := "" if cfg.RoutingMode == "" { var legacy struct { Provider string `json:"provider"` LewanProductID string `json:"lewan_product_id"` DLCGoodID string `json:"dlc_good_id"` } if strings.TrimSpace(fulfillmentConfig) != "" { _ = json.Unmarshal([]byte(fulfillmentConfig), &legacy) } legacyProvider = strings.TrimSpace(legacy.Provider) if cfg.LewanProductID == "" && legacy.LewanProductID != "" { cfg.LewanProductID = legacy.LewanProductID } if cfg.DLCGoodID == "" && legacy.DLCGoodID != "" { cfg.DLCGoodID = legacy.DLCGoodID } } // 默认兜底:若第一平台有内置 GoodID,补齐 DLCGoodID builtinDLC := deliveryGoodID("dlc", sku) if cfg.DLCGoodID == "" && builtinDLC != "" { cfg.DLCGoodID = builtinDLC } // 若未配置 RoutingMode:优先判断是否为 Rock 渠道专属商品 if cfg.RoutingMode == "" { if strings.HasPrefix(sku, "rock_") { cfg.RoutingMode = RoutingModeRock cfg.Priority = []string{ProviderRock} } else if cfg.LewanProductID != "" && cfg.DLCGoodID != "" { cfg.RoutingMode = RoutingModePriority cfg.Priority = []string{ProviderLewan, ProviderDLC} } else if cfg.LewanProductID != "" || legacyProvider == ProviderLewan { cfg.RoutingMode = RoutingModeLewan } else if legacyProvider == ProviderDLC { cfg.RoutingMode = RoutingModeDLC } else { cfg.RoutingMode = RoutingModeDLC } } if len(cfg.Priority) == 0 && cfg.RoutingMode == RoutingModePriority { cfg.Priority = []string{ProviderLewan, ProviderDLC} } return cfg } // DeliveryOrderState 抽象商品履约状态 type DeliveryOrderState struct { CanShip bool CannotShipReason string Product *DeliveryProduct Good map[string]interface{} Provider string } // ProviderBindResult 抽象绑定返回 type ProviderBindResult struct { BindUUID string `json:"bind_uuid"` BindURL string `json:"bind_url"` QRURL string `json:"qr_url"` Bound bool `json:"bound"` GameAccount string `json:"game_account,omitempty"` RoleName string `json:"role_name,omitempty"` GameChannel string `json:"game_channel,omitempty"` ExpectedGameAccount string `json:"expected_game_account,omitempty"` Mismatch bool `json:"mismatch,omitempty"` Message string `json:"message,omitempty"` Provider string `json:"provider,omitempty"` // PatchResultData 绑定成功后写入订单 result_data(如 lewan_order_id) PatchResultData map[string]interface{} `json:"-"` } // ProviderSubmitResult 抽象提交返回 type ProviderSubmitResult struct { ProviderOrderNo string `json:"provider_order_no"` NextStatus string `json:"next_status"` // delivering / delivered Message string `json:"message"` GameAccount map[string]interface{} `json:"game_account,omitempty"` UpstreamOrder interface{} `json:"delivery,omitempty"` PatchResultData map[string]interface{} `json:"patch_result_data,omitempty"` } // ProviderSyncResult 抽象主动查单同步状态返回 type ProviderSyncResult struct { Status string // success / failed / delivering / "" ProviderOrderNo string FailReason string GameUID string RoleName string RoleArea string RawPayload string } // FulfillmentProvider 统一履约提供商接口 type FulfillmentProvider interface { Name() string PrepareDelivery(ctx context.Context, order *model.FulfillmentOrder, openOrder *OpenOrderQuery) (*DeliveryOrderState, error) Bind(ctx context.Context, order *model.FulfillmentOrder, gameAccount string) (*ProviderBindResult, error) CheckBind(ctx context.Context, order *model.FulfillmentOrder, bindUUID string) (*ProviderBindResult, error) Submit(ctx context.Context, order *model.FulfillmentOrder, gameAccount, bindUUID string) (*ProviderSubmitResult, error) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) Cancel(ctx context.Context, order *model.FulfillmentOrder) error } // FulfillmentRouter 履约路由器 type FulfillmentRouter struct { providers map[string]FulfillmentProvider } func NewFulfillmentRouter(providers ...FulfillmentProvider) *FulfillmentRouter { m := make(map[string]FulfillmentProvider) for _, p := range providers { if p != nil { m[p.Name()] = p } } return &FulfillmentRouter{providers: m} } func (r *FulfillmentRouter) GetProvider(name string) FulfillmentProvider { return r.providers[name] } // ResolveProviders 返回订单可选的履约渠道列表(按优先级排序) func (r *FulfillmentRouter) ResolveProviders(order *model.FulfillmentOrder) ([]FulfillmentProvider, ProductRoutingConfig) { sku := "" configStr := "" if order != nil { sku = order.ProductSKU if order.MerchantProduct != nil { configStr = order.MerchantProduct.FulfillmentConfig } } cfg := ParseProductRoutingConfig(configStr, sku) // 如果订单已有选定渠道记录(例如已经对乐玩或 DLC 发起过 bind/submit),则锁定该渠道 if order != nil && strings.TrimSpace(order.ResultData) != "" { if active := resultDataString(order.ResultData, "provider"); active != "" { if p, ok := r.providers[active]; ok { return []FulfillmentProvider{p}, cfg } } } var list []FulfillmentProvider switch cfg.RoutingMode { case RoutingModeLewan: if p, ok := r.providers[ProviderLewan]; ok { list = append(list, p) } // 显式 lewan 但商品同时具备 DLC 配置时,乐玩不可用(密钥未配/上游失败)允许回退 DLC, // 避免「套装-浪漫天命」等双渠道商品在 LEWAN_* 未注入时提交 UID 直接失败。 if cfg.DLCGoodID != "" { if p, ok := r.providers[ProviderDLC]; ok { list = append(list, p) } } case RoutingModeDLC: if p, ok := r.providers[ProviderDLC]; ok { list = append(list, p) } case RoutingModeRock: if p, ok := r.providers[ProviderRock]; ok { list = append(list, p) } case RoutingModePriority: for _, name := range cfg.Priority { if p, ok := r.providers[name]; ok { list = append(list, p) } } } // 最终安全兜底:若解析为空,优先尝试 DLC,若无则尝试任意可用 provider if len(list) == 0 { if dlc, ok := r.providers[ProviderDLC]; ok { list = append(list, dlc) } else { for _, p := range r.providers { list = append(list, p) break } } } return list, cfg } func qrCodeURL(targetURL string) string { return "https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=" + url.QueryEscape(targetURL) } func truncateText(s string, maxLen int) string { if len(s) <= maxLen { return s } return s[:maxLen] + "..." }