combo

package module
v1.0.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 8, 2025 License: MIT Imports: 19 Imported by: 0

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
}

Documentation

Index

Constants

View Source
const (
	// Name of this SDK
	SdkName = "combo-sdk-go"

	// Version of this SDK
	SdkVersion = "1.0.0"
)

Variables

This section is empty.

Functions

func NewGmHandler added in v0.3.0

func NewGmHandler(cfg Config, listener GmListener) (http.Handler, error)

NewGmHandler 创建一个用于处理世游服务端发送的 GM 命令的 http.Handler。

游戏侧需要将此 Handler 注册到游戏的 HTTP 服务中。

注意:注册 Handler 时,应当使用 HTTP POST。

func NewNotificationHandler

func NewNotificationHandler(cfg Config, listener NotificationListener) (http.Handler, error)

NewNotificationHandler 创建一个用于接收世游服务端推送的通知的 http.Handler。

游戏侧需要将此 Handler 注册到游戏的 HTTP 服务中。

注意:注册 Handler 时,应当使用 HTTP POST。

Types

type AdPayload

type AdPayload struct {
	// ComboId 是世游分配的聚合用户 ID。
	// 游戏侧应当使用 ComboId 作为用户的唯一标识。
	ComboId string

	// PlacementId 是广告位 ID,游戏侧用它确定发放什么样的广告激励。
	PlacementId string

	// ImpressionId 是世游服务端创建的,标识单次广告播放的唯一 ID。
	ImpressionId string
}

AdPayload 包含了激励广告的播放信息。

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client 是一个用来调用 Combo Server API 的 API Client。

Client 的方法对应了 Combo Server API 的各个接口,每个方法均会返回两个值, 第一个值是 API 调用的结果,第二个值是 error。 如果 API 调用成功,则 error 为 nil。 如果 API 调用失败,则 error 为对应的错误信息。

API 明确返回的错误,例如参数错误、签名错误、内部错误等等,error 的类型是 *ErrorResponse, 可以用 errors.As 将 error 转换为 *ErrorResponse 类型,从而进一步获取详细的错误信息。

func NewClient

func NewClient(cfg Config, options ...ClientOption) (*Client, error)

NewClient 创建一个新的 Server API 的 client。

func (*Client) CreateOrder

func (c *Client) CreateOrder(ctx context.Context, input *CreateOrderInput) (*CreateOrderOutput, error)

创建订单,发起一个应用内购买 + 支付的流程。

func (*Client) EnterGame

func (c *Client) EnterGame(ctx context.Context, input *EnterGameInput) (*EnterGameOutput, error)

通知世游服务端玩家进入游戏世界(上线)。

此接口仅用于中宣部防沉迷系统的上下线数据上报。

func (*Client) LeaveGame

func (c *Client) LeaveGame(ctx context.Context, input *LeaveGameInput) (*LeaveGameOutput, error)

通知世游服务端玩家离开游戏世界(下线)。

此接口仅用于中宣部防沉迷系统的上下线数据上报。

type ClientOption

type ClientOption func(*Client)

ClientOption 是函数式风格的的可选项,用于创建 Client。

func WithHttpClient

func WithHttpClient(client HttpClient) ClientOption

WithHttpClient 用于指定自定义的 HttpClient。

如果不指定 HttpClient,则默认使用 http.DefaultClient。

type Config

type Config struct {
	// API Endpoint
	Endpoint Endpoint

	// 游戏的 Game ID
	GameId GameId

	// 游戏的 Secret Key
	SecretKey SecretKey
}

Config 包含了 Combo SDK 运行所必需的配置项。

type CreateOrderInput

