# 08 数据一致性边界：数据库保证什么？应用层还需要保证什么？

本节的目的：把“责任边界”讲清楚，避免团队误以为数据库已经覆盖了所有一致性问题。

---

## 1. 数据库层已经显式保证的内容（来自脚本）

### 1.1 约束（Constraints）保证

- **枚举合法性**：大量使用 `CHECK (status IN (...))`
  - 例：`ml_products.status`、`ml_orders.order_status/payment_status/shipping_status`
- **唯一性**：
  - `order_no UNIQUE`
  - `coupon_code UNIQUE`
  - `ml_shopping_cart UNIQUE(user_id, product_id, sku_id)`
  - `ml_shops.merchant_id UNIQUE`（一商家一店）
  - `ml_delivery_tasks.order_id UNIQUE`（一订单一配送任务）

### 1.2 触发器保证

- **`updated_at` 自动维护**：避免应用层漏写
- **默认地址唯一**：`ensure_single_default_address()`
- **SKU → SPU 库存汇总**：`update_product_stock()`
- **订单状态副作用（complete 脚本）**：`handle_order_status_change()` 自动写时间戳，并累计销量

### 1.3 RLS（行级安全）保证

- 用户私有数据仅可访问自己的行（档案/地址/购物车/收藏/浏览/券等）
- 订单允许买家/卖家访问
- 商品公开查询仅限上架

---

## 2. 数据库层“目前未完全覆盖”的一致性问题（需要显式补齐）

> 以下并不意味着当前设计不好，而是典型电商系统里必须明确“谁来保证”。

### 2.1 下单扣库存、防超卖

脚本可见“库存汇总”，但没有看到：

- 下单时对 `ml_product_skus.stock` 的原子扣减
- 订单取消/超时未支付的库存回补
- 库存冻结（预占）机制

**风险**：并发下单可能超卖。

**建议补齐方案**（按复杂度递增）：

- **方案 A（最小补齐）**：提供一个原子扣减函数
  - `update ml_product_skus set stock = stock - :qty where id=:sku_id and stock >= :qty;`
  - 受影响行数为 1 表示扣减成功，否则失败
- **方案 B（常见电商）**：引入“冻结库存”
  - SKU 增加 `reserved_stock` 或独立冻结表
  - 下单冻结、支付确认扣减、取消释放
- **方案 C（审计与对账）**：引入库存流水
  - 每次扣减/回补记录流水，便于审计

### 2.2 支付对账与幂等

脚本中订单有 `payment_status/paid_amount`，但未见：

- 支付流水表（第三方支付回调存证）
- 支付回调幂等键
- 退款流水/退款幂等

**建议**：补交易流水表（或在应用层引入专门支付子系统）并明确幂等策略。

### 2.3 优惠券核销一致性

当前有 `ml_user_coupons.status/used_at/order_id`，但未见：

- “同一张券只能用一次”的强事务保证（除非应用层做 CAS 更新）
- 与订单金额计算的强一致校验

建议：

- 用条件更新核销：`where status=1` 确保并发只成功一次
- 关键核销与订单创建在同一事务内

### 2.4 统计字段回滚

脚本在订单完成时累计 `sale_count`，但未看到：

- 退款/取消是否回滚 `sale_count`

建议：

- 明确统计字段口径：
  - `sale_count` 是“累计成交量”还是“累计下单量”
- 若需可回滚：要补对应触发器/作业，或使用流水聚合。

---

## 3. 建议的边界划分（团队共识）

- **数据库层**：
  - 基础合法性（约束/唯一性）
  - 关键自动维护字段（updated_at、默认地址唯一、库存汇总、SEO 等）
  - 访问控制（RLS）
- **应用层**：
  - 复杂事务（下单扣库存、支付幂等、退款）
  - 业务规则组合（优惠叠加、分摊、拆单、风控）
  - 跨域协调（订阅与订单的统一计费/对账）

---

## 4. 推荐补充的“最小一致性清单”（可用于评审）

- 下单扣减库存是否原子？
- 未支付关闭订单是否回补库存？
- 支付回调是否幂等？
- 退款回调是否幂等？
- 优惠券核销是否并发安全？
- 统计字段口径是否明确、是否需要回滚？
