Files
affiliate_dash/backend/internal/service/provider_rock.go
T
yml2213 ef7f67dd0c feat(fulfillment): 接入Rock发货平台(0666.pro)代理履约、通用库存池与UID强一致风控
1. 数据库新增 012_rock_fulfillment 迁移,创建 rock_configs 配置表与 rock_tokens 通用库存表,补齐黑色ROCK长袖T恤与蓝色运动服上衣商品目录;
2. 封装 RockClient 与 RockService,实现 0666.pro 平台 Bearer Token 极简连通校验、批量采购50个入库、水位监控巡检与自动补货;
3. 新增 RockProvider 实现标准履约路由,引入 Anti-Ghost 旧绑定基线比对与 UID 强一致门禁,扫错号硬阻断并记录为 mismatch 状态;
4. 针对黑色ROCK与蓝色运动服实现动态发货(绑定后发货前动态选择 skin),并提取腾讯官方发货流水号 fulfillId 对账;
5. 新增全局自动发货总开关(Kill Switch),支持一键软熔断降级转人工接单处理;
6. 前端新增 PlatformRock 管理大屏,提供总开关切换、Token配置与连通测试、库存监控、一键补货及错单解冻重置。
2026-10-05 14:56:11 +08:00

275 lines
7.6 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
}
fulfillIDStr := ""
if redeemResp.Delivery.FulfillID > 0 {
fulfillIDStr = fmt.Sprintf("%d", redeemResp.Delivery.FulfillID)
}
patch := map[string]interface{}{
"provider": ProviderRock,
"provider_order_no": fulfillIDStr,
"fulfill_id": redeemResp.Delivery.FulfillID,
"skin": redeemResp.Delivery.Skin,
"currency": redeemResp.Delivery.Currency,
"currency_quantity": redeemResp.Delivery.Quantity,
}
return &ProviderSubmitResult{
ProviderOrderNo: fulfillIDStr,
NextStatus: model.OrderStatusDelivered,
Message: "发货成功!道具及附赠货币已发放至游戏,请登录游戏邮件查收",
GameAccount: map[string]interface{}{
"role_name": resultDataString(order.ResultData, "role_name"),
"game_channel": resultDataString(order.ResultData, "game_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
}