package service import ( "context" "errors" "fmt" "strconv" "strings" "affiliate_dash/internal/model" ) // RockProvider 实现 Rock 发货平台履约逻辑 type RockProvider struct { rockService *RockService } func NewRockProvider(rockService *RockService) *RockProvider { return &RockProvider{ rockService: rockService, } } func (p *RockProvider) Name() string { return ProviderRock } func (p *RockProvider) PrepareDelivery(ctx context.Context, order *model.FulfillmentOrder, openOrder *OpenOrderQuery) (*DeliveryOrderState, error) { if p.rockService == nil { return nil, errors.New("Rock 发货服务未初始化") } cfg, err := p.rockService.GetConfig(ctx) if err != nil { return nil, fmt.Errorf("读取 Rock 配置失败: %w", err) } // 1. 全局软熔断总开关检查 (Kill Switch) if !cfg.AutoDeliveryEnabled { return &DeliveryOrderState{ CanShip: false, CannotShipReason: "当前该商品自动发货通道维护中,已转交人工客服/打手为您处理", Provider: ProviderRock, }, nil } // 2. 检查可用库存 // 注意:若当前订单已经分配并锁定了专属 Token (using 或 bound 状态),说明该订单库存已经预占成功, // 绝不能因为池子中剩余的空闲库存为 0 而阻断该订单的发货与扫码提货流程! hasAssignedToken := false orderNo := "" if order != nil && strings.TrimSpace(order.OrderNo) != "" { orderNo = strings.TrimSpace(order.OrderNo) } else if openOrder != nil && strings.TrimSpace(openOrder.OrderNo) != "" { orderNo = strings.TrimSpace(openOrder.OrderNo) } if orderNo != "" { var assigned model.RockToken findErr := p.rockService.db.WithContext(ctx).Unscoped(). Where("assigned_order_no = ? AND status IN ?", orderNo, []string{ model.RockStatusUsing, model.RockStatusBound, model.RockStatusMismatch, model.RockStatusAbnormal, }). Order("id DESC"). First(&assigned).Error if findErr == nil { if assigned.Status == model.RockStatusAbnormal { return &DeliveryOrderState{ CanShip: false, CannotShipReason: rockOrderTokenLockedMessage, Provider: ProviderRock, }, nil } hasAssignedToken = true } } if !hasAssignedToken { stats, err := p.rockService.GetStats(ctx) if err != nil { return nil, fmt.Errorf("获取库存失败: %w", err) } if stats.AvailableStock <= 0 { // 若开启了自动补货,立即触发一次补货检查 go p.rockService.CheckStockAndAutoPurchase(context.Background()) return &DeliveryOrderState{ CanShip: false, CannotShipReason: "当前商品库存补充中,已通知管理员自动补货,请稍后刷新或联系客服", Provider: ProviderRock, }, nil } } sku := "" productName := "和平精英道具" if order != nil { sku = order.ProductSKU if order.MerchantProduct != nil && order.MerchantProduct.DisplayName != "" { productName = order.MerchantProduct.DisplayName } } if openOrder != nil && openOrder.Product != nil && openOrder.Product.Name != "" { productName = openOrder.Product.Name } deliveryProd := &DeliveryProduct{ Name: productName, SKU: sku, Game: "和平精英", } return &DeliveryOrderState{ CanShip: true, Product: deliveryProd, Provider: ProviderRock, }, nil } func (p *RockProvider) Bind(ctx context.Context, order *model.FulfillmentOrder, gameAccount string) (*ProviderBindResult, error) { if p.rockService == nil { return nil, errors.New("Rock 发货服务未初始化") } trimmedUID := strings.TrimSpace(gameAccount) if trimmedUID == "" { return nil, errors.New("请先填写正确的游戏数字账号(UID)") } // 分配或锁定一个 Token 并获取上游活动链接 token, detail, err := p.rockService.AssignTokenForOrder(ctx, order.OrderNo, trimmedUID) if err != nil { return nil, err } patch := map[string]interface{}{ "provider": ProviderRock, "rock_token_id": token.ID, "rock_redeem_code": token.RedeemCode, "expected_uid": trimmedUID, } bindURL := strings.TrimSpace(detail.BindURL) qrURL := detail.QRURL if qrURL == "" && bindURL != "" { qrURL = qrCodeURL(bindURL) } isBound := token.Status == model.RockStatusBound message := "请使用手机微信/QQ扫描上方二维码绑定游戏角色" if isBound { message = "账号绑定成功" if token.ActualRoleName != "" { patch["role_name"] = token.ActualRoleName } if token.ActualChannel != "" { patch["game_channel"] = token.ActualChannel } if token.ActualRoleID != "" { patch["actual_role_id"] = token.ActualRoleID } } return &ProviderBindResult{ BindUUID: fmt.Sprintf("%d", token.ID), BindURL: bindURL, QRURL: qrURL, Bound: isBound, RoleName: token.ActualRoleName, GameAccount: token.ActualRoleID, GameChannel: token.ActualChannel, ExpectedGameAccount: trimmedUID, Message: message, Provider: ProviderRock, PatchResultData: patch, }, nil } func (p *RockProvider) CheckBind(ctx context.Context, order *model.FulfillmentOrder, bindUUID string) (*ProviderBindResult, error) { if p.rockService == nil { return nil, errors.New("Rock 发货服务未初始化") } tokenIDUint, err := strconv.ParseUint(strings.TrimSpace(bindUUID), 10, 64) if err != nil { return nil, fmt.Errorf("无效的 bind_uuid: %s", bindUUID) } expectedUID := resultDataString(order.ResultData, "expected_uid") if expectedUID == "" { expectedUID = resultDataString(order.RequestData, "game_account") } if expectedUID == "" { expectedUID = resultDataString(order.ResultData, "game_account") } res, err := p.rockService.CheckBindingAndVerifyUID(ctx, uint(tokenIDUint), order.OrderNo, expectedUID) if err != nil { return nil, err } patch := map[string]interface{}{ "provider": ProviderRock, } if res.RoleName != "" { patch["role_name"] = res.RoleName } if res.Channel != "" { patch["game_channel"] = res.Channel } if res.RoleID != "" { patch["actual_role_id"] = res.RoleID } result := &ProviderBindResult{ BindUUID: bindUUID, Bound: res.Bound, RoleName: res.RoleName, GameAccount: res.RoleID, GameChannel: res.Channel, ExpectedGameAccount: res.ExpectedUID, Mismatch: res.Mismatch, Message: res.Message, Provider: ProviderRock, PatchResultData: patch, } // 还在等扫码时,把同一提取码的最新活动链接交回去。下游只展示,不负责重取。 if !res.Bound && !res.Mismatch { bindURL, qrURL := p.rockService.freshBindLink(ctx, uint(tokenIDUint), order.OrderNo) result.BindURL = bindURL result.QRURL = qrURL } return result, nil } func (p *RockProvider) Submit(ctx context.Context, order *model.FulfillmentOrder, gameAccount, bindUUID string) (*ProviderSubmitResult, error) { if p.rockService == nil { return nil, errors.New("Rock 发货服务未初始化") } sku := order.ProductSKU expectedUID := strings.TrimSpace(gameAccount) if expectedUID == "" { expectedUID = resultDataString(order.ResultData, "expected_uid") } redeemResp, err := p.rockService.RedeemTokenForOrder(ctx, order.OrderNo, sku, expectedUID) if err != nil { return nil, err } var token model.RockToken _ = p.rockService.db.WithContext(ctx).Unscoped().Where("assigned_order_no = ?", order.OrderNo).Order("id DESC").First(&token).Error fulfillIDStr := "" if token.UpstreamFulfillID > 0 { fulfillIDStr = fmt.Sprintf("%d", token.UpstreamFulfillID) } skinName := redeemResp.Token.RedeemSkin if skinName == "" { skinName = token.TargetSkin } currencyName := redeemResp.Token.RedeemCurrency if currencyName == "" { currencyName = token.TargetCurrency } roleName := token.ActualRoleName if roleName == "" { roleName = resultDataString(order.ResultData, "role_name") } channel := token.ActualChannel if channel == "" { channel = resultDataString(order.ResultData, "game_channel") } patch := map[string]interface{}{ "provider": ProviderRock, "provider_order_no": fulfillIDStr, "fulfill_id": token.UpstreamFulfillID, "skin": skinName, "currency": currencyName, "currency_quantity": token.TargetCurrencyQuantity, "role_name": roleName, "game_channel": channel, "game_uid": expectedUID, } return &ProviderSubmitResult{ ProviderOrderNo: fulfillIDStr, NextStatus: model.OrderStatusDelivered, Message: "发货成功!道具及附赠货币已发放至游戏,请登录游戏邮件查收", GameAccount: map[string]interface{}{ "role_name": roleName, "game_channel": channel, "role_id": expectedUID, }, PatchResultData: patch, }, nil } func (p *RockProvider) SyncStatus(ctx context.Context, order *model.FulfillmentOrder) (*ProviderSyncResult, error) { if p.rockService == nil { return nil, errors.New("Rock 发货服务未初始化") } var token model.RockToken err := p.rockService.db.WithContext(ctx).Unscoped(). Where("assigned_order_no = ?", order.OrderNo). Order("id DESC"). First(&token).Error if err != nil { return &ProviderSyncResult{ Status: "", }, nil } switch token.Status { case model.RockStatusRedeemed: return &ProviderSyncResult{ Status: "success", ProviderOrderNo: fmt.Sprintf("%d", token.UpstreamFulfillID), GameUID: token.ActualRoleID, RoleName: token.ActualRoleName, RoleArea: token.ActualChannel, }, nil case model.RockStatusMismatch: return &ProviderSyncResult{ Status: "failed", FailReason: token.MismatchReason, }, nil default: return &ProviderSyncResult{ Status: "delivering", }, nil } } func (p *RockProvider) Cancel(ctx context.Context, order *model.FulfillmentOrder) error { if p.rockService == nil { return nil } // 若订单尚未扫码绑定成功,将处于 using 状态的 Token 安全重置回 available var token model.RockToken err := p.rockService.db.WithContext(ctx).Unscoped(). Where("assigned_order_no = ? AND status = ?", order.OrderNo, model.RockStatusUsing). First(&token).Error if err == nil { _ = p.rockService.db.WithContext(ctx).Unscoped().Model(&token).Updates(map[string]interface{}{ "status": model.RockStatusAvailable, "assigned_order_no": "", "expected_uid": "", "using_at": nil, }).Error } return nil }