Files
affiliate_dash/backend/internal/service/provider.go
T
yml2213 5f642eb7a6 feat: 优化发货交付页面与凭据生成,支持一键复制结果图片及官方商品图片本地缓存
- 交付界面重构:隐藏内部订单号、流水号与发货渠道,展示聚焦于买家游戏UID、角色名与交付时间
- 发货结果凭据生成:采用 Canvas 生成高清发货存证图,优化存证说明文案与自适应排版
- 一键复制图片:支持直接将结果凭证图片写入系统剪贴板,方便客服在聊天窗口快捷粘贴
- 官方图片本地化缓存:全量下载乐玩商品官方主图并建立本地持久化路由,规避外部源站跨域及破损问题
- 修复乐玩渠道角色UID提取与履约状态幂等回填逻辑
2026-09-14 17:19:12 +08:00

224 lines
7.1 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"
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] + "..."
}