type CreateOrderInput struct {
	// 用于标识创建订单请求的唯一 ID。
	ReferenceId string `json:"reference_id"`

	// 发起购买的用户的唯一标识。
	ComboId string `json:"combo_id"`

	// 要购买的商品 ID。
	ProductId string `json:"product_id"`

	// 客户端的运行平台。
	Platform Platform `json:"platform"`

	// 游戏侧接收发货通知的服务端地址。
	// 这个地址对应的服务端应当实现 NotificationListener 接口。
	// 发货通知会调用 NotificationListener.HandleShipOrder() 方法。
	// 发货通知的数据格式请参考 ShipOrderNotification。
	NotifyUrl string `json:"notify_url"`

	// 要购买的商品的数量。
	Quantity int `json:"quantity,omitempty"`

	// 订单上下文,在发货通知中透传回游戏。
	Context string `json:"context,omitempty"`

	// 订单的元数据。
	Meta OrderMeta `json:"meta,omitempty"`
}

type CreateOrderOutput

type CreateOrderOutput struct {

	// 世游服务端创建的,标识订单的唯一 ID。
	OrderId string `json:"order_id"`

	// 世游服务端创建的订单 token,用于后续支付流程。
	OrderToken string `json:"order_token"`

	// 订单失效时间。Unix timestamp in seconds。
	ExpiresAt int64 `json:"expires_at"`
	// contains filtered or unexported fields
}

func (*CreateOrderOutput) StatusCode

func (r *CreateOrderOutput) StatusCode() int

HTTP 状态码,例如 200, 400, 500...

func (*CreateOrderOutput) TraceId

func (r *CreateOrderOutput) TraceId() string

世游服务端生成的,用于追踪本次请求的唯一 ID。

建议游戏侧将 TraceId 记录到日志中,以便于调试和问题排查。

type Endpoint

type Endpoint string

Combo API 端点。

const (
	// 中国大陆 API 端点,用于国内发行
	Endpoint_China Endpoint = "https://api.seayoo.com"

	// 全球的 API 端点,用于海外发行
	Endpoint_Global Endpoint = "https://api.seayoo.io"
)

type EnterGameInput

type EnterGameInput struct {
	// 聚合用户标识。
	ComboId string `json:"combo_id"`

	// 游戏会话标识。
	// 单次游戏会话的上下线动作必须使用同一会话标识上报。
	SessionId string `json:"session_id"`
}

type EnterGameOutput

type EnterGameOutput struct {
	// contains filtered or unexported fields
}

func (*EnterGameOutput) StatusCode

func (r *EnterGameOutput) StatusCode() int

HTTP 状态码,例如 200, 400, 500...

func (*EnterGameOutput) TraceId

func (r *EnterGameOutput) TraceId() string

世游服务端生成的,用于追踪本次请求的唯一 ID。

建议游戏侧将 TraceId 记录到日志中,以便于调试和问题排查。

type ErrorResponse

type ErrorResponse struct {

	// 业务错误码,示例 invalid_request, internal_error。
	ErrorCode string `json:"error"`

	// 错误的描述信息。
	ErrorMessage string `json:"message"`
	// contains filtered or unexported fields
}

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
}

func (*ErrorResponse) Error

func (r *ErrorResponse) Error() string

ErrorResponse 实现了 error 接口。

如果游戏侧需要将错误信息记录到日志中,可以直接将 ErrorResponse 作为 error 类型输出。实例如下:

import "log"

// ...

if err != nil {
    log.Printf("failed to call API: %v\n", err)
}

func (*ErrorResponse) StatusCode

func (r *ErrorResponse) StatusCode() int

HTTP 状态码,例如 200, 400, 500...

func (*ErrorResponse) TraceId

func (r *ErrorResponse) TraceId() string

世游服务端生成的,用于追踪本次请求的唯一 ID。

建议游戏侧将 TraceId 记录到日志中,以便于调试和问题排查。

type GameId

type GameId string

由世游为游戏分配,用于标识游戏的业务代号。

type GmError added in v0.3.0

type GmError string

GmError 是 GM 命令处理失败时返回的错误类型。

