在这里插入图片描述

在这里插入图片描述

实例:预算管理(Budget)|技术:6 类预算、25 笔支出、超支故事

一、种子数据的设计目标

实例 17 的种子数据要撑起「进度面板 + 超支预警 + 明细列表」,设计目标:

  1. 6 类预算:餐饮/交通/购物/娱乐/住房/学习——每类一条预算;
  2. 25 笔支出:覆盖 6 类(餐饮 9 / 交通 4 / 购物 6 / 娱乐 4 / 住房 1 / 学习 1)——明细列表充实;
  3. 超支故事购物超支(已花 1223 > 预算 2000?否——设计一个超支分类)——需要算清楚;
  4. 时间分布:30 天内(daysAgo 0~30)——明细列表时间递进;
  5. 当前月份: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% → 演示超支预警完整闭环。

五、验证方法

  1. hilog 日志:搜 BudgetDao,看到「已填充 6 类预算 + 25 笔支出种子数据」;
  2. 页面显示:总预算卡 35%/¥2598/¥7500、6 条分类进度、餐饮明细 9 笔;
  3. 数据库直查
    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 外卖
    
  4. 交互验证:记一笔大额支出触发超支、删除支出恢复、切换分类明细联动。

六、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 全覆盖,每类都有「最新一笔」(天数最小的那笔)撑起明细列表首行。

九、种子设计复盘:三个关键取舍

  1. 超支故事留白:种子最高的是购物 61%,刻意不预设超支分类——让「一开屏全绿、现场记一笔触发标红」成为可演示的完整闭环。若种子直接带超支分类,预警逻辑(已花 > 预算)虽一开屏可见,但**「为什么变红、删掉支出又变绿」的因果教学就丢了**——留白 = 把触发权交给用户;
  2. 餐饮笔数最多(9/25 = 36%):餐饮是每日高频消费(三餐 + 奶茶/外卖),9 笔让它成为明细列表最丰富的分类;住房仅 1 笔(电费)按月账单——笔数分布即消费频率的投影
  3. 大额分散、零头真实: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()-MMpadStart(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;)。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