Categorygithub.com/seayoo-io/combo-sdk-go
modulepackage
0.5.0
Repository: https://github.com/seayoo-io/combo-sdk-go.git
Documentation: pkg.go.dev

# README

Combo SDK for Go

combo-sdk-go 是世游核心系统 (Combo) 为 Go 提供的 SDK。

提供以下服务端功能,供游戏侧使用:

  • 验证世游服务端签发的 Identity Token
  • 请求 Server REST API 并解析响应
  • 接收 Server Notifications 并回复响应

combo-sdk-go 会将 API 的请求响应结构、签名计算与签名验证、HTTP 状态码等实现细节封装起来,提供 Go 的强类型 API,降低游戏侧接入世游系统时出错的可能性,提高接入的速度。

API Reference

初始化

package main

import "github.com/seayoo-io/combo-sdk-go"

func main() {
    cfg := combo.Config{
        Endpoint:  combo.Endpoint_China, // or combo.Endpoint_Global
        GameId:    combo.GameId("<GAME_ID>"),
        SecretKey: combo.SecretKey("sk_<SECRET_KEY>"),
    }
    // Use cfg...
}

登录验证

package main

import (
    "fmt"

    "github.com/seayoo-io/combo-sdk-go"
)

func main() {
    cfg := combo.Config{
        Endpoint:  combo.Endpoint_China, // or combo.Endpoint_Global
        GameId:    combo.GameId("<GAME_ID>"),
        SecretKey: combo.SecretKey("sk_<SECRET_KEY>"),
    }

    verifier, err := combo.NewTokenVerifier(cfg)
    if err != nil {
        panic(err)
    }

    // TokenVerifier 是可以复用的,不需要每次验证 Token 都创建一个 TokenVerifier。
    VerifyIdentityToken(verifier, "<IDENTITY_TOKEN_1>")
    VerifyIdentityToken(verifier, "<IDENTITY_TOKEN_2>")
    VerifyIdentityToken(verifier, "<IDENTITY_TOKEN_3>")
}

func VerifyIdentityToken(verifier *combo.TokenVerifier, token string) {
    payload, err := verifier.VerifyIdentityToken(token)
    if err != nil {
        fmt.Printf("failed to verify identity token: %v\n", err)
        return
    }
    fmt.Printf("ComboId: %s\n", payload.ComboId)
    fmt.Printf("IdP: %s\n", payload.IdP)
    fmt.Printf("ExternalId: %s\n", payload.ExternalId)
    fmt.Printf("ExternalName: %s\n", payload.ExternalName)
}

创建订单

package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/seayoo-io/combo-sdk-go"
)

func main() {
    cfg := combo.Config{
        Endpoint:  combo.Endpoint_China, // or combo.Endpoint_Global
        GameId:    combo.GameId("<GAME_ID>"),
        SecretKey: combo.SecretKey("sk_<SECRET_KEY>"),
    }

    client, err := combo.NewClient(cfg)
    if err != nil {
        panic(err)
    }

    // Client 是可以复用的,不需要每次请求都创建一个新的 Client
    output, err := client.CreateOrder(context.Background(), &combo.CreateOrderInput{
        Platform:    combo.Platform_iOS,
        ReferenceId: "20b5f268-22e1-4677-9a3f-ed8c9f22ff0f",
        ComboId:     "1231229080370001",
        ProductId:   "xcom_product_648",
        Quantity:    1,
        NotifyUrl:   "https://example.com/notifications",
        Context:     "<WHAT_YOU_PUT_HERE_IS_WHAT_YOU_GET_IN_NOTIFICATION>",
        Meta: combo.OrderMeta{
            ZoneId:    "10000",
            ServerId:  "10001",
            RoleId:    "3888",
            RoleName:  "小明",
            RoleLevel: 59,
        },
    })

    if err != nil {
        var er *combo.ErrorResponse
        if errors.As(err, &er) {
            fmt.Println("failed to create order, got ErrorResponse:")
            fmt.Printf("StatusCode: %d\n", er.StatusCode())
            fmt.Printf("TraceId: %s\n", er.TraceId())
            fmt.Printf("ErrorCode: %s\n", er.ErrorCode)
            fmt.Printf("ErrorMessage: %s\n", er.ErrorMessage)
        } else {
            fmt.Printf("failed to create order: %v\n", err)
        }
        return
    }

    fmt.Println("successfully created order:")
    fmt.Printf("TraceId: %s\n", output.TraceId())
    fmt.Printf("OrderId: %s\n", output.OrderId)
    fmt.Printf("OrderToken: %s\n", output.OrderToken)
    fmt.Printf("ExpiresAt: %v\n", time.Unix(output.ExpiresAt, 0))
}

