Skip to content

商店与支付 Skill ​

运行环境与充值范围 ​

  • test(项目内测):对战平台只允许消耗账号已有星火币,不能发起充值。
  • formal(项目上线):星火对战平台支持正常的星火币充值流程,按真实支付验证。
  • 内测完全自助、由用户自行操作;充值只在项目上线运行在 formal,或由工作人员开通 TapTap 测试后开放。不要把自助内测当作支付验收。

充值能力按运行时环境区分,不要写成“星火对战平台不支持充值”,也不要用内测的已有余额消费代替充值验收。

何时使用 ​

任务涉及以下内容时使用本 Skill:

  • 游戏内商店、充值入口、星火币商品、购买记录或周期商品
  • GameSystemUI.ShopSystem、ShopWindow、ShopSystemClient、ShopSystemServer
  • GameDataShop、GameDataShopCategory、GameDataShopItem、GameDataLobbyResource、GameDataResourceAmount
  • PaymentClient.RequestPayment、PaymentClient.GetCurrentBalance、已废弃的 PaymentClient.GetBalanceInfo、PaymentServer.SubscribePayment、PaymentServer.PaymentHandler、PaymentServer.PaymentCompensationHandler
  • 排查购买失败、支付成功但没发货、金额不一致、服务端收不到支付通知、订单统计缺失

先读公开文档:docs/systems/ShopAndPaymentSystem.md。任务涉及 editor/data、数据编辑器 MCP 或 JSON 数编时,同时读 docs/ai/skills/data-editor/SKILL.md。

方案选择 ​

优先级从高到低:

  1. ShopWindow + ShopSystemClient:标准商店 UI,推荐默认方案。
  2. 自定义 UI + ShopSystemClient:项目自己画 UI,但购买逻辑、验单、扣款、发货仍走 GameSystemUI 商店系统。
  3. PaymentClient + PaymentServer:完全自定义支付流程,只有在内置商店无法表达业务事务时使用。

如果使用 GameSystemUI 商店系统,不要手动注册 PaymentServer.PaymentHandler、PaymentServer.PaymentCompensationHandler,也不要手动调用 PaymentServer.SubscribePayment。订阅、验单、扣款、正式发货、退化发货、项目货币补偿和订单统计由商店系统处理。

GameData 来源选择 ​

不要把“AI 直接写 C# GameData”当成所有项目的默认习惯。

  • 用户需要可视化数据编辑器调参、团队策划维护、继承模板、MCP 修改 editor/data 时,优先让数据编辑器 / JSON 做静态来源。
  • 用户明确走纯代码或纯 AI 辅助开发,不需要编辑器管理这组商店数据时,可以在 C# 中用 Game.OnGameDataInitialization 注册 GameDataShop* 数据。
  • 同一张静态 GameData 表不要同时由 C# 和 editor/data 定义同一批条目;否则客户端、服务端、数据编辑器预览和 AI 都难判断哪边是事实来源。
  • Game.GameModeLink 不要在 OnRegisterGameClass 里读取;需要按游戏模式注册数据时,在 OnGameDataInitialization 回调里判断。

代码注册商店数据时,集中定义完整字符串 ID,例如:

csharp
private const string Pack10Id =
    "$GameEntry.ScopeData.GameDataShopItem.PayShop.coin_pack_10";

private static readonly GameLink<GameDataShopItem, GameDataShopItem> Pack10 =
    new(Pack10Id);

支付商品 ID 必须和服务端能解析到的商品 ID 完全一致。不要用短 ID 或 raw UTF-8 字面量替代需要跨端一致的完整 Link 字符串。

硬规则 ​

  • 真实货币商品的 Cost 必须指向 IsPremiumCurrency = true 的 GameDataLobbyResource。
  • PaymentClient.RequestPayment(...) 返回 true 只代表平台支付界面请求已发出,不代表支付成功。
  • RequestPayment.productId 必须等于服务端商品 ID;price 必须等于商品 Cost.Amount。
  • 奖励只能由服务端发放;客户端不能在请求发出后直接加货币、物品或奖励。
  • 使用 ShopSystem 时,以 ShopSystemClient.OnPurchaseSuccess / OnPurchaseFailed 作为客户端结果。
  • 使用底层 PaymentServer 时,以服务端 PaymentServer.OnPaymentDelivered 作为最终成功结果。
  • NameAlreadyExists 不等于“商品一定已发放”;应查询订单结算状态,并观察退化发货、补偿或最终失败结果。
  • GameSystemUI 会在退化发货失败后按实际扣除的 notification.Gemstone 返还商品 Cost.Resource 对应的 CloudDataKey;用户原先已有的项目货币不属于本次补偿。
  • 不使用 GameSystemUI 时,必须自行注册 PaymentServer.PaymentCompensationHandler;OnPaymentCompensated 只是补偿成功后的通知,不能替代 Handler。
  • 正式发货、退化发货和补偿必须使用同一订单结算幂等标记并保持互斥。
  • 支付按钮发出请求后要加锁,避免连续点击生成多个真实订单。
  • 购买相关静态事件只注册一次;可销毁 UI 对象应在销毁时取消订阅。
  • PaymentClient.GetCurrentBalance() 只做低频查询:打开页面、购买前、支付成功后刷新。
  • PaymentClient.GetBalanceInfo() 和 PaymentBalanceInfo.TotalCoin 已废弃;TotalCoin 不是当前项目消费统计。项目消费应由服务端按当前项目、当前用户查询。
  • 服务端项目消费查询使用 PaymentServer.QueryCurrentProjectConsumptionAsync(user);它只返回当前项目、当前游戏局用户已结算的星火币累计消耗,失败返回 null。Host 必须校验用户属于当前游戏局并固定项目 ID;不要把客户端 TotalCoin 当作项目消费,也不要暴露任意用户或项目的查询参数。
  • 纯客户端调试和编辑器调试通常不会打开真实平台支付。真实支付必须在发布后的平台环境低价验证。

