# 0.4.0:体力、开屏/激励广告、字段排行榜 本机公共服务新增三个可选模块。完整配置见包内config.features.example.json,AI运行接口定义由网站runtime-openapi.json提供。旧config.example.json不配置features时三个模块关闭,原登录/礼包/存档/上报继续可用。 每个游戏独立实例、数据库与配置。所有以下接口都携带本游戏公共服务Bearer,路径前缀为 `/v1/games/{gameID}`,不能让客户端直接访问。玩家必须先经公共服务登录创建;查询和发奖不能自动造账号。接入站点的SDK_PORTAL_TOKEN不用于这些接口。 ## 体力的唯一权威源 features.stamina配置initial、maximum、recoverySeconds、allowOverflow及products。initial仅初始化新玩家,maximum是自然恢复上限,服务器按秒计算离线恢复,无需定时进程。达到上限不囤积恢复时间,消耗后从正确时间继续计算;进程重启不能重复恢复。 规则修改后重新启动,已有余额不重置,恢复时间基准更新为启动时刻(时钟回退不会向前挪旧基准),避免新恢复速率追溯计算旧时间。一个游戏数据库仅运行同一份配置的一个实例。 | 方法 | 路径 | 用途 | | --- | --- | --- | | GET | /stamina/players/{playerID} | 当前balance、自然maximum、nextRecoveryAt(ISO时间或null) | | POST | /stamina/consume | 幂等消耗 | | POST | /stamina/reward | 可信游戏后端幂等奖励 | | GET | /stamina/players/{playerID}/logs?afterId=0&limit=50 | 按流水ID增序,每次最多100 | | POST | /stamina/purchases | 按配置商品预留购买订单 | | GET | /stamina/purchases/{orderID}?playerId=... | 查当前购买状态 | | POST | /stamina/purchases/{orderID}/confirm | 游戏扣款成功后确认 | | POST | /stamina/purchases/{orderID}/cancel | 取消未确认订单 | 消耗/奖励正文: ```json {"playerId":"实际玩家ID","eventId":"稳定游戏业务ID","amount":1,"reason":"level_start"} ``` eventId全游戏幂等,同ID改玩家/数量/用途或消费与奖励互换返回409。余额不足返回409。奖励受allowOverflow约束:不允许溢出时可能封顶,实际增加数查看流水;允许时可超过自然上限,但余额硬上限10亿防止异常增长。日志中的自然恢复也入账,不从客户端存档覆盖余额。 购买正文: ```json {"playerId":"实际玩家ID","product":"small","idempotencyKey":"purchase-business-001"} ``` 返回冻结的orderId、currency、price、amount、status。游戏用orderId在自己的数据库事务中记录扣款并实际扣除钻石等货币,成功后用 `{"playerId":"实际玩家ID","debitReference":"游戏扣款凭据"}` 确认。SDK确认与增加体力同事务执行,已付款购买足额入账,可超过自然恢复上限,不丢失购买的体力。 reserved → confirmed或cancelled,终态不可互换;相同确认凭据重试不重复加体力,不同凭据409。网络不明时查询状态,不盲目退款。如果要退款,先成功取消SDK reserved订单,再在游戏数据库按orderId唯一键幂等退款;confirmed不能取消或退款。订单不自动超时取消,游戏应持久化待处理购买、定期对账恢复;SDK不持有或代扣游戏货币。 幂等写接口重放返回首次响应快照。创建订单重试即使返回reserved,也应GET查询最新状态,不能因此再次扣款。游戏后端认证玩家、验证玩法并决定业务数量,不能提供允许客户端随意加体力的透传接口。 ## 仅开屏和激励广告 features.ads.placements只接受splash和rewarded,Banner配置直接拒绝。开屏提供广告位配置与行为记录,不发奖励,也不把“shown”当平台证明。广告真正展示仍由游戏客户端使用平台支持的广告组件,SDK不会编造wx.createSplashAd之类客户端能力。 | 方法 | 路径 | 用途 | | --- | --- | --- | | GET | /ads/placements | 可用广告位,不含密钥 | | POST | /ads/splash-events | eventId/playerId/placementId/event,event为shown/closed/failed | | POST | /ads/contexts | playerId/placementId/idempotencyKey,创建激励上下文 | | GET | /ads/contexts/{contextID}?playerId=... | 查上下文与资格状态 | | GET | /ads/wechat-callback | 原始平台回调经游戏后端转发 | | POST | /ads/contexts/{contextID}/redeem | 验证后领取,正文playerId | | POST | /ads/contexts/{contextID}/confirm | 外部道具真实发放后确认 | 激励流程:创建上下文 → 将playerId透传广告userId、contextId透传customData → 接收已验证的平台回调 → verified → 游戏调用redeem。不要使用onClose或客户端ad-view直接领奖。 上下文冻结广告位、平台reward_item/reward_amount和实际奖励配置,回调必须匹配玩家、custom_data与平台奖励。一个平台transaction_id只能对应一份上下文,一份上下文也只能接收一次交易;重复相同回调可确认,不同内容/跨玩家冲突。有效期仅限制首次平台验证,已验证资格不会因为客户端稍晚领取而过期。 - reward.kind=stamina:redeem与SDK体力入账同一事务。若不允许溢出且空间不足,返回409并保持verified,玩家消耗后可再次领取,不吞掉资格。 - reward.kind=external:redeem返回grant_pending,contextId就是唯一发奖凭据。游戏在本地事务用此ID记录凭据并实际增加货币或道具,然后传 `{"playerId":"实际玩家ID","fulfillmentReference":"本地发奖凭据"}` confirm;重复确认不重复发放。SDK不代替游戏发道具。 - 同一创建idempotencyKey重试可能返回首次pending快照;GET取当前状态。callback/兑换均持久化,重启后可继续。 ### 微信回调兼容协议与验证边界 目前实现显式命名的 `wechat-sha256-aes256cbc` 兼容适配器,与现有水果游戏代码中的协议形态对齐。Token从独立环境变量读取,AESKey为Base64编码的32字节密钥。签名为token、timestamp、nonce及有值时的原encrypt字符串排序拼接后SHA256;先验签再AES-256-CBC解密,IV为密文字节前16字节,严格校验PKCS7,时间偏差按callbackSkewSeconds限制。 cipherEncoding必须明确指定raw(URL解码后原始字节)或base64(再做Base64解码)。公网游戏回调代理必须保留完整RawQuery,不自行拆参后重新编码;示例game-client.go包含ForwardAdCallback。GET挑战返回echostr JSON,验证回调返回is_valid JSON;is_valid=true表示已验证且领取资格持久保存,不表示外部游戏道具已发放。 本次官方文档入口未能正常读取,因此不把现有游戏参考代码和自建加密测试当成官方协议或真实渠道已验证。上线前必须用实际微信广告位确认签名、编码、字段和回包格式;如当前渠道不同,应扩展并测试对应适配器,不能开启“跳过验证”降级。单测覆盖raw/base64与伪造、过期、重复、错误填充,但尚不能替代真实微信回调联调。 ## 一个字段一个永久榜单 features.leaderboards每项为 `{"field":"max_passed_level","order":"desc"}`。字段由游戏配置,最多64个,同字段不可重复;支持asc/desc。同分固定按playerId字典序升序,排名为1、2、3的顺序排名。不分日/周,不做周期重置、累计事件加分或排行榜领奖。 | 方法 | 路径 | 用途 | | --- | --- | --- | | GET | /leaderboards | 字段与排序配置 | | POST | /leaderboards/scores | 提交已验证的当前字段值 | | GET | /leaderboards/{field}?offset=0&limit=50 | 分页榜单,每次最多100 | | GET | /leaderboards/{field}/players/{playerID} | 当前玩家值、revision和rank | ```json {"eventId":"level-record-001","playerId":"实际玩家ID","field":"max_passed_level","value":5,"revision":1} ``` revision为每玩家每字段的递增版本,必须在游戏业务事务中持久分配,不能靠内存计数。旧版本或同版本不同值409;同事件同内容幂等。SDK保存字段当前值,不自行推断“最高分”“最佳耗时”业务规则,游戏后端应先计算真实值,再上报。数值有限且绝对值不超过10的15次方。 游戏使用本地outbox把可信通关/耗时记录和待提交成绩放在同一事务,提交失败重试同eventId与revision。禁止让客户端随意选择字段和分数、禁止把虚拟玩家当成真实玩家提交领奖。 ## 存储、维护与升级 0.4.0把数据库从schema2迁移到3,原账号/礼包/存档/事件保留,新增体力流水、购买订单、广告资格和榜单表。升级前先用旧版本备份,再运行新版本;0.3.1不认识schema3,回退需停服后恢复升级前快照,不能手动降低user_version。 操作幂等和广告交易记录保留用于防重复,不自动清理或以对象存储替代SQLite在线库;应监控磁盘、备份并按真实游戏负载测试。2核4G和万游戏部署不是本次单测已经证明的吞吐指标。