结论
这种方案被审核拒绝的风险较高。
账户、会员等级和余额由服务器管理,本身不是问题。问题在于这些余额会在 iPhone 游戏中被消耗,属于数字商品、游戏币、会员权益或游戏功能。用户在 App 内获取此类权益,通常需要使用 Apple 的 In-App Purchase(IAP)。
以下设计尤其容易违反审核规则:
只能在网站购买游戏余额或会员,App 内没有对应的 IAP。
余额不足时,App 提示用户”前往网站充值”。
App 提供充值网页链接、二维码、价格说明或其他引导性文案。
以”支付发生在服务器端”为由绕过 IAP。
Apple 审核主要看用户购买的内容及其消费场景,不取决于支付接口部署在客户端还是服务器端。
为什么服务器支付仍可能违规
游戏中的 gold、silver 会员、游戏余额、游戏次数和虚拟货币,通常都属于数字权益。如果这些权益可以在 iPhone App 内使用或消耗,原则上就适用 App Store 的应用内购买规则。
以下技术安排通常没有问题:
用户账户注册在自己的服务器完成。
App 使用账号或 registration ID 登录。
会员状态和余额由服务器保存。
每次游戏后由服务器扣减余额。
App 从服务器读取剩余余额。
但余额的购买方式是另一个问题。服务器记账不能代替 IAP。即使 App 只负责收发数据,只要用户在网站购买的余额用于解锁或消费 App 内的数字服务,仍可能被认定为规避应用内购买。
此外,“余额不足,请前往网站充值”是明显的站外购买引导。如果没有适用资格、entitlement 或特定地区的例外,审核风险会更高。
推荐的合规方案
较稳妥的实现流程如下:
使用 StoreKit 在 iPhone App 内销售会员和游戏点数。
Apple 完成交易后,将交易凭证或签名交易数据发送给业务服务器。
服务器验证交易,并确认该交易未被重复入账。
验证成功后,服务器增加会员权益或余额。
每次 gameplay 仍由服务器扣款。
App 只显示服务器返回的最终余额。
如果游戏同时运行在 Web 和 iPhone 上,可以继续使用统一账户体系。用户在其他平台获得的权益能否在 iPhone App 中使用,以及 App 内是否必须提供相同权益的 IAP,要根据权益类型、业务模式、销售地区和当时有效的审核规则判断。
对于跨平台游戏,较为保守的设计是允许统一账户同步权益,同时在 iOS App 内提供 Apple IAP,并且不引导用户前往网站购买。
StoreKit 2 示例
下面的代码演示如何在 App 内购买游戏点数。产品必须先在 App Store Connect 中创建。
import StoreKit
enum PurchaseError: Error {
case productNotFound
case verificationFailed
}
func purchaseCredits(productID: String) async throws {
let products = try await Product.products(for: [productID])
guard let product = products.first else {
throw PurchaseError.productNotFound
}
let result = try await product.purchase()
switch result {
case .success(let verificationResult):
let transaction = try checkVerified(verificationResult)
try await sendTransactionToServer(
transactionID: String(transaction.id),
productID: transaction.productID,
signedTransaction: verificationResult.jwsRepresentation
)
await transaction.finish()
case .pending:
// 等待家长批准、付款授权或其他异步处理。
break
case .userCancelled:
break
@unknown default:
break
}
}
func checkVerified
_ result: VerificationResult
) throws -> T {
switch result {
case .verified(let value):
return value
case .unverified:
throw PurchaseError.verificationFailed
}
}
func sendTransactionToServer(
transactionID: String,
productID: String,
signedTransaction: String
) async throws {
// 将交易数据提交到业务服务器。
// 服务器应验证交易,并通过 transactionID 保证幂等入账。
}
服务器端不能因为客户端声称”购买成功”就直接增加余额,至少应完成以下处理:
1. 接收 signedTransaction 和当前登录用户 ID
2. 验证 Apple 签名及交易信息
3. 检查 bundleId、productId、environment 等字段
4. 检查 transactionId 是否已经处理
5. 在数据库事务中增加余额并记录交易
6. 返回服务器计算后的最新余额
下面是一个简化的幂等入账逻辑:
BEGIN;
INSERT INTO apple_transactions (
transaction_id,
user_id,
product_id
)
VALUES (
:transaction_id,
:user_id,
:product_id
)
ON CONFLICT (transaction_id) DO NOTHING;
-- 只有成功插入新交易时才能增加余额。
UPDATE game_accounts
SET balance = balance + :credit_amount
WHERE user_id = :user_id
AND :transaction_was_inserted = TRUE;
COMMIT;
具体的签名验证方式应以当前 App Store Server API、StoreKit 和 Apple 官方文档为准,不能只依赖上述示意代码。
余额不足时应该如何提示
不建议显示:
余额不足,请前往 example.com 充值。
也不建议提供以下内容:
“网站购买更优惠”等文案;
外部购买按钮;
可点击的网址;
跳转 Safari 的充值入口;
通过二维码或客服消息变相引导购买。
更安全的做法是在 App 内提供购买入口:
余额不足,请购买游戏点数后继续。
按钮应触发 StoreKit 商品页面,而不是打开网站。
如果决定不在 App 内提供购买功能,需要谨慎评估 App 是否还能展示和消费站外购买的游戏余额。即使 App 内没有购买链接,只保留站外充值机制,也不代表一定能通过审核。
其他注意事项
gold、silver 等会员如果用于解锁数字游戏功能,通常也应通过 IAP 销售。
按次扣除的预付余额通常具有 consumable 商品特征。
不应使用网页视图、第三方支付 SDK 或服务器接口,在 App 内完成数字商品支付。
交易完成后应以服务器余额为准,防止客户端修改余额。
必须处理重复回调、退款、撤销、家庭批准和未完成交易。
如果余额具有现金价值,或者可以提现、转让,可能还会涉及金融、虚拟货币或赌博类审核要求。
不同 storefront、地区法规以及 Apple 授予的特定 entitlement 可能存在例外,但不能默认这些例外适用于普通游戏。
因此,当前方案中的”网站充值后在 iPhone 游戏中消费”和”余额不足时引导用户前往服务器充值”都有明显的审核风险。更稳妥的上架方案是将 iOS 端充值改为 StoreKit IAP,同时保留服务器统一验证、记账和扣款。
备注:内容仅供参考。