本文基于真实项目 com.xiaobingkj.cunying(存盈)的源码与 124 条 Git 提交记录写成,覆盖从工程奠基、核心功能实现、CLI 签名打包,到通过华为应用市场审核正式上架的全过程。文中代码片段均摘自项目源码,构建命令对照官方文档验证过。

首页利率分析
在这里插入图片描述在这里插入图片描述在这里插入图片描述

一、项目概览

存盈是一款个人存款管理应用:把分散在多家银行的定期存单集中记录成台账,按日计算已产生利息与到期利息,内置 162 家银行的定期存款挂牌利率,配饼图和柱状图分析存款分布,另有桌面服务卡片、JSON 备份导入导出、Premium 会员(华为应用内支付)。

项目要素实际值
包名 / 应用名com.xiaobingkj.cunying / 存盈
当前版本versionName 1.1.5 / versionCode 1000015
技术栈ArkTS(声明式 UI)+ relationalStore(加密 RDB)+ HDS 组件库 + mpchart
工具链DevEco Studio 6.1 / HarmonyOS SDK:兼容 6.0.1(21)、目标 6.1.0(23)
设备形态手机、折叠屏、平板(一多架构,预留 PC/TV/穿戴入口)
上架渠道华为应用市场(AppGallery,含 IAP 内购 + 隐私协议托管)
配套成果软件著作权「存盈鸿蒙版」V1.0.9(2026-08-06 完成,源程序 78234 行,文档 121 页)
下载地址https://appgallery.huawei.com/app/detail?id=com.xiaobingkj.cunying&channelId=SHARE&source=appshare

为什么强调 CLI?本项目把 DevEco Studio 当编辑器,把**命令行工具链(hvigorw / ohpm / hdc)**当作构建、签名、安装、验证的唯一入口。直接收益有三:环境隔离可控(7 套 product 环境)、构建可脚本化复现、后续接流水线零成本。

二、第0阶段:工程奠基

2.1 选型:fork 华为官方「HMOS代码工坊」

存盈没有从空工程起步,而是 fork 了华为官方开源的 sample_in_harmonyos(HMOS代码工坊,Apache 2.0 协议)。这个决定换来了三样东西:

  1. 成熟的一多(一次开发、多端部署)三层工程结构;
  2. HDS(HarmonyOS Design System)组件与 ArkUI 实践:HdsNavigation、HdsTabs、断点自适应、深色模式;
  3. 华为账号、埋点、推送、卡片、意图框架等系统能力的标准接入范式。这些恰是上架审核常踩的坑,官方示例已经趟平。

代价是做大量减法:删除上游的组件库和样例业务、清理 mock 数据。Git 记录了剥离过程:

6be9c60 去掉活期          ← 业务转向:只做定期存单
c66e268 新增银行icon      ← 引入银行视觉资产
3abbc61 存款              ← 第一个存款业务提交
4e580fe appicon,appname  ← 品牌替换
827fda3 design、bundleid  ← 包名定稿 com.xiaobingkj.cunying
2dd2ee2 删除无用资源      ← 持续瘦身

2.2 三层工程结构

sample_in_harmonyos_deposit/
├── AppScope/app.json5            # 应用级配置(包名、版本、图标)
├── common/                       # ① 公共基础层(HAR)
│   ├── accountservice/           #   华为账号(AccountKit)
│   ├── database/                 #   DepositRdbHelper 等 RDB 封装
│   ├── routermanager/            #   路由管理
│   ├── service/                  #   DepositManager / DepositTransferService / DepositWidgetManager
│   ├── model/                    #   DepositData 纯数据模型
│   └── trackmanager/ storagemanager/ util/ ...
├── features/                     # ② 业务特性层(HAR,按域拆分)
│   ├── abilitycommon/            #   首页壳、启动隐私门、AddDepositSheet
│   ├── commonbusiness/           #   Tab 模型、Banner 等跨 feature 公共件
│   ├── componentlibrary/         #   存款首页 + 存单卡片组件
│   ├── devpractices/             #   利率页 + 162 家银行利率数据
│   ├── exploration/              #   分析页(mpchart 图表)
│   ├── mine/                     #   我的页 + Premium 会员 + 设置
│   └── widgetcommon/             #   卡片公共组件
├── products/                     # ③ 产品定制层(entry HAP)
│   ├── phone/                    #   手机/平板入口(当前启用)
│   └── pc/ tv/ wearable/         #   预留入口(build-profile 中注释保留)
├── bank_rates/                   # 站外利率研究脚本(Python)
└── build-profile.json5           # 多环境 product + 签名配置(CLI 的核心输入)

