Files
affiliate_dash/backend/internal/service/provider_rock.go
T
yml2213 83870843ed fix(rock): 修复兑换发货401未鉴权错误与买家角色名提取解析
1. 根因修复(兑换发货鉴权):对比原生平台发货抓包证实 POST /api/redeem/:id 必须携带 Authorization: Bearer <token>,Origin 及 Referer 头,补齐相关请求头彻底杜绝 401 报错;
2. 根因修复(角色昵称解析):逆向解构 0666.pro 响应报文,将角色名提取从错误的 binding.role_name 切换为 token.bound_account,消除提货页角色名显示为 '-' 的问题;
3. 协议对齐(响应结构重构):将 RockRedeemResponse 与真实上游返回对齐(包含 token 与 status 结构),剔除臆想的 delivery 字段;
4. 单元测试与方案文档:更新单测覆盖鉴权与字段映射断言,在方案文档 7.4 节录入实测抓包报文镜像比对表。
2026-10-05 16:30:18 +08:00

296 lines
8.0 KiB
Go

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. 检查可用库存
stats, err := p.rockService.GetStats(ctx)
if err != nil {
return nil, fmt.Errorf("获取库存失败: %w", err)
}
if stats.AvailableStock <= 0 {
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)
}
return &ProviderBindResult{
BindUUID: fmt.Sprintf("%d", token.ID),
BindURL: bindURL,
QRURL: qrURL,
Bound: false,
ExpectedGameAccount: trimmedUID,
Message: "请使用手机微信/QQ扫描上方二维码绑定游戏角色",
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), 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
}
return &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,
}, 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).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
}
patch := map[string]interface{}{
"provider": ProviderRock,
"provider_order_no": fulfillIDStr,
"fulfill_id": token.UpstreamFulfillID,
"skin": skinName,
"currency": currencyName,
"currency_quantity": token.TargetCurrencyQuantity,
}
roleName := token.ActualRoleName
if roleName == "" {
roleName = resultDataString(order.ResultData, "role_name")
}
channel := token.ActualChannel
if channel == "" {
channel = resultDataString(order.ResultData, "game_channel")
}
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).
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).
Where("assigned_order_no = ? AND status = ?", order.OrderNo, model.RockStatusUsing).
First(&token).Error
if err == nil {
_ = p.rockService.db.WithContext(ctx).Model(&token).Updates(map[string]interface{}{
"status": model.RockStatusAvailable,
"assigned_order_no": "",
"expected_uid": "",
}).Error
}
return nil
}