# DevEco CLI实战:鸿蒙App「存盈」从0开发到正式上架
本文基于真实项目
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 协议)。这个决定换来了三样东西:
- 成熟的一多(一次开发、多端部署)三层工程结构;
- HDS(HarmonyOS Design System)组件与 ArkUI 实践:HdsNavigation、HdsTabs、断点自适应、深色模式;
- 华为账号、埋点、推送、卡片、意图框架等系统能力的标准接入范式。这些恰是上架审核常踩的坑,官方示例已经趟平。
代价是做大量减法:删除上游的组件库和样例业务、清理 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' }],
}
}
三个要点:
defaultproduct 用调试签名(device配置,证书由 DevEco Studio 自动申请,落在~/.ohos/config/),日常真机自测不用手工管证书;dev/uat/prod等环境用同一套发布签名(default配置),测试包与上架包的签名主体一致,避免「测试环境能装、上架包被拒签」这类问题;- 统一开启
strictMode.useNormalizedOHMUrl,模块间 import 走规范化路径,为发布构建的 tree-shaking 与混淆铺路。
依赖版本统一收口在根目录 VersionFile.json5(hypium/hamock/imageknife 版本),由 oh-package.json5 的 parameterFile 机制引用。改一处版本,全工程生效。
三、阶段一:核心业务的数据层
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]); // 发布到全局状态
}
}
每次写操作后都会 publish 到 AppStorage 并触发 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.IAPKit 的 NONCONSUMABLE(一次买断)商品 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.json5 的 signingConfigs.default(signAlg: 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.3 → 645b517 修复审核问题 → 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 |
| 7 | DB 迁移从第一天就写版本表:晚写一天,线上数据就多一分风险 | DepositMetaTable + schema v2 迁移 |
| 8 | 把不信任的输入当攻击面:导入备份逐字段校验 + 上限 + 事务 | DepositTransferService.validateBackup |
| 9 | 付费转化不断点:付费墙关闭后自动落库暂存的表单 | pendingDeposit 机制 |
| 10 | 审核反馈小步收敛:一类问题一个小版本,重提审 diff 可解释 | 1.0.3 → 1.0.9 → 1.1.5 节奏 |
| 11 | Profile 会失效:AGC 上每新增服务(IAP/账号)都要重签 p7b | Update 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(多环境与签名)。
更多推荐





所有评论(0)