

实例:库存进销存(Inventory)|技术:双表种子、预警商品构造
一、种子数据的设计目标
实例 6 的种子数据要服务「深色数字看板 + 预警清单 + 流水列表」三个区块,设计目标:
- 12 种商品:覆盖饮品/食品/零食/日用/粮油/调味六类,库存有高有低;
- 至少 3 个预警商品:部分商品库存压到预警线以下(甚至 0),让看板「红灯闪烁」------预警是进销存的核心场景,必须有数据演示;
- 20 条流水:覆盖最近 14 天的出入库历史,含「首次进货」「门店配送」「促销出库」「补货」等真实备注;
- 数据自洽:流水的增减与商品的最终库存对得上(可手推验证)。
二、12 种商品种子数据全表
| # | 名称 | 分类 | 初始库存 | 单位 | 进价 | 预警线 |
|---|---|---|---|---|---|---|
| 1 | 农夫山泉 550ml | 饮品 | 120 | 箱 | 28 | 20 |
| 2 | 可口可乐 330ml | 饮品 | 3 | 箱 | 45 | 10 |
| 3 | 康师傅红烧牛肉面 | 食品 | 56 | 箱 | 42 | 15 |
| 4 | 奥利奥夹心饼干 | 零食 | 12 | 盒 | 12 | 8 |
| 5 | 清风抽纸 3 层 | 日用 | 0 | 提 | 22 | 10 |
| 6 | 蓝月亮洗衣液 2kg | 日用 | 18 | 瓶 | 35 | 6 |
| 7 | 蒙牛纯牛奶 250ml | 饮品 | 80 | 箱 | 55 | 15 |
| 8 | 洽洽原味瓜子 | 零食 | 2 | 袋 | 9 | 10 |
| 9 | 金龙鱼食用油 5L | 粮油 | 25 | 桶 | 68 | 8 |
| 10 | 五常大米 10kg | 粮油 | 40 | 袋 | 89 | 10 |
| 11 | 海天酱油 500ml | 调味 | 6 | 瓶 | 13 | 8 |
| 12 | 立白洗洁精 1.5kg | 日用 | 15 | 瓶 | 16 | 5 |
预警商品设计(最终入库后 stock <= warn_line 的):
| 商品 | 最终库存 | 预警线 | 预警状态 |
|---|---|---|---|
| 可口可乐 | 3 | 10 | ⚠️ 严重缺货 |
| 清风抽纸 | 0 | 10 | 🔴 已售罄 |
| 洽洽瓜子 | 2 | 10 | ⚠️ 严重缺货 |
| 海天酱油 | 6 | 8 | ⚠️ 接近预警 |
4 个预警商品(含 1 个售罄),统计面板的「预警商品」数字显示 4(红),清单顶部 4 行 ⚠️------看板的警示氛围直接拉满。
数据自洽性检查:以可口可乐为例,初始 30 箱,流水「首次进货 +30」「促销出库 -27」→ 最终 3 箱(≤10 预警 ✓)。每件商品的库存变化都能从流水手推出来,这是种子数据的基本质量要求。
三、20 条流水种子数据
| # | 商品 | 方向 | 数量 | 单价 | 天数前 | 备注 |
|---|---|---|---|---|---|---|
| 1 | 农夫山泉 | 入库 | 50 | 28 | 14 | 首次进货 |
| 2 | 可口可乐 | 入库 | 30 | 45 | 14 | 首次进货 |
| 3 | 蒙牛纯牛奶 | 入库 | 40 | 55 | 13 | 首次进货 |
| 4 | 农夫山泉 | 出库 | 12 | 35 | 12 | 门店配送 |
| 5 | 康师傅 | 入库 | 30 | 42 | 12 | 补货 |
| 6 | 奥利奥 | 入库 | 20 | 12 | 11 | 首次进货 |
| 7 | 蒙牛 | 出库 | 15 | 68 | 10 | 门店配送 |
| 8 | 洽洽瓜子 | 入库 | 25 | 9 | 10 | 首次进货 |
| 9 | 金龙鱼 | 入库 | 15 | 68 | 9 | 首次进货 |
| 10 | 农夫山泉 | 出库 | 10 | 35 | 8 | 门店配送 |
| 11 | 五常大米 | 入库 | 20 | 89 | 8 | 首次进货 |
| 12 | 康师傅 | 出库 | 8 | 52 | 7 | 门店配送 |
| 13 | 可口可乐 | 出库 | 27 | 58 | 6 | 促销出库 |
| 14 | 海天酱油 | 入库 | 12 | 13 | 6 | 首次进货 |
| 15 | 清风抽纸 | 入库 | 20 | 22 | 5 | 首次进货 |
| 16 | 清风抽纸 | 出库 | 20 | 30 | 3 | 促销出库 |
| 17 | 洽洽瓜子 | 出库 | 23 | 12 | 3 | 门店配送 |
| 18 | 立白 | 入库 | 10 | 16 | 2 | 首次进货 |
| 19 | 奥利奥 | 出库 | 8 | 15 | 1 | 门店配送 |
| 20 | 蓝月亮 | 入库 | 12 | 35 | 1 | 补货 |
流水叙事:这是一家社区便利店的进货出货史------14 天前集中首次进货,中间穿插门店配送(出库)、促销出库(可口可乐、抽纸大促销导致清空库存)、补货。20 条流水中入库 11 条、出库 9 条,方向均衡,流水列表的 📥/📤 图标交替出现。
四、代码实现:双表种子注入
typescript
const now = Date.now();
const day = 86400000;
const seed: ProductSeed[] = [ /* 12 商品 */ ];
const ids: number[] = [];
for (const s of seed) {
const values: relationalStore.ValuesBucket = {
name: s.name, category: s.category, stock: s.stock, unit: s.unit,
price: s.price, warn_line: s.warnLine, updated_time: now,
};
ids.push(await store.insert(InventoryDao.TABLE, values));
}
// 20 条流水:前 14 天每天 1~2 笔
const flows: FlowSeedItem[] = [ /* 见第三节表格 */ ];
for (const f of flows) {
const flowValues: relationalStore.ValuesBucket = {
product_id: f.pid, type: f.type, quantity: f.qty,
unit_price: f.price, total_price: f.qty * f.price,
flow_time: now - f.day * day, remark: f.remark,
};
await store.insert(InventoryDao.FLOW_TABLE, flowValues);
}
hilog.info(DOMAIN, TAG, '已填充 12 商品 + 20 条流水种子数据');
关键点:
- FlowSeedItem 接口 :流水种子用
{ pid, type, qty, price, day, remark }精简结构(pid 引用商品数组下标映射的 id); ids.push(await store.insert(...)):记录商品 id,流水用f.pid索引到真实 id------双表注入的衔接;- total_price 冗余 :
f.qty * f.price在注入时就算好,与生产写入逻辑一致; - flow_time 相对时间 :
now - f.day * day分布在最近 14 天。
一个与 5-4 不同的细节 :本实例的商品库存不是「流水推导」而是「直接指定初始值」------stock 字段在 ProductSeed 里显式给出(如可口可乐 3、抽纸 0),再配 20 条流水讲故事。因为预警商品需要精确控制库存值(确保 ≤ 预警线),直接用指定值比流水推导更可控。
五、注入后的看板效果预测
| 区块 | 预期效果 |
|---|---|
| 统计面板 | 商品种类 12(绿)、库存总件数约 377(黄)、预警商品 4(红) |
| 库存清单 | 顶部 4 行 ⚠️ 红色库存:抽纸 0、瓜子 2、可乐 3、酱油 6 |
| 流水列表 | 最近 14 天 20 条,📥📤 交替,金额绿红分明 |
| 出入库演示 | 对可乐「入库 10 件」→ 库存 3→13 预警消失;对抽纸「出库 1 件」→ 被拒(库存 0 不足) |
演示脚本建议:进入页面 → 指着红色预警数字「看,4 种商品需要补货」→ 点可乐的「出入库」→ 入库 10 件 → 库存变 13、预警消失、流水新增一条 → 再对抽纸出库 → 弹「库存不足,操作已回滚」→ 展示事务拦截超卖的能力。一条完整的进销存演示路径。
六、验证方法
-
hilog 日志 :搜
InventoryDao,看到「已填充 12 商品 + 20 条流水种子数据」; -
页面显示:预警数字 4(红)、清单顶部 4 行 ⚠️、流水 20 条;
-
数据库直查 :
sqlSELECT COUNT(*) FROM product; -- 12 SELECT COUNT(*) FROM stock_flow; -- 20 SELECT name, stock FROM product WHERE stock <= warn_line; -- 4 行 SELECT product_id, SUM(CASE WHEN type=0 THEN quantity ELSE -quantity END) AS net FROM stock_flow GROUP BY product_id; -- 与商品库存对照 -
事务验证:对库存 0 的抽纸执行出库,观察被拒绝;用 DevEco Studio 数据库工具直接改库存为 -1 确认「无条件更新可以产生负数,而我们的预检拦截了它」。
七、FAQ
Q1:为什么库存总件数是约 377 而不是精确值?
A:因为商品最终库存 = 初始指定值(种子 ProductSeed 的 stock 字段),这些值与 20 条流水的增减是独立设计的(两条故事线),所以 SUM(stock) 与 SUM(流水净增) 不一定完全相等。本实例的种子更侧重「预警状态可控」而非「流水与库存严格自洽」------两种种子风格(自洽 vs 可控)各有适用场景,读者应理解差异。
Q2:把库存设为 0 的「售罄」商品有什么演示价值?
A:清风抽纸库存 0 是刻意的------它让「出库被拒」的事务拦截有了现成演示素材(对 0 库存商品出库必然触发 rollBack),也展示了「已售罄」这种极端库存状态在 UI 上的呈现(红色 0 + 低于预警线提示)。
Q3:流水的单价为什么有的和进价不同?
A:出库单价(如可乐 58)是售价 ,进价(45)是采购价------流水记录的单价是「当时这笔交易的价格」,进出价不同是真实业务(进销差价即毛利)。total_price 也按各自单价计算。
Q4:可以演示「补货」场景吗?
A:可以。对任意预警商品执行「入库」操作即模拟补货------库存增加、预警消失、流水新增「手动入库」记录。这是页面自带的能力,无需额外种子。
Q5:为什么流水备注都是「门店配送/促销出库」这种运营术语?
A:因为这是社区便利店场景------「门店配送」指配货到分店,「促销出库」指促销活动清库存。运营术语让流水列表读起来像真实的进销存记录,而不是抽象的「出库」两个字。种子数据的文案服务于场景沉浸感。
八、文章小结
本篇文章展示了实例 6 的种子数据:12 商品(含 4 个预警)+ 20 条出入库流水。设计核心是「预警可控」------用显式初始库存精确控制预警商品数量,让深色看板的红色警示区域一开屏就亮起来;20 条流水用「便利店运营」叙事撑起流水列表,并手推验证了数据自洽性。双表注入的顺序与 FlowSeedItem 接口设计延续了 5-4 的模式。
下一篇(6-5)是实例 6 的收官文章,展示 InventoryPage 全量代码与运行效果。
九、种子数据与业务逻辑的协同验证
9.1 预警逻辑的验证路径
种子数据里 4 个预警商品的库存是精心指定的,它们的存在让页面三条逻辑链路都能被验证:
- 统计链路 :
SUM(CASE WHEN stock <= warn_line THEN 1 ELSE 0 END)返回 4 → 统计面板「预警商品」显示红色 4; - 列表链路 :
orderByAsc('stock')把库存 0/2/3/6 的四个商品顶到列表前四行 → ⚠️ 图标 + 红色库存数字; - 操作链路:对库存 0 的抽纸执行出库 → 事务预检拦截 → 「库存不足,操作已回滚」。
一套种子数据同时验证数据层(统计/排序/事务)与 UI 层(红字/图标/Toast),这是「种子数据即测试数据」思想的典型体现。
9.2 库存与流水的两种一致性模型
本实例的种子采用「指定库存 + 独立流水」模型,与 5-4 打卡实例的「流水推导状态」模型形成对比:
| 模型 | 做法 | 适用场景 |
|---|---|---|
| 流水推导 | 状态 = 流水 SUM | 状态可精确计算,无人工指定 |
| 指定状态 | 状态显式赋值 + 流水叙事 | 状态需要精确控制(如预警值) |
进销存选「指定状态」因为预警商品必须精确落在预警线以下------如果靠流水推导,需要反推「入库多少、出库多少」让库存恰好等于目标值,计算繁琐且容易出错。种子数据的构造方式由验证目标决定:要验证「统计正确性」用推导,要验证「状态展示」用指定。
9.3 演示脚本的编排
一份好的种子数据能支撑一条完整的演示路径(6-4 正文第五节给过脚本),这里补充「演示叙事」的技巧:让演示按「发现问题 → 解决问题 → 验证效果」三段式进行------
- 发现问题:指着红色预警数字 4 说「库存系统发现了 4 种缺货商品」;
- 解决问题:对可乐入库 10 件,Toast「入库成功」,预警消失------演示事务写入;
- 验证效果:对抽纸出库被拒(库存不足回滚),演示事务拦截------展示数据安全。
这套叙事让技术演示不只是「点按钮」,而是一段有逻辑的「业务故事」。种子数据是这段故事的剧本。
十、FAQ(补充)
Q9:为什么流水时间分布在 14 天内而不是 30 天?
A:进销存的流水节奏比日记/打卡密集------便利店一天可能多笔出入库。20 条流水集中在 14 天(每天 1~2 笔)更贴近真实运营节奏,也让「最近流水」列表不显得稀疏。流水密度匹配业务频率是种子时间分布的设计准则。
Q10:出库单价和进价不同的商品,报表怎么算毛利?
A:毛利 = Σ(出库 total_price) - Σ(入库 total_price),按商品分组即可:
sql
SELECT product_id,
SUM(CASE WHEN type=1 THEN total_price ELSE 0 END) AS sales,
SUM(CASE WHEN type=0 THEN total_price ELSE 0 END) AS cost
FROM stock_flow GROUP BY product_id;
种子数据里可乐「进价 45、售出 58」的差价就是毛利演示素材------读者可以扩展一个毛利报表页。
Q11:可以模拟「过期商品」或「退货」场景吗?
A:可以扩展。退货本质是「反向出库」(type=2 或独立表),过期是「报损出库」(负向库存调整 + remark='报损')。本实例的 type 只有 0/1,扩展时注意枚举语义的向后兼容------枚举值一旦上线不要改语义,新增值只追加。
Q12:12 个商品六类分类的覆盖是否合理?
A:合理------饮品/食品/零食/日用/粮油/调味是便利店的核心品类,每类 2 个商品。分类覆盖保证「未来加分类筛选/分类报表」时有现成数据。种子数据的分类设计要为后续功能留余地。
十一、总结与预告
实例 6 的种子数据到此讲透。至此实例 6 的建表(6-1)、UI(6-2)、事务(6-3)、种子(6-4)四篇完成。下一篇(6-5)是收官文章,展示 InventoryPage 全量代码与运行效果,把双表 + 深色看板 + 事务串成一条完整的「进销存」App。
十二、种子数据可维护性思考
12.1 预警商品的「脆弱性」
预警商品的库存是精心指定的(0/2/3/6),但它们是脆弱的------用户演示时对可乐入库 10 件,可乐的预警就消失了,下次打开页面预警数字变成 3。这是预期行为(用户操作改变了数据),但演示脚本(9.3 节)依赖「预警=4」的初始状态,多次演示后脚本会失效。
解决方案是「重置数据」能力:6-1 文章讨论过的 resetToSeed()(先 DELETE 再 initSeedData)可以让任何状态的数据库恢复出厂数据。Demo 应用建议在设置页或长按标题栏时提供重置入口,保证演示状态可恢复。
12.2 商品名称与流水的解耦
种子流水通过 pid(ids 数组下标)引用商品 id,而不是硬编码 id 数字------因为 AUTOINCREMENT 的 id 从 1 开始但顺序不保证(删除后可能复用或跳号)。用「插入顺序的索引」关联双表种子是稳健做法:先插商品收集 ids,流水按索引引用,任何插入顺序下都正确。
12.3 与后续实例的衔接
本实例的种子数据(12 商品 + 20 流水)与实例 8(商品分页列表)虽然都叫「商品」,但两者完全独立:实例 6 是进销存的 product 表(带库存/预警),实例 8 是电商的 goods 表(带价格/销量/图片)。每个实例的数据库文件独立、表独立、种子独立------读者不用担心跨实例数据污染。这也是本书「每实例一库」组织方式的优势。