在这里插入图片描述

实例:订单与明细(Order)|技术:双表种子注入、状态覆盖、总价回填

一、种子数据的设计目标

实例 9 的种子数据要验证「主从嵌套 + 状态流转」,设计目标:

  1. 6 个订单:数量适中,主从列表有内容但不拥挤;
  2. 15 条明细:每单 1~3 件商品,明细区有层次;
  3. 5 种状态全覆盖:待支付/已支付/已发货/已完成/已取消各至少 1 单——状态筛选条每个按钮都有数据;
  4. 总价回填:明细小计之和 = 订单总价(模拟下单时的计算逻辑)。

二、6 个订单种子数据

# 订单号 状态 收货人 电话 地址 时间偏移
1 SO20250601001 0 待支付 张小帅 13911112222 北京市朝阳区建国路 88 号 6 天前
2 SO20250602002 1 已支付 李美美 13922223333 上海市浦东新区张江高科 5 天前
3 SO20250603003 2 已发货 王大壮 13933334444 广州市天河区体育西路 4 天前
4 SO20250604004 3 已完成 赵甜甜 13944445555 深圳市南山区科技园 3 天前
5 SO20250605005 3 已完成 刘一鸣 13955556666 杭州市西湖区文三路 2 天前
6 SO20250606006 4 已取消 陈小雨 13966667777 成都市武侯区天府大道 1 天前

状态覆盖设计:待支付(1 单)、已支付(1 单)、已发货(1 单)、已完成(2 单)、已取消(1 单)——筛选条的 5 个状态按钮都有真实数据。每种状态至少一单是状态类种子数据的基本要求——否则「状态流转演示」找不到起始数据。

收货人叙事:张小帅、李美美、王大壮…每个名字配一个城市地址(北京/上海/广州/深圳/杭州/成都),像一份真实的电商订单簿。订单数据的地理多样性让演示有「全国发货」的代入感。

三、15 条明细种子数据

# 所属订单 商品 单价 数量 小计
1 1 无线蓝牙耳机 299 1 299
2 1 快充数据线 39 2 78
3 1 手机壳 25 1 25
4 2 机械键盘 459 1 459
5 2 电竞鼠标 199 1 199
6 2 鼠标垫 49 1 49
7 3 运动跑鞋 699 1 699
8 3 运动袜 29 3 87
9 4 坚果礼盒 159 2 318
10 4 进口巧克力 89 1 89
11 5 保温杯 129 1 129
12 5 充电宝 149 1 149
13 5 数据收纳包 45 1 45
14 6 折叠雨伞 59 1 59
15 6 帆布包 79 1 79

明细设计:每单 1~3 件,覆盖「单件订单」(订单 4 两件、订单 6 两件)与「多件订单」(订单 1 三件、订单 5 三件)两种形态。商品名都是真实商品(蓝牙耳机、机械键盘、跑鞋),明细区看起来像真实购物车。

总价验证(手推):

  • 订单 1:299 + 78 + 25 = 402
  • 订单 2:459 + 199 + 49 = 707
  • 订单 3:699 + 87 = 786
  • 订单 4:318 + 89 = 407
  • 订单 5:129 + 149 + 45 = 323
  • 订单 6:59 + 79 = 138

四、代码实现:三段式注入

种子注入分三段:插订单(先拿 id)→ 插明细(引用 id)→ 回填总价(SUM 计算)

// 1. 插入 6 个订单(total_amount 先填 0,稍后回填)
const orders: OrderSeed[] = [ /* 见第二节表格 */ ];
for (const o of orders) {
  const orderValues: relationalStore.ValuesBucket = {
    order_no: o.no, status: o.status, total_amount: 0,
    customer: o.customer, phone: o.phone, address: o.address,
    created_time: now - o.daysAgo * day,
  };
  await store.insert(OrderDao.TABLE, orderValues);
}
// 2. 插入 15 条明细(orderId 是表格里的 1~6,对应插入顺序)
const items: ItemSeed[] = [ /* 见第三节表格 */ ];
for (const it of items) {
  const itemValues: relationalStore.ValuesBucket = {
    order_id: it.orderId, product_name: it.productName,
    price: it.price, quantity: it.quantity,
    subtotal: it.price * it.quantity,
  };
  await store.insert(OrderDao.ITEM_TABLE, itemValues);
}
// 3. 回填订单总价:按订单分组 SUM 明细小计
const ordersResult = await store.querySql(`SELECT id FROM ${OrderDao.TABLE}`);
const ids: number[] = [];
while (ordersResult.goToNextRow()) {
  ids.push(ordersResult.getLong(ordersResult.getColumnIndex('id')));
}
ordersResult.close();
for (const id of ids) {
  const sumResult = await store.querySql(
    `SELECT SUM(subtotal) AS total FROM ${OrderDao.ITEM_TABLE} WHERE order_id = ${id}`
  );
  let totalAmount = 0;
  if (sumResult.goToNextRow()) {
    totalAmount = sumResult.getDouble(sumResult.getColumnIndex('total'));
  }
  sumResult.close();
  const values: relationalStore.ValuesBucket = { total_amount: totalAmount };
  const pred = new relationalStore.RdbPredicates(OrderDao.TABLE);
  pred.equalTo('id', id);
  await store.update(values, pred);
}