三层职责边界清晰:common 不含 UI 资源(DepositData 的注释写着 “UI resources must not be stored in this model”),features 互相不横向依赖,products 只做设备形态差异。这样后续把 pc/tv 模块重新启用时,业务代码不用动。

2.3 用 build-profile.json5 定义 7 套环境

这是 CLI 工作流的地基。根 build-profile.json5 定义了 7 个 product,全部指向同一份代码,差异只在签名配置:

{
  app: {
    signingConfigs: [
      {
        name: 'device',                    // 调试签名:DevEco 自动生成于 ~/.ohos/config/
        type: 'HarmonyOS',
        material: { certpath: '...cer', profile: '...p7b', storeFile: '...p12', ... }
      },
      {
        name: 'default',                   // 发布签名:手工三件套(见第七节)
        type: 'HarmonyOS',
        material: { certpath: './dis.cer', profile: './disRelease.p7b', storeFile: './dis.p12', ... }
      }
    ],
    products: [
      { name: 'default', signingConfig: 'device', compatibleSdkVersion: '6.0.1(21)', targetSdkVersion: '6.1.0(23)', runtimeOS: 'HarmonyOS', ... },
      { name: 'dev',     signingConfig: 'default', ... },
      { name: 'uat',     signingConfig: 'default', ... },
      { name: 'prod',    signingConfig: 'default', ... },
      // uat_mirror / icsl / beta 同理
    ],
    buildModeSet: [{ name: 'debug' }, { name: 'release' }],
  }
}

三个要点:

  • default product 用调试签名device 配置,证书由 DevEco Studio 自动申请,落在 ~/.ohos/config/),日常真机自测不用手工管证书;
  • dev/uat/prod 等环境用同一套发布签名default 配置),测试包与上架包的签名主体一致,避免「测试环境能装、上架包被拒签」这类问题;
  • 统一开启 strictMode.useNormalizedOHMUrl,模块间 import 走规范化路径,为发布构建的 tree-shaking 与混淆铺路。

依赖版本统一收口在根目录 VersionFile.json5(hypium/hamock/imageknife 版本),由 oh-package.json5parameterFile 机制引用。改一处版本,全工程生效。

三、阶段一:核心业务的数据层

Git 提交 3d0a4c1 新增存款8045bc6 持久化数据+卡片 显示当时的实现顺序:模型 → 持久化 → 卡片,UI 最后才接上。这是本项目最值得照搬的做法:把领域逻辑写成不依赖 UI 的纯函数,让页面、数据库、备份、卡片四处复用。

3.1 纯数据模型与利息算法(common/model/DepositData.ets)

export interface DepositData {
  id: number;
  bankCode: string;
  bankName: string;
  depositNo: string;        // v2 新增:存单号(见 3.2 迁移)
  principal: number;
  interestRate: number;     // 年利率 %
  termMonths: number;
  term: string;
  startDate: string;        // 'YYYY-MM-DD'
  endDate: string;
  generatedInterest: number; // 已产生利息(派生)
  maturityInterest: number;  // 到期利息(派生)
  isMatured: boolean;        // 派生
  createdAt: number;
}

// 到期利息:本金 × 年利率 × 期限月数 / 12
export function calculateDepositMaturityInterest(deposit: DepositData): number {
  return roundMoney(deposit.principal * deposit.interestRate / 100 * deposit.termMonths / 12);
}