const (
	// 请求中的 HTTP method 不正确,没有按照预期使用 POST。
	GmError_InvalidHttpMethod GmError = "invalid_http_method"

	// 请求中的 Content-Type 不是 application/json。
	GmError_InvalidContentType GmError = "invalid_content_type"

	// 对 HTTP 请求的签名验证不通过。这意味着 HTTP 请求不可信。
	GmError_InvalidSignature GmError = "invalid_signature"

	// 请求的结构不正确。例如,缺少必要的字段,或字段类型不正确。
	GmError_InvalidRequest GmError = "invalid_request"

	// 游戏侧不认识请求中的 GM 命令。
	GmError_InvalidCommand GmError = "invalid_command"

	// GM 命令的参数不正确。例如,参数缺少必要的字段,或参数的字段类型不正确。
	GmError_InvalidArgs GmError = "invalid_args"

	// GM 命令发送频率过高,被游戏侧限流,命令未被处理。
	GmError_ThrottlingError GmError = "throttling_error"

	// 幂等处理重试请求时,idempotency_key 所对应的原始请求尚未处理完毕。
	GmError_IdempotencyConflict GmError = "idempotency_conflict"

	// 幂等处理重试请求时,请求内容和 idempotency_key 所对应的原始请求内容不一致。
	GmError_IdempotencyMismatch GmError = "idempotency_mismatch"
)

客户端错误。 这些错误通常是由于世游侧发送的请求不正确导致的。

const (
	// 游戏当前处于停服维护状态,无法处理收到的 GM 命令。
	GmError_MaintenanceError GmError = "maintenance_error"

	// 网络通信错误导致 GM 命令执行失败。
	GmError_NetworkError GmError = "network_error"

	// 数据库操作异常导致 GM 命令执行失败。
	GmError_DatabaseError GmError = "database_error"

	// GM 命令处理超时。
	GmError_TimeoutError GmError = "timeout_error"

	// 处理 GM 命令时内部出错。可作为兜底的通用错误类型。
	GmError_InternalError GmError = "internal_error"
)

服务端错误。 这些错误通常是游戏侧处理 GM 命令时出现问题导致的。

type GmErrorResponse added in v0.3.0

type GmErrorResponse struct {
	// 错误类型。建议游戏侧优先使用 Combo SDK 预定义好的 GmError 值。
	Error GmError `json:"error"`
	// 错误描述信息。预期是给人看的,便于联调和问题排查。
	Message string `json:"message"`
	// 如果为 true,表示无法确定 GM 命令是否被执行。默认语义是 GM 命令未被执行(否则应当返回成功响应)。
	Uncertain bool `json:"uncertain,omitempty"`
}

GmErrorResponse 是 GM 命令处理失败时返回的响应。

type GmListener added in v0.3.0

type GmListener interface {
	// HandleGmRequest 负责执行 GM 命令。
	// resp 是 GM 命令执行成功时的返回值,结构需要 GM 协议文件中 rpc 的 Response 一致。
	// Combo SDK 会对 resp 做 JSON 序列化操作。
	// - 如果游戏侧希望自己控制 JSON 序列化的过程,resp 实现 interface json.Marshaler 即可。
	// - 如果游戏侧希望直接返回 JSON 序列化的结果,确保 resp 的类型是 json.RawMessage 即可。
	// err 是 GM 命令执行失败时的错误信息。
	// - 如果 GM 命令执行成功,应当返回 resp, nil。
	// - 如果 GM 命令执行失败,应当返回 nil, err。
	HandleGmRequest(ctx context.Context, req *GmRequest) (resp any, err *GmErrorResponse)
}

GmListener 是一个用于接收并处理世游服务端发送的 GM 命令的接口。 游戏侧需要实现此接口并根据 GM 命令执行对应的业务逻辑。

func NewIdempotentGmListener added in v0.5.0

func NewIdempotentGmListener(cfg IdempotentGmListenerConfig) GmListener

NewIdempotentGmListener 创建一个具有幂等性处理能力的 GmListener。

type GmRequest added in v0.3.0

