预算廿五笔流水:鸿蒙预算种子数据与超支效果


实例:预算管理(Budget)|技术:6 类预算、25 笔支出、超支故事
一、种子数据的设计目标
实例 17 的种子数据要撑起「进度面板 + 超支预警 + 明细列表」,设计目标:
- 6 类预算:餐饮/交通/购物/娱乐/住房/学习——每类一条预算;
- 25 笔支出:覆盖 6 类(餐饮 9 / 交通 4 / 购物 6 / 娱乐 4 / 住房 1 / 学习 1)——明细列表充实;
- 超支故事:购物超支(已花 1223 > 预算 2000?否——设计一个超支分类)——需要算清楚;
- 时间分布:30 天内(daysAgo 0~30)——明细列表时间递进;
- 当前月份:month 动态取当前月——种子与「本月」语义一致。
二、6 类预算种子数据
| 分类 | 预算(元) | 颜色 | 支出笔数 | 已花合计(元) |
|---|---|---|---|---|
| 餐饮 | 1500 | 橙 #F59E0B | 9 | 622.5 |
| 交通 | 400 | 蓝 #3B82F6 | 4 | 129 |
| 购物 | 2000 | 粉 #EC4899 | 6 | 1223 |
| 娱乐 | 600 | 紫 #8B5CF6 | 4 | 259 |
| 住房 | 2500 | 绿 #10B981 | 1 | 320 |
| 学习 | 500 | 青 #06B6D4 | 1 | 45 |
| 合计 | 7500 | 25 | 2598.5 |
支出明细(25 笔,daysAgo 表示几天前):
餐饮 9 笔(¥622.5):28.5 楼下快餐(30)、45.0 朋友聚餐(27)、15.5 早餐(23)、156.0 火锅(19)、30.0 奶茶(15)、45.5 工作餐(11)、60.0 外卖(7)、32.0 超市便当(4)、210.0 生日聚餐(1)。
交通 4 笔(¥129):12.0 地铁通勤(29)、9.0 公交(21)、12.0 打车(9)、96.0 高铁票(2)。
购物 6 笔(¥1223):198.0 买件外套(28)、260.0 日用品(22)、420.0 球鞋(16)、135.0 书两本(10)、168.0 护肤品(5)、42.0 零食(0)。
娱乐 4 笔(¥259):68.0 电影票(24)、88.0 游戏充值(20)、78.0 KTV(13)、25.0 咖啡(6)。
住房 1 笔(¥320):320.0 电费(25)。
学习 1 笔(¥45):45.0 在线课程(18)。
设计要点:
- 超支故事的设计:购物预算 2000、已花 1223(62%)——没有超支分类? 计算全部:餐饮 622.5/1500(42%)、交通 129/400(32%)、购物 1223/2000(61%)、娱乐 259/600(43%)、住房 320/2500(13%)、学习 45/500(9%)——总支出 2598.5/7500 = 35%,无超支。演示脚本可以现场记一笔大额支出触发超支(如购物记 ¥1000 零食 → 购物 2223 > 2000 超支!)——**「种子不超支、演示触发超支」**让预警更有戏剧性(用户亲手触发);
- 支出集中于餐饮(9 笔最频繁)——真实消费习惯(每日三餐);
- 单笔大额:火锅 156、生日聚餐 210、球鞋 420、高铁票 96——大额支出让金额有层次;
- daysAgo 0~30:覆盖近一个月——明细列表「6月25日 · 零食」到「5月26日 · 楼下快餐」;
- 备注丰富:每笔都有备注(奶茶/火锅/球鞋…)——明细列表「日期 · 备注」可读。
三、代码实现
const now = new Date();
const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
const budgetSeeds: BudgetSeed[] = [
{ category: '餐饮', amount: 1500, color: '#F59E0B' },
// ... 6 类
];
for (const s of budgetSeeds) {
await store.insert(BudgetDao.TABLE, {
category: s.category, amount: s.amount, month: month,
color: s.color, created_time: Date.now(),
});
}
const expenseSeeds: ExpenseSeed[] = [
{ category: '餐饮', amount: 28.5, note: '楼下快餐', daysAgo: 30 },
// ... 25 笔
];
for (const s of expenseSeeds) {
await store.insert(BudgetDao.EXPENSE_TABLE, {
category: s.category, amount: s.amount, note: s.note,
spend_time: Date.now() - s.daysAgo * 86400000,
});
}
month 动态生成:now.getFullYear()-MM(补零)——种子预算属于「当前月」,与页面 currentMonth 一致。spend_time 相对时间:Date.now() - daysAgo * 86400000——daysAgo 转时间戳(教学可读)。幂等保护:查 budget COUNT,非空即返回。
四、注入后的预算面板效果预测
| 区块 | 预期效果 |
|---|---|
| 标题栏 | 「💹 预算管理 · 2024-06 · 预算 vs 实际」 |
| 总预算卡 | 「35%」+「¥2598 / ¥7500」+ 绿色进度条 + 「剩余可用 ¥4901.50」 |
| 分类进度条 | 餐饮 42%(¥623/¥1500)· 交通 32% · 购物 61% · 娱乐 43% · 住房 13% · 学习 9%——全部绿色标签 |
| 明细列表 | 「餐饮 支出明细」9 笔(生日聚餐 -¥210.00 最顶) |
| 切换分类 | 点购物 → 「购物 支出明细」6 笔 |
演示脚本:指着总预算卡说「这个月预算 7500,花了 2598,还剩 4900,进度 35%」→ 指分类「购物花得最多,61%,1223 块——球鞋就 420」→ 点购物看明细「买件外套 198、日用品 260、球鞋 420…」→ 点「+」记一笔「购物 ¥2000 双人沙发」→ 保存 → 购物分类变「超支!」红色、进度条变红、总进度升到 61% → 演示超支预警完整闭环。
五、验证方法
- hilog 日志:搜
BudgetDao,看到「已填充 6 类预算 + 25 笔支出种子数据」; - 页面显示:总预算卡 35%/¥2598/¥7500、6 条分类进度、餐饮明细 9 笔;
- 数据库直查:
SELECT COUNT(*) FROM budget; -- 6 SELECT COUNT(*) FROM expense; -- 25 SELECT category, SUM(amount) FROM expense GROUP BY category; -- 餐饮 622.5 / 交通 129 / 购物 1223 / 娱乐 259 / 住房 320 / 学习 45 SELECT SUM(amount) FROM budget; -- 7500 SELECT SUM(amount) FROM expense; -- 2598.5 SELECT amount, note FROM expense WHERE category = '餐饮' ORDER BY spend_time DESC LIMIT 3; -- 210.0 生日聚餐 / 32.0 超市便当 / 60.0 外卖 - 交互验证:记一笔大额支出触发超支、删除支出恢复、切换分类明细联动。
六、FAQ
Q1:为什么种子数据不预设一个超支分类?
A:预设超支让「一开屏就有红色预警」,但演示超支的完整闭环更有价值——种子全绿 → 现场记一笔大额支出 → 亲手触发超支标红 → 理解「已花 > 预算」的规则。**「种子留白、演示触发」**比预设更有教学效果。
Q2:餐饮 9 笔、住房 1 笔的分布依据?
A:真实消费频率——餐饮每日发生(9 笔 30 天内)、住房按账单(1 笔电费)。支出分布反映消费习惯,也让分类明细列表「餐饮最丰富、住房最单薄」的对比真实。
Q3:为什么购物已花 1223 是最高的?
A:球鞋 420 + 外套 198 + 护肤品 168 + 日用品 260 = 1046(大头)+ 书 135 + 零食 42——购物是「大额非必需品」的聚集地,61% 的进度让「购物」成为演示记大额支出的最佳候选(容易超支)。
Q4:30 天前的支出还属于「本月」吗?
A:种子按「当前月」存预算,但支出不按月过滤(totalForMonth 全表 SUM、明细按分类不过滤月份)——30 天前的支出会算进「本月总支出」。Demo 简化:不做「跨月支出归档」;真实应用需按月过滤支出(WHERE spend_time BETWEEN 月初 AND 月末)。
Q5:daysAgo 0 的「零食」是什么时候?
A:Date.now() - 0 * 86400000 = 今天——「今天刚买的零食」,明细列表时间最顶。daysAgo 0 代表今天,让种子有「最新一笔」的即时感。
Q6:为什么预算 7500、支出 2598.5 这种「零头」?
A:真实感——预算整数(好记)、支出零头(28.5 奶茶、156.0 火锅)。**「预算整数、流水零头」**符合真实财务数据的形态,也让金额显示(-¥28.50、-¥210.00)有变化。
七、文章小结
本篇文章展示了实例 17 的种子数据:6 类预算(7500 元合计)+ 25 笔支出(2598.5 元合计,餐饮 9 笔最频繁)。核心是「超支故事的种子设计」——种子全绿(最高购物 61%),演示时现场记大额支出触发超支预警闭环;预算整数、流水零头的真实形态;30 天时间分布让明细列表有层次。预算面板一开屏进度清晰、明细充实。
下一篇(17-5)是实例 17 的收官文章,展示 BudgetPage 全量代码与运行效果。
八、25 笔支出种子数据全量表
正文里 6 类预算用表格、25 笔支出用文本列举——本节把 25 笔支出补全为**「分类 / 金额 / 备注 / 天数」四列标准表**,与 expenseSeeds 数组一一对应(按分类分组、组内按天数降序):
| # | 分类 | 金额(元) | 备注 | 天数(daysAgo) |
|---|---|---|---|---|
| 1 | 餐饮 | 28.5 | 楼下快餐 | 30 |
| 2 | 餐饮 | 45.0 | 朋友聚餐 | 27 |
| 3 | 餐饮 | 15.5 | 早餐 | 23 |
| 4 | 餐饮 | 156.0 | 火锅 | 19 |
| 5 | 餐饮 | 30.0 | 奶茶 | 15 |
| 6 | 餐饮 | 45.5 | 工作餐 | 11 |
| 7 | 餐饮 | 60.0 | 外卖 | 7 |
| 8 | 餐饮 | 32.0 | 超市便当 | 4 |
| 9 | 餐饮 | 210.0 | 生日聚餐 | 1 |
| 10 | 交通 | 12.0 | 地铁通勤 | 29 |
| 11 | 交通 | 9.0 | 公交 | 21 |
| 12 | 交通 | 12.0 | 打车 | 9 |
| 13 | 交通 | 96.0 | 高铁票 | 2 |
| 14 | 购物 | 198.0 | 买件外套 | 28 |
| 15 | 购物 | 260.0 | 日用品 | 22 |
| 16 | 购物 | 420.0 | 球鞋 | 16 |
| 17 | 购物 | 135.0 | 书两本 | 10 |
| 18 | 购物 | 168.0 | 护肤品 | 5 |
| 19 | 购物 | 42.0 | 零食 | 0 |
| 20 | 娱乐 | 68.0 | 电影票 | 24 |
| 21 | 娱乐 | 88.0 | 游戏充值 | 20 |
| 22 | 娱乐 | 78.0 | KTV | 13 |
| 23 | 娱乐 | 25.0 | 咖啡 | 6 |
| 24 | 住房 | 320.0 | 电费 | 25 |
| 25 | 学习 | 45.0 | 在线课程 | 18 |
表校验:餐饮 9 笔合计 622.5、交通 4 笔 129、购物 6 笔 1223、娱乐 4 笔 259、住房 1 笔 320、学习 1 笔 45——25 笔合计 2598.5,与预算表「已花合计」逐行对得上;daysAgo 0~30 全覆盖,每类都有「最新一笔」(天数最小的那笔)撑起明细列表首行。
九、种子设计复盘:三个关键取舍
- 超支故事留白:种子最高的是购物 61%,刻意不预设超支分类——让「一开屏全绿、现场记一笔触发标红」成为可演示的完整闭环。若种子直接带超支分类,预警逻辑(已花 > 预算)虽一开屏可见,但**「为什么变红、删掉支出又变绿」的因果教学就丢了**——留白 = 把触发权交给用户;
- 餐饮笔数最多(9/25 = 36%):餐饮是每日高频消费(三餐 + 奶茶/外卖),9 笔让它成为明细列表最丰富的分类;住房仅 1 笔(电费)按月账单——笔数分布即消费频率的投影;
- 大额分散、零头真实:420 球鞋、210 生日聚餐、156 火锅、96 高铁票分散在 4 个分类(避免某类全是零头);金额带 .5/.0 零头(28.5、45.5)贴近真实流水,
-¥210.00的格式化显示也有变化。
十、initSeedData 完整代码(month 动态生成)
static async initSeedData(context: common.Context): Promise<void> {
const store = await BudgetDao.getStore(context);
// 幂等保护:已有预算数据则跳过,防止多次启动重复插入
const countPred = new relationalStore.RdbPredicates(BudgetDao.TABLE);
const result = await store.query(countPred);
if (result.rowCount > 0) { result.close(); return; }
result.close();
// month 动态生成:种子属于「当前月」,与页面 currentMonth 语义一致
const now = new Date();
const month = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}`;
// 6 类预算:分类 / 金额 / 颜色
const budgets = [
{ category: '餐饮', amount: 1500, color: '#F59E0B' },
{ category: '交通', amount: 400, color: '#3B82F6' },
{ category: '购物', amount: 2000, color: '#EC4899' },
{ category: '娱乐', amount: 600, color: '#8B5CF6' },
{ category: '住房', amount: 2500, color: '#10B981' },
{ category: '学习', amount: 500, color: '#06B6D4' },
];
for (const b of budgets) {
const values = new relationalStore.ValuesBucket();
values.put('category', b.category);
values.put('amount', b.amount);
values.put('month', month);
values.put('color', b.color);
values.put('created_time', Date.now());
await store.insert(BudgetDao.TABLE, values);
}
// 25 笔支出:分类 / 金额 / 备注 / 天数(与第八节表格一一对应)
const expenses = [
{ category: '餐饮', amount: 28.5, note: '楼下快餐', daysAgo: 30 },
{ category: '餐饮', amount: 45.0, note: '朋友聚餐', daysAgo: 27 },
{ category: '餐饮', amount: 15.5, note: '早餐', daysAgo: 23 },
{ category: '餐饮', amount: 156.0, note: '火锅', daysAgo: 19 },
{ category: '餐饮', amount: 30.0, note: '奶茶', daysAgo: 15 },
{ category: '餐饮', amount: 45.5, note: '工作餐', daysAgo: 11 },
{ category: '餐饮', amount: 60.0, note: '外卖', daysAgo: 7 },
{ category: '餐饮', amount: 32.0, note: '超市便当', daysAgo: 4 },
{ category: '餐饮', amount: 210.0, note: '生日聚餐', daysAgo: 1 },
{ category: '交通', amount: 12.0, note: '地铁通勤', daysAgo: 29 },
{ category: '交通', amount: 9.0, note: '公交', daysAgo: 21 },
{ category: '交通', amount: 12.0, note: '打车', daysAgo: 9 },
{ category: '交通', amount: 96.0, note: '高铁票', daysAgo: 2 },
{ category: '购物', amount: 198.0, note: '买件外套', daysAgo: 28 },
{ category: '购物', amount: 260.0, note: '日用品', daysAgo: 22 },
{ category: '购物', amount: 420.0, note: '球鞋', daysAgo: 16 },
{ category: '购物', amount: 135.0, note: '书两本', daysAgo: 10 },
{ category: '购物', amount: 168.0, note: '护肤品', daysAgo: 5 },
{ category: '购物', amount: 42.0, note: '零食', daysAgo: 0 },
{ category: '娱乐', amount: 68.0, note: '电影票', daysAgo: 24 },
{ category: '娱乐', amount: 88.0, note: '游戏充值', daysAgo: 20 },
{ category: '娱乐', amount: 78.0, note: 'KTV', daysAgo: 13 },
{ category: '娱乐', amount: 25.0, note: '咖啡', daysAgo: 6 },
{ category: '住房', amount: 320.0, note: '电费', daysAgo: 25 },
{ category: '学习', amount: 45.0, note: '在线课程', daysAgo: 18 },
];
for (const e of expenses) {
const values = new relationalStore.ValuesBucket();
values.put('category', e.category);
values.put('amount', e.amount);
values.put('note', e.note);
values.put('spend_time', Date.now() - e.daysAgo * 86400000);
await store.insert(BudgetDao.EXPENSE_TABLE, values);
}
hilog.info(0x0000, 'BudgetDao', '已填充 6 类预算 + 25 笔支出种子数据');
}
month 动态生成:getFullYear()-MM(padStart(2,'0') 补零)——种子永远落在「运行当天所在的月份」,与页面标题 currentMonth 同步;幂等保护:rowCount > 0 直接 return——多次启动不重复插入;spend_time 相对时间:now - daysAgo * 86400000——同一次运行内所有支出时间基准一致,明细按时间倒序呈现「零食(0 天) → 楼下快餐(30 天)」的完整递进。
十一、注入后的进度面板效果预测
| 区块 | 预测效果 | 依据 |
|---|---|---|
| 标题栏 | 「💹 预算管理 · 2024-06 · 预算 vs 实际」 | month 动态生成当前月 |
| 总预算卡 | 35% + ¥2598/¥7500 + 绿色进度条 + 剩余可用 ¥4901.50 | 总支出/总预算 = 2598.5/7500 |
| 分类进度条 | 购物 61%(最高)→ 娱乐 43% → 餐饮 42% → 交通 32% → 住房 13% → 学习 9%,全部绿色标签 | spentByCategory 分类聚合 |
| 明细列表 | 默认餐饮 9 笔,首行「生日聚餐 -¥210.00」 | 按 spend_time 倒序 |
| 切换分类 | 点购物 → 6 笔(球鞋 -¥420.00 最顶) | selectedCategory 联动 |
演示脚本:指总预算卡「这个月预算 7500,花了 2598,剩 4900,进度 35%」→ 指购物分类「61%,花得最多——球鞋 420 占大头」→ 点购物看明细「外套 198、日用品 260、球鞋 420…」→ 点「+」记一笔「购物 ¥2000 双人沙发」→ 保存 → 购物变「超支!」红色、进度条变红、总进度升到 61% → 演示超支预警完整闭环(用户亲手触发)。
十二、验证 SQL
SELECT COUNT(*) FROM budget; -- 6
SELECT COUNT(*) FROM expense; -- 25
SELECT category, COUNT(*), SUM(amount) FROM expense GROUP BY category;
-- 餐饮 9/622.5、交通 4/129、购物 6/1223、娱乐 4/259、住房 1/320、学习 1/45
SELECT SUM(amount) FROM budget; -- 7500
SELECT SUM(amount) FROM expense; -- 2598.5
SELECT note, amount, spend_time FROM expense WHERE category = '购物'
ORDER BY spend_time DESC LIMIT 3; -- 零食 42.0 / 护肤品 168.0 / 书两本 135.0
SELECT month, COUNT(*) FROM budget GROUP BY month; -- 1 行(当前月,验证幂等)
验证要点:① count 两行 = 6/25(种子注入成功);② GROUP BY 的分类合计与预算表「已花合计」逐行一致(数据自洽);③ 购物明细倒序前 3 笔 = 零食/护肤品/书两本(时间排序生效);④ budget 按月 GROUP BY 只有当前月 1 行(month 动态生成 + 幂等保护双重生效)。
十三、FAQ
Q7:25 笔支出为什么不用「日期」而用「天数」?
A:种子要「每次运行都新鲜」——写死日期(如 2024-06-01)会让演示几个月后明细全部变成「过去」,与「本月」语义脱节。daysAgo 相对今天,spend_time = now - daysAgo * 86400000,每次冷启动都落在近 30 天窗口内。
Q8:预算表用「月 + 分类」保证唯一吗?
A:Demo 里没有唯一约束,靠幂等保护兜底(rowCount > 0 跳过)——同一分类不会重复插入。真实应用建议给 (month, category) 建唯一索引双保险。
Q9:种子数据为什么放 Dao 而不是独立文件?
A:种子与 Dao 同文件便于 getStore 复用 + 教学阅读(一个文件看懂建表 + 插入);独立 seed 文件适合种子规模大的项目(10+ 表),本例 2 表 31 条不值得拆。
Q10:验证 GROUP BY 发现购物 1223 对不上?
A:检查是否重复初始化——幂等保护依赖 rowCount > 0,若表结构变更后没清库,旧种子 + 新种子会叠加。改种子先清库(卸载重装,或 DELETE FROM expense; DELETE FROM budget;)。
更多推荐




所有评论(0)