// 已产生利息:到期利息 × 已存天数 / 总天数(封顶于到期日)
export function calculateDepositGeneratedInterest(deposit: DepositData, currentTime: number = Date.now()): number {
  const startTime = parseLocalDate(deposit.startDate);
  const endTime = parseLocalDate(deposit.endDate);
  const currentDay = getLocalDayStart(currentTime);
  if (Number.isNaN(startTime) || Number.isNaN(endTime) || endTime <= startTime || currentDay <= startTime) {
    return 0;
  }
  const interestEndTime = Math.min(currentDay, endTime);
  const elapsedDays = Math.floor((interestEndTime - startTime) / MILLISECONDS_PER_DAY);
  const termDays = Math.floor((endTime - startTime) / MILLISECONDS_PER_DAY);
  const maturityInterest = calculateDepositMaturityInterest(deposit);
  return roundMoney(maturityInterest * elapsedDays / termDays);
}

三个细节:roundMoney 统一四舍五入到分,金额不在 UI 层再算;日期用本地零点对齐(getLocalDayStart),避免时区导致的「跨日少一天利息」;派生字段用同一个 currentTime 计算(calculateDepositData),列表内所有存单的利息口径才一致。

3.2 加密关系型数据库 + Schema 迁移(common/database/DepositRdbHelper.ets)

const STORE_CONFIG: relationalStore.StoreConfig = {
  name: 'deposit.db',
  securityLevel: relationalStore.SecurityLevel.S3,   // 资金相关数据:高安全等级
  encrypt: true,                                     // 落盘加密
};

private async migrateSchema(store: relationalStore.RdbStore): Promise<void> {
  const versionText = await this.getMeta(store, DB_SCHEMA_VERSION_KEY);
  const version = versionText === undefined ? 0 : Number(versionText);
  if (version >= DB_SCHEMA_VERSION) { return; }
  store.beginTransaction();
  try {
    // 加列前先 PRAGMA table_info 判断列是否存在——新装用户建表语句已含该列
    if (version < 2 && !(await this.hasColumn(store, TABLE_NAME, 'depositNo'))) {
      await store.executeSql(MIGRATION_ADD_DEPOSIT_NO);
    }
    await store.insert(META_TABLE_NAME,
      { metaKey: DB_SCHEMA_VERSION_KEY, metaValue: String(DB_SCHEMA_VERSION) },
      relationalStore.ConflictResolution.ON_CONFLICT_REPLACE);
    store.commit();
  } catch (error) {
    store.rollBack();
    throw new Error(error.message);
  }
}

配套一张 DepositMetaTable 元数据表存 schema 版本号,迁移走事务、失败回滚。v1→v2 的真实案例是 f3b3743 增加存单号:用户要记录银行的存单编号,一条 ALTER TABLE ADD COLUMN depositNo TEXT NOT NULL DEFAULT '' 完成在线升级,老数据自动补空串,备份导入端也做了 normalizeDepositNo(undefined) → '' 的兼容。

3.3 业务编排层 DepositManager:单飞初始化 + 失败重试

export class DepositManager {
  private static initializing: Promise<DepositData[]> | undefined;

  public static async initialize(context: Context): Promise<DepositData[]> {
    DepositManager.prepareStorage();
    if (DepositManager.initializing) { return DepositManager.initializing; }  // 单飞
    const initializing = DepositManager.load(context);
    DepositManager.initializing = initializing;
    try { return await initializing; }
    finally { if (DepositManager.initializing === initializing) { DepositManager.initializing = undefined; } }
  }

  private static async insertWithRetry(repository, deposit) {
    try { await repository.insert(deposit); }
    catch (error) {                            // SQLite busy/锁场景:延迟 80ms 重试一次
      await DepositManager.waitBeforeRetry();
      await repository.insert(deposit);
    }
  }

  private static publish(deposits: DepositData[]): void {
    AppStorage.setOrCreate<DepositData[]>(DEPOSIT_STORAGE_KEY, [...deposits]);  // 发布到全局状态
  }
}