type GmRequest struct {
	// Version 是 GM 请求的的版本号。当前版本固定为 2.0。
	Version string
	// Origin 是发送本次 GM 请求的来源系统标识。可用于日志记录、数据埋点。
	Origin string
	// Id 是本次 GM 请求的唯一 ID。用于日志记录、调试、问题排查。
	Id string
	// IdempotencyKey 是本次 GM 请求的 Idempotency Key。如果有值则应当执行幂等处理逻辑。
	IdempotencyKey string
	// Cmd 是 GM 命令标识。取值和 GM 协议文件中的 rpc 名称对应。
	Cmd string
	// Args 是和 Cmd 对应的 GM 命令参数。这是一个异构的 JSON Object,结构和 GM 协议文件中 rpc 的 Request 一致。
	// 游戏侧需要根据 Cmd 的值,对 Args 进行 JSON 反序列化。
	Args json.RawMessage
}

type HttpClient

type HttpClient interface {
	Do(*http.Request) (*http.Response, error)
}

HttpClient 用于发送 HTTP 请求。如果需要对 HTTP 请求的行为和参数进行自定义设置,可以实现此接口。

通常来说 *http.Client 可以满足绝大部分需求。

type IdP

type IdP string

Identity Provider (IdP) 是世游定义的用户身份提供方,俗称账号系统。

const (
	// 游客登录
	IdP_Guest IdP = "guest"

	// 世游通行证
	IdP_Seayoo IdP = "seayoo"

	// Sign in with Apple
	IdP_Apple IdP = "apple"

	// Google Account
	IdP_Google IdP = "google"

	// Facebook Login
	IdP_Facebook IdP = "facebook"

	// 小米账号
	IdP_Xiaomi IdP = "xiaomi"

	// 微信登录
	IdP_Weixin IdP = "weixin"

	// OPPO 账号
	IdP_Oppo IdP = "oppo"

	// VIVO 账号
	IdP_Vivo IdP = "vivo"

	// 华为账号
	IdP_Huawei IdP = "huawei"

	// 荣耀账号
	IdP_Honor IdP = "honor"

	// UC(九游)登录
	IdP_UC IdP = "uc"

	// TapTap 登录
	IdP_TapTap IdP = "taptap"

	// 哔哩哔哩(B站)账号
	IdP_Bilibili IdP = "bilibili"

	// 应用宝 YSDK 登录
	IdP_Yingyongbao IdP = "yingyongbao"

	// 4399 账号登录
	IdP_4399 IdP = "4399"

	// 抖音账号
	IdP_Douyin IdP = "douyin"

	// 雷电模拟器账号
	IdP_Leidian IdP = "leidian"

	// 猫窝游戏
	IdP_Maowo IdP = "maowo"

	// 联想
	IdP_Lenovo = "lenovo"

	// 魅族
	IdP_Meizu = "meizu"

	// 酷派
	IdP_Coolpad = "coolpad"

	// 努比亚
	IdP_Nubia = "nubia"

	// 绝峰游戏
	IdP_Juefeng = "juefeng"

	// 魅拓游戏
	Idp_Meituo IdP = "meituo"

	// 微信小游戏
	Idp_MinigameWeixin IdP = "minigame_weixin"

	// MuMu 模拟器
	Idp_Mumu IdP = "mumu"

	// 虎牙游戏
	Idp_Huya IdP = "huya"
)

type IdempotencyStore added in v0.5.0

type IdempotencyStore interface {
	// SetNX 用于原子性地存储幂等记录并返回旧值。
	// value 仅在 key 不存在时才会被存储 (Only set the key if it does not already exist)。
	// 返回值是 key 存在时的旧值。如果 key 不存在则返回空字符串。
	SetNX(ctx context.Context, key, value string) (string, error)

	// SetXX 用于原子性地更新已存在的幂等记录。
	// value 仅在 key 存在时才会被存储 (Only set the key if it already exists)。
	SetXX(ctx context.Context, key, value string) error
}

IdempotencyStore 是一个用于存储 GM 命令的幂等记录的接口。

Combo SDK 内置了 Redis 和 Memory 两种实现,可分别通过 NewMemoryIdempotencyStore() 和 NewRedisIdempotencyStore() 创建。