处理发货通知

package main

import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/seayoo-io/combo-sdk-go"
)

func main() {
    cfg := combo.Config{
        Endpoint:  combo.Endpoint_China, // or combo.Endpoint_Global
        GameId:    combo.GameId("<GAME_ID>"),
        SecretKey: combo.SecretKey("sk_<SECRET_KEY>"),
    }

    handler, err := combo.NewNotificationHandler(cfg, &NotificationListener{})
    if err != nil {
        panic(err)
    }
    http.Handle("/notifications", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

type NotificationListener struct{}

func (l *NotificationListener) HandleShipOrder(ctx context.Context, id combo.NotificationId, payload *combo.ShipOrderNotification) error {
    fmt.Printf("received ship order notification: %s\n", id)
    fmt.Printf("OrderId: %s\n", payload.OrderId)
    fmt.Printf("ReferenceId: %s\n", payload.ReferenceId)
    fmt.Printf("ComboId: %s\n", payload.ComboId)
    fmt.Printf("ProductId: %s\n", payload.ProductId)
    fmt.Printf("Quantity: %d\n", payload.Quantity)
    fmt.Printf("Currency: %s\n", payload.Currency)
    fmt.Printf("ComboId: %d\n", payload.Amount)
    fmt.Printf("Context: %s\n", payload.Context)
    return nil
}

处理 GM 命令

假定 GM 接入协议文件如下:

syntax = "proto3";
package demo;

import "gm.proto";

service Demo {
  rpc ListRoles(ListRolesRequest) returns (ListRolesResponse) {
    option (combo.cmd_name) = "获取角色列表";
    option (combo.cmd_desc) = "获取 Combo ID 在指定区服下的游戏角色列表";
  }
}

enum RoleStatus {
  option (combo.enum_name) = "角色状态";

  UNKNOWN = 0 [(combo.value_name) = "未知"];
  ONLINE = 1  [(combo.value_name) = "在线"];
  OFFLINE = 2 [(combo.value_name) = "离线"];
}

message Role {
  string role_id = 1    [(combo.field_name) = "角色 ID"];
  string role_name = 2  [(combo.field_name) = "角色名称"];
  int32 level = 3       [(combo.field_name) = "角色等级"];
  RoleStatus status = 4 [(combo.field_name) = "角色状态"];
}

message ListRolesRequest {
  string combo_id = 1 [(combo.field_name) = "Combo ID", (combo.required) = true];
  int32 server_id = 2 [(combo.field_name) = "区服 ID", (combo.field_desc) = "游戏服务器的唯一 ID", (combo.required) = true];
}

message ListRolesResponse {
  repeated Role roles = 1 [(combo.field_name) = "角色列表"];
}

处理上述 GM 协议的示例代码:

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"net/http"

	"github.com/seayoo-io/combo-sdk-go"
)

func main() {
	cfg := combo.Config{
		Endpoint:  combo.Endpoint_China, // or combo.Endpoint_Global
		GameId:    combo.GameId("<GAME_ID>"),
		SecretKey: combo.SecretKey("sk_<SECRET_KEY>"),
	}

	// 创建具有幂等性处理能力的 GmListener,使用 Redis 作为幂等数据存储
	listener := combo.NewIdempotentGmListener(combo.IdempotentGmListenerConfig{
		Store: combo.NewRedisIdempotencyStore(
			combo.RedisIdempotencyStoreConfig{
				// 这里不假设 Redis 的运维部署方式,游戏侧可自行灵活创建和配置 Redis Client
				Client: redis.NewClient(&redis.Options{Addr: "localhost:6379"}),
			},
		),
		Listener: &GmListener{},
	})
	handler, err := combo.NewGmHandler(cfg, listener)
	if err != nil {
		panic(err)
	}
	http.Handle("/gm", handler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

type GmListener struct{}

func (l *GmListener) HandleGmRequest(ctx context.Context, req *combo.GmRequest) (resp any, err *combo.GmErrorResponse) {
	fmt.Printf("HandleGmRequest: Version=%s, Id=%s\n", req.Version, req.Id)
	fmt.Printf("Cmd: %s\n", req.Cmd)
	fmt.Printf("Args: %s\n", string(req.Args))
	switch req.Cmd {
	case "ListRoles":
		var r ListRolesRequest
		if err := json.Unmarshal(req.Args, &r); err != nil {
			return nil, &combo.GmErrorResponse{
				Error:   combo.GmError_InvalidArgs,
				Message: err.Error(),
			}
		}
		return ListRoles(ctx, &r)
	default:
		return nil, &combo.GmErrorResponse{
			Error:   combo.GmError_InvalidCommand,
			Message: "Unknown command: " + req.Cmd,
		}
	}
}

type RoleStatus int32

const (
	RoleStatus_Online  RoleStatus = 1
	RoleStatus_Offline RoleStatus = 2
)

type Role struct {
	RoleId   string     `json:"role_id"`
	RoleName string     `json:"role_name"`
	Level    int32      `json:"level"`
	Status   RoleStatus `json:"status"`
}

type ListRolesRequest struct {
	ComboId  string `json:"combo_id"`
	ServerId int32  `json:"server_id"`
}

type ListRolesResponse struct {
	Roles []*Role `json:"roles"`
}

func ListRoles(ctx context.Context, req *ListRolesRequest) (resp *ListRolesResponse, err *combo.GmErrorResponse) {
	fmt.Printf("[ListRoles] ComboId: %s\n", req.ComboId)
	fmt.Printf("[ListRoles] ServerId: %d\n", req.ServerId)
	return &ListRolesResponse{
		Roles: []*Role{
			{
				RoleId:   "845284226758233306",
				RoleName: "洪文泽",
				Level:    37,
				Status:   RoleStatus_Online,
			},
			{
				RoleId:   "844716741320391248",
				RoleName: "擎天-豆腐",
				Level:    5,
				Status:   RoleStatus_Offline,
			},
		},
	}, nil
}

# Functions

NewClient 创建一个新的 Server API 的 client。.
NewGmHandler 创建一个用于处理世游服务端发送的 GM 命令的 http.Handler。 游戏侧需要将此 Handler 注册到游戏的 HTTP 服务中。 注意:注册 Handler 时,应当使用 HTTP POST。.
NewIdempotentGmListener 创建一个具有幂等性处理能力的 GmListener。.
NewMemoryIdempotencyStore 创建一个基于 Memory 的 IdempotencyStore 实现。 注意:该实现仅用于开发调试,不适合生产环境。 数据仅在内存中存储,重启服务后数据会丢失。数据不会过期,不会自动清理。.
NewNotificationHandler 创建一个用于接收世游服务端推送的通知的 http.Handler。 游戏侧需要将此 Handler 注册到游戏的 HTTP 服务中。 注意:注册 Handler 时,应当使用 HTTP POST。.
NewRedisIdempotencyStore 创建一个基于 Redis 的 IdempotencyStore 实现。 数据会存储在 Redis 中,可以保证数据的的高可用性和到期自动清理。推荐生产环境使用。 注意:本实现需要 Redis >= 7.0.
NewTokenVerifier 创建一个新的 TokenVerifier。.
WithHttpClient 用于指定自定义的 HttpClient。 如果不指定 HttpClient,则默认使用 http.DefaultClient。.

# Constants

中国大陆 API 端点,用于国内发行.
全球的 API 端点,用于海外发行.
数据库操作异常导致 GM 命令执行失败。.
幂等处理重试请求时,idempotency_key 所对应的原始请求尚未处理完毕。.
幂等处理重试请求时,请求内容和 idempotency_key 所对应的原始请求内容不一致。.
处理 GM 命令时内部出错。可作为兜底的通用错误类型。.
GM 命令的参数不正确。例如,参数缺少必要的字段,或参数的字段类型不正确。.
游戏侧不认识请求中的 GM 命令。.
请求中的 Content-Type 不是 application/json。.
请求中的 HTTP method 不正确,没有按照预期使用 POST。.
请求的结构不正确。例如,缺少必要的字段,或字段类型不正确。.
对 HTTP 请求的签名验证不通过。这意味着 HTTP 请求不可信。.
游戏当前处于停服维护状态,无法处理收到的 GM 命令。.
网络通信错误导致 GM 命令执行失败。.
GM 命令发送频率过高,被游戏侧限流,命令未被处理。.
GM 命令处理超时。.
4399 账号登录.
Sign in with Apple.
哔哩哔哩(B站)账号.
设备登录(游客).
抖音账号.
Facebook Login.
Google Account.
荣耀账号.
华为账号.
雷电模拟器账号.
OPPO 账号.
世游通行证.
TapTap 登录.
UC(九游)登录.
VIVO 账号.
微信登录.
小米账号.
应用宝 YSDK 登录.
安卓平台,包括华为鸿蒙系统、小米澎湃 OS 等基于 Android 的操作系统.
苹果的 iOS 和 iPadOS.
macOS 桌面平台.
微信小游戏.
Windows (PC) 桌面平台.
Name of this SDK.
Version of this SDK.

# Structs

AdPayload 包含了激励广告的播放信息。.
Client 是一个用来调用 Combo Server API 的 API Client。 Client 的方法对应了 Combo Server API 的各个接口,每个方法均会返回两个值, 第一个值是 API 调用的结果,第二个值是 error。 如果 API 调用成功,则 error 为 nil。 如果 API 调用失败,则 error 为对应的错误信息。 API 明确返回的错误,例如参数错误、签名错误、内部错误等等,error 的类型是 *ErrorResponse, 可以用 errors.As 将 error 转换为 *ErrorResponse 类型,从而进一步获取详细的错误信息。.
Config 包含了 Combo SDK 运行所必需的配置项。.
No description provided by the author
No description provided by the author
No description provided by the author
No description provided by the author
ErrorResponse 对应 Combo Server API 返回的的错误响应。 游戏侧可使用 errors.As 来获取错误详细信息,示例如下: import "log" import "github.com/seayoo-io/combo-sdk-go" // an API call to Client.CreateOrder() returns nil, err if err != nil { var er *combo.ErrorResponse if errors.As(err, &er) { log.Printf(`failed to call API: error=%s, message="%s"\n`, er.ErrorCode, er.ErrorMessage) } return }.
GmErrorResponse 是 GM 命令处理失败时返回的响应。.
No description provided by the author
IdempotentGmListenerConfig 包含了创建具有幂等性处理能力的 GmListener 时所必需的配置项。.
IdentityPayload 包含了用户的身份信息。.
No description provided by the author
No description provided by the author
OrderMeta 包含了订单的元数据。 大部分元数据用于数据分析与查询,游戏侧应当尽量提供。 某些元数据在特定的支付场景下是必须的,例如微信小游戏的 iOS 支付场景。.
RedisIdempotencyStoreConfig 包含了创建基于 Redis 的 IdempotencyStore 时所必需的配置项。.
RefundNotification 是订单退款通知的数据结构,包含了被退款订单的详细信息。.
ShipOrderNotification 是订单发货通知的数据结构,包含了已支付订单的详细信息。.
TokenVerifier 用于验证世游服务端颁发的 Token。.

# Interfaces

GmListener 是一个用于接收并处理世游服务端发送的 GM 命令的接口。 游戏侧需要实现此接口并根据 GM 命令执行对应的业务逻辑。.
HttpClient 用于发送 HTTP 请求。如果需要对 HTTP 请求的行为和参数进行自定义设置,可以实现此接口。 通常来说 *http.Client 可以满足绝大部分需求。.
IdempotencyStore 是一个用于存储 GM 命令的幂等记录的接口。 Combo SDK 内置了 Redis 和 Memory 两种实现,可分别通过 NewMemoryIdempotencyStore() 和 NewRedisIdempotencyStore() 创建。 游戏侧也可以选择自行实现 IdempotencyStore 接口。.
NotificationListener 是一个用于接收世游服务端推送的通知的接口。 游戏侧需要实现此接口并执行对应的业务逻辑。.

# Type aliases

ClientOption 是函数式风格的的可选项,用于创建 Client。.
Combo API 端点。.
由世游为游戏分配,用于标识游戏的业务代号。.
GmError 是 GM 命令处理失败时返回的错误类型。.
Identity Provider (IdP) 是世游定义的用户身份提供方,俗称账号系统。.
每次通知的唯一 ID。游戏侧可用此值来对通知进行去重。.
游戏客户端运行平台。.
由世游侧为游戏分配,游戏侧和世游侧共享的密钥。 此密钥用于签名计算与验证。.