HarmonyOS开发:游戏发布——应用市场上架与内购集成

举报
Jack20 发表于 2026/06/28 21:02:07 2026/06/28
【摘要】 HarmonyOS开发:游戏发布——应用市场上架与内购集成📌 核心要点:游戏做完了只是走了一半,打包签名、应用市场审核、IAP内购集成、防沉迷实名认证,每一步都有坑,审核被拒一次就多等一周,内购出bug就是钱的事。 背景与动机你花了三个月做了一款游戏,功能完整、性能流畅、体验丝滑。然后呢?然后你发现——发布比开发还难。打包要签名,签名要证书,证书要申请。应用市场有审核规范,图标尺寸不对、...

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在发布和支付方面有几个更新:

  1. 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}`)
}
  1. 应用市场审核加速:HarmonyOS 6的应用市场审核流程优化,游戏类应用审核时间从7天缩短到3天。但首次提交仍需完整审核。

  2. 多渠道打包:DevEco Studio支持一键多渠道打包,不同渠道可以配置不同的签名、资源、IAP商品ID,不需要手动改代码。

  3. 隐私合规检测工具:DevEco Studio内置了隐私合规检测,打包前自动扫描权限使用、数据收集等合规问题,提前发现审核风险。

总结

游戏发布是开发的最后一公里,但往往是最磨人的一公里。打包签名要细心,应用市场审核要耐心,内购集成要严谨,防沉迷要合规。

核心原则:服务端验证是底线。所有涉及钱和身份的操作,都不能只信客户端。IAP支付要服务端验证,防沉迷时长要服务端计算,实名认证要走官方通道。

审核被拒不可怕,可怕的是不知道为什么被拒。仔细看审核意见,逐条修改,重新提交。每次被拒都是一次学习机会。

评估维度 学习难度 使用频率 重要程度
打包签名 ★★★☆☆ ★★☆☆☆ ★★★★★
应用市场规范 ★★☆☆☆ ★★★☆☆ ★★★★★
IAP内购集成 ★★★★☆ ★★★★★ ★★★★★
服务端验证 ★★★★☆ ★★★★★ ★★★★★
防沉迷接入 ★★★☆☆ ★★★★☆ ★★★★★
隐私合规 ★★☆☆☆ ★★★☆☆ ★★★★★

下一篇是整个游戏系列的收官之作——完整2D游戏开发实战。把前面学的所有东西串起来,从零到一做出一个能玩的游戏。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。