每次写操作后都会 publishAppStorage 并触发 DepositWidgetManager.refreshAll() 刷新桌面卡片。页面、卡片、备份三条消费路径共用同一份数据出口。

四、阶段二:UI 层

4.1 应用壳与隐私门

MainTabPage(真正的启动页)在进入主界面前先过隐私授权:

private async handlePrivacyConsent(): Promise<void> {
  if (DEBUG) {                                 // debug 构建跳过,方便自测
    await this.enterMainPage();
    return;
  }
  const currentResults = privacyManager.getAppPrivacyResult();
  if (!this.isPrivacyAgreed(currentResults)) {
    const consentResult = await privacyManager.requestAppPrivacyConsent(this.context);
    if (!this.isPrivacyAgreed(consentResult.results)) {
      await this.terminateApp();               // 拒绝隐私协议 → 退出应用
      return;
    }
  }
  await this.enterMainPage();
}

隐私协议本体托管在 AGC(应用市场云端托管),module.json5 中声明:

metadata: [
  { name: 'client_id', value: '6917611939258371956' },          // 华为账号服务所需
  { name: 'appgallery_privacy_hosted', value: '1' },              // 隐私协议托管开关
  { name: 'appgallery_privacy_link_privacy_statement',
    value: 'https://agreement-drcn.hispace.dbankcloud.cn/...' }   // 必须为 https
]

收益很直接:隐私协议内容变更不需要发版,在 AGC 后台改完即对全体用户生效。审核被拒后改协议文案,重试成本极低。

4.2 五 Tab 结构(features/abilitycommon/view/HomeView.ets)

┌─────────────────────────────────────────────┐
│  存款(Home)  利率(Sample)  [+]  分析  我的   │
│  ComponentHomeView  PracticeHomeView  —  ExplorationHomeView  MineHomeView │
└─────────────────────────────────────────────┘
  • 存款:汇总卡片(本金总额/本息/已产生利息/到期利息/进行中-已到期数)+ 存单卡片列表;
  • 利率:162 家银行挂牌利率,支持期限筛选与银行名搜索;
  • [+]:中央按钮不切 Tab,onContentWillChange 拦截并弹出 AddDepositSheet 半模态(bindSheet + SheetKeyboardAvoidMode.TRANSLATE_AND_RESIZE);
  • 分析:基于全部存单的银行本金占比饼图 + 各银行本金/利息柱状图,可按年查看;
  • 我的:华为账号登录、备份导入导出、Premium 会员、设置与关于。

Tab 容器用 HdsTabs + preloadItems 预加载首屏之外的 Tab,冷启动到可交互的体感好于懒加载方案。

4.3 添加存单:银行利率与表单的联动(AddDepositSheet.ets)

表单的关键交互是选银行 → 选期限 → 自动带出利率,且利率可手改(对应提交 7909046 手动利率;银行实际执行利率常与挂牌不同,这个改动来自真实用户反馈):

@State selectedBankIndex: number = 0;
@State selectedPeriodIndex: number = 0;
@State interestRate: string = BANK_RATE_LIST[0].rates[0].value.replace('%', '');

private updateInterestRateFromSelection(): void {
  const selectedRate = BANK_RATE_LIST[this.selectedBankIndex].rates[this.selectedPeriodIndex];
  this.interestRate = selectedRate.value.replace('%', '');   // 自动带出,可被用户覆盖
}

// 保存时统一用 calculateDepositData 以同一时间基准派生利息字段
const deposit: DepositData = calculateDepositData({
  id: createdAt, bankCode: bank.id.toString(), bankName: bank.name,
  depositNo: this.depositNo.trim(), principal: amountValue, interestRate: rateValue,
  termMonths, term: rate.period,
  startDate: this.formatStorageDate(this.depositDate),
  endDate: this.formatStorageDate(maturityDate), ...
});

五、阶段三:数据从哪来,162 家银行利率

利率数据不可能靠手敲。项目根目录的 bank_rates/ 是一条站外研究流程:

