# DevEco CLI实战:鸿蒙App「至客」从0开发到正式上架
DevEco CLI实战:鸿蒙App「至客」从0开发到正式上架
本文基于真实工程
sample_in_harmonyos_zhike的实际代码撰写。文中所有目录结构、配置片段、命令行与实现细节均出自该项目,可直接对照源码验证。适合想用命令行工具链完整走通「HarmonyOS 应用开发 → 调试 → 打包 → 上架」全流程的开发者阅读。
| 工作台 | 客户 | 分析 |
|---|---|---|
![]() | ![]() | ![]() |
0. 项目背景:至客是什么
「至客」(全称:至客客户订单管理系统,bundleName:com.xiaobingkj.zhike)是一款运行在 HarmonyOS 手机/平板上的轻量级客户、产品与订单管理工具,面向个体经营者与小微商户:
- 客户管理:列表 + 模糊搜索,新增/编辑/删除(删除二次确认);
- 产品管理:商品目录,记录销售价与采购价;
- 订单管理:选客户 + 选产品生成订单,免费版限 3 条,Premium 会员解除限制;
- 订单到期提醒:写入系统日历,到期当天 09:00 提醒,提前量可选 1/3/7 天;
- 至客 Premium:基于华为应用内购买(IAP)的一次性永久会员;
- 服务卡片:桌面卡片展示「最近到期客户 + 累计毛利」。
工程的关键决策是:不重复造轮子,基于华为官方开源的「HMOS代码工坊」(Apache-2.0 协议,GitCode 仓库 HarmonyOS_Samples/sample_in_harmonyos)的多端工程骨架做二次开发,把官方示例中已经打磨好的「一多三层架构、路由封装、MVVM 基类、卡片框架、账号体系」直接复用,把精力集中在业务实现与上架合规上。
开发环境与版本基线(来自软著申请资料与本机实际环境):
| 项 | 值 |
|---|---|
| 开发机 | Apple M2 / 24GB 内存 / macOS |
| IDE | DevEco Studio 6.1.0 Release 及以上(内置 hvigor、ohpm、node、hdc 命令行工具链) |
| compatibleSdkVersion | 6.0.1(21) |
| targetSdkVersion | 6.1.0(23) |
| 语言 | ArkTS(声明式 UI) |
| 应用版本 | versionName 1.1.5 / versionCode 1000015 |
| 源码规模 | 约 3.1 万行 |
1. 开发阶段总览
整个项目从 0 到上架划分为七个阶段,每个阶段有明确的交付物:
| 阶段 | 内容 | 关键交付物 |
|---|---|---|
| 一、工程初始化 | CLI 工具链确认、骨架选取、工程结构搭建 | 可 Sync、可运行的空壳工程 |
| 二、架构设计 | 一多三层、路由体系、MVVM 基类 | 模块依赖图、PageEnum + router_map |
| 三、核心功能实现 | RDB 存储、日历提醒、IAP 会员、服务卡片 | 可完整体验的业务闭环 |
| 四、多环境与签名 | 7 套 product、调试/发布证书分离 | build-profile.json5 签名矩阵 |
| 五、调试与验证 | hdc 设备调试、日志体系、内存调优 | 稳定的 debug 包 |
| 六、命令行打包 | hvigorw assembleApp、代码混淆、产物校验 | *-prod-signed.app 上架包 |
| 七、应用市场上架 | 隐私合规、权限最小化、软著、AGC 提审 | AppGallery 正式上架 |
| 八、下载链接 | https://appgallery.huawei.com/app/detail?id=com.xiaobingkj.zhike&channelId=SHARE&source=appshare | AppGallery |
2. 阶段一:工程初始化——先摸清 CLI 工具链
2.1 工具链盘点
DevEco Studio 安装后自带完整命令行工具链(本机位于 /Applications/DevEco-Studio.app/Contents/tools/):
| 工具 | 作用 | 本项目中的典型用法 |
|---|---|---|
hvigor / hvigorw | 构建任务编排(对标 Gradle) | 编译、签名、打 App 包 |
ohpm | 包管理(对标 npm) | 安装 HAR 依赖如 @ohos/imageknife |
hdc | 设备调试(对标 adb) | 安装 HAP、看日志、传文件 |
node | hvigor 的运行时 | 执行 hmosword-build 集成脚本 |
日常开发可以在 IDE 里点按钮,但打包与持续集成建议走命令行——这是后面阶段六能一键出上架包的前提。
2.2 工程骨架:直接复用官方多端示例
新建工程用 IDE 模板即可,但「至客」选了更省力的路线:拉取开源的「HMOS代码工坊」作为骨架。这个工程的价值在于它把一个多端 HarmonyOS 应用的工程组织方式完整示范了出来:
sample_in_harmonyos_zhike/
├── AppScope/ # 应用级配置(app.json5:bundleName、版本、图标、应用名)
│ └── resources/ # 应用级多语言资源(应用名"至客"就在这里)
├── common/ # 通用 HAR:路由、存储、账号、埋点、网络、通用组件
├── features/ # 业务 HAR 层
│ ├── abilitycommon/ # 手机/平板/PC 共用主框架:Splash、HomeView、Tab 容器
│ ├── commonbusiness/ # Banner、详情容器、列表加载等业务公共层
│ ├── componentlibrary/ # 组件库业务
│ ├── devpractices/ # Sample 业务
│ ├── exploration/ # 实践文章业务
│ ├── mine/ # 我的页 —— 至客业务的主阵地(订单/产品/会员)
│ └── widgetcommon/ # 服务卡片公共能力
├── products/ # entry 层(不同设备形态的入口)
│ ├── phone/ # 手机/平板入口(当前上架形态)
│ ├── pc/ tv/ wearable/ # 其他端入口(暂缓,见 2.3)
├── build-profile.json5 # 工程级:模块、产品形态、SDK、签名配置
├── oh-package.json5 # 工程级依赖入口
├── VersionFile.json5 # 依赖版本参数化
└── hvigorfile.ts # hvigor 插件声明
三层职责一句话讲清:products 管入口,features 管业务,common 沉淀能力。
2.3 上架形态裁剪:注释掉用不到的端
骨架默认支持手机、PC、TV、穿戴四种形态,但「至客」首版只上手机/平板。做法很朴素——直接在 build-profile.json5 的 modules 里注释掉 wearable、tv、pc 三个 entry,只保留 phone:
modules: [
{ name: 'phone', srcPath: './products/phone', targets: [/* 7 套 product 映射 */] },
{ name: 'mine', srcPath: './features/mine' },
// { name: 'wearable', srcPath: './products/wearable', ... },
// { name: 'tv', srcPath: './products/tv', ... },
// { name: 'pc', srcPath: './products/pc', ... },
{ name: 'abilitycommon', srcPath: './features/abilitycommon' },
// ...
]
多端能力保留在代码里(后续版本可以随时恢复编译),但发布形态按节奏裁剪。裁剪后编译产物更小、审核面更窄、上架更快。
2.4 应用身份:AppScope 三件套
上架前要核对的应用级身份信息集中在 AppScope/app.json5:
{
"app": {
"bundleName": "com.xiaobingkj.zhike", // 包名:发布后不可更改
"vendor": "xiaobingkj",
"versionCode": 1000015, // 每次提审要递增
"versionName": "1.1.5",
"icon": "$media:hmos_layered_image",
"label": "$string:hmos_app_name" // 指向 AppScope 资源中的"至客"
}
}
这里有个容易吃亏的点:bundleName 一旦上架就终身绑定,起名时就要用自己持有的域名倒序(xiaobingkj.com → com.xiaobingkj.*);versionCode 用「主版本×1000000 + 递增序号」这类规则化管理,避免上架时忘记加版本号被 AGC 打回。
3. 阶段二:架构设计——一多三层 + 集中路由 + MVVM
3.1 模块依赖关系
各模块类型与职责(可直接对照 build-profile.json5):
| 模块 | 类型 | 职责 |
|---|---|---|
products/phone | entry | 手机/平板入口,含 EntryAbility、PhoneFormAbility(卡片)、liveForm 扩展 |
features/abilitycommon | har | Splash、HomeView、Tab 主框架、生命周期复用助手 |
features/mine | har | 至客核心业务:订单列表/详情、产品管理、会员、设置、关于 |
features/commonbusiness | har | Banner、详情容器、加载更多、Tab 状态 |
common | har | 路由、存储、RDB、日历提醒、IAP 会员服务、工具组件 |
业务代码(客户/订单/产品)尽量下沉到 HAR 而不是塞进 entry 模块,好处有三个:entry 保持轻、业务可以被未来恢复的 pc/tv 端复用、HAR 可以单独做单元测试。
3.2 集中路由:PageEnum + router_map + NavPathStack
骨架的路由体系是三件套配合:
- 页面声明:每个 HAR 的
router_map.json声明可被导航打开的页面; - 名称常量:所有路由名统一收敛在
common/.../model/PageEnum.ets,不用魔法字符串; - 导航封装:
routermanager里的PageContext包装NavPathStack,提供openPage()统一入口。
业务侧打开页面的写法(摘自 MinePageVM.ets,真实代码):
pageContext?.openPage({ routerName: PageEnum.ORDER_LIST_VIEW }, true);
路由名集中管理后,「加一个页面」变成三步固定动作——建 View、登记 router_map、在 PageEnum 加枚举,不容易漏配。
3.3 MVVM 基类:BaseVM / BaseState / BaseVMEvent
common 模块提供了 viewmodel/BaseVM.ets 等基类,所有页面 VM 继承它,事件驱动刷新状态:
export class MinePageVM extends BaseHomeViewModel<MinePageState> {
private static instance: MinePageVM;
// 页面数据源:登录项、Premium 卡片、设置、订单列表、产品管理
public listGroupData: ListGroup[] = [ /* ... */ ];
public sendEvent<T>(event: MineEventParam<T>): void | boolean { /* ... */ }
}
VM 一律单例(getInstance()),避免 Tab 切换反复重建数据源;页面状态(MinePageState)与 VM 分文件维护,状态结构一目了然。
3.4 多端生命周期复用:BaseAbilityHelper
EntryAbility 本身只有 60 行,全部生命周期委托给 features/abilitycommon 的 BaseAbilityHelper:
export default class EntryAbility extends UIAbility {
private baseAbilityHelper: BaseAbilityHelper = new BaseAbilityHelper();
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
AppStorage.setOrCreate<boolean>(StorageKey.IS_SIDEBAR_LAYOUT, false);
this.baseAbilityHelper.doOnCreate(this.context, want, launchParam, true);
}
onWindowStageCreate(windowStage: window.WindowStage): void {
this.baseAbilityHelper.doOnWindowStageCreate(windowStage);
}
// onNewWant/onForeground/onBackground... 同样委托
}
这正是「一次开发、多端部署」的关键:PC 端的 PcAbility 委托同一个 Helper,只是初始化参数不同(比如 IS_SIDEBAR_LAYOUT),UI 层自动切换侧栏/底部 Tab 布局。
4. 阶段三:核心功能实现
这一阶段业务量最大,坑也最容易集中在这里。逐个拆解四大核心实现。
4.1 本地数据层:加密 RDB(BusinessRdbHelper)
数据层位于 common/src/main/ets/database/BusinessRdbHelper.ets,设计要点直接看代码:
const STORE_CONFIG: relationalStore.StoreConfig = {
name: 'zhike_business.db',
securityLevel: relationalStore.SecurityLevel.S3, // 数据库安全等级 S3
encrypt: true, // 落盘加密
};
const CREATE_ORDER_TABLE: string =
'CREATE TABLE IF NOT EXISTS OrderTable(' +
'id TEXT PRIMARY KEY, customerId TEXT NOT NULL, customerName TEXT NOT NULL, ' +
'productId TEXT NOT NULL, productName TEXT NOT NULL, salePrice REAL NOT NULL, ' +
'purchasePrice REAL NOT NULL, orderDate TEXT NOT NULL, ' +
'createdAt INTEGER NOT NULL, updatedAt INTEGER NOT NULL)';
三张表(CustomerTable / ProductTable / OrderTable),订单表冗余了 customerName/productName,避免列表查询跨表 JOIN。写入统一用 ON_CONFLICT_REPLACE,天然支持「保存即 upsert」:
public async insertOrder(order: OrderData): Promise<void> {
const store: relationalStore.RdbStore = await this.getStore();
await store.insert(ORDER_TABLE, this.orderBucket(order),
relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
}
查询侧统一模式:RdbPredicates 排序 → query → 游标遍历 → finally 中 resultSet.close()。这个 finally 在全文件里严格执行,是防游标泄漏的好习惯。
数据层几条可以直接照搬的做法:
- RDB 帮助类做成单例(
getInstance(context)),配合initialize()预热; - 涉及经营数据(客户、价格、毛利)记得
encrypt: true+S3,上架审核与用户信任都加分; - 表设计冗余展示字段,列表页一次查询拿全数据;
- 所有时间戳字段用 INTEGER 毫秒值(
createdAt/updatedAt),排序和增量同步都靠它。
4.2 订单到期提醒:CalendarKit(OrderReminderService)
common/src/main/ets/service/OrderReminderService.ets 实现了「订单到期写入系统日历」,有几个细节处理得比较讲究:
① 提前量可配置且白名单校验:
const SUPPORTED_ADVANCE_DAYS: number[] = [1, 3, 7]; // 只允许这三档
const DEFAULT_ADVANCE_DAYS: number = 7;
public static setAdvanceDays(days: number): void {
const validDays: number = SUPPORTED_ADVANCE_DAYS.includes(days) ? days : DEFAULT_ADVANCE_DAYS;
PreferenceManager.getInstance().setValue<number>(ADVANCE_DAYS_KEY, validDays);
}
配置持久化用轻量 PreferenceManager,读取时非法值一律回退默认档——设置项不信任输入,先过白名单。
② 权限异步生效问题用轮询解决:requestPermissionsFromUser 返回后权限状态可能尚未同步生效,代码里做了 20 次 × 100ms 的轮询确认,日历句柄获取也做了 5 次 × 200ms 重试:
const PERMISSION_CHECK_INTERVAL: number = 100;
const PERMISSION_CHECK_ATTEMPTS: number = 20;
const CALENDAR_RETRY_INTERVAL: number = 200;
const CALENDAR_RETRY_ATTEMPTS: number = 5;
③ 失败降级而不是崩溃:权限被拒、日历不可用时只记日志并返回 undefined,订单照常保存——辅助能力不能阻塞主流程:
if (!hasPermission) {
Logger.info(TAG, 'Skip creating calendar event because calendar permission was not granted.');
return undefined;
}
4.3 会员体系:IAPKit 非消耗型商品(PremiumMembershipService)
common/src/main/ets/service/PremiumMembershipService.ets 对接华为应用内购买,商品是非消耗型(NONCONSUMABLE)永久会员:
export const LIFETIME_PRODUCT_ID: string = 'com.xiaobingkj.zhike.lifetime';
public static async load(context: common.UIAbilityContext): Promise<PremiumMembershipStatus> {
if (!PremiumMembershipService.isAvailable()) {
return PremiumMembershipService.unavailableStatus(); // 设备不支持 IAP 时优雅降级
}
await iap.queryEnvironmentStatus(context);
const result: iap.QueryPurchaseResult = await iap.queryPurchases(context, {
productType: iap.ProductType.NONCONSUMABLE,
queryType: iap.PurchaseQueryType.CURRENT_ENTITLEMENT, // 只查当前有效权益
});
return PremiumMembershipService.hasLifetimeEntitlement(result.purchaseDataList ?? []) ? ... : ...;
}
两个容易忽略的细节:
① JWS 凭证本地解析:purchaseDataList 是 JWS 字符串,代码手动做 base64url 解码并解析 payload,校验 productId 与撤销标记:
private static decodeJwsPayload(jws: string): string {
const parts: string[] = jws.split('.');
if (parts.length !== 3) { throw new Error('Invalid JWS'); }
const bytes: Uint8Array = new util.Base64Helper()
.decodeSync(parts[1], util.Type.BASIC_URL_SAFE);
return util.TextDecoder.create('utf-8', { ignoreBOM: true })
.decodeToString(bytes, { stream: false });
}
② 错误码逐条转译成用户话术:ACCOUNT_NOT_LOGGED_IN → 请先登录华为账号、NETWORK_ERROR → 网络连接异常…、801 → 当前设备暂不支持应用内购买。付费链路上每个失败分支都有明确的用户提示,而不是把错误码直接抛给用户。
UI 侧的会员入口是 features/mine 的 PremiumCard 组件(渐变图标 + 按压缩放动效),点击后走购买流程;「我的」页每次进入调用 refreshPremiumMembership() 重新查权益,会员状态始终以 IAP 服务端查询为准,不做本地永久标记——换机、重装、多设备都能正确恢复。
4.4 服务卡片:最近到期客户 + 累计毛利
桌面卡片是至客的差异化功能(products/phone/src/main/ets/widget/pages/WidgetCard.ets + PhoneFormAbility.ets):
① 卡片 UI 用 LocalStorageProp 接收数据,卡片与 FormAbility 之间解耦:
@Entry(businessWidgetStorage)
@Component
struct WidgetCard {
@LocalStorageProp('nearestCustomerName1') nearestCustomerName1: string = '';
@LocalStorageProp('nearestCustomerName2') nearestCustomerName2: string = '';
@LocalStorageProp('totalProfit') totalProfit: string = '¥0.00';
// 蓝色渐变背景:最近到期客户 ×2 + 累计毛利
}
② 点击卡片拉起应用用 postCardAction router 动作直达 EntryAbility:
.onClick(() => {
postCardAction(this, { action: 'router', abilityName: 'EntryAbility' });
})
③ 数据刷新链路:PhoneFormAbility.onAddForm / onUpdateForm → 识别卡片类型(formName 持久化在 FormRdbHelper)→ 从 BusinessRdbHelper 拉全量客户/产品/订单 → BusinessWidgetManager.updateForm() 计算最近到期客户与累计毛利 → formBindingData 推给卡片:
private async refreshBusinessWidget(formId: string): Promise<void> {
const helper: BusinessRdbHelper = BusinessRdbHelper.getInstance(this.context);
await helper.initialize();
const customers = await helper.queryCustomers();
const products = await helper.queryProducts();
const orders = await helper.queryOrders();
await BusinessWidgetManager.updateForm(formId, customers, products, orders);
}
卡片数据从同一份 RDB 读取,主应用与卡片天然一致;卡片实例信息(formId/name/dimension)单独建表持久化,onRemoveForm 时同步清理,避免脏数据。
5. 阶段四:多环境与签名——7 套 product 矩阵
build-profile.json5 是命令行构建的「总开关」,至客在这里配置了 7 个 product:
| product | 签名配置 | 用途 |
|---|---|---|
default | device(DevEco 自动生成的调试证书,位于 ~/.ohos/config/) | 本地真机调试 |
dev / uat / uat_mirror / icsl / beta | default(正式证书) | 各测试环境 |
prod | default(正式证书) | 上架正式包 |
"signingConfigs": [
{ "name": "device", "type": "HarmonyOS", "material": { /* 调试证书,~/.ohos/config/ 自动生成 */ } },
{ "name": "default", "type": "HarmonyOS", "material": {
"certpath": "./dis.cer", "keyAlias": "key",
"profile": "./disRelease.p7b", "signAlg": "SHA256withECDSA",
"storeFile": "./dis.p12"
/* keyPassword / storePassword 已脱敏,实际在文件中以密文存储 */
} }
],
"products": [
{ name: 'default', signingConfig: "device",
compatibleSdkVersion: '6.0.1(21)', targetSdkVersion: '6.1.0(23)',
runtimeOS: 'HarmonyOS',
buildOption: { strictMode: { useNormalizedOHMUrl: true } } },
{ name: 'prod', signingConfig: 'default', /* 同 SDK 配置 */ },
/* dev / uat / uat_mirror / icsl / beta ... */
]
正式签名材料(dis.cer / dis.p12 / disRelease.p7b)放在工程根目录,从 AGC(AppGallery Connect)后台申请生成。
这套配置有几个值得注意的地方:
- 调试与发布证书分开:
device走 DevEco 自动签名,default走 AGC 发布证书,不要混用; useNormalizedOHMUrl: true:规范化模块导入 URL,工程内 HAR 互引用统一走@ohos/common别名,模块迁移不破;- 环境差异靠 product 而不是改代码:同一份代码,
hvigorw --mode module -p product=prod一条命令切换环境; - 证书文件不要提交明文密码,p12/p7b 只保留在本地(建议后续加入
.gitignore管理)。
6. 阶段五:调试与验证
6.1 hdc:真机调试三板斧
# 查看设备
hdc list targets
# 安装调试包(debug 签名)
hdc install entry-default-signed.hap
# 实时过滤应用日志(Logger TAG 规范:'[BusinessRdbHelper]' '[OrderReminderService]' ...)
hdc hilog | grep -E "zhike|EntryAbility|PhoneFormAbility"
6.2 日志体系
common 模块统一封装 Logger,所有类顶部声明常量 TAG(如 const TAG = '[OrderReminderService]'),错误路径一律 Logger.error 带上下文(orderId、formId、err.code、err.message)。卡片、IAP、日历这类后台链路,没有结构化日志排障会非常被动。
6.3 构建内存调优(M2/24GB 实测)
hvigor/hvigor-config.json5 中针对构建内存做了显式调优,对中等规模工程(3 万行 + 8 个模块)很有参考价值:
"properties": {
"hvigor.pool.cache.capacity": 0, // 关闭内存缓存
"hvigor.pool.maxSize": 5, // 限制并行池规模
"ohos.arkCompile.maxSize": 3, // 限制 ArkTS 编译并行度
"hvigor.enableMemoryCache": false // 关闭内存缓存,降低峰值内存
}
默认配置下 daemon 进程 maxOldSpaceSize 为 8192MB,内存吃紧时会出现构建进程被杀,按需收紧上述参数即可稳定增量编译。
7. 阶段六:命令行打包上架包
这是「DevEco CLI 实战」的核心环节:不打开 IDE,纯命令行产出上架 .app 包。
7.1 安装依赖并构建
# 1. 安装工程依赖(ohpm 会读取 oh-package.json5 + VersionFile.json5 参数化版本)
ohpm install --all
# 2. 命令行打出 prod 环境的 Release App 包
hvigorw assembleApp --mode module -p product=prod -p buildMode=release
VersionFile.json5 把依赖版本参数化(hypium 1.0.19、imageknife 3.2.8 等),升版本只改这一个文件。
7.2 产物校验
构建产物输出在 build/outputs/prod/:
build/outputs/prod/
├── sample_in_harmonyos_zhike-prod-signed.app # ← 上架包(已签名)
├── sample_in_harmonyos_zhike-prod-unsigned.app # 未签名包(留档比对)
├── app-symbol.zip # 符号表(崩溃分析用,记得归档!)
├── pac.json / pack.info # 打包元信息
提醒一句:app-symbol.zip 是混淆后崩溃堆栈还原的唯一依据,每次发版都要和 .app 一起归档,否则线上崩溃无法定位。
7.3 代码混淆
products/phone/obfuscation-rules.txt 启用了两级混淆:
-enable-property-obfuscation # 属性名混淆
-enable-toplevel-obfuscation # 顶层作用域名混淆
混淆后上线前逐项过一遍:
- RDB 表名/列名是字符串常量,未受属性混淆影响(建表 SQL 独立于对象属性,安全);
-
postCardAction、router_map、module.json5中的字符串引用未混淆; - IAP 的 JWS 解析基于 JSON.parse,接口字段(
jwsPurchaseOrder、productId)未被属性混淆破坏——涉及跨进程/跨系统数据结构要在 keep 列表中保护; - 混淆后全功能回归一遍:卡片、日历、IAP 三条链路。
8. 阶段七:AppGallery 正式上架
8.1 module.json5 的上架合规范式
上架成败一半取决于 module.json5 的细节,至客的配置可以直接参考:
"metadata": [
{ "name": "client_id", "value": "6917610957443786045" }, // AGC 后台申请,账号/推送等 Kit 需要
{ "name": "appgallery_privacy_hosted", "value": "1" }, // 隐私政策托管在华为服务器
{ "name": "appgallery_privacy_link_privacy_statement",
"value": "https://agreement-drcn.hispace.dbankcloud.cn/..." } // 隐私声明必须是 https
]
8.2 权限最小化:注释掉不用到的权限
审核中最容易被拒的就是「权限滥用」。至客的做法很克制——把骨架中继承来的 INTERNET、GET_NETWORK_INFO 权限直接注释掉(当前版本数据全本地化 + 华为云备份由系统通道完成,不需要应用自己持有网络权限),只保留四个:
"requestPermissions": [
{ "name": "ohos.permission.VIBRATE", "reason": "$string:vibrator_reason" },
{ "name": "ohos.permission.GYROSCOPE" },
{ "name": "ohos.permission.READ_CALENDAR", "reason": "$string:calendar_reason", "usedScene": { "when": "inuse" } },
{ "name": "ohos.permission.WRITE_CALENDAR", "reason": "$string:calendar_reason", "usedScene": { "when": "inuse" } }
]
几个细节:
- 每个权限都有
reason字符串资源(且提供英文)+usedScene.when: "inuse"; - 用不到的权限宁可注释掉等用到再恢复,也不要「先申请着」;
- 二次开发开源骨架时,把继承来的权限清单逐条重审——原工程的联网需求不代表你的业务需要。
8.3 提审材料闭环
| 材料 | 来源 | 备注 |
|---|---|---|
| 上架包 | hvigorw assembleApp 产物 prod-signed.app | versionCode 递增后重新构建 |
| 符号表 | app-symbol.zip | 归档,接入崩溃分析 |
| 隐私声明 | AGC 托管(appgallery_privacy_hosted=1) | https 链接写入 metadata |
| 截图/介绍 | 真机截图 + 功能说明 | 手机/平板两套尺寸 |
| 软件著作权 | 《至客客户订单管理系统》登记 | 源程序量 31031 行,含 60 页源码文档与操作手册 |
| 内购商品 | com.xiaobingkj.zhike.lifetime(非消耗型) | AGC「我的内购」配置 |
软著材料(代码前 30 页 + 后 30 页、操作手册、申请表)直接从工程与真机导出生成,「代码 → 文档 → 权属证明」一气呵成,对个人开发者上架与后期维权都有实际帮助。
9. 全流程经验清单
9.1 架构与工程
| # | 经验 | 依据 |
|---|---|---|
| 1 | 站在官方开源骨架上二次开发,架构问题官方已趟过坑 | 基于 Apache-2.0「HMOS代码工坊」 |
| 2 | products/features/common 三层分离,业务下沉 HAR,entry 保持薄 | 工程模块表 |
| 3 | 路由名集中 PageEnum + router_map,不用魔法字符串 | 路由三件套 |
| 4 | Ability 生命周期委托共享 Helper,多端只差初始化参数 | EntryAbility → BaseAbilityHelper |
| 5 | 发布形态按节奏裁剪(注释多余 entry),后续版本再放开 | build-profile modules |
9.2 功能实现
| # | 经验 | 依据 |
|---|---|---|
| 6 | 经营数据 RDB 记得 encrypt: true + S3 | BusinessRdbHelper |
| 7 | 订单表冗余客户/产品名称,列表免 JOIN | OrderTable DDL |
| 8 | 游标遍历 try/finally close | 全部查询方法 |
| 9 | 系统能力(日历)失败只降级不阻塞主流程 | OrderReminderService |
| 10 | 权限异步生效要轮询确认,不要假设同步 | 20×100ms 重试设计 |
| 11 | 设置项读取先过白名单再回退默认值 | setAdvanceDays |
| 12 | IAP 权益每次进入页面重新查服务端,不做本地永久标记 | refreshPremiumMembership |
| 13 | 付费链路每个错误码都转译成人话 | PremiumMembershipService.errorMessage |
| 14 | 卡片数据与主应用共用同一份数据库,天然一致 | refreshBusinessWidget |
9.3 构建与上架
| # | 经验 | 依据 |
|---|---|---|
| 15 | 环境切换靠 -p product=xxx,不靠改代码 | 7 套 product 矩阵 |
| 16 | 调试证书与发布证书分开 | device / default 双签名配置 |
| 17 | 依赖版本参数化到 VersionFile.json5 | ohpm 工作流 |
| 18 | 发版要归档 app-symbol.zip | 混淆崩溃还原 |
| 19 | 权限最小化:继承来的权限逐条重审,用不到就注释 | module.json5 |
| 20 | 隐私政策托管 AGC + https 链接写入 metadata | appgallery_privacy_hosted |
| 21 | 软著材料与工程同步生成,形成权属闭环 | 软著申请资料目录 |
9.4 收尾
回头看这七个阶段,真正决定项目能不能顺利上架的,往往不是写代码的那几天,而是架构分层、签名矩阵、权限清单这些早期决策——代码可以改,bundleName 和权限声明一旦提交审核就很难回头。把这几个环节当重点对待,剩下的就是按部就班。
附:常用命令速查
# 依赖
ohpm install --all
# 调试构建(真机)
hvigorw assembleHap --mode module -p product=default -p buildMode=debug
# 上架构建
hvigorw assembleApp --mode module -p product=prod -p buildMode=release
# 设备
hdc list targets
hdc install build/outputs/default/entry-default-signed.hap
hdc hilog | grep zhike
# 清理
hvigorw clean
本文源码基线:sample_in_harmonyos_zhike(versionCode 1000015 / versionName 1.1.5),HarmonyOS SDK 6.0.1(21) 兼容 / 6.1.0(23) 目标。
更多推荐





所有评论(0)