Nakama游戏内购集成:从支付流程到订单验证的完整实践指南

Nakama游戏内购集成:从支付流程到订单验证的完整实践指南

【免费下载链接】nakama Distributed server for social and realtime games and apps. 【免费下载链接】nakama 项目地址: https://gitcode.com/GitHub_Trending/na/nakama

引言:游戏内购的痛难点

你是否还在为游戏内购系统的复杂验证流程头疼?玩家支付成功却收不到道具?不同平台的收据验证逻辑千差万别?Nakama作为开源的分布式游戏服务器框架,提供了一套完整的内购集成方案,支持Apple、Google、Huawei等多平台支付验证,让开发者专注于游戏逻辑而非支付细节。

读完本文你将掌握:

  • Nakama内购系统的架构设计与核心组件
  • 多平台支付流程的实现细节(Apple/Google/Huawei)
  • 订单验证的安全机制与防欺诈策略
  • 数据库设计与订单状态管理
  • 完整的集成步骤与代码示例

Nakama内购系统架构概览

Nakama的内购系统采用模块化设计,主要由四大组件构成:API接口层、验证服务层、数据持久层和通知回调层。这种分层架构确保了各平台验证逻辑的隔离性和可扩展性。

核心组件交互流程

mermaid

技术栈选型

组件技术选型优势
服务端框架Go高性能、并发安全、跨平台
脚本引擎Lua轻量高效、热更新支持
数据库PostgreSQL事务支持、JSONB类型、高可靠性
网络请求原生HTTP客户端减少依赖、可控性强
加密算法RSA/SHA256符合各平台安全标准

支付流程详解:从客户端到服务器

Nakama的内购流程遵循标准的"客户端-服务器-平台"三方验证模式,确保每一笔交易的合法性和安全性。以下是完整的流程分解:

1. 客户端购买流程

mermaid

2. 服务器验证逻辑

Nakama对每个平台的验证流程进行了优化,以Apple为例,验证服务会先尝试生产环境验证,若返回21007错误码(表示沙盒环境收据),则自动重试沙盒环境:

-- 摘自data/modules/iap_verifier.lua
function M.verify_payment_apple(request)
  local url_sandbox = "https://sandbox.itunes.apple.com/verifyReceipt"
  local url_production = "https://buy.itunes.apple.com/verifyReceipt"

  local http_body = nk.json_encode({
    ["receipt-data"] = request.receipt,
    ["password"] = request.password,
    ["exclude-old-transactions"] = request.exclude_old_transactions
  })

  -- 先尝试生产环境验证
  local success, code, _, body = pcall(nk.http_request, url_production, "POST", http_headers, http_body)
  if success and code == 200 then
    local response = nk.json_decode(body)
    if response.status == 0 then  -- 验证成功
      return response
    elseif response.status == 21007 then  -- 需要沙盒验证
      -- 重试沙盒环境
      local success, code, _, body = pcall(nk.http_request, url_sandbox, "POST", http_headers, http_body)
      if success and code == 200 then
        return nk.json_decode(body)
      end
    end
  end
  error(body)
end

Google验证则采用OAuth2.0认证流程,先获取访问令牌,再查询购买状态:

-- 摘自data/modules/iap_verifier.lua
function M.google_obtain_access_token(client_email, private_key)
  local auth_url = "https://accounts.google.com/o/oauth2/token"
  local scope = "https://www.googleapis.com/auth/androidpublisher"
  local iat = nk.time() / 1000
  local exp = iat + 3600  -- 令牌有效期1小时

  -- 生成JWT令牌
  local jwt_claimset = {
    ["iss"] = client_email,
    ["scope"] = scope,
    ["aud"] = auth_url,
    ["exp"] = exp,
    ["iat"] = iat
  }
  local jwt_token = nk.jwt_generate("RS256", private_key, jwt_claimset)
  
  -- 获取访问令牌
  local form_data = "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=" .. jwt_token
  local success, code, _, body = pcall(nk.http_request, auth_url, "POST", http_headers, form_data)
  if success and code == 200 then
    return nk.json_decode(body)["access_token"]
  end
  error(body)
end

订单验证实现:多平台适配策略

Nakama支持主流应用商店的内购验证,每个平台都有其独特的验证流程和安全机制。以下是各平台的实现要点:

平台验证参数对比

参数Apple App StoreGoogle PlayHuawei AppGallery
验证端点https://buy.itunes.apple.com/verifyReceipthttps://www.googleapis.com/androidpublisher/v3https://orders-dre.iap.hicloud.com
认证方式共享密钥JWT令牌公钥证书
收据格式Base64编码JSON对象JSON+签名
重试机制自动环境切换令牌过期重试签名验证失败重试
订阅支持原生支持单独API单独API

核心验证代码实现

Go层的验证逻辑处理参数校验、结果解析和订单存储:

// 摘自server/core_purchase.go
func ValidatePurchasesApple(ctx context.Context, logger *zap.Logger, db *sql.DB, userID uuid.UUID, password, receipt string, persist bool) (*api.ValidatePurchaseResponse, error) {
    // 调用iap包验证收据
    validation, raw, err := iap.ValidateReceiptApple(ctx, httpc, receipt, password)
    if err != nil {
        return nil, err
    }

    -- 检查验证状态
    if validation.Status != iap.AppleReceiptIsValid {
        if validation.IsRetryable {
            return nil, status.Error(codes.Unavailable, "Apple IAP verification is currently unavailable. Try again later.")
        }
        return nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("Invalid Receipt. Status: %d", validation.Status))
    }

    -- 解析环境信息
    env := api.StoreEnvironment_PRODUCTION
    if validation.Environment == iap.AppleSandboxEnvironment {
        env = api.StoreEnvironment_SANDBOX
    }

    -- 处理交易记录
    storagePurchases := make([]*storagePurchase, 0)
    for _, purchase := range validation.Receipt.InApp {
        -- 跳过订阅过期交易
        if purchase.ExpiresDateMs != "" {
            continue
        }
        
        -- 解析购买时间
        purchaseTime, err := strconv.ParseInt(purchase.PurchaseDateMs, 10, 64)
        if err != nil {
            return nil, err
        }

        storagePurchases = append(storagePurchases, &storagePurchase{
            userID:        userID,
            store:         api.StoreProvider_APPLE_APP_STORE,
            productId:     purchase.ProductID,
            transactionId: purchase.TransactionId,
            rawResponse:   string(raw),
            purchaseTime:  parseMillisecondUnixTimestamp(purchaseTime),
            environment:   env,
        })
    }

    -- 存储订单信息
    if persist {
        purchases, err := upsertPurchases(ctx, db, storagePurchases)
        if err != nil {
            return nil, err
        }
        -- 构建并返回响应
        return buildValidationResponse(purchases, raw), nil
    }
    
    -- 不持久化时直接返回验证结果
    return buildValidationResponse(storagePurchases, raw), nil
}

数据库设计:订单存储与状态管理

Nakama使用PostgreSQL存储内购订单信息,表结构设计考虑了事务完整性、查询性能和扩展性。

purchase表结构设计

-- 摘自migrate/sql/20210416090601-purchase.sql
CREATE TABLE IF NOT EXISTS purchase (
    PRIMARY KEY (transaction_id),
    FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,

    create_time    TIMESTAMPTZ  NOT NULL DEFAULT now(),  -- 记录创建时间
    environment    SMALLINT     NOT NULL DEFAULT 0,     -- 环境: 0=未知,1=沙盒,2=生产
    product_id     VARCHAR(512) NOT NULL,               -- 商品ID
    purchase_time  TIMESTAMPTZ  NOT NULL DEFAULT now(),  -- 购买时间
    raw_response   JSONB        NOT NULL DEFAULT '{}',  -- 平台原始响应
    store          SMALLINT     NOT NULL DEFAULT 0,     -- 商店: 0=Apple,1=Google,2=Huawei
    transaction_id VARCHAR(512) NOT NULL CHECK (length(transaction_id) > 0),  -- 交易ID
    update_time    TIMESTAMPTZ  NOT NULL DEFAULT now(),  -- 记录更新时间
    user_id        UUID         DEFAULT NULL            -- 用户ID
);

-- 索引设计优化查询性能
CREATE INDEX IF NOT EXISTS purchase_user_id_purchase_time_transaction_id_idx
    ON purchase (user_id, purchase_time DESC, transaction_id);

订单状态流转

订单在其生命周期中会经历不同状态,Nakama通过事务和状态字段确保数据一致性:

mermaid

配置与集成步骤:从0到1实现内购功能

1. 环境准备

# 克隆仓库
git clone https://gitcode.com/GitHub_Trending/na/nakama.git
cd nakama

# 配置依赖
go mod download

# 初始化数据库
createdb nakama
go run main.go migrate up --database.address postgres://user:password@localhost:5432/nakama?sslmode=disable

2. 配置内购参数

修改配置文件config.yml,添加各平台的内购配置:

# 内购配置示例
iap:
  apple:
    shared_password: "your_apple_shared_secret"  # Apple共享密钥
  google:
    client_email: "your-service-account@project.iam.gserviceaccount.com"  # Google服务账号
    private_key: "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"  # Google私钥
  huawei:
    public_key: "your_huawei_public_key"  # Huawei公钥
    client_id: "your_huawei_client_id"
    client_secret: "your_huawei_client_secret"

3. 实现Lua业务逻辑

创建data/modules/purchase_handler.lua处理购买完成后的业务逻辑:

