# 04 触发器与函数（数据库承载的业务规则）

本节汇总数据库内实现的关键触发器与函数，说明：

- 它们解决什么业务问题
- 触发时机是什么
- 对数据一致性与性能的影响
- 典型使用/触发示例

---

## 1. 通用触发器：自动维护 `updated_at`

### 1.1 `public.update_updated_at_column()`（complete 脚本）

- **目的**：统一把 `updated_at` 设置为当前时间，避免应用层漏写。
- **触发时机**：`BEFORE UPDATE`

典型触发表（complete 脚本中出现）：

- `ml_user_profiles`
- `ml_user_addresses`
- `ml_products`
- `ml_product_skus`
- `ml_shops`
- `ml_orders`
- `ml_shopping_cart`

触发效果示例：

```sql
update public.ml_products
set name = '新标题'
where id = '...product_uuid...'::uuid;

-- updated_at 会自动变为 now()
```

### 1.2 `public.set_updated_at()`（订阅脚本）

订阅模块在 `doc_mall/create_mall_subscription_tables.sql` 里定义了一个更轻量的 `set_updated_at()`，并对：

- `ml_subscription_plans`
- `ml_user_subscriptions`

设置 `BEFORE UPDATE` 触发器。

---

## 2. 地址一致性：默认地址唯一

### `public.ensure_single_default_address()`

- **目的**：保证同一个用户最多只有一个 `is_default = true` 的地址。
- **触发时机**：`BEFORE INSERT OR UPDATE ON ml_user_addresses`
- **核心逻辑**：当新行/更新行被设为默认时，把该用户其他地址全部置为非默认。

示例：

```sql
update public.ml_user_addresses
set is_default = true
where id = '...address_uuid...'::uuid;

-- 触发器会把同 user_id 的其他地址 is_default 设为 false
```

注意事项：

- 这是“业务规则下沉 DB”的典型。
- 若存在并发更新两条地址为默认，最终仍会收敛为“最后提交事务的那条为默认”。

---

## 3. 库存汇总：SKU 维护，SPU 汇总

### `public.update_product_stock()`

- **目的**：当 SKU 改变时，自动汇总刷新商品表的库存字段，避免每次展示都 `join + group by`。
- **触发时机**：`AFTER INSERT OR UPDATE OR DELETE ON ml_product_skus`
- **影响字段**：
  - `ml_products.total_stock`
  - `ml_products.available_stock`

示例：

```sql
update public.ml_product_skus
set stock = 8
where id = '...sku_uuid...'::uuid;

-- 触发器会汇总该 product_id 下所有 status=1 的 SKU stock
-- 并更新到 ml_products.total_stock / available_stock
```

注意事项：

- 这种“汇总字段”设计利于读性能，但写入 SKU 会产生额外更新 SPU 的成本。
- 若 SKU 更新频率很高，需要评估热点商品写放大。

---

## 4. 订单状态副作用（complete 脚本）

### `public.handle_order_status_change()`

- **目的**：订单状态变化时，自动写入关键时间点，并在完成时累计销量。
- **触发时机**：`BEFORE UPDATE ON ml_orders`

核心行为（按脚本逻辑）：

- 从 `order_status: 1 -> 2`：写入 `paid_at = now()`
- 从 `order_status: 2 -> 3`：写入 `shipped_at = now()`
- 从 `order_status: 3 -> 4`：写入 `delivered_at/completed_at = now()`，并累计 `ml_products.sale_count`

示例：

```sql
update public.ml_orders
set order_status = 2
where id = '...order_uuid...'::uuid;

-- paid_at 自动写入
```

销量累计示例（订单完成）：

```sql
update public.ml_orders
set order_status = 4
where id = '...order_uuid...'::uuid;

-- 会对订单明细涉及的商品 sale_count 做累加
```

注意事项：

- 若业务上“支付状态”与“订单状态”不是严格 1->2 的映射，可能需要调整触发条件。
- 累计销量属于“统计字段”，适合下沉 DB；但需要考虑退款/取消是否回滚销量（脚本未体现）。

---

## 5. 生成类函数

### 5.1 `public.generate_order_no()` + `public.ml_order_seq`

- **目的**：生成业务订单号（形如 `MLYYYYMMDD000001`）。
- **依赖**：序列 `ml_order_seq`

示例：

```sql
select public.generate_order_no();
```

建议：

- 若订单号生成需要“并发唯一 + 分库分表友好”，可以进一步引入节点号/随机段。

### 5.2 `public.generate_coupon_code()`

- **目的**：生成券码（脚本中为 `CP` + 8 位随机字符）。

示例：

```sql
select public.generate_coupon_code();
```

注意事项：

- 随机生成+唯一约束在极端高并发下可能出现冲突重试需求（一般可接受）。

---

## 6. 查询辅助函数

### 6.1 `public.get_user_default_address(p_user_id uuid)`

- **目的**：快速获取用户默认地址，并拼接 `full_address`。

示例：

```sql
select *
from public.get_user_default_address('...user_uuid...'::uuid);
```

### 6.2 `public.calculate_cart_total(p_user_id uuid)`

- **目的**：计算用户购物车选中商品的总金额（按 SKU 价 * 数量）。

示例：

```sql
select public.calculate_cart_total('...user_uuid...'::uuid);
```

注意事项：

- 该函数读取多表（cart + sku + product），并包含状态过滤。
- 若购物车行数很多或频繁调用，需要关注执行计划与索引。

### 6.3 `public.get_product_available_stock(p_product_id uuid, p_sku_id uuid default null)`

- **目的**：查询商品或指定 SKU 的可用库存。

示例：

```sql
-- 查 SKU
select public.get_product_available_stock('...product_uuid...'::uuid, '...sku_uuid...'::uuid);

-- 查商品汇总
select public.get_product_available_stock('...product_uuid...'::uuid, null);
```

---

## 7. SEO 相关函数（complete 脚本）

- `get_product_by_cid(p_cid int)`
- `get_category_by_cid(p_cid int)`
- `get_brand_by_cid(p_cid int)`
- `get_shop_by_cid(p_cid int)`
- `generate_seo_url(p_type, p_cid, p_slug)`
- `update_seo_slugs()`

示例：

```sql
select * from public.get_category_by_cid(1001);
select public.generate_seo_url('shop', 88, 'my-shop');
```

注意事项：

- `update_seo_slugs()` 使用正则把名称转为 slug：
  - 适合初始化/批处理
  - 需要注意多语言、重复 slug、空字符等边界