游戏侧也可以选择自行实现 IdempotencyStore 接口。

func NewMemoryIdempotencyStore added in v0.5.0

func NewMemoryIdempotencyStore() IdempotencyStore

NewMemoryIdempotencyStore 创建一个基于 Memory 的 IdempotencyStore 实现。

注意:该实现仅用于开发调试,不适合生产环境。

数据仅在内存中存储,重启服务后数据会丢失。数据不会过期,不会自动清理。

func NewRedisIdempotencyStore added in v0.5.0

func NewRedisIdempotencyStore(cfg RedisIdempotencyStoreConfig) IdempotencyStore

NewRedisIdempotencyStore 创建一个基于 Redis 的 IdempotencyStore 实现。

数据会存储在 Redis 中,可以保证数据的的高可用性和到期自动清理。推荐生产环境使用。

注意:本实现需要 Redis >= 7.0

type IdempotentGmListenerConfig added in v0.5.0

type IdempotentGmListenerConfig struct {
	Store    IdempotencyStore // 幂等性数据存储。实现可以是 Redis 或 Memory,也可以自行实现 IdempotencyStore
	Listener GmListener       // 实际执行业务逻辑的 GmListener
	Logger   *slog.Logger     // 记录日志的 logger,如果不指定,则默认会使用输出到 stderr 的 TextHandler
}

IdempotentGmListenerConfig 包含了创建具有幂等性处理能力的 GmListener 时所必需的配置项。

type IdentityPayload

type IdentityPayload struct {
	// ComboId 是世游分配的聚合用户 ID。
	// 游戏侧应当使用 ComboId 作为用户的唯一标识,即游戏帐号。
	ComboId string

	// IdP (Identity Provider) 是用户身份的提供者。
	// 游戏侧可以使用 IdP 做业务辅助判断,例如判定用户是否使用了某个特定的登录方式。
	IdP IdP

	// ExternalId 是用户在外部 IdP 中的唯一标识。
	//
	// 例如:
	// - 如果用户使用世游通行证登录,那么 ExternalId 就是用户的世游通行证 ID。
	// - 如果用户使用 Google Account 登录,那么 ExternalId 就是用户在 Google 中的账号标识。
	// - 如果用户使用微信登录,那么 ExternalId 就是用户在微信中的 OpenId。
	//
	// 注意:
	// 游戏侧不应当使用 ExternalId 作为用户标识,但可以将 ExternalId 用于特定的业务逻辑。
	ExternalId string

	// ExternalName 是用户在外部 IdP 中的名称,通常是用户的昵称。
	ExternalName string

	// WeixinUnionid 是用户在微信中的 UnionId。
	// 游戏侧可以使用 WeixinUnionid 实现多端互通。
	//
	// 注意:WeixinUnionid 只在 IdP 为 weixin 时才会有值。
	WeixinUnionid string

	// DeviceId 是用户在登录时使用的设备的唯一 ID。
	DeviceId string

	// Distro 是游戏客户端的发行版本标识。
	// 游戏侧可将 Distro 用于服务端数据埋点,以及特定的业务逻辑判断。
	Distro string

	// Variant 是游戏客户端的分包标识。
	// 游戏侧可将 Variant 用于服务端数据埋点,以及特定的业务逻辑判断。
	//
	// 注意:Variant 只在客户端是分包时才会有值。当客户端不是分包的情况下,Variant 为空字符串。
	Variant string

	// Age 是根据用户的实名认证信息得到的年龄。0 表示未知。
	//
	// 在某些特殊场景下,游戏侧可用 Age 来自行处理防沉迷。
	//
	// 注意:Age 不保证返回精确的年龄信息,仅保证用于防沉迷处理时的准确度够用。
	// 例如:
	//	当某个用户真实年龄为 35 岁时,Age 可能返回 18
	//	当某个用户真实年龄为 17 岁时,Age 可能返回 16
	Age int
}

