Files
affiliate_dash/backend/internal/service/provider.go
T
yml2213 ef7f67dd0c feat(fulfillment): 接入Rock发货平台(0666.pro)代理履约、通用库存池与UID强一致风控
1. 数据库新增 012_rock_fulfillment 迁移,创建 rock_configs 配置表与 rock_tokens 通用库存表,补齐黑色ROCK长袖T恤与蓝色运动服上衣商品目录;
2. 封装 RockClient 与 RockService,实现 0666.pro 平台 Bearer Token 极简连通校验、批量采购50个入库、水位监控巡检与自动补货;
3. 新增 RockProvider 实现标准履约路由,引入 Anti-Ghost 旧绑定基线比对与 UID 强一致门禁,扫错号硬阻断并记录为 mismatch 状态;
4. 针对黑色ROCK与蓝色运动服实现动态发货(绑定后发货前动态选择 skin),并提取腾讯官方发货流水号 fulfillId 对账;
5. 新增全局自动发货总开关(Kill Switch),支持一键软熔断降级转人工接单处理;
6. 前端新增 PlatformRock 管理大屏,提供总开关切换、Token配置与连通测试、库存监控、一键补货及错单解冻重置。
2026-10-05 14:56:11 +08:00

244 lines
7.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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] + "..."
}