HarmonyOS开发:游戏发布——应用市场上架与内购集成
HarmonyOS开发:游戏发布——应用市场上架与内购集成
📌 核心要点:游戏做完了只是走了一半,打包签名、应用市场审核、IAP内购集成、防沉迷实名认证,每一步都有坑,审核被拒一次就多等一周,内购出bug就是钱的事。
背景与动机
你花了三个月做了一款游戏,功能完整、性能流畅、体验丝滑。然后呢?
然后你发现——发布比开发还难。
打包要签名,签名要证书,证书要申请。应用市场有审核规范,图标尺寸不对、权限声明不全、隐私政策缺失,直接打回。内购集成涉及支付,一个回调没处理好,玩家付了钱没收到道具,直接投诉。防沉迷和实名认证是硬性要求,不做就上不了架。
这些事,开发的时候没人告诉你,但发布的时候一个都绕不过去。
这篇就帮你把发布流程从头到尾捋一遍,让你少走弯路。
核心原理
游戏发布全流程
从开发完成到玩家能下载,中间要经过这些步骤:
graph TB
A[开发完成] --> B[打包签名]
B --> C[应用市场提交]
C --> D[审核]
D -->|通过| E[上架发布]
D -->|被拒| F[修改后重新提交]
F --> C
subgraph 打包签名
B1[配置build-profile]
B2[申请签名证书]
B3[配置权限声明]
B4[编译打包HAP]
end
subgraph 审核要点
D1[功能完整性]
D2[隐私合规]
D3[内容审核]
D4[性能达标]
D5[防沉迷]
end
subgraph 上架后
E1[版本更新]
E2[运营数据]
E3[用户反馈]
E4[内购结算]
end
B -.-> B1
D -.-> D1
E -.-> E1
classDef mainStyle fill:#27AE60,stroke:#229954,color:#fff,font-weight:bold
classDef stepStyle fill:#3498DB,stroke:#2980B9,color:#fff
classDef detailStyle fill:#F39C12,stroke:#E67E22,color:#fff
classDef failStyle fill:#E74C3C,stroke:#C0392B,color:#fff
class A,B,C,D,E mainStyle
class F failStyle
class B1,B2,B3,B4 stepStyle
class D1,D2,D3,D4,D5 detailStyle
class E1,E2,E3,E4 detailStyle
IAP内购架构
内购(In-App Purchase)是游戏变现的核心方式。鸿蒙的IAP流程:
客户端请求商品 → IAP服务返回商品信息 → 用户确认支付 → 支付完成 →
服务端验证 → 发放道具 → 客户端确认消耗
关键点:服务端验证是必须的。客户端的支付结果可以被篡改,只有服务端向华为IAP服务器验证后才能确认支付成功。
防沉迷与实名认证
国内游戏上架必须接入防沉迷系统。核心要求:
| 要求 | 说明 |
|---|---|
| 实名认证 | 所有玩家必须实名认证 |
| 未成年人时间限制 | 工作日1小时/天,节假日2小时/天 |
| 未成年人时段限制 | 20:00-21:00之外禁止游戏 |
| 未成年人充值限制 | 8岁以下禁止充值,8-16岁单次≤50元,16-18岁单次≤100元 |
| 游客模式 | 未实名用户只能体验1小时,且不能充值 |
代码实战
基础用法:打包配置与签名
打包前先配好签名和权限。
// build-profile.json5 - 打包配置
{
"app": {
"signingConfigs": [
{
"name": "default",
"type": "HarmonyOS",
"material": {
"certpath": "certs/game_release.cer", // 发布证书
"storeFile": "certs/game_release.p12", // 密钥库文件
"storePassword": "your_store_password", // 密钥库密码
"keyAlias": "game_release_key", // 密钥别名
"keyPassword": "your_key_password", // 密钥密码
"profile": "certs/game_release.p7b", // 发布Profile
"signAlg": "SHA256withECDSA", // 签名算法
"storeFileHash": "your_hash" // 密钥库哈希
}
}
],
"products": [
{
"name": "default",
"signingConfig": "default",
"compatibleSdkVersion": "5.0.0(12)",
"runtimeOS": "HarmonyOS"
}
]
},
"modules": [
{
"name": "entry",
"srcPath": "./entry",
"targets": [
{
"name": "default",
"applyToProducts": ["default"]
}
]
}
]
}
// module.json5 - 模块配置与权限声明
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": ["phone", "tablet"],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:ability_desc",
"icon": "$media:app_icon",
"label": "$string:ability_label",
"startWindowIcon": "$media:app_icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": ["entity.system.home"],
"actions": ["action.system.home"]
}
]
}
],
// 权限声明
"requestPermissions": [
{
"name": "ohos.permission.INTERNET",
"reason": "$string:internet_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
},
{
"name": "ohos.permission.GYROSCOPE",
"reason": "$string:gyro_permission_reason",
"usedScene": {
"abilities": ["EntryAbility"],
"when": "inuse"
}
}
]
}
}
进阶用法:IAP内购集成
内购是游戏变现的核心,必须稳。
// IAPManager.ets - 内购管理器
import { iap } from '@kit.IAPKit'
import { BusinessError } from '@kit.BasicServicesKit'
// 商品类型
enum ProductType {
CONSUMABLE = 'consumable', // 消耗型(金币、钻石)
NON_CONSUMABLE = 'nonConsumable', // 非消耗型(去广告、解锁角色)
SUBSCRIPTION = 'subscription' // 订阅型(月卡、通行证)
}
// 内购商品定义
interface IAPProduct {
productId: string // 商品ID(在AppGallery Connect配置)
productType: ProductType
name: string
description: string
price: string
}
// 内购管理器
class IAPManager {
private products: Map<string, IAPProduct> = new Map()
private pendingPurchases: Map<string, iap.PurchaseInfo> = new Map()
// 初始化商品列表
initProducts(productList: IAPProduct[]): void {
for (const p of productList) {
this.products.set(p.productId, p)
}
}
// 查询商品信息(从IAP服务获取最新价格)
async queryProducts(productIds: string[]): Promise<IAPProduct[]> {
try {
const result = await iap.queryProducts({
productIds: productIds,
type: iap.ProductType.CONSUMABLE
})
const products: IAPProduct[] = []
for (const item of result.productInfoList) {
const localProduct = this.products.get(item.productId)
if (localProduct) {
localProduct.price = item.price // 使用服务器返回的价格
products.push(localProduct)
}
}
return products
} catch (e) {
const err = e as BusinessError
console.error(`查询商品失败: ${err.code} - ${err.message}`)
return []
}
}
// 发起购买
async purchase(productId: string): Promise<boolean> {
const product = this.products.get(productId)
if (!product) {
console.error(`商品不存在: ${productId}`)
return false
}
try {
// 创建购买请求
const purchaseResult = await iap.createPurchase({
productId: productId,
developerPayload: JSON.stringify({
timestamp: Date.now(),
productId: productId
})
})
// 购买成功,保存待确认的购买信息
for (const purchase of purchaseResult.purchaseInfoList) {
this.pendingPurchases.set(purchase.purchaseToken, purchase)
}
// 服务端验证(关键步骤!)
const verified = await this.verifyPurchaseOnServer(purchaseResult)
if (verified) {
// 发放道具
await this.deliverProduct(productId)
// 确认消耗(消耗型商品)
if (product.productType === ProductType.CONSUMABLE) {
await this.consumePurchase(purchaseResult)
}
return true
} else {
console.error('服务端验证失败')
return false
}
} catch (e) {
const err = e as BusinessError
// 用户取消购买
if (err.code === iap.IAPErrorCode.ORDER_PRODUCT_CANCEL) {
console.info('用户取消购买')
return false
}
console.error(`购买失败: ${err.code} - ${err.message}`)
return false
}
}
// 服务端验证购买
private async verifyPurchaseOnServer(purchaseResult: iap.PurchaseResult): Promise<boolean> {
// 这里应该调用你自己的服务端API
// 服务端再调用华为IAP服务端API验证
try {
const response = await fetch('https://your-server.com/api/verify-purchase', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
purchaseToken: purchaseResult.purchaseInfoList[0]?.purchaseToken,
productId: purchaseResult.purchaseInfoList[0]?.productId,
signature: purchaseResult.purchaseInfoList[0]?.signature
})
})
const data = await response.json()
return data.verified === true
} catch (e) {
console.error('服务端验证请求失败: ' + e)
return false
}
}
// 确认消耗型商品
private async consumePurchase(purchaseResult: iap.PurchaseResult): Promise<void> {
for (const purchase of purchaseResult.purchaseInfoList) {
try {
await iap.consumePurchase({
purchaseToken: purchase.purchaseToken,
developerPayload: ''
})
this.pendingPurchases.delete(purchase.purchaseToken)
} catch (e) {
console.error('确认消耗失败: ' + e)
}
}
}
// 发放商品道具
private async deliverProduct(productId: string): Promise<void> {
// 根据productId给玩家发放对应道具
// 这部分逻辑和你的游戏数据系统对接
console.info(`发放商品: ${productId}`)
}
// 恢复购买(非消耗型商品)
async restorePurchases(): Promise<string[]> {
try {
const result = await iap.queryPurchases({
type: iap.ProductType.CONSUMABLE
})
const restoredIds: string[] = []
for (const purchase of result.purchaseInfoList) {
// 验证并恢复
const verified = await this.verifyPurchaseOnServer({
purchaseInfoList: [purchase]
} as iap.PurchaseResult)
if (verified) {
restoredIds.push(purchase.productId)
await this.deliverProduct(purchase.productId)
}
}
return restoredIds
} catch (e) {
console.error('恢复购买失败: ' + e)
return []
}
}
// 处理未完成的购买(应用启动时调用)
async handlePendingPurchases(): Promise<void> {
try {
const result = await iap.queryPurchases({
type: iap.ProductType.CONSUMABLE
})
for (const purchase of result.purchaseInfoList) {
if (purchase.purchaseState === iap.PurchaseState.PURCHASED) {
// 有未确认的购买,重新验证和发放
const verified = await this.verifyPurchaseOnServer({
purchaseInfoList: [purchase]
} as iap.PurchaseResult)
if (verified) {
await this.deliverProduct(purchase.productId)
const product = this.products.get(purchase.productId)
if (product?.productType === ProductType.CONSUMABLE) {
await iap.consumePurchase({
purchaseToken: purchase.purchaseToken,
developerPayload: ''
})
}
}
}
}
} catch (e) {
console.error('处理未完成购买失败: ' + e)
}
}
}
完整示例:防沉迷与实名认证
// AntiAddiction.ets - 防沉迷与实名认证
import { identity } from '@kit.BasicServicesKit'
// 玩家身份状态
enum PlayerIdentityStatus {
UNKNOWN, // 未认证
ADULT, // 成年
MINOR, // 未成年人
GUEST // 游客
}
// 游戏时段限制
interface TimeRestriction {
maxMinutesPerDay: number // 每天最大游戏分钟数
allowedHours: number[] // 允许游戏的小时段(如[20,21]表示20:00-21:59)
maxRechargePerMonth: number // 每月最大充值金额(分)
}
// 防沉迷管理器
class AntiAddictionManager {
private status: PlayerIdentityStatus = PlayerIdentityStatus.UNKNOWN
private todayPlayMinutes: number = 0
private todayDate: string = ''
private lastPlayCheckTime: number = 0
// 不同身份的时段限制
private restrictions: Map<PlayerIdentityStatus, TimeRestriction> = new Map([
[PlayerIdentityStatus.ADULT, {
maxMinutesPerDay: 9999,
allowedHours: Array.from({ length: 24 }, (_, i) => i), // 全天
maxRechargePerMonth: 999900
}],
[PlayerIdentityStatus.MINOR, {
maxMinutesPerDay: 60, // 工作日1小时
allowedHours: [20, 21], // 仅20:00-21:59
maxRechargePerMonth: 10000 // 100元
}],
[PlayerIdentityStatus.GUEST, {
maxMinutesPerDay: 60, // 游客1小时体验
allowedHours: Array.from({ length: 24 }, (_, i) => i),
maxRechargePerMonth: 0 // 不能充值
}]
])
// 实名认证
async authenticate(realName: string, idNumber: string): Promise<boolean> {
try {
// 调用华为实名认证服务
const result = await identity.realNameAuthenticate({
realName: realName,
identityNumber: idNumber
})
if (result.authResult === identity.AuthResult.SUCCESS) {
// 判断是否成年
const age = this.calculateAge(idNumber)
this.status = age >= 18 ? PlayerIdentityStatus.ADULT : PlayerIdentityStatus.MINOR
return true
}
return false
} catch (e) {
console.error('实名认证失败: ' + e)
return false
}
}
// 设置游客模式
setGuestMode(): void {
this.status = PlayerIdentityStatus.GUEST
}
// 检查是否可以游戏
canPlay(): { allowed: boolean; reason: string; remainingMinutes: number } {
if (this.status === PlayerIdentityStatus.UNKNOWN) {
return { allowed: false, reason: '请先完成实名认证', remainingMinutes: 0 }
}
const restriction = this.restrictions.get(this.status)!
const now = new Date()
const currentHour = now.getHours()
// 检查时段
if (!restriction.allowedHours.includes(currentHour)) {
return {
allowed: false,
reason: '当前时段不允许游戏',
remainingMinutes: 0
}
}
// 检查时长
this.updatePlayTime()
const remaining = restriction.maxMinutesPerDay - this.todayPlayMinutes
if (remaining <= 0) {
return {
allowed: false,
reason: '今日游戏时长已用完',
remainingMinutes: 0
}
}
// 即将到时提醒
if (remaining <= 15) {
return {
allowed: true,
reason: `今日剩余${remaining}分钟`,
remainingMinutes: remaining
}
}
return { allowed: true, reason: '', remainingMinutes: remaining }
}
// 检查是否可以充值
canRecharge(amountCents: number): { allowed: boolean; reason: string } {
if (this.status === PlayerIdentityStatus.GUEST) {
return { allowed: false, reason: '游客模式不能充值' }
}
if (this.status === PlayerIdentityStatus.UNKNOWN) {
return { allowed: false, reason: '请先完成实名认证' }
}
const restriction = this.restrictions.get(this.status)!
if (amountCents > restriction.maxRechargePerMonth) {
return { allowed: false, reason: '超出充值限额' }
}
return { allowed: true, reason: '' }
}
// 更新游戏时长
private updatePlayTime(): void {
const today = new Date().toISOString().split('T')[0]
if (this.todayDate !== today) {
this.todayDate = today
this.todayPlayMinutes = 0
}
const now = Date.now()
if (this.lastPlayCheckTime > 0) {
const elapsed = (now - this.lastPlayCheckTime) / 60000
this.todayPlayMinutes += elapsed
}
this.lastPlayCheckTime = now
}
// 根据身份证号计算年龄
private calculateAge(idNumber: string): number {
// 简化实现:从身份证号提取出生日期
const birthYear = parseInt(idNumber.substring(6, 10))
const birthMonth = parseInt(idNumber.substring(10, 12))
const birthDay = parseInt(idNumber.substring(12, 14))
const now = new Date()
let age = now.getFullYear() - birthYear
if (now.getMonth() + 1 < birthMonth ||
(now.getMonth() + 1 === birthMonth && now.getDate() < birthDay)) {
age--
}
return age
}
// 获取身份状态
getStatus(): PlayerIdentityStatus {
return this.status
}
// 获取今日剩余游戏时间(分钟)
getRemainingMinutes(): number {
const restriction = this.restrictions.get(this.status)
if (!restriction) return 0
this.updatePlayTime()
return Math.max(0, restriction.maxMinutesPerDay - this.todayPlayMinutes)
}
}
踩坑与注意事项
坑1:签名证书丢失
签名证书丢了就没法更新应用了。你必须把证书文件和密码安全备份,丢了就完了——只能重新发布一个新包名的应用,老用户全丢。
坑2:IAP回调丢失
玩家付了钱但网络断了,你的服务端没收到支付回调。解决方案:应用每次启动时调用queryPurchases检查未完成的购买,重新验证和发放。
坑3:审核被拒的常见原因
- 隐私政策不完整(必须包含数据收集、使用、存储说明)
- 权限申请没有合理理由(每个权限都要解释为什么需要)
- 游戏内容违规(暴力、色情、政治敏感)
- 功能不完整(有按钮但点击没反应)
- 崩溃或ANR(审核时崩溃直接拒)
坑4:内购价格显示
商品价格必须从IAP服务查询,不能硬编码。不同地区价格不同(汇率、税率),硬编码价格会导致显示错误,审核会被拒。
坑5:防沉迷的时间计算
防沉迷的"今日游戏时长"要在服务端计算,不能只靠客户端。客户端可以被修改,服务端才是权威。但服务端计算需要频繁上报,要注意性能。
HarmonyOS 6适配说明
HarmonyOS 6在发布和支付方面有几个更新:
- IAP Kit增强:新增了订阅管理API,玩家可以在游戏内直接管理订阅状态(查看、取消、续费),不需要跳转到系统设置。
// HarmonyOS 6 订阅管理
import { iap } from '@kit.IAPKit'
// 查询订阅状态
const subscriptions = await iap.querySubscriptions({
type: iap.ProductType.SUBSCRIPTION
})
for (const sub of subscriptions.subscriptionInfoList) {
console.info(`订阅: ${sub.productId}, 状态: ${sub.subscriptionState}`)
}
-
应用市场审核加速:HarmonyOS 6的应用市场审核流程优化,游戏类应用审核时间从7天缩短到3天。但首次提交仍需完整审核。
-
多渠道打包:DevEco Studio支持一键多渠道打包,不同渠道可以配置不同的签名、资源、IAP商品ID,不需要手动改代码。
-
隐私合规检测工具:DevEco Studio内置了隐私合规检测,打包前自动扫描权限使用、数据收集等合规问题,提前发现审核风险。
总结
游戏发布是开发的最后一公里,但往往是最磨人的一公里。打包签名要细心,应用市场审核要耐心,内购集成要严谨,防沉迷要合规。
核心原则:服务端验证是底线。所有涉及钱和身份的操作,都不能只信客户端。IAP支付要服务端验证,防沉迷时长要服务端计算,实名认证要走官方通道。
审核被拒不可怕,可怕的是不知道为什么被拒。仔细看审核意见,逐条修改,重新提交。每次被拒都是一次学习机会。
| 评估维度 | 学习难度 | 使用频率 | 重要程度 |
|---|---|---|---|
| 打包签名 | ★★★☆☆ | ★★☆☆☆ | ★★★★★ |
| 应用市场规范 | ★★☆☆☆ | ★★★☆☆ | ★★★★★ |
| IAP内购集成 | ★★★★☆ | ★★★★★ | ★★★★★ |
| 服务端验证 | ★★★★☆ | ★★★★★ | ★★★★★ |
| 防沉迷接入 | ★★★☆☆ | ★★★★☆ | ★★★★★ |
| 隐私合规 | ★★☆☆☆ | ★★★☆☆ | ★★★★★ |
下一篇是整个游戏系列的收官之作——完整2D游戏开发实战。把前面学的所有东西串起来,从零到一做出一个能玩的游戏。
- 点赞
- 收藏
- 关注作者
评论(0)