Nakama游戏内购集成:从支付流程到订单验证的完整实践指南
引言:游戏内购的痛难点
你是否还在为游戏内购系统的复杂验证流程头疼?玩家支付成功却收不到道具?不同平台的收据验证逻辑千差万别?Nakama作为开源的分布式游戏服务器框架,提供了一套完整的内购集成方案,支持Apple、Google、Huawei等多平台支付验证,让开发者专注于游戏逻辑而非支付细节。
读完本文你将掌握:
- Nakama内购系统的架构设计与核心组件
- 多平台支付流程的实现细节(Apple/Google/Huawei)
- 订单验证的安全机制与防欺诈策略
- 数据库设计与订单状态管理
- 完整的集成步骤与代码示例
Nakama内购系统架构概览
Nakama的内购系统采用模块化设计,主要由四大组件构成:API接口层、验证服务层、数据持久层和通知回调层。这种分层架构确保了各平台验证逻辑的隔离性和可扩展性。
核心组件交互流程
技术栈选型
| 组件 | 技术选型 | 优势 |
|---|---|---|
| 服务端框架 | Go | 高性能、并发安全、跨平台 |
| 脚本引擎 | Lua | 轻量高效、热更新支持 |
| 数据库 | PostgreSQL | 事务支持、JSONB类型、高可靠性 |
| 网络请求 | 原生HTTP客户端 | 减少依赖、可控性强 |
| 加密算法 | RSA/SHA256 | 符合各平台安全标准 |
支付流程详解:从客户端到服务器
Nakama的内购流程遵循标准的"客户端-服务器-平台"三方验证模式,确保每一笔交易的合法性和安全性。以下是完整的流程分解:
1. 客户端购买流程
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 Store | Google Play | Huawei AppGallery |
|---|---|---|---|
| 验证端点 | https://buy.itunes.apple.com/verifyReceipt | https://www.googleapis.com/androidpublisher/v3 | https://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通过事务和状态字段确保数据一致性:
配置与集成步骤:从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. 性能优化建议
- 缓存验证结果:对相同收据的重复验证请求进行缓存
- 异步处理业务逻辑:将道具发放等耗时操作放入异步队列
- 数据库索引优化:为频繁查询的字段建立合适索引
- 批量处理订阅通知:对订阅状态变更通知进行批量处理
总结与展望
Nakama提供了一套完整的游戏内购解决方案,通过模块化设计和多平台支持,帮助开发者快速实现安全可靠的支付系统。本文详细介绍了内购流程的架构设计、代码实现和集成步骤,涵盖了从收据验证到业务逻辑处理的全流程。
关键要点回顾
- Nakama内购系统采用分层架构,确保各组件解耦和可扩展
- 多平台验证逻辑统一封装,简化跨平台开发复杂度
- 严格的订单状态管理和防重复处理机制保障交易安全
- 灵活的Lua脚本系统支持快速迭代业务逻辑
未来扩展方向
- 数字支付集成:支持更多数字支付方式
- 自动化处理:使用自动化流程管理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考



