主题
商店与支付 Skill
运行环境与充值范围
test(项目内测):对战平台只允许消耗账号已有星火币,不能发起充值。formal(项目上线):星火对战平台支持正常的星火币充值流程,按真实支付验证。- 内测完全自助、由用户自行操作;充值只在项目上线运行在
formal,或由工作人员开通 TapTap 测试后开放。不要把自助内测当作支付验收。
充值能力按运行时环境区分,不要写成“星火对战平台不支持充值”,也不要用内测的已有余额消费代替充值验收。
何时使用
任务涉及以下内容时使用本 Skill:
- 游戏内商店、充值入口、星火币商品、购买记录或周期商品
GameSystemUI.ShopSystem、ShopWindow、ShopSystemClient、ShopSystemServerGameDataShop、GameDataShopCategory、GameDataShopItem、GameDataLobbyResource、GameDataResourceAmountPaymentClient.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。
方案选择
优先级从高到低:
- ShopWindow + ShopSystemClient:标准商店 UI,推荐默认方案。
- 自定义 UI + ShopSystemClient:项目自己画 UI,但购买逻辑、验单、扣款、发货仍走 GameSystemUI 商店系统。
- 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。底层方案必须自行完成:
- 用户连接时
PaymentServer.SubscribePayment(user)。 - 用户断开时
PaymentServer.UnsubscribePayment(user)。 - 注册
PaymentServer.PaymentHandler。 - 校验
ProductionId、支付金额、用户状态、商品状态和发货条件。 - 通过
context.Transaction追加发货操作。 - 按需要调用
context.SetPurchaseStat(...)补充后台购买统计。 - Handler 返回
true,由框架统一追加星火币扣除并提交事务。 - 注册
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,框架不能替项目决定返还哪种货币;此时订单会进入未完成/需人工处理状态,应记录错误并通过项目自己的订单后台补偿。
排查顺序
优先按下面顺序查,不要一上来改支付底层:
- 商品 ID:客户端 itemId / productId 是否和服务端
GameDataShopItem完整 ID 完全一致。 - 数据来源:商品、Cost、Reward、Resource 是否在客户端和服务端同一初始化路径可见。
- 星火币资源:
Cost.Resource.Data.IsPremiumCurrency == true。 - 金额:请求价格是否直接来自
item.Cost.Data.Amount。 - 客户端:购买按钮是否重复点击、事件是否重复订阅、是否误把
RequestPayment=true当成功。 - 服务端:如果是底层支付,是否订阅用户支付通道并注册
PaymentHandler。 - 补偿能力:如果是底层支付,是否注册
PaymentCompensationHandler,且商品的Cost.Resource是否能映射到项目付费货币。 - 日志:优先看编辑器安装目录
<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。真实扣款链路在发布后的平台环境用一笔低价订单验证,并在创作者中心确认「运营配置 -> 游戏基础配置 -> 游戏大厅配置」里的「进入游戏保留大厅局」按项目支付流程需要设置。