第三段回填的巧妙之处:不手动算每个订单的总价,而是用 SQL 按订单分组 SUM 明细小计,再逐单 UPDATE——模拟了真实下单的「总价 = Σ明细」逻辑,同时保证种子数据的总价与明细严格自洽。种子数据用 SQL 反推关联值,比手填更可靠(手填容易算错)。

ItemSeed 的 orderId 语义items 数组里的 orderId 是 1~6——它与订单插入顺序对应(第一个插入的订单 id=1)。依赖 AUTOINCREMENT 从 1 递增是可行的(新库首次插入),但不绝对保险;更稳的做法是用 ids[] 数组收集插入返回值再引用(实例 6 用过)。本实例用固定 1~6 简化,读者注意这个假设。

五、注入后的页面效果预测

区块 预期效果
状态统计条 全部 6 · 待支付 1 · 已支付 1 · 已发货 1 · 已完成 2 · 已取消 1
订单列表 6 张卡片,每张内嵌 1~3 行明细,总价分别为 402/707/786/407/323/138
状态标签 待支付橙 / 已支付蓝 / 已发货紫 / 已完成绿 / 已取消灰
操作按钮 订单 1:取消 + 支付;订单 2/3:推进按钮;订单 4/5/6:仅删除

演示脚本:指着订单 1「SO20250601001 待支付」→ 点「→ 已支付」→ 状态变蓝、统计条更新(待支付 0、已支付 2)→ 再点两次推进到「已发货」「已完成」→ 展示状态机流转。点「取消订单」把订单 6 已取消的流程再走一遍——五种状态的完整流转演示。

六、验证方法

  1. hilog 日志:搜 OrderDao,看到「已填充 6 单 15 条明细种子数据」;
  2. 页面显示:6 张订单卡 + 15 行明细 + 状态统计条数字(1/1/1/2/1);
  3. 数据库直查
    SELECT COUNT(*) FROM orders;      -- 6
    SELECT COUNT(*) FROM order_item;  -- 15
    SELECT o.order_no, o.total_amount, SUM(i.subtotal) AS calc
    FROM orders o LEFT JOIN order_item i ON i.order_id = o.id
    GROUP BY o.id;
    -- total_amount 与 calc 每行相等(总价自洽验证)
    SELECT status, COUNT(*) FROM orders GROUP BY status;  -- 0:1 1:1 2:1 3:2 4:1
    

七、FAQ

Q1:为什么订单 4 和 5 都是「已完成」?
A:两个已完成订单让「已完成」状态有 2 条数据——展示「同一状态多订单」的列表形态,也让筛选「已完成」时列表不至于只有一行。状态分布 1/1/1/2/1 比全 1 更有层次。

Q2:订单号 SO20250601001 是写死的吗?
A:是写死的(种子数据要可读可验证),真实下单用 SO${Date.now()} 动态生成(9-3 文章讲过)。种子用「可读的固定单号」便于演示和 SQL 查询对照。

Q3:总价回填为什么要查一遍再更新?
A:因为插入明细时订单的 total_amount 是 0(先填 0 占位)。回填用「SUM 明细 → UPDATE」保证总价与明细自洽——如果手填总价,可能填错或与明细不一致。让数据库算,不让手算

Q4:可以演示「下单」吗(页面有下单功能吗)?
A:数据层 createOrder 已就绪,但页面未提供下单 UI(聚焦订单管理与状态流转)。读者可自行扩展:悬浮「下单」按钮 + 表单(收货人 + 商品行)+ createOrder + refresh。种子数据是下单结果的展示,扩展下单后能形成完整闭环。

Q5:明细的 subtotal 和订单 total_amount 是冗余的吗?
A:是,但目的不同——subtotal 是「单件商品小计」(明细行展示),total_amount 是「订单合计」(卡片展示)。两者都冗余(可从价格×数量推导),但高频展示场景冗余换取零计算。展示层的冗余与业务层的规范:总价由 SUM 回填保证一致性。

八、文章小结

本篇文章展示了实例 9 的种子数据:6 订单(5 状态全覆盖)+ 15 明细(1~3 件/单)+ SQL 回填总价。核心是「状态全覆盖」与「总价自洽」两个设计——状态覆盖让筛选条和流转演示有数据支撑,SQL 回填让主从数据严格一致。三段式注入(先主后从再回填)是「一对多种子」的标准流程。

下一篇(9-5)是实例 9 的收官文章,展示 OrderPage 全量代码与运行效果。

九、种子数据速查:六单十五明细全景图

正文(一~八节)已完整演示设计目标与三段式注入,本节把 6 单 15 明细整理成速查手册:订单维度、明细维度两张表 + 插入顺序 + 状态覆盖 + 效果预测 + 验证 SQL + FAQ,实现时对照本节即可。

9.1 订单维度速查(订单号/客户/状态/金额)

