# 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、空字符等边界