# DevEco CLI实战:鸿蒙App「启芽学伴」从0开发到正式上架

0 阅读14分钟

「启芽学伴」是我做的一款面向 3-8 岁儿童的鸿蒙启蒙教育 App(BundleName:com.xiaobingkj.workbench),已经在应用市场上架,当前版本 1.1.5(versionCode 1000015),软著也登记完了(源程序量 31476 行)。这篇文章复盘它从搭工程到提审的完整过程,所有代码片段、配置和审核意见都出自仓库 sample_in_harmonyos_workbench,可以逐行对照。

工作台课程积分
在这里插入图片描述在这里插入图片描述在这里插入图片描述

一、项目背景与技术选型

1.1 为什么不从空白工程起步

「启芽学伴」的底座是华为开源的「HMOS代码工坊」(sample_in_harmonyos,Apache 2.0 协议)。当时考虑过从空白工程搭起,最后放弃了,原因很直接:

考量维度官方样板带来的直接收益
架构成熟度common / features / products 三层模块化架构、Stage 模型、动态路由(routerMap)都是现成的
合规安全Apache 2.0 允许商用闭源二开,保留版权声明和 LICENSE 文件即可
上架即战力华为账号接入、服务卡片、锁屏卡片、3D 互动卡片、意图框架这些"审核加分项"已经被打磨过

这些基建自己从 0 写一遍,估计要多花一个月;而业务差异部分(儿童课程、语音评测、积分体系、内购)全部自研,约 3.1 万行源程序,正好构成软著申报主体。

1.2 技术栈

项目选型说明
系统基线compatibleSdkVersion 6.0.1(21) / targetSdkVersion 6.1.0(23)兼容旧机型、吃到新 API
开发范式ArkTS + ArkUI 声明式,Stage 模型全量 @Component + @State/@StorageProp 状态驱动
语音能力@kit.CoreSpeechKit(textToSpeech + speechRecognizer)TTS 示范朗读 + ASR 跟读评测
数据持久化Preferences(JSON 文档)+ RDB学习记录与积分流水本地化,预留云端同步映射
内购@kit.IAPKit 非消耗型商品永久会员一次买断
UI 体系@kit.UIDesignKit(HDS 组件 + 新材质)hdsMaterial.IMMERSIVE 渐变模糊、响应式断点
构建Hvigor(DevEco Studio 内置 CLI:hvigorw多 product 多环境命令行构建
应用商店appgallery.huawei.com/app/detail?…AppGalley

二、工程架构:三层模块 + 多产品维度

2.1 目录结构(业务定制后)

├── AppScope/app.json5               # 应用级配置:bundleName / versionCode / 图标
├── build-profile.json5              # 签名 + 7 个 product 环境 + 模块清单
├── common/                          # ① 公共层
│   └── src/main/ets/
│       ├── storagemanager/          #   LearningDataRepository(学习数据仓储,512 行)
│       ├── model/LearningData.ets   #   LearningRecord / PointsTransaction 等实体
│       ├── routermanager/           #   动态路由
│       └── component / util / view  #   公共组件、工具、页面
├── features/                        # ② 业务层
│   ├── initialization/              #   首启引导(孩子信息 → 科目 → 每日目标)
│   ├── devpractices/                #   课程中心:四科打卡 + 语音评测(重点)
│   │   ├── component/DailyCourseCenter.ets   # 1088 行
│   │   ├── model/DailyCourseData.ets         # 665 行:课程内容生成
│   │   └── service/CoreSpeechService.ets     # TTS/ASR 引擎封装
│   │   └── service/SpeechAssessment.ets      # 发音评测算法
│   ├── pointsplayground/            #   积分乐园:等级/兑换
│   ├── exploration/                 #   学习报告(周/月图表)
│   ├── mine/                        #   我的 + 会员 Paywall + IAP 服务
│   ├── componentlibrary/            #   组件库学习(继承自样板)
│   └── commonbusiness / abilitycommon / widgetcommon
└── products/                        # ③ 产品层(设备入口)
    └── phone/                       #   手机/平板 entry(上架目标)
        ├── src/main/ets/entryability/EntryAbility.ets
        ├── src/main/ets/page/       #   MainPage / SplashPage
        ├── src/main/ets/widget/     #   服务卡片、锁屏卡片、3D 互动卡片
        └── src/main/module.json5    #   权限、client_id、隐私声明链接

有个上架决策值得单独说:products/pcproducts/tvproducts/wearable 三个产品入口在 build-profile.json5 里被注释裁掉了,首版只发 phone/tablet(deviceTypes: ["phone", "tablet"])。从样板转商业 App,先做减法——审核面小一圈,适配也省一堆。

2.2 多环境 product:CLI 构建的地基

build-profile.json5 定义了 7 个 product:default(设备调试签名)、devuatuat_mirroricslbetaprod(正式发布签名)。所有 product 共享同一套源码,靠签名配置与构建模式区分:

{
  app: {
    signingConfigs: [
      { name: 'default', type: 'HarmonyOS',
        material: {
          certpath: './dis.cer',        // AGC 发布证书
          keyAlias: 'key',
          profile: './disRelease.p7b',  // Release Profile
          signAlg: 'SHA256withECDSA',
          storeFile: './dis.p12',       // 密钥库(密码切勿提交明文)
        } },
      { name: 'device', /* DevEco 自动签名,用于真机调试 */ },
    ],
    products: [
      { name: 'prod', signingConfig: 'default',
        compatibleSdkVersion: '6.0.1(21)', targetSdkVersion: '6.1.0(23)',
        runtimeOS: 'HarmonyOS',
        buildOption: { strictMode: { useNormalizedOHMUrl: true } } },
      // dev / uat / beta ... 结构相同
    ],
    buildModeSet: [{ name: 'debug' }, { name: 'release' }],
  },
}

踩过一个坑:useNormalizedOHMUrl: true 要在所有模块统一开启(样板已经保证),否则多 HAP/多模块的路由解析会在 release 包上出问题。

2.3 Hvigor 内存调优(Apple Silicon 大工程必做)

在 M 系列 Mac 上编译这种 10 模块级工程,daemon 进程容易内存溢出。hvigor/hvigor-config.json5 的实际调优参数:

{
  "properties": {
    "hvigor.pool.cache.capacity": 0,   // 关闭内存缓存(默认 4)
    "hvigor.pool.maxSize": 5,          // 限制并行池
    "ohos.arkCompile.maxSize": 3,      // ArkTS 编译并发收敛
    "hvigor.enableMemoryCache": false
  }
}

代价是增量编译变慢,换来的是 prod release 全量构建稳定通过——做 CI 的前提。

三、开发阶段划分(还原自 Git 历史)

仓库 105 个提交把从样板到上架的过程记得清清楚楚,整理成七个阶段:

阶段里程碑提交产出
① 底座接入配置 Update agconnect-services.jsonbundleName 改为自有、AGC 项目绑定、签名体系建立
② 骨架搭建初始化流程 tabbar 工作台页面首启引导、五 Tab 主框架(首页/样例/积分乐园/实践/我的)
③ 核心业务课程中心、积分乐园、学习报告 增加古诗 几大课程四科打卡 + 积分 + 报告
④ 适配打磨平板端 真实数据 登录 去掉阅读、书写断点响应式、真实内容库、业务瘦身
⑤ 打包签名sign icon 引导页图片 1.0.5发布证书配置、图标与启动资产生成
⑥ 审核修复录音跟读识别不准确 举报功能 客服微信两轮应用市场审核驳回的针对性修复
⑦ 商业化与确权内购 agent fixIAP 永久会员上线;软著材料与版本迭代至 1.1.5

四、核心功能实现拆解

4.1 首启引导:InitializationPage(1286 行)

五步向导采集 LearningProfile(孩子昵称、性别、生日、学段、科目、每日目标分钟数),手机/平板两套布局:手机 5 步(姓名→生日→阶段→科目→目标),平板收敛为 2 步双栏。两个细节:

  • COURSE_CATALOG_VERSION = 2:课程目录版本号写进 Profile,以后升级可以据此判断要不要重新生成课程数据;
  • 完成状态落 Preferences(INITIALIZATION_COMPLETED_KEY),EntryAbility 启动时决定走引导还是 Splash→MainPage。

4.2 课程中心:确定性"每日内容"生成

DailyCourseData.ets 负责四科每日内容的确定性生成——同一天多次打开看到同一套内容,且与学习记录联动去重:

科目每日内容生成逻辑
拼音10 组音节四声拼读按日期播种 + 过滤已学 contentId
数学20 道加减法按累计已学题数逐级开放 10→20→…→100 以内
识字15 个生字3000 字小学常用字库,按常用度分批
古诗每日一首小学推荐 75 首库

课程加载时通过 LearningDataRepository.getContentMasteryBefore(subject, dateKey) 取"今天之前"的掌握度数据,把到期复习项优先排进当日课程——这是 LEARNING_DATA_SCHEMA.md 里定义的间隔复习算法:

  • 掌握度 = 完成一次 +20 分、错一次 -10 分,区间 0~100;
  • 复习间隔按掌握度映射为 1 / 3 / 7 / 14 / 30 天;
  • 排序:到期更早、掌握度更低者优先。

4.3 语音评测打卡:本项目最硬的骨头

这直接对应一轮真实审核驳回:"课程中心-拼音/识字/古诗,录音跟读功能识别不准确,导致无法完成打卡。" 修复后的实现分两层。

引擎层 CoreSpeechService(单例),统一封装 TTS 与 ASR:

import { speechRecognizer, textToSpeech } from '@kit.CoreSpeechKit';

// 示范朗读:TTS 0.85 倍速更适合儿童跟读
const createParams: textToSpeech.CreateEngineParams = {
  language: 'zh-CN', person: 0, online: 1,
  extraParams: { 'style': 'interaction-broadcast', 'locate': 'CN' },
};

// 跟读识别:short 模式 + VAD 双端点检测
const startParams: speechRecognizer.StartParams = {
  sessionId: this.activeSessionId,
  audioInfo: { audioType: 'pcm', sampleRate: 16000, sampleBit: 16, soundChannel: 1 },
  extraParams: {
    'recognitionMode': 0,
    'vadBegin': 8000,   // 8s 无声自动开始判定
    'vadEnd': 5000,     // 5s 静音视为说完
    'maxAudioDuration': 60000,
  },
};

两个工程细节:recognitionGeneration 递增令牌 + sessionId 双重校验,杜绝"快速切换课程后旧识别结果窜入新页面"的竞态;每次 start 前先动态申请 ohos.permission.MICROPHONE

算法层 SpeechAssessment——审核驳回后的修复重点。CoreSpeechKit 是文本识别器,不是发音打分引擎,直接字符串比对会因同音字选词差异误判"读错"。修复方案是三级匹配:

// 1) 归一化:剥离语气词/标点,阿拉伯数字转汉字(ASR 常返回数字)
// 2) 去声调拼音化:i18n.Transliterator('Any-Latn;Latin-Ascii') 把汉字转拉丁
//    再抹掉 āáǎà 等声调符号 → 纯无声调拼音串
// 3) Levenshtein 相似度 + 分科阈值
export function isPinyinGroupSpeechMatch(actualText, expectedText): boolean {
  const actualPinyin = toToneIndependentPinyin(actualText);
  const expectedPinyin = toToneIndependentPinyin(expectedText);
  return calculateSpeechSimilarity(actualPinyin, expectedPinyin) >= 0.7; // 拼音 0.7
}
// 古诗:多候选句 + 文本/拼音双通道取最大相似度,阈值 0.78
// 识字:识别文本"包含"目标字,或拼音序列包含即通过

给儿童做跟读评测,"是否包含正确发音"远比"逐字全等"重要。去声调 + 相似度阈值 + 多候选,把打卡通过率从"经常卡死"拉到顺畅,审核也就过了。

4.4 数据层:单仓储 + 版本号驱动的 UI 刷新

LearningDataRepository(common 层单例)以 JSON 文档形式管理两类实体:

// 学习记录:userId + 日期 + 科目 生成稳定主键,天然幂等防重复打卡
const recordId = `course_${document.userId}_${input.dateKey}_${input.subjectCode}`;
// 积分流水:EARN / REDEEM 双向记账,关联学习记录

页面侧的刷新机制我挺喜欢:仓储每次写入后递增 AppStorage 中的 LEARNING_DATA_REVISION,各页面用 @StorageProp + @Watch 订阅:

@StorageProp(StorageKey.LEARNING_DATA_REVISION)
@Watch('reloadPoints') learningDataRevision: number = 0;

于是积分乐园、首页指标、学习报告零耦合联动——任何一科打卡完成,三个页面的统计同步刷新,不用手动传事件。LEARNING_DATA_SCHEMA.md 还预留了华为云 Cloud DB 映射表(两个数组 → 两个 ObjectType,标量字段直接映射),以后接云只换仓储适配层,页面逻辑一行不动。

4.5 积分乐园:等级 + 现金兑换

积分规则:拼音 10 分、识字 15 分、古诗 15 分、数学 20 分;等级每 1000 分一级、Lv10 封顶;兑换档位 500→5 元 / 1000→10 元 / 2000→20 元 / 5000→50 元(兑换需家长确认——儿童教育合规细节)。布局按断点自适应:WIDTH_SM 两列、平板四列。

4.6 会员内购:IAPKit 全流程

免费策略是累计 10 次打卡(FREE_CHECK_IN_LIMIT = 10),到上限后在打卡动作上直接拉起 PaywallView。商品为非消耗型 com.xiaobingkj.workbench.lifetime

// 查询商品(拿到本地货币价格展示)
const products: iap.Product[] = await iap.queryProducts(context, {
  productType: iap.ProductType.NONCONSUMABLE,
  productIds: [LIFETIME_PRODUCT_ID],
});
// 发起购买 → confirmDelivery 确认发货 → 处理 PRODUCT_OWNED / USER_CANCELED
const result: iap.CreatePurchaseResult = await iap.createPurchase(context, {
  productId: LIFETIME_PRODUCT_ID, productType: iap.ProductType.NONCONSUMABLE,
});

权益恢复是内购的隐藏考点:换机/重装后要从 queryPurchases(CURRENT_ENTITLEMENT) 恢复。返回的 purchaseData 是 JWS 封装,PremiumMembershipService.decodeJwsPayload()util.Base64Helper 解出 payload 校验 productId 与撤销标记——这 145 行的服务类值得单独存一份当模板。

五、调试与真机验证

  1. 调试签名:product default 走 DevEco 自动签名(~/.ohos/config/device_* 三件套),真机即插即跑;
  2. 真机联调语音:ASR 依赖真实麦克风与系统服务,模拟器替代不了——"识别不准"的迭代全部在真机完成。这也是审核驳回给的教训:上线前核心流程要在真机完整走一遍;
  3. 日志规范:common 层统一 Logger(debug/info/error 分级 + TAG),排查审核问题时能快速分清是引擎错误还是算法判定;
  4. 多设备验证:平板断点布局(WidthBreakpoint.WIDTH_SM 分支)在折叠屏展开态与平板各过一遍。

六、CLI 签名打包与上架产物

6.1 证书体系(AGC 侧准备)

在 AppGallery Connect 创建应用(绑定 com.xiaobingkj.workbench)后申请三件套:发布证书 dis.cer、密钥库 dis.p12、Release Profile disRelease.p7b(内含权限声明与分发范围),与 build-profile 的 signingConfigs 对应。

6.2 命令行构建

DevEco Studio 的构建内核就是 Hvigor,hvigorw 即其 CLI 入口(macOS 位于 /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/)。本工程实际使用形态:

# 依赖同步(等价于 IDE 的 Sync)
hvigorw --sync

# 真机调试包(debug + 设备签名 product)
hvigorw assembleHap --mode module -p product=default -p buildMode=debug

# 应用市场上架包(release + 发布签名 prod)
hvigorw assembleApp --mode module -p product=prod -p buildMode=release

# hdc 安装验证(上架包最后一步本机验证)
hdc list targets
hdc install build/outputs/prod/sample_in_harmonyos_workbench-prod-signed.app

产物落在 build/outputs/prod/

文件用途
sample_in_harmonyos_workbench-prod-signed.app提交 AGC 的上架包
...-unsigned.app未签原始包(备查)
app-symbol.zip + 各模块 sourceMaps.map崩溃符号表,务必随版本留存归档

配合 7 个 product,同一套源码命令行就能产出 dev/uat/beta/prod 四类包体,后续接 CI/CD 不用再改工程。

七、应用市场审核:两次驳回实录与修复

上架资料(app_store_assets/app-store-1920x1280/ 的宣传图、截图、隐私声明 URL)之外,真正有价值的是两条驳回意见及修复方式——儿童教育类 App 的合规红线清单:

驳回一:语音评测卡死(功能性缺陷)

原始意见:课程中心-拼音/识字/古诗,录音跟读功能识别不准确,导致无法完成打卡。

修复即 4.3 节的算法三级匹配(归一化→去声调拼音→相似度阈值),并针对古诗增加多候选句。功能性审核是真机走完整业务流程的,"技术上能跑"和"孩子能顺利打卡"是两回事。

驳回二:未成年人保护合规(法规性缺陷)

原始意见:您的应用为面向未成年人的教育类应用,但应用内未提供举报功能,不符合相关法律法规要求。

修复方式很轻:MinePageVM 的"我的"页加一个举报入口,通过 Want 拉起系统浏览器跳腾讯兔小巢反馈页:

const REPORT_URL: string = 'https://txc.qq.com/products/801545';
const want: Want = {
  action: 'ohos.want.action.viewData',
  entities: ['entity.system.browsable'],
  uri: REPORT_URL,
};
ProcessUtil.startAbility(context, want);

同时补了客服微信入口(教育类应用审核要求可触达的人工客服渠道)。

主动合规:权限做减法

products/phone/src/main/module.json5INTERNETGET_NETWORK_INFO 权限注释移除了(App 离线可用),只留 MICROPHONE(跟读必需)、VIBRATE(答对震动反馈)、GYROSCOPE;并配置 appgallery_privacy_hosted = 1 与隐私声明 https 链接、华为账号 client_id。权限越少,隐私审核越顺——所有保留权限都要配 reason 字符串与 usedScene(何时、哪个 Ability 使用)。

八、软件著作权:上架后的确权

工程里的 软件著作权申请资料/ 完整保留了申报流水线:

材料规格要点
申请表信息版本 V1.1.1,开发完成/首次发表 2026-08-04开发目的、主要功能、技术特点三段口径与代码事实一致
源程序材料前 30 页 + 后 30 页(每页 50 行)从 28 个文件抽取 8774 行,DailyCourseCenter.etsLearningDataRepository.ets 等业务文件优先,证明原创性
操作手册全页面截图 + 操作流程与真实页面一一对应,不写抽象功能列表
软件说明原创、单独开发、原始取得二开工程申报重点:突出自研业务代码占比与原创功能

九、几条经验

  1. 样板二开是个人开发者的最优解:用 Apache 2.0 官方样板承接架构、账号、卡片这些重基建,自研收敛在业务层;上架时果断裁掉非目标产品(pc/tv/wearable 注释掉)。
  2. product 多环境从第一天就建好default/dev/uat/prod 的签名分离让 CLI 构建天然支持环境矩阵,后期零重构。
  3. ASR 不等于发音评测:文本识别做跟读判定要加"归一化 + 拼音化 + 相似度阈值"三层兜底,否则用户会卡死在最后一步(也是审核驳回高发区)。
  4. 数据层单仓储 + revision 版本号AppStorage 全局版本号 + @Watch 是鸿蒙多页面数据联动成本最低的做法,还为云端同步预留了干净边界。
  5. 把审核当需求排期:未成年人举报入口、客服触达、权限最小化、隐私声明、真机闭环测试,这些当 P0 需求做,而不是驳回后的救火项。
  6. CLI 化构建先行hvigorw assembleApp -p product=prod -p buildMode=release + 符号表归档,每次提审可复现、可回溯。
  7. 确权与上架同步:软著材料(60 页代码 + 操作手册)上架后就能产出,Git 提交历史本身就是开发过程的证据链。
  8. 版本号纪律versionCode 按位累进(1000015),versionName 与软著版本号、AGC 版本记录三处对齐,出问题能快速回滚定位。

工程基线:HarmonyOS 6.0.1 Release 及以上 / DevEco Studio 6.1.0 Release 及以上。主要源文件行数:DailyCourseCenter 1088、InitializationPage 1286、LearningDataRepository 512、DailyCourseData 665、CoreSpeechService 274、SpeechAssessment 153、PremiumMembershipService 145、PaywallView 594。