IdentityPayload 包含了用户的身份信息。

type LeaveGameInput

type LeaveGameInput struct {
	// 聚合用户标识。
	ComboId string `json:"combo_id"`

	// 游戏会话标识。
	// 单次游戏会话的上下线动作必须使用同一会话标识上报。
	SessionId string `json:"session_id"`
}

type LeaveGameOutput

type LeaveGameOutput struct {
	// contains filtered or unexported fields
}

func (*LeaveGameOutput) StatusCode

func (r *LeaveGameOutput) StatusCode() int

HTTP 状态码,例如 200, 400, 500...

func (*LeaveGameOutput) TraceId

func (r *LeaveGameOutput) TraceId() string

世游服务端生成的,用于追踪本次请求的唯一 ID。

建议游戏侧将 TraceId 记录到日志中,以便于调试和问题排查。

type NotificationId

type NotificationId string

每次通知的唯一 ID。游戏侧可用于日志记录、调试、问题排查。

type NotificationListener

type NotificationListener interface {

	// HandleShipOrder 用于处理订单发货通知。
	// 世游服务端会在订单状态变更为已支付时,向游戏侧推送发货通知。
	// 游戏侧需要在收到通知后,根据通知中的订单信息,发货给用户:
	// - 如果游戏内发货成功,则应当返回 nil。
	// - 如果游戏内发货出现错误,则应当返回对应的 error。世游服务端会在稍后重试推送发货通知。
	HandleShipOrder(ctx context.Context, id NotificationId, payload *ShipOrderNotification) error

	// HandleRefund 用于处理订单退款通知。
	// 目前仅支持对苹果 App Store 和谷歌 Play 商店的退款进行处理。
	// 世游服务端会处理这些商店的退款通知,如果退款验证通过,则会向游戏侧推送退款通知。
	// - 如果游戏内成功处理了退款通知,则应当返回 nil。
	// - 如果游戏内处理退款时出现错误,则应当返回对应的 error。世游服务端会在稍后重试推送退款通知。
	HandleRefund(ctx context.Context, id NotificationId, payload *RefundNotification) error
}

NotificationListener 是一个用于接收世游服务端推送的通知的接口。

游戏侧需要实现此接口并执行对应的业务逻辑。

type OrderMeta

type OrderMeta struct {
	// 游戏大区 ID。
	ZoneId string `json:"zone_id,omitempty"`

	// 游戏服务器 ID。
	ServerId string `json:"server_id,omitempty"`

	// 游戏角色 ID。
	RoleId string `json:"role_id,omitempty"`

	// 游戏角色名。
	RoleName string `json:"role_name,omitempty"`

	// 游戏角色的等级。
	RoleLevel int `json:"role_level,omitempty"`

	// 微信小游戏的 App ID。
	// 微信小游戏的 iOS 支付场景必须传入,即 Platform == Platform_Weixin
	WeixinAppid string `json:"weixin_appid,omitempty"`

	// 微信小游戏的玩家 OpenID。
	// 微信小游戏的 iOS 支付场景必须传入,即 Platform == Platform_Weixin
	WeixinOpenid string `json:"weixin_openid,omitempty"`
}

OrderMeta 包含了订单的元数据。

大部分元数据用于数据分析与查询,游戏侧应当尽量提供。

某些元数据在特定的支付场景下是必须的,例如微信小游戏的 iOS 支付场景。

type Platform

type Platform string

游戏客户端运行平台。

const (
	// 苹果的 iOS 和 iPadOS
	Platform_iOS Platform = "ios"

	// 安卓平台,包括华为鸿蒙系统、小米澎湃 OS 等基于 Android 的操作系统
	Platform_Android Platform = "android"

	// Windows (PC) 桌面平台
	Platform_Windows Platform = "windows"

	// macOS 桌面平台
	Platform_macOS Platform = "macos"

	// 微信小游戏
	//
	// Deprecated: 已被 Platform_WebGL 取代
	Platform_Weixin Platform = "weixin"

	// WebGL 平台,包括微信、抖音等小游戏,以及 HTML5 网页游戏
	Platform_WebGL Platform = "webgl"

	// 华为的 HarmonyOS NEXT 鸿蒙应用开发平台
	Platform_HarmonyOS Platform = "harmonyos"
)

