# 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 及以上(内置 hvigorohpmnodehdc 命令行工具链)
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 正式上架
八、下载链接 appgallery.huawei.com/app/detail?... 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 应用的工程组织方式完整示范了出来:

text 复制代码
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.json5modules 里注释掉 wearabletvpc 三个 entry,只保留 phone

json5 复制代码
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

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.comcom.xiaobingkj.*);versionCode 用「主版本×1000000 + 递增序号」这类规则化管理,避免上架时忘记加版本号被 AGC 打回。


3. 阶段二:架构设计------一多三层 + 集中路由 + MVVM

3.1 模块依赖关系

各模块类型与职责(可直接对照 build-profile.json5):

模块 类型 职责
products/phone entry 手机/平板入口,含 EntryAbilityPhoneFormAbility(卡片)、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 可以单独做单元测试。

骨架的路由体系是三件套配合:

  1. 页面声明 :每个 HAR 的 router_map.json 声明可被导航打开的页面;
  2. 名称常量 :所有路由名统一收敛在 common/.../model/PageEnum.ets,不用魔法字符串;
  3. 导航封装routermanager 里的 PageContext 包装 NavPathStack,提供 openPage() 统一入口。

业务侧打开页面的写法(摘自 MinePageVM.ets,真实代码):

typescript 复制代码
pageContext?.openPage({ routerName: PageEnum.ORDER_LIST_VIEW }, true);

路由名集中管理后,「加一个页面」变成三步固定动作------建 View、登记 router_map、在 PageEnum 加枚举,不容易漏配。

3.3 MVVM 基类:BaseVM / BaseState / BaseVMEvent

common 模块提供了 viewmodel/BaseVM.ets 等基类,所有页面 VM 继承它,事件驱动刷新状态:

typescript 复制代码
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/abilitycommonBaseAbilityHelper

typescript 复制代码
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,设计要点直接看代码:

typescript 复制代码
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」:

typescript 复制代码
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 → 游标遍历 → finallyresultSet.close()。这个 finally 在全文件里严格执行,是防游标泄漏的好习惯。

数据层几条可以直接照搬的做法:

  • RDB 帮助类做成单例getInstance(context)),配合 initialize() 预热;
  • 涉及经营数据(客户、价格、毛利)记得 encrypt: true + S3,上架审核与用户信任都加分;
  • 表设计冗余展示字段,列表页一次查询拿全数据;
  • 所有时间戳字段用 INTEGER 毫秒值(createdAt/updatedAt),排序和增量同步都靠它。

4.2 订单到期提醒:CalendarKit(OrderReminderService)

common/src/main/ets/service/OrderReminderService.ets 实现了「订单到期写入系统日历」,有几个细节处理得比较讲究:

① 提前量可配置且白名单校验

typescript 复制代码
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 重试:

typescript 复制代码
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,订单照常保存------辅助能力不能阻塞主流程:

typescript 复制代码
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)永久会员:

typescript 复制代码
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 与撤销标记:

typescript 复制代码
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/minePremiumCard 组件(渐变图标 + 按压缩放动效),点击后走购买流程;「我的」页每次进入调用 refreshPremiumMembership() 重新查权益,会员状态始终以 IAP 服务端查询为准,不做本地永久标记------换机、重装、多设备都能正确恢复。

4.4 服务卡片:最近到期客户 + 累计毛利

桌面卡片是至客的差异化功能(products/phone/src/main/ets/widget/pages/WidgetCard.ets + PhoneFormAbility.ets):

① 卡片 UI 用 LocalStorageProp 接收数据,卡片与 FormAbility 之间解耦:

typescript 复制代码
@Entry(businessWidgetStorage)
@Component
struct WidgetCard {
  @LocalStorageProp('nearestCustomerName1') nearestCustomerName1: string = '';
  @LocalStorageProp('nearestCustomerName2') nearestCustomerName2: string = '';
  @LocalStorageProp('totalProfit') totalProfit: string = '¥0.00';
  // 蓝色渐变背景:最近到期客户 ×2 + 累计毛利
}

② 点击卡片拉起应用postCardAction router 动作直达 EntryAbility

typescript 复制代码
.onClick(() => {
  postCardAction(this, { action: 'router', abilityName: 'EntryAbility' });
})

③ 数据刷新链路PhoneFormAbility.onAddForm / onUpdateForm → 识别卡片类型(formName 持久化在 FormRdbHelper)→ 从 BusinessRdbHelper 拉全量客户/产品/订单 → BusinessWidgetManager.updateForm() 计算最近到期客户与累计毛利 → formBindingData 推给卡片:

typescript 复制代码
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(正式证书) 上架正式包
json5 复制代码
"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)后台申请生成。

这套配置有几个值得注意的地方:

  1. 调试与发布证书分开device 走 DevEco 自动签名,default 走 AGC 发布证书,不要混用;
  2. useNormalizedOHMUrl: true :规范化模块导入 URL,工程内 HAR 互引用统一走 @ohos/common 别名,模块迁移不破;
  3. 环境差异靠 product 而不是改代码 :同一份代码,hvigorw --mode module -p product=prod 一条命令切换环境;
  4. 证书文件不要提交明文密码 ,p12/p7b 只保留在本地(建议后续加入 .gitignore 管理)。

6. 阶段五:调试与验证

6.1 hdc:真机调试三板斧

bash 复制代码
# 查看设备
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 个模块)很有参考价值:

json5 复制代码
"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 安装依赖并构建

bash 复制代码
# 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.19imageknife 3.2.8 等),升版本只改这一个文件。

7.2 产物校验

构建产物输出在 build/outputs/prod/

text 复制代码
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 启用了两级混淆:

text 复制代码
-enable-property-obfuscation      # 属性名混淆
-enable-toplevel-obfuscation      # 顶层作用域名混淆

混淆后上线前逐项过一遍:

  • RDB 表名/列名是字符串常量,未受属性混淆影响(建表 SQL 独立于对象属性,安全);
  • postCardActionrouter_mapmodule.json5 中的字符串引用未混淆;
  • IAP 的 JWS 解析基于 JSON.parse,接口字段(jwsPurchaseOrderproductId)未被属性混淆破坏------涉及跨进程/跨系统数据结构要在 keep 列表中保护;
  • 混淆后全功能回归一遍:卡片、日历、IAP 三条链路。

8. 阶段七:AppGallery 正式上架

8.1 module.json5 的上架合规范式

上架成败一半取决于 module.json5 的细节,至客的配置可以直接参考:

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 权限直接注释掉(当前版本数据全本地化 + 华为云备份由系统通道完成,不需要应用自己持有网络权限),只保留四个:

json5 复制代码
"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 和权限声明一旦提交审核就很难回头。把这几个环节当重点对待,剩下的就是按部就班。


附:常用命令速查

bash 复制代码
# 依赖
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) 目标。

相关推荐
半生过往1 小时前
前端工程师学习智能体开发(一)
前端·学习·状态模式
白雾茫茫丶1 小时前
Vibecoding 一个主题切换动画库:13 种揭幕方式
前端·vue.js·react.js
计算机魔术师1 小时前
谷歌宣布 TPU 互联架构支持 100 万芯片规模,电力供应成 AI 基建核心瓶颈
前端
雪芽蓝域zzs1 小时前
第四十九节:TagsView 右键菜单(带三角箭头)给每个 tag 增加**鼠标右键菜单**(右键标签弹出:关闭、关闭其他、关闭全部)
前端·javascript·vue.js
এ慕ོ冬℘゜2 小时前
从零开发移动端医院挂号页面:适配、交互与前端实现详解
前端·javascript
熊猫钓鱼>_>2 小时前
声临其境:HarmonyOS 空间音频全链路开发实战
音频·harmonyos·arkts·鸿蒙·arkui·空间·hap
掘金挖土2 小时前
前端手摸手跑路之 AI 应用开发(六)
前端·后端
liuyt20222 小时前
【javaweb】day4
前端
志尊宝2 小时前
Vue3 零基础每日笔记(024):组件的创建与使用——从 import 到自动导入
前端·vue.js·笔记·前端框架·html5