bank_rates/
├── fetch_bank_rates.py     # 抓取脚本
├── rates.json              # schema_version: 2,含数据来源分级
├── rates.csv / report.md   # 人工核对报告
└── .workbuddy/memory/      # 定时自动化运行记录(利率巡检)

rates.json 的头部设计值得照抄,数据分级、来源留痕:

{
  "schema_version": 2,
  "source_note": "official=银行官网直采;webfetch=官网经网页抓取通道核实;snapshot=同花顺合作数据库快照",
  "stats": { "banks_total": 18, "official": 1, "webfetch": 3, "snapshot": 14 },
  "rates": [ { "bank": "中国银行", "update_date": "2025-05-20", "source": "snapshot", "fixed_1y": 0.95, ... } ]
}

研究成果最终写死进端侧常量 features/devpractices/.../InterestRateData.ets。离线优先:应用不需要网络权限就能展示利率(对应提交 3289661 删除网络权限,上架的权限审查面随之缩小):

// 162 家银行:18 家精确核验 + 144 家沿用主流城商行档位
export const BANK_RATE_LIST: BankRate[] = [
  new BankRate(1, '中国工商银行', $r('app.media.bank_icbc'), R_6BIG),
  new BankRate(6, '中国邮政储蓄银行', $r('app.media.bank_psbc'), R_PSBC),
  new BankRate(22, '北京银行', $r('app.media.bank_ext_bjbank'), R_BJ),
  new BankRate(19, '深圳农村商业银行', $r('app.media.bank_ext_srcb')),  // 缺省 DEFAULT_RATES
  // ... 共 162 条
];

源码头部注释记录了每档利率的核验时间与口径(国有六大行 2025-05-20 集体调降、北京银行 2026-05-28 起执行……)。提交 6e566fb 利率更新 说明这条流程在持续运转:调研(rates.json)→ 写入端侧常量(InterestRateData.ets)→ 发版。

六、阶段四:进阶能力

6.1 桌面服务卡片

卡片页(products/phone/ets/widget/pages/WidgetCard.ets)用 @LocalStorageProp 接收数据:

@Entry(depositWidgetStorage)
@Component
struct WidgetCard {
  @LocalStorageProp('totalPrincipalAndInterest') totalPrincipalAndInterest: string = '¥0.00';
  @LocalStorageProp('totalInterest') totalInterest: string = '¥0.00';
  @LocalStorageProp('activeCount') depositActiveCount: string = '0';
  ...
}

宿主侧 DepositWidgetManager 负责推送:

public static async refreshAll(context: Context, deposits: DepositData[]): Promise<void> {
  // FormRdbHelper 查询本应用所有已添加卡片的 formId,逐一 formProvider.updateForm
  const forms = await FormRdbHelper.getInstance(context).queryFormByName(DEPOSIT_WIDGET_NAME);
  await Promise.all(forms.map((item) => DepositWidgetManager.updateForm(item.formId, deposits)));
}

DepositManager 的每次增删改都会调用 refreshAll,卡片与主界面天然一致,不需要额外同步机制。

6.2 备份导入导出:把不信任的输入当攻击面

DepositTransferService 通过系统 DocumentViewPicker 选择/保存 JSON 文件。导入侧的校验做法可以照搬:

  • 文件大小上限 5 MB、记录数上限 10000 条;
  • schemaVersion 必须匹配,字段逐项类型/范围校验(金额 0~1e12、利率 0~100、日期正则 + 可解析、id 去重);
  • 全量替换走事务,校验失败整体拒绝,不留半脏数据。

6.3 Premium 会员与华为应用内支付(IAP)

免费用户限 3 笔存单(FREE_DEPOSIT_LIMIT = 3),超出时拦截保存并弹出付费墙:

const membership = await PremiumMembershipService.load(abilityContext);
if (membership.productId !== LIFETIME_PRODUCT_ID) {
  this.pendingDeposit = deposit;        // 暂存用户已填好的存单
  this.showPaywallAfterAddSheet = true; // 先弹付费墙
  this.showAddDepositSheet = false;
  return false;
}

