package service import ( "context" "encoding/json" "errors" "fmt" "log" "strings" "time" "affiliate_dash/internal/model" "gorm.io/gorm" ) // RedeemTokenForOrder 执行最终发货兑换(动态判定皮肤) func (s *RockService) RedeemTokenForOrder(ctx context.Context, orderNo, sku, expectedUID string) (*RockRedeemResponse, error) { cfg, err := s.GetConfig(ctx) if err != nil { return nil, err } if !cfg.AutoDeliveryEnabled { return nil, fmt.Errorf("Rock 自动发货通道维护中,已关闭自动兑换") } var token model.RockToken err = s.db.WithContext(ctx).Unscoped(). Where("assigned_order_no = ? AND status = ?", orderNo, model.RockStatusBound). First(&token).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("未找到该订单已绑定的有效 Token,或当前状态不可发货") } return nil, err } // 最终双重防御:核验 UID trimmedExpected := strings.TrimSpace(expectedUID) if trimmedExpected == "" { trimmedExpected = strings.TrimSpace(token.ExpectedUID) } if trimmedExpected != "" && token.ActualRoleID != trimmedExpected { return nil, fmt.Errorf("终审拦截:扫码UID(%s)与预期UID(%s)不符,禁止发货!", token.ActualRoleID, trimmedExpected) } // 根据商品 SKU 动态决定兑换的皮肤名称 targetSkin := model.RockSkinBlackTshirt if sku == model.RockSKUBlueSportswear { targetSkin = model.RockSkinBlueSportswear } // 确保买家角色名存在 actualRoleName := strings.TrimSpace(token.ActualRoleName) if actualRoleName == "" { if ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID); refErr == nil && ref != nil { actualRoleName = ref.GetRoleName() if actualRoleName != "" { _ = s.db.WithContext(ctx).Unscoped().Model(&token).Update("actual_role_name", actualRoleName).Error } } } if actualRoleName == "" { actualRoleName = token.ActualRoleID } // 调上游兑换发货接口(携带 Bearer Token 鉴权) redeemResp, err := s.client.RedeemSkin(ctx, cfg.BearerToken, token.UpstreamTokenID, targetSkin, "幸运币", actualRoleName) if err != nil { return nil, fmt.Errorf("调用上游兑换失败: %w", err) } // 提取官方 fulfillId var fulfillID int64 now := time.Now() ref, refErr := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID) if refErr == nil && len(ref.DeliveryList) > 0 { fulfillID = ref.DeliveryList[len(ref.DeliveryList)-1].FulfillID } // 状态跃迁至 redeemed _ = s.db.WithContext(ctx).Unscoped().Model(&token).Updates(map[string]interface{}{ "status": model.RockStatusRedeemed, "upstream_fulfill_id": fulfillID, "target_skin": targetSkin, "target_currency": "幸运币", "target_currency_quantity": 1, "redeemed_at": now, }).Error return redeemResp, nil } // ReleaseUnboundUsingToken 把尚未扫码绑定的使用中链接释放回可用库存。 // 已绑定或上游已出现新扫码时拒绝释放,避免退款后的旧二维码被当成新库存发出去。 func (s *RockService) ReleaseUnboundUsingToken(ctx context.Context, tokenID uint) error { var token model.RockToken if err := s.db.WithContext(ctx).First(&token, tokenID).Error; err != nil { return err } if token.Status != model.RockStatusUsing { return fmt.Errorf("仅扫码使用中且尚未绑定的链接可以释放回库存") } if token.BoundAt != nil || strings.TrimSpace(token.ActualRoleID) != "" { return fmt.Errorf("该链接已扫码绑定,不能释放回库存") } cfg, err := s.GetConfig(ctx) if err != nil { return err } if strings.TrimSpace(cfg.BearerToken) == "" || strings.TrimSpace(token.UpstreamTokenID) == "" { return fmt.Errorf("缺少上游凭证,无法确认是否已扫码绑定") } ref, err := s.client.RefreshToken(ctx, cfg.BearerToken, token.UpstreamTokenID) if err != nil { return fmt.Errorf("确认上游绑定状态失败: %w", err) } if rockBuyerNewlyBound(&token, ref) { return fmt.Errorf("上游已扫码绑定角色 %s(%s),不能释放", ref.GetRoleName(), ref.GetRoleID()) } s.refreshCache.Delete(token.UpstreamTokenID) orderNo := token.AssignedOrderNo if err := s.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{ "status": model.RockStatusAvailable, "assigned_order_no": "", "expected_uid": "", "actual_role_name": "", "actual_role_id": "", "actual_channel": "", "initial_bound_snapshot": "", "mismatch_at": nil, "mismatch_reason": "", "bound_at": nil, "using_at": nil, "updated_at": time.Now(), }).Error; err != nil { return err } s.detachReleasedRockToken(ctx, orderNo, token.ID) log.Printf("[RockService] 人工释放未绑定库存: token_id=%d order=%s", token.ID, orderNo) return nil } // detachReleasedRockToken 清掉原订单上指向这条链接的记录,避免旧发货页继续占用它。 func (s *RockService) detachReleasedRockToken(ctx context.Context, orderNo string, tokenID uint) { orderNo = strings.TrimSpace(orderNo) if orderNo == "" || tokenID == 0 { return } var order model.FulfillmentOrder if err := s.db.WithContext(ctx).Where("order_no = ?", orderNo).First(&order).Error; err != nil { return } if resultDataNumber(order.ResultData, "rock_token_id") != int64(tokenID) { return } raw := resultDataMap(order.ResultData) delete(raw, "rock_token_id") delete(raw, "rock_redeem_code") raw["rock_token_released"] = true encoded, err := json.Marshal(raw) if err != nil { return } _ = s.db.WithContext(ctx).Model(&order).Update("result_data", string(encoded)).Error }