订单号 客户 状态 金额(回填后) 明细数
SO20250601001 张小帅 0 待支付 402 3
SO20250602002 李美美 1 已支付 707 3
SO20250603003 王大壮 2 已发货 786 2
SO20250604004 赵甜甜 3 已完成 407 2
SO20250605005 刘一鸣 3 已完成 323 3
SO20250606006 陈小雨 4 已取消 138 2

金额列即「SUM(subtotal) 回填」后的 total_amount;明细数合计 3+3+2+2+3+2 = 15,与「6 单 15 明细」严格对上。

9.2 明细维度速查(15 行)

所属订单 商品 单价 数量 小计
1 无线蓝牙耳机 299 1 299
1 快充数据线 39 2 78
1 手机壳 25 1 25
2 机械键盘 459 1 459
2 电竞鼠标 199 1 199
2 鼠标垫 49 1 49
3 运动跑鞋 699 1 699
3 运动袜 29 3 87
4 坚果礼盒 159 2 318
4 进口巧克力 89 1 89
5 保温杯 129 1 129
5 充电宝 149 1 149
5 数据收纳包 45 1 45
6 折叠雨伞 59 1 59
6 帆布包 79 1 79

9.3 主从种子的插入顺序:先主后从、收集 id

主从(一对多)种子有一条铁律:先插主表、收集自增 id,再插从表、用收集到的 id 作外键。三段式完整流程:

// 段一:先插订单(主),insert 返回值即自增 id,收集进 orderIds
const orderIds: number[] = [];
for (const o of orderSeeds) {
  const orderId = await store.insert(OrderDao.TABLE, toOrderBucket(o));
  orderIds.push(orderId);          // 收集 id,供明细引用
}
// 段二:再插明细(从),order_id 引用收集到的 id,而非写死 1~6
for (let i = 0; i < itemSeeds.length; i++) {
  const it = itemSeeds[i];
  const bucket: relationalStore.ValuesBucket = {
    order_id: orderIds[it.orderIndex - 1],   // 关键:用收集的 id
    product_name: it.productName, price: it.price,
    quantity: it.quantity, subtotal: it.price * it.quantity,
  };
  await store.insert(OrderDao.ITEM_TABLE, bucket);
}
// 段三:回填 total_amount(SUM 明细),见第四节

为什么收集 id 而不是写死 1~6:写死只在「全新空库首次插入」时成立;一旦库里有历史数据,自增 id 已前进,写死的 order_id 会指错订单甚至触发外键约束失败。收集 id 让种子在任何库状态下都自洽——这是主从种子的通用姿势。

9.4 五种状态覆盖一览

状态 覆盖订单 界面行为
待支付 0 SO…001 取消 + 支付按钮(流转起点)
已支付 1 SO…002 推进「发货」按钮
已发货 2 SO…003 推进「完成」按钮
已完成 3 SO…004、SO…005 终态:仅删除
已取消 4 SO…006 终态:仅删除

分布 1/1/1/2/1:筛选条 5 个按钮全有数据;流转演示从订单 1 一路推到已完成,订单 6 演示取消分支——五种状态的全路径都能走通

9.5 注入后的主从列表效果预测

界面区块 注入后预期
状态统计条 全部 6 · 待支付 1 · 已支付 1 · 已发货 1 · 已完成 2 · 已取消 1
订单列表 6 张订单卡;订单 1 内嵌耳机/数据线/手机壳 3 行,订单 5 内嵌 3 行,其余 2 行——卡片高度随明细数自然错落
状态标签 待支付橙 / 已支付蓝 / 已发货紫 / 已完成绿 / 已取消灰
操作按钮 订单 1 取消+支付;订单 2/3 各一个推进按钮;订单 4/5/6 仅删除

9.6 验证 SQL:按订单统计明细数

-- 主从数量自洽:每单明细数应为 3/3/2/2/3/2,合计 15
SELECT o.order_no, o.total_amount,
       COUNT(i.id) AS item_count, SUM(i.subtotal) AS calc
FROM orders o
LEFT JOIN order_item i ON i.order_id = o.id
GROUP BY o.id
ORDER BY o.id;

两条断言:① item_count 依次为 3/3/2/2/3/2;② total_amountcalc 每行相等(总价回填自洽)。用 LEFT JOIN 而非 INNER JOIN——无明细的订单会以 item_count=0 现身,孤儿订单一目了然。

9.7 种子 FAQ

Q1:种子数据能重复执行吗?
A:不能直接重复——order_no 有 UNIQUE 约束,重复插入抛约束异常。initSeedData 应先 DELETE FROM order_item; DELETE FROM orders; 清库再注入,或先 SELECT COUNT(*) FROM orders 判断已初始化则跳过(幂等种子)。

Q2:orderIds 收集的 id 顺序一定和订单数组一致吗?
A:是的——单线程按数组顺序逐条 insert,自增 id 严格递增,orderIds[i] 对应 orderSeeds[i]。只要不并发插入,顺序即下标。

Q3:LEFT JOIN 验证时 SUM 为什么不会重复计算?
A:一对多连接中,每个明细只属于一个订单,按订单分组后 SUM(subtotal) 是各明细小计之和,不会像「多对多」那样出现笛卡尔积翻倍。这也是主从模型比多对多简单的地方。

Logo

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

更多推荐