购买完成后在 handlePaywallDisappear 中把 pendingDeposit 落库。用户付费后不用重新填写表单,转化过程没有断点。IAP 使用 @kit.IAPKitNONCONSUMABLE(一次买断)商品 com.xiaobingkj.cunying.lifetime,支持 queryProducts 拉取本地化价格与恢复购买。

6.4 意图框架与快捷方式

module.json5 配置了 shortcuts(长按图标直达「添加存单」),HomeView.jumpPage() 解析 wantParams.shortCutKey === 'AddDepositPage' 直开弹窗;同时接入 InsightIntent(小艺)意图执行器。这类系统级入口容易被纯图形界面开发忽略,但对上架后的自然量有实际帮助。

七、阶段五:CLI 调试、签名与打包

7.1 日常调试:一条命令跑通

# 清理
hvigorw clean --no-daemon

# 构建调试 HAP(default product → 调试自动签名)
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon
# 产物:products/phone/build/default/outputs/default/phone-default-signed.hap

# 推设备、安装、启动
hdc file send products/phone/build/default/outputs/default/phone-default-signed.hap \
  data/local/tmp/phone-default-signed.hap
hdc shell bm install -p data/local/tmp/phone-default-signed.hap
hdc shell aa start -a EntryAbility -b com.xiaobingkj.cunying -m phone

三个习惯:--no-daemon 让构建进程即起即灭,CI/脚本环境不会残留守护进程;不知道参数怎么写时,看 DevEco Studio 构建日志里打印的命令照抄;日志调试用 hdc shell hilog | grep DepositManager,项目的 Logger 带 [DepositManager] 这类 TAG,过滤很快。

7.2 发布签名三件套

AGC(AppGallery Connect)上完成「我的项目 → 证书、APP ID 及 Profile」流程后拿到三个文件,本项目的处置方式是直接入库进项目根目录(私有仓库场景;开源或多人协作时应改为 CI 注入):

文件说明项目中的落位
dis.cer发布数字证书./dis.cer
dis.p12密钥库(含私钥)./dis.p12
disRelease.p7b发布 Profile(含权限/服务声明)./disRelease.p7b,2026-08-28 更新过一次(新增 IAP 服务后的重新签发)

对应 build-profile.json5signingConfigs.defaultsignAlg: SHA256withECDSA)。Profile 是会过期、会失效的:每次在 AGC 上新增服务(如 IAP、账号)或调整权限声明,都要重新生成 p7b 并替换。提交 aea89ee Update disRelease.p7b 记录了这次踩坑。

7.3 发布打包:HAP 与 App 包

# 1. 模块级:构建 release HAP(prod 环境 + 发布签名)
hvigorw assembleHap --mode module -p product=prod -p buildMode=release --no-daemon
# 产物:products/phone/build/prod/outputs/prod/phone-prod-signed.hap
#        products/phone/build/prod/outputs/prod/phone-prod-unsigned.hap

# 2. 工程级:打包 .app(多 HAP/上架提交格式)
hvigorw assembleApp --mode project -p product=prod -p buildMode=release --no-daemon
# 产物:build/outputs/prod/sample_in_harmonyos_deposit-prod-signed.app

release 构建同时开启代码混淆(products/phone/obfuscation-rules.txt):

-enable-property-obfuscation
-enable-toplevel-obfuscation
-enable-export-obfuscation
-keep
../../features/abilitycommon/src/main/ets/util/ContextConfig.ets
-keep-property-name
articleInfo

注意 -keep 白名单:被反射/JSON 序列化触达的文件与属性要保留,否则混淆后运行时才炸。这个清单是踩坑后逐步长出来的。

八、阶段六:提交应用市场与审核

8.1 上架前的合规清单

