- 交付界面重构:隐藏内部订单号、流水号与发货渠道,展示聚焦于买家游戏UID、角色名与交付时间 - 发货结果凭据生成:采用 Canvas 生成高清发货存证图,优化存证说明文案与自适应排版 - 一键复制图片:支持直接将结果凭证图片写入系统剪贴板,方便客服在聊天窗口快捷粘贴 - 官方图片本地化缓存:全量下载乐玩商品官方主图并建立本地持久化路由,规避外部源站跨域及破损问题 - 修复乐玩渠道角色UID提取与履约状态幂等回填逻辑
224 lines
7.1 KiB
Go
224 lines
7.1 KiB
Go
package service
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"affiliate_dash/internal/model"
|
||
)
|
||
|
||
const (
|
||
ProviderDLC = "dlc"
|
||
ProviderLewan = "lewan"
|
||
|
||
RoutingModePriority = "priority"
|
||
RoutingModeLewan = "lewan"
|
||
RoutingModeDLC = "dlc"
|
||
)
|
||
|
||
// 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)
|
||
}
|
||
|
||
// 兼容老配置或单 provider 字段
|
||
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)
|
||
}
|
||
if legacy.Provider == ProviderLewan || legacy.LewanProductID != "" {
|
||
cfg.RoutingMode = RoutingModeLewan
|
||
cfg.LewanProductID = legacy.LewanProductID
|
||
} else if legacy.Provider == ProviderDLC {
|
||
cfg.RoutingMode = RoutingModeDLC
|
||
cfg.DLCGoodID = legacy.DLCGoodID
|
||
}
|
||
}
|
||
|
||
// 默认兜底:若第一平台有内置 GoodID,补齐 DLCGoodID
|
||
builtinDLC := deliveryGoodID("dlc", sku)
|
||
if cfg.DLCGoodID == "" && builtinDLC != "" {
|
||
cfg.DLCGoodID = builtinDLC
|
||
}
|
||
|
||
// 若未配置 RoutingMode
|
||
if cfg.RoutingMode == "" {
|
||
if cfg.LewanProductID != "" && cfg.DLCGoodID != "" {
|
||
cfg.RoutingMode = RoutingModePriority
|
||
cfg.Priority = []string{ProviderLewan, ProviderDLC}
|
||
} else if cfg.LewanProductID != "" {
|
||
cfg.RoutingMode = RoutingModeLewan
|
||
} 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)
|
||
}
|
||
case RoutingModeDLC:
|
||
if p, ok := r.providers[ProviderDLC]; 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] + "..."
|
||
}
|