欢迎大家加入开源鸿蒙跨平台开发者社区,一起共建开源鸿蒙跨平台生态。
本文对应模块:
pages.js中转账相关 JS 逻辑(如saveTransfer)、db.js中账户与交易的处理方式,以及这些逻辑如何在首页和账户管理页中体现账户余额变化。
1. 模块目标:保证“钱没变多,但账户余额正确变化”
转账与记收入/记支出最大的差异在于:
- 总资产不变:只是在不同账户之间“搬家”;
- 账户余额要同时变更:一个减,一个加;
- 历史记录要可追踪:方便后续对账和分析。
本模块要梳理的是:从点击“保存转账”开始,到accounts和transactions两张表被正确更新,以及首页仪表板和账户管理页面如何反映这些变化的全过程。
2. 转账入口:saveTransfer 的基本结构
在pages.js中,转账业务的入口函数通常类似如下(示意):
// ==================== 保存转账 ====================asyncsaveTransfer(){constamountInput=document.getElementById('transfer-amount');constamount=parseFloat(amountInput?.value)||0;if(!amount||amount<=0){Toast.error('请输入有效的转账金额');return;}constfromSelect=document.getElementById('transfer-from');consttoSelect=document.getElementById('transfer-to');constfromAccountId=fromSelect?.value;consttoAccountId=toSelect?.value;if(!fromAccountId||!toAccountId||fromAccountId===toAccountId){Toast.error('请选择不同的转出账户和转入账户');return;}constdateInput=document.getElementById('transfer-date');constdate=dateInput?.value||newDate().toISOString().slice(0,10);constremarkInput=document.getElementById('transfer-remark');constremark=remarkInput?.value?.trim()||'';// 交给数据库层处理具体记录与余额联动awaitthis._performTransfer({amount,fromAccountId,toAccountId,date,remark,});Toast.success('转账成功');// 可选:刷新账户页面或仪表板// this.renderPage('accounts');}这里saveTransfer的职责很清晰:
- 从 UI 中获取金额、源账户、目标账户、日期和备注;
- 做最基础的校验(金额有效、账户不为空且不相同);
- 把业务参数打包交给内部
_performTransfer方法处理; - 成功后给出提示,并根据需要刷新界面。
3. 内部转账实现:_performTransfer 如何更新数据
_performTransfer的典型实现思路是:
- 从数据库中取出源账户和目标账户信息;
- 校验源账户余额是否足以支付转账金额;
- 在
transactions表中记录一条或两条转账记录; - 更新
accounts表中两个账户的余额; - 保证整个过程要么全部成功,要么全部失败(理想情况下可以用事务语义模拟)。
示意代码如下(根据项目结构合理还原):
async_performTransfer({amount,fromAccountId,toAccountId,date,remark}){// 1. 获取账户信息constaccounts=awaitwindow.financeDB.getAccounts();constfromAccount=accounts.find(acc=>acc.id===fromAccountId);consttoAccount=accounts.find(acc=>acc.id===toAccountId);if(!fromAccount||!toAccount){Toast.error('账户信息异常,请重试');return;}if(fromAccount.balance<amount){Toast.error('转出账户余额不足');return;}// 2. 构造转账交易记录(你可以选择一条记录标记转出和转入,也可以两条记录)consttransferOut={type:'transfer-out',amount,accountId:fromAccountId,category:'TRANSFER',date,remark:remark||`转出到${toAccount.name}`,};consttransferIn={type:'transfer-in',amount,accountId:toAccountId,category:'TRANSFER',date,remark:remark||`自${fromAccount.name}转入`,};// 3. 写入交易表awaitwindow.financeDB.addTransaction(transferOut);awaitwindow.financeDB.addTransaction(transferIn);// 4. 更新账户余额fromAccount.balance-=amount;toAccount.balance+=amount;awaitwindow.financeDB.updateAccount(fromAccount);awaitwindow.financeDB.updateAccount(toAccount);}说明:具体项目中可能使用不同的
type或分类标记转账,这里采用transfer-out/transfer-in只是为了强调“方向”,实际实现以你仓库里的字段为准。
4. db.js 视角:账户与交易的关系
在db.js中,账户和交易分别由两张表承载:
// 账户表if(!db.objectStoreNames.contains('accounts')){constaccountStore=db.createObjectStore('accounts',{keyPath:'id'});accountStore.createIndex('type','type',{unique:false});accountStore.createIndex('createdAt','createdAt',{unique:false});}// 交易表if(!db.objectStoreNames.contains('transactions')){consttransStore=db.createObjectStore('transactions',{keyPath:'id'});transStore.createIndex('accountId','accountId',{unique:false});transStore.createIndex('type','type',{unique:false});transStore.createIndex('date','date',{unique:false});transStore.createIndex('category','category',{unique:false});}以及对应的操作方法:
// 获取所有账户asyncgetAccounts(){returnthis.getAll('accounts');}// 更新账户asyncupdateAccount(account){account.updatedAt=newDate().toISOString();returnthis.update('accounts',account);}// 添加交易asyncaddTransaction(transaction){transaction.id=this.generateId();transaction.createdAt=newDate().toISOString();returnthis.add('transactions',transaction);}从数据库角度看,转账只是两类操作的组合:
- 向
transactions表里写两条(或一条带方向信息的)记录; - 更新
accounts表中两个账户的balance字段。
在首页仪表板和账户管理页面中,账户余额通常是通过对历史交易进行聚合计算得到的,或者在每次操作时直接维护余额字段。从你当前项目的结构来看,更偏向于在账户对象中直接维护一个balance字段,并在记账/转账时更新它。
5. 首页与账户管理页:如何反映转账结果
5.1 首页仪表板中的总资产
在首页模块中,总资产一般是通过汇总所有账户余额得到的:
constaccounts=awaitwindow.financeDB.getAccounts();consttotal=accounts.reduce((sum,acc)=>sum+acc.balance,0);由于转账操作会同时更新两个账户的balance:
- 一个减
amount; - 一个加
amount;
所以总资产total在数学上保持不变,这也符合“转账不改变总资产”的业务定义。
5.2 账户管理页面中的每个账户余额
账户管理页面通常会列出所有账户及其余额:
asyncloadAccountsPage(){constaccounts=awaitwindow.financeDB.getAccounts();consttotal=accounts.reduce((sum,acc)=>sum+acc.balance,0);// 使用 accounts 渲染账户列表,每项显示名称、类型、余额等}在执行了_performTransfer后:
fromAccount.balance已减去转账金额;toAccount.balance已加上转账金额;- 当
loadAccountsPage重新读取accounts时,就能看到最新的余额信息。
这就是“账户联动”的最直接体现:转账逻辑中的一次更新,被首页和账户管理页面同时感知到。
6. ArkTS 与转账:更多出现在数据导出导入场景
与记收入/记支出相同,转账业务本身完全可以在 Web + IndexedDB 层完成,而 ArkTS 更多出现在:
数据导出:
- 用户希望备份所有账户信息和交易历史(包括转账记录);
- JS 调用
financeDB.exportData()导出当前数据库快照; - 通过
cordova.exec('FileManager', 'exportData', [...])交给 ArkTS 插件写入文件。
数据导入:
- 从备份恢复时,转账记录仍然是
transactions表中的普通记录(可能 type 为 ‘transfer-out’/‘transfer-in’ 或类似标记); - ArkTS 插件负责从文件读取 JSON 字符串;
- JS 调用
financeDB.importData()将这些记录重新写回数据库。
- 从备份恢复时,转账记录仍然是
在这两个场景中,ArkTS 不关心“这笔交易是转账还是普通支出”,它只负责把数据整体搬运进出。而转账的业务含义,主要体现在 JS 层对交易类型和账户余额的处理上。
7. 小结:转账业务逻辑模块的关键要点
综合来看,“转账业务逻辑与账户联动”模块的关键设计点可以归纳为:
入口函数清晰:
- 通过
saveTransfer统一处理输入采集和基础校验; - 所有转账相关逻辑集中在
_performTransfer,便于维护和测试。
- 通过
账户与交易双表联动:
- 每次转账都会在
transactions表中记录对应的交易信息; - 同时更新
accounts表中源账户和目标账户的余额,保证数据一致性。
- 每次转账都会在
与首页仪表板、账户管理页自然联动:
- 首页总资产通过汇总账户余额得到,转账不会改变总额;
- 账户管理页直接展示每个账户的
balance,转账完成后只需重新加载数据即可反映变化。
与 ArkTS / FileManager 插件弱耦合:
- 转账业务完全在 Web + IndexedDB 侧完成;
- ArkTS 只在数据导出/导入时参与,负责文件读写,不干预业务语义。
为后续对账与分析打基础:
- 通过在
transactions表中记录带方向含义的转账记录(transfer-out/transfer-in),将来可以很方便地对某个账户的转入转出进行详细分析。
- 通过在
理解了这个模块后,你就能完整地串起:“用户点下转账按钮 → 两个账户余额变化 → 首页与账户管理页同步更新 → 将来导出这些数据做分析或备份”的整个链路。
ArkTS 侧与转账数据的协同
在导入导出场景中,转账记录和其他交易一样,由 ArkTS FileManager 插件负责落盘和恢复。下面是FileManagerPlugin.ets中导出方法的精简 ArkTS 示例:
import{CordovaPlugin,CallbackContext}from'@magongshou/harmony-cordova/Index';import{PluginResult,MessageStatus}from'@magongshou/harmony-cordova/Index';import{common}from'@kit.AbilityKit';import{fileIo}from'@kit.CoreFileKit';exportclassFileManagerPluginextendsCordovaPlugin{asyncexportData(callbackContext:CallbackContext,args:string[]):Promise<void>{try{// args[0]:前端通过 financeDB.exportData() 打包好的 JSON 字符串constjson=args[0];constcontext=getContext()ascommon.UIAbilityContext;constcacheDir:string=context.cacheDir;constfilePath:string=`${cacheDir}/finance-backup.json`;constfile=awaitfileIo.open(filePath,fileIo.OpenMode.WRITE_ONLY|fileIo.OpenMode.CREATE);awaitfileIo.write(file.fd,json);awaitfileIo.close(file.fd);constresult=PluginResult.createByString(MessageStatus.OK,filePath);callbackContext.sendPluginResult(result);}catch(error){constresult=PluginResult.createByString(MessageStatus.ERROR,(errorasError).message);callbackContext.sendPluginResult(result);}}}在本模块的语境下,这段 ArkTS 代码意味着:
- 所有包含转账在内的交易记录,都会先在 Web 层汇总为一个 JSON;
- 通过
cordova.exec('FileManager', 'exportData', [...])把这份 JSON 交给 ArkTS 插件; - 插件负责选择合适的存储位置写入文件,使得用户可以在设备之间迁移包括“转账历史”在内的全部账本数据。