自定义 UI 推荐模式 ​

自定义 UI 仍优先调用 ShopSystemClient.Purchase(itemId, quantity)。余额不足且商品消耗资源是星火币时,商店系统会自动发起平台支付,并在服务端完成验单、扣款、发货和统计。

生成 UI 代码时:

  • 用商品 GameDataShopItem 和 GameDataResourceAmount 渲染名称、描述、图标、价格和奖励。
  • 点击购买前检查 _purchasePending 或 ShopSystemClient.IsPurchasing,请求发出后立刻锁按钮。
  • 监听 ShopSystemClient.OnPurchaseSuccess 后刷新商店和资源余额。
  • 监听 ShopSystemClient.OnPurchaseFailed 后恢复按钮,并用 ShopSystemClient.GetResultMessage(response.Result) 展示失败原因。
  • 可以设置 UI 超时解除按钮,但超时不代表订单失败;最终仍等服务端结果事件。

底层支付 API ​

只有完全绕过内置商店时才直接使用 PaymentClient + PaymentServer。底层方案必须自行完成:

  1. 用户连接时 PaymentServer.SubscribePayment(user)。
  2. 用户断开时 PaymentServer.UnsubscribePayment(user)。
  3. 注册 PaymentServer.PaymentHandler。
  4. 校验 ProductionId、支付金额、用户状态、商品状态和发货条件。
  5. 通过 context.Transaction 追加发货操作。
  6. 按需要调用 context.SetPurchaseStat(...) 补充后台购买统计。
  7. Handler 返回 true,由框架统一追加星火币扣除并提交事务。
  8. 注册 PaymentServer.PaymentCompensationHandler,根据 ProductionId、商品价格和 Cost.Resource 映射项目付费货币;在独立补偿事务中追加 context.Transaction.AddCurrency(...)。

不要在 PaymentHandler 中调用 Transaction.ExecuteAsync();否则发货和扣币会拆成两个事务,破坏原子性。

补偿 Handler 示例:

csharp
PaymentServer.PaymentCompensationHandler = (user, context) =>
{
    var notification = context.Notification;
    // 实际项目应查商品配置,不要无条件硬编码货币键和金额。
    context.Transaction.AddCurrency("premium_currency", notification.Gemstone);
    return Task.FromResult(true);
};

如果未注册补偿 Handler,或 Handler 返回 false,框架不能替项目决定返还哪种货币;此时订单会进入未完成/需人工处理状态,应记录错误并通过项目自己的订单后台补偿。

排查顺序 ​

优先按下面顺序查,不要一上来改支付底层:

  1. 商品 ID:客户端 itemId / productId 是否和服务端 GameDataShopItem 完整 ID 完全一致。
  2. 数据来源:商品、Cost、Reward、Resource 是否在客户端和服务端同一初始化路径可见。
  3. 星火币资源:Cost.Resource.Data.IsPremiumCurrency == true。
  4. 金额:请求价格是否直接来自 item.Cost.Data.Amount。
  5. 客户端:购买按钮是否重复点击、事件是否重复订阅、是否误把 RequestPayment=true 当成功。
  6. 服务端:如果是底层支付,是否订阅用户支付通道并注册 PaymentHandler。
  7. 补偿能力:如果是底层支付,是否注册 PaymentCompensationHandler,且商品的 Cost.Resource 是否能映射到项目付费货币。
  8. 日志:优先看编辑器安装目录 <EditorRoot>/logs/server 和 <EditorRoot>/logs/client,不要把项目根目录的 Client-Debug.log / Server-Debug.log 当实时游玩日志。

验证 ​

支付相关改动至少编译:

powershell
dotnet build src/GameEntry.csproj -c Client-Debug
dotnet build src/GameEntry.csproj -c Server-Debug

发布前建议同时验证 Client-Release 和 Server-Release。真实扣款链路在发布后的平台环境用一笔低价订单验证,并在创作者中心确认「运营配置 -> 游戏基础配置 -> 游戏大厅配置」里的「进入游戏保留大厅局」按项目支付流程需要设置。