type RedisIdempotencyStoreConfig added in v0.5.0

type RedisIdempotencyStoreConfig struct {
	Client redis.Cmdable // Redis 客户端。这里不假设 Redis 的运维部署方式。可以是 redis.Client 或者 redis.ClusterClient,由游戏侧自行创建和配置。
	TTL    time.Duration // Idempotency Key 的过期时间,如果不指定,则默认为 24 小时。
	Prefix string        // Idempotency Key 的前缀,如果不指定,则默认为空字符串。
}

RedisIdempotencyStoreConfig 包含了创建基于 Redis 的 IdempotencyStore 时所必需的配置项。

type RefundNotification added in v0.2.0

type RefundNotification struct {
	// 世游服务端创建的,标识订单的唯一 ID。
	OrderId string `json:"order_id"`

	// 游戏侧用于标识创建订单请求的唯一 ID。
	ReferenceId string `json:"reference_id"`

	// 发起购买的用户的唯一标识。
	ComboId string `json:"combo_id"`

	// 购买的商品 ID。
	ProductId string `json:"product_id"`

	// 购买的商品的数量。
	Quantity int `json:"quantity"`

	// 订单币种代码。例如 USD CNY。
	Currency string `json:"currency"`

	// 订单金额,单位为分。
	Amount int `json:"amount"`

	// 游戏侧创建订单时提供的订单上下文,透传回游戏。
	Context string `json:"context"`
}

RefundNotification 是订单退款通知的数据结构,包含了被退款订单的详细信息。

type SecretKey

type SecretKey []byte

由世游侧为游戏分配,游戏侧和世游侧共享的密钥。 此密钥用于签名计算与验证。

type ShipOrderNotification

type ShipOrderNotification struct {
	// 世游服务端创建的,标识订单的唯一 ID。
	OrderId string `json:"order_id"`

	// 游戏侧用于标识创建订单请求的唯一 ID。
	ReferenceId string `json:"reference_id"`

	// 发起购买的用户的唯一标识。
	ComboId string `json:"combo_id"`

	// 购买的商品 ID。
	ProductId string `json:"product_id"`

	// 购买的商品的数量。
	Quantity int `json:"quantity"`

	// 订单币种代码。例如 USD CNY。
	Currency string `json:"currency"`

	// 订单金额,单位为分。
	Amount int `json:"amount"`

	// 游戏侧创建订单时提供的订单上下文,透传回游戏。
	Context string `json:"context"`

	// 是否是沙盒订单。沙盒订单意味着此订单并未产生真实的付款。
	// 预期此字段仅用于记录日志和数据埋点。无论是否是沙盒订单,游戏侧都应当发货。
	IsSandbox bool `json:"is_sandbox"`
}

ShipOrderNotification 是订单发货通知的数据结构,包含了已支付订单的详细信息。

type TokenVerifier

type TokenVerifier struct {
	// contains filtered or unexported fields
}

TokenVerifier 用于验证世游服务端颁发的 Token。

func NewTokenVerifier

func NewTokenVerifier(cfg Config) (*TokenVerifier, error)

NewTokenVerifier 创建一个新的 TokenVerifier。

func (*TokenVerifier) VerifyAdToken

func (v *TokenVerifier) VerifyAdToken(tokenString string) (*AdPayload, error)

VerifyAdToken 对 AdToken 进行验证。

如果验证通过,返回 AdPayload。如果验证不通过,返回 error。

func (*TokenVerifier) VerifyIdentityToken

func (v *TokenVerifier) VerifyIdentityToken(tokenString string) (*IdentityPayload, error)

VerifyIdentityToken 对 IdentityToken 进行验证。

如果验证通过,返回 IdentityPayload。如果验证不通过,返回 error。

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL