「启芽学伴」是我做的一款面向 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/pc、products/tv、products/wearable 三个产品入口在 build-profile.json5 里被注释裁掉了,首版只发 phone/tablet(deviceTypes: ["phone", "tablet"])。从样板转商业 App,先做减法——审核面小一圈,适配也省一堆。
2.2 多环境 product:CLI 构建的地基
build-profile.json5 定义了 7 个 product:default(设备调试签名)、dev、uat、uat_mirror、icsl、beta、prod(正式发布签名)。所有 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.json | bundleName 改为自有、AGC 项目绑定、签名体系建立 |
| ② 骨架搭建 | 初始化流程 tabbar 工作台页面 | 首启引导、五 Tab 主框架(首页/样例/积分乐园/实践/我的) |
| ③ 核心业务 | 课程中心、积分乐园、学习报告 增加古诗 几大课程 | 四科打卡 + 积分 + 报告 |
| ④ 适配打磨 | 平板端 真实数据 登录 去掉阅读、书写 | 断点响应式、真实内容库、业务瘦身 |
| ⑤ 打包签名 | sign icon 引导页图片 1.0.5 | 发布证书配置、图标与启动资产生成 |
| ⑥ 审核修复 | 录音跟读识别不准确 举报功能 客服微信 | 两轮应用市场审核驳回的针对性修复 |
| ⑦ 商业化与确权 | 内购 agent fix | IAP 永久会员上线;软著材料与版本迭代至 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 行的服务类值得单独存一份当模板。
五、调试与真机验证
- 调试签名:product
default走 DevEco 自动签名(~/.ohos/config/device_*三件套),真机即插即跑; - 真机联调语音:ASR 依赖真实麦克风与系统服务,模拟器替代不了——"识别不准"的迭代全部在真机完成。这也是审核驳回给的教训:上线前核心流程要在真机完整走一遍;
- 日志规范:common 层统一
Logger(debug/info/error 分级 + TAG),排查审核问题时能快速分清是引擎错误还是算法判定; - 多设备验证:平板断点布局(
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.json5 把 INTERNET、GET_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.ets、LearningDataRepository.ets 等业务文件优先,证明原创性 |
| 操作手册 | 全页面截图 + 操作流程 | 与真实页面一一对应,不写抽象功能列表 |
| 软件说明 | 原创、单独开发、原始取得 | 二开工程申报重点:突出自研业务代码占比与原创功能 |
九、几条经验
- 样板二开是个人开发者的最优解:用 Apache 2.0 官方样板承接架构、账号、卡片这些重基建,自研收敛在业务层;上架时果断裁掉非目标产品(pc/tv/wearable 注释掉)。
- product 多环境从第一天就建好:
default/dev/uat/prod的签名分离让 CLI 构建天然支持环境矩阵,后期零重构。 - ASR 不等于发音评测:文本识别做跟读判定要加"归一化 + 拼音化 + 相似度阈值"三层兜底,否则用户会卡死在最后一步(也是审核驳回高发区)。
- 数据层单仓储 + revision 版本号:
AppStorage全局版本号 +@Watch是鸿蒙多页面数据联动成本最低的做法,还为云端同步预留了干净边界。 - 把审核当需求排期:未成年人举报入口、客服触达、权限最小化、隐私声明、真机闭环测试,这些当 P0 需求做,而不是驳回后的救火项。
- CLI 化构建先行:
hvigorw assembleApp -p product=prod -p buildMode=release+ 符号表归档,每次提审可复现、可回溯。 - 确权与上架同步:软著材料(60 页代码 + 操作手册)上架后就能产出,Git 提交历史本身就是开发过程的证据链。
- 版本号纪律:
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。