-- 购买完成回调处理
nk.register_purchase_notification_apple(function(context, payload)
    local user_id = context.user_id
    local product_id = payload.product_id
    local transaction_id = payload.transaction_id
    
    -- 记录购买日志
    nk.logger_info(string.format("User %s purchased product %s (transaction %s)", user_id, product_id, transaction_id))
    
    -- 根据商品ID发放道具
    if product_id == "com.example.coins.100" then
        -- 调用游戏逻辑发放100金币
        local result = nk.call("grant_coins", {user_id = user_id, amount = 100})
        if not result.success then
            nk.logger_error(string.format("Failed to grant coins to user %s", user_id))
            -- 可以实现重试逻辑或人工处理流程
        end
    elseif product_id == "com.example.vip.monthly" then
        -- 开通月VIP会员
        nk.call("activate_vip", {user_id = user_id, duration_days = 30})
    end
    
    return true
end)

4. 客户端集成示例(Unity)

// Unity客户端调用示例
IEnumerator PurchaseProduct(string productId)
{
    // 初始化Unity IAP
    var builder = ConfigurationBuilder.Instance(StandardPurchasingModule.Instance());
    builder.AddProduct(productId, ProductType.Consumable);
    UnityPurchasing.Initialize(this, builder);
    
    // 等待初始化完成
    while (!IsInitialized)
        yield return null;
    
    // 发起购买
    var product = m_StoreController.products.WithID(productId);
    if (product != null && product.availableToPurchase)
    {
        Debug.Log($"Purchasing product: {product.definition.id}");
        m_StoreController.InitiatePurchase(product);
    }
    else
    {
        Debug.Log("Product not available for purchase");
    }
}

// 购买完成回调
public void ProcessPurchase(PurchaseEventArgs args)
{
    // 获取收据
    string receipt = args.purchasedProduct.receipt;
    
    // 提交到Nakama服务器验证
    var client = new Nakama.Client("defaultkey", "127.0.0.1", 7350);
    var session = await client.AuthenticateDeviceAsync(SystemInfo.deviceUniqueIdentifier);
    
    var request = new ValidatePurchaseAppleRequest
    {
        Receipt = receipt,
        Persist = true
    };
    
    var response = await client.ValidatePurchaseAppleAsync(session, request);
    if (response.ValidatedPurchases.Count > 0)
    {
        Debug.Log("Purchase validated successfully");
        // 更新UI显示购买成功
    }
    else
    {
        Debug.LogError("Purchase validation failed");
    }
}

5. 启动服务器

# 启动Nakama服务器
go run main.go --config config.yml

常见问题与解决方案

1. 收据验证失败

问题原因解决方案
沙盒收据提交到生产环境实现环境自动切换逻辑,如iap_verifier.lua中的处理
网络连接问题增加HTTP请求超时重试机制,设置合理的timeout
配置参数错误检查Apple共享密钥、Google私钥等配置是否正确
收据已被使用确保每个收据只验证一次,使用transaction_id去重

2. 订单重复处理

// 防止重复处理的关键代码 (server/core_purchase.go)
func upsertPurchases(ctx context.Context, db *sql.DB, purchases []*storagePurchase) ([]*storagePurchase, error) {
    // 使用ON CONFLICT避免重复插入
    query := `
    INSERT INTO purchase
        (user_id, store, transaction_id, product_id, purchase_time, raw_response, environment)
    VALUES ($1, $2, $3, $4, $5, $6, $7)
    ON CONFLICT (transaction_id) DO UPDATE SET
        update_time = now()
    RETURNING create_time, update_time`
    
    // 执行批量插入
    // ...
}

3. 性能优化建议

  1. 缓存验证结果:对相同收据的重复验证请求进行缓存
  2. 异步处理业务逻辑:将道具发放等耗时操作放入异步队列
  3. 数据库索引优化:为频繁查询的字段建立合适索引
  4. 批量处理订阅通知:对订阅状态变更通知进行批量处理

总结与展望

Nakama提供了一套完整的游戏内购解决方案,通过模块化设计和多平台支持,帮助开发者快速实现安全可靠的支付系统。本文详细介绍了内购流程的架构设计、代码实现和集成步骤,涵盖了从收据验证到业务逻辑处理的全流程。

关键要点回顾

  • Nakama内购系统采用分层架构,确保各组件解耦和可扩展
  • 多平台验证逻辑统一封装,简化跨平台开发复杂度
  • 严格的订单状态管理和防重复处理机制保障交易安全
  • 灵活的Lua脚本系统支持快速迭代业务逻辑

未来扩展方向

  1. 数字支付集成:支持更多数字支付方式
  2. 自动化处理:使用自动化流程管理

【免费下载链接】nakama Distributed server for social and realtime games and apps. 【免费下载链接】nakama 项目地址: https://gitcode.com/GitHub_Trending/na/nakama

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值