检查项存盈的实际做法
隐私协议AGC 云端托管(appgallery_privacy_hosted=1),改文案不发版
隐私授权门启动即弹 requestAppPrivacyConsent,拒绝则退出
权限最小化删除网络权限,利率全离线;仅保留必要权限且逐项给 reason
应用内容合规645b517 修复审核问题、去掉范小兵、登录华为账号:个人开发者信息不得出现在应用内,账号体系改用华为账号
内购合规所有付费走 IAP,商品在 AGC 配置(com.xiaobingkj.cunying.lifetime
软件著作权「存盈鸿蒙版」V1.0.9:2026-08-06 开发完成并首次发表,源程序 78234 行、文档 121 页、操作手册 + 前30/后30页代码打印稿

审核反馈的修复节奏在 Git 里看得清楚:3fa44e8 修复启动失败cba2676 1.0.3645b517 修复审核问题b89d102 1.0.9。每次被拒都收敛为一次小版本,不把多类修改混在一个大版本里,重提审的 diff 始终可解释。

8.2 版本演进

1.0.3 (2026-08-05)  首版过审上架
1.0.9 (2026-08-16)  审核问题修复 + 华为账号登录 + 隐私托管
1.0.x (2026-08-28)  IAP 内购上线 + 联系方式
1.1.5 (2026-09-13)  利率数据更新 + 存单号字段(含 DB schema v2 迁移)

app.json5 中 versionCode 与 versionName 同步递增(1.1.5 对应 1000015),每个 versionCode 只能提交一次,规则一旦定死就不能变。

九、可复用经验

#经验本项目证据
1别从零造轮子:官方大型开源示例(一多架构 + HDS + 系统能力接入)能省掉大部分架构决策fork sample_in_harmonyos,124 提交完成从 fork 到上架
2数据层先行,纯函数下沉:领域计算不碰 UI,页面/RDB/备份/卡片四处复用DepositData 纯模型 + calculate* 函数族
3多环境用 product 表达,不改代码:7 套环境一个签名分界(调试/发布)build-profile.json5 products
4把 CLI 当唯一构建入口:图形界面只是编辑器,构建命令可复现、可脚本化hvigorw assembleHap/assembleApp 全流程
5离线优先既是体验也是合规:删掉网络权限,审核面直接缩小利率数据写死进端侧常量
6外部数据要有研究流程:调研产物结构化留痕(来源分级/更新时间),再写入代码bank_rates/ → rates.json → InterestRateData.ets
7DB 迁移从第一天就写版本表:晚写一天,线上数据就多一分风险DepositMetaTable + schema v2 迁移
8把不信任的输入当攻击面:导入备份逐字段校验 + 上限 + 事务DepositTransferService.validateBackup
9付费转化不断点:付费墙关闭后自动落库暂存的表单pendingDeposit 机制
10审核反馈小步收敛:一类问题一个小版本,重提审 diff 可解释1.0.3 → 1.0.9 → 1.1.5 节奏
11Profile 会失效:AGC 上每新增服务(IAP/账号)都要重签 p7bUpdate disRelease.p7b 提交
12混淆白名单是踩坑清单:反射/序列化路径必须 -keep,尽早开混淆obfuscation-rules.txt

十、结语

存盈的完整时间线:2025-06 fork 开源基座,2026-07-13 起两周内完成核心业务(模型/持久化/卡片/图表/本息计算),2026-08-05 首版过审,2026-08-16 华为账号与合规修复,2026-08-28 IAP 上线,2026-09-13 发布 v1.1.5 并备齐软著申请材料。

一个人、一套 CLI、一个 fork 来的工程结构,两个月走完从零到上架再到商业化的全过程。回头看,管用的就三条:让领域逻辑成为纯函数,让构建流程成为一行命令,让合规长在架构里而不是留到提审前再补。


附:项目关键路径索引:common/src/main/ets/{model,service,database}/(数据层)、features/abilitycommon/src/main/ets/view/HomeView.ets(应用壳)、features/devpractices/.../InterestRateData.ets(利率数据)、features/exploration/.../AnalysisContent.ets(图表)、features/mine/.../{PaywallView,PremiumMembershipService}.ets(IAP)、products/phone/(entry 与卡片)、bank_rates/(利率研究)、build-profile.json5(多环境与签名)。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