1. 项目背景与目标
本文围绕珠宝行业(宝石原料、生产、订单、库存、加盟连锁)的业务场景,基于 PostgreSQL 18 设计一套完整的 ERP 与数据仓库(DW)数据库方案。内容覆盖从 OLTP 业务建模到 OLAP 分析建模、从范式化到反范式化、从多租户隔离到行级安全(RLS)的完整链路,适合数据库架构师、后端开发与数据分析人员参考。
2. 总体架构与设计思路
本章介绍整体分层:OLTP 业务库与 DW 分析库的职责划分、Schema 规划、以及从业务表到维度建模的映射关系,帮助读者建立全局视角。
3. 基础码表设计(1NF~5NF 范式化)
讲解宝石品类、贵金属材质、订单状态、工单状态等基础码表的建模方式,说明查找表(Lookup Table)在范式化中的作用与约束设计。
4. 多租户模型:加盟商主表
以加盟商表作为租户隔离键,介绍多租户架构的落地方式,以及租户维度在后续所有业务表中的贯穿设计。
5. 核心业务表设计
本章覆盖宝石原料库存表、首饰款式表、销售订单主从表、生产工单表的设计要点,包括外键约束、业务校验约束(CHECK)、索引策略与注释规范。
6. 产品分类树:三种层次模型对比
分别实现邻接表(Adjacency List)、嵌套集(Nested Set)与闭包表(Closure Table)三种分类树模型,对比各自的查询性能与维护成本,并给出适用场景建议。
7. SCD2 缓慢变化维度设计
以首饰款式维度为例,讲解 SCD2 的历史版本保留机制,包括代理键、有效期字段与当前版本标记的设计。
8. 审计日志与数据校验
介绍全表审计日志表的设计(JSONB 存储变更前后数据),以及数据质量校验存储过程的实现思路。
9. 数据仓库:星型 / 雪花模型
讲解 DW 层的日期维度、加盟商维度、事实表(销售、工单)建模,以及从 OLTP 到 DW 的 ETL 映射关系。
10. 库存告警触发器
实现库存低于安全阈值时自动写入告警日志的触发器函数,并说明触发时机与业务规则。
11. BI 物化视图与报表加速
介绍加盟商销售汇总、月度销售趋势、款式销量排行、库存周转、工单良率等物化视图的设计,以及并发刷新存储过程的实现。
12. RLS 行级安全与租户隔离
讲解 BI 物化视图上的行级安全策略,通过角色与租户会话变量实现数据隔离,保障多租户数据安全。
13. 运维与压测
介绍统计信息收集、AWR 压测存储过程、并发库存扣减等运维脚本的设计思路。
14. 测试数据与验证
提供码表、加盟商、宝石原料、订单、工单、分类树、日期维度等批量测试数据,并说明如何验证触发器、物化视图与 RLS 策略是否生效。
15. 总结与扩展建议
总结整套数据库设计的核心要点,并给出后续扩展方向,如性能调优、数据归档、实时数仓等建议。
sql
/*
================================================================================
Project: Jewelry ERP & Data Warehouse
Database: PostgreSQL 18
Domain: Jewelry Industry (Gem Raw Material, Production, Order, Inventory, Franchise)
Model: 1NF~5NF Normalization, Denormalization, Lookup/Junction Table, SCD2, Hierarchical Tree(Adjacency List / Nested Set / Closure Table), Multi-Tenant, Star/Snowflake DW
Features: Audit Log, Data Validation, ETL, Concurrency Control, Lock Analysis, AWR Pressure Test, RMAN Backup, Statistics Collection, DDD Domain Model, BI Report, Materialized View, RLS Row Level Security, Stock Alert Trigger
Author: Enterprise Database Design
Script Type: Full Single SQL Script, Execute in psql at one time
================================================================================
*/
-- Drop schema if exists for clean rebuild, comment out in production
DROP SCHEMA IF EXISTS jewelry_oltp CASCADE;
DROP SCHEMA IF EXISTS jewelry_dw CASCADE;
CREATE SCHEMA jewelry_oltp;
CREATE SCHEMA jewelry_dw;
SET search_path TO jewelry_oltp, jewelry_dw, public;
-- ==============================================================================
-- 1. Base Lookup Tables (Normalization, 1NF~5NF, lookup tables)
-- ==============================================================================
CREATE TABLE jewelry_oltp.gemstone_type (
gem_type_id SERIAL PRIMARY KEY,
gem_type_code VARCHAR(50) NOT NULL UNIQUE,
gem_type_name VARCHAR(100) NOT NULL,
hardness NUMERIC(4,2),
refractive_index NUMERIC(6,4),
color_desc VARCHAR(200),
create_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT uk_gem_type_code UNIQUE(gem_type_code)
);
COMMENT ON TABLE jewelry_oltp.gemstone_type IS '宝石品类基础码表:钻石、红宝石、蓝宝石、祖母绿等';
COMMENT ON COLUMN jewelry_oltp.gemstone_type.gem_type_code IS '品类编码';
COMMENT ON COLUMN jewelry_oltp.gemstone_type.gem_type_name IS '品类名称';
CREATE TABLE jewelry_oltp.metal_type (
metal_type_id SERIAL PRIMARY KEY,
metal_code VARCHAR(50) NOT NULL UNIQUE,
metal_name VARCHAR(100) NOT NULL,
purity_desc VARCHAR(100),
unit_price NUMERIC(18,4),
create_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_time TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE jewelry_oltp.metal_type IS '贵金属材质码表:18K金、铂金PT950、足金999';
CREATE TABLE jewelry_oltp.order_status (
status_id SERIAL PRIMARY KEY,
status_code VARCHAR(30) NOT NULL UNIQUE,
status_name VARCHAR(80) NOT NULL,
sort_no INT NOT NULL,
create_time TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE jewelry_oltp.order_status IS '订单状态字典:待支付、已支付、生产中、已发货、已取消';
CREATE TABLE jewelry_oltp.work_order_status (
wo_status_id SERIAL PRIMARY KEY,
status_code VARCHAR(30) NOT NULL UNIQUE,
status_name VARCHAR(80) NOT NULL,
create_time TIMESTAMP WITH TIME ZONE DEFAULT now()
);
COMMENT ON TABLE jewelry_oltp.work_order_status IS '工单状态字典:待排产、加工中、质检、返工、完工';
-- ==============================================================================
-- 2. Multi-Tenant Table: Franchisee (多租户加盟商主表)
-- ==============================================================================
CREATE TABLE jewelry_oltp.franchisee (
tenant_id SERIAL PRIMARY KEY,
franchisee_code VARCHAR(50) NOT NULL UNIQUE,
franchisee_name VARCHAR(120) NOT NULL,
contact_person VARCHAR(80),
contact_phone VARCHAR(20),
address TEXT,
is_active BOOLEAN DEFAULT TRUE,
create_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT uk_franchisee_code UNIQUE(franchisee_code)
);
CREATE INDEX idx_franchisee_active ON jewelry_oltp.franchisee(is_active);
COMMENT ON TABLE jewelry_oltp.franchisee IS '多租户加盟商主表,tenant_id为租户隔离键';
-- ==============================================================================
-- 3. Gem Raw Material 宝石原料表(OLTP核心库存表)
-- ==============================================================================
CREATE TABLE jewelry_oltp.gem_raw_material (
raw_id BIGSERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
gem_type_id INT NOT NULL,
certificate_no VARCHAR(100) NOT NULL,
weight_ct NUMERIC(10,3) NOT NULL,
color_grade VARCHAR(30),
clarity_grade VARCHAR(30),
cut_grade VARCHAR(30),
purchase_price NUMERIC(18,4) NOT NULL,
stock_qty INT NOT NULL DEFAULT 0,
supplier_name VARCHAR(120),
purchase_date DATE,
is_deleted BOOLEAN DEFAULT FALSE,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_gem_raw_tenant FOREIGN KEY (tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id),
CONSTRAINT fk_gem_raw_gemtype FOREIGN KEY (gem_type_id) REFERENCES jewelry_oltp.gemstone_type(gem_type_id),
CONSTRAINT uk_gem_certificate UNIQUE(certificate_no),
CONSTRAINT chk_gem_weight CHECK (weight_ct > 0),
CONSTRAINT chk_gem_stock_qty CHECK (stock_qty >= 0)
);
CREATE INDEX idx_gem_raw_tenant ON jewelry_oltp.gem_raw_material(tenant_id);
CREATE INDEX idx_gem_raw_gemtype ON jewelry_oltp.gem_raw_material(gem_type_id);
CREATE INDEX idx_gem_raw_cert ON jewelry_oltp.gem_raw_material(certificate_no);
COMMENT ON TABLE jewelry_oltp.gem_raw_material IS '宝石原料库存主表,业务校验约束:克拉重量>0,库存不能负数';
-- ==============================================================================
-- 4. Jewelry Style 首饰款式表(产品目录)
-- ==============================================================================
CREATE TABLE jewelry_oltp.jewelry_style (
style_id BIGSERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
style_code VARCHAR(60) NOT NULL,
style_name VARCHAR(120) NOT NULL,
metal_type_id INT NOT NULL,
main_gem_type_id INT,
retail_price NUMERIC(18,4),
is_on_sale BOOLEAN DEFAULT TRUE,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_style_tenant FOREIGN KEY (tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id),
CONSTRAINT fk_style_metal FOREIGN KEY (metal_type_id) REFERENCES jewelry_oltp.metal_type(metal_type_id),
CONSTRAINT uk_style_code_per_tenant UNIQUE(tenant_id, style_code)
);
CREATE INDEX idx_style_tenant ON jewelry_oltp.jewelry_style(tenant_id);
COMMENT ON TABLE jewelry_oltp.jewelry_style IS '首饰款式主表,租户+款式码联合唯一';
-- ==============================================================================
-- 5. Sales Order & Order Item (订单主从,Junction关联表)
-- ==============================================================================
CREATE TABLE jewelry_oltp.sales_order (
order_id BIGSERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
order_no VARCHAR(60) NOT NULL UNIQUE,
customer_name VARCHAR(100),
customer_phone VARCHAR(20),
total_amount NUMERIC(18,4) NOT NULL DEFAULT 0,
status_id INT NOT NULL,
order_date TIMESTAMP WITH TIME ZONE DEFAULT now(),
is_cancel BOOLEAN DEFAULT FALSE,
cancel_reason TEXT,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_order_tenant FOREIGN KEY (tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id),
CONSTRAINT fk_order_status FOREIGN KEY (status_id) REFERENCES jewelry_oltp.order_status(status_id),
CONSTRAINT uk_order_no UNIQUE(order_no)
);
CREATE INDEX idx_sales_order_tenant ON jewelry_oltp.sales_order(tenant_id);
CREATE INDEX idx_sales_order_date ON jewelry_oltp.sales_order(order_date);
COMMENT ON TABLE jewelry_oltp.sales_order IS '销售订单主表';
CREATE TABLE jewelry_oltp.sales_order_item (
item_id BIGSERIAL PRIMARY KEY,
order_id BIGINT NOT NULL,
style_id BIGINT NOT NULL,
qty INT NOT NULL,
unit_price NUMERIC(18,4) NOT NULL,
sub_total NUMERIC(18,4) NOT NULL,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_orderitem_order FOREIGN KEY(order_id) REFERENCES jewelry_oltp.sales_order(order_id),
CONSTRAINT fk_orderitem_style FOREIGN KEY(style_id) REFERENCES jewelry_oltp.jewelry_style(style_id),
CONSTRAINT chk_item_qty CHECK(qty >0)
);
CREATE INDEX idx_orderitem_order ON jewelry_oltp.sales_order_item(order_id);
COMMENT ON TABLE jewelry_oltp.sales_order_item IS '订单明细,Junction关联表:订单-首饰款式';
-- ==============================================================================
-- 6. Work Order 生产工单表
-- ==============================================================================
CREATE TABLE jewelry_oltp.work_order (
wo_id BIGSERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
wo_no VARCHAR(60) NOT NULL UNIQUE,
style_id BIGINT NOT NULL,
plan_qty INT NOT NULL,
actual_qty INT DEFAULT 0,
yield_rate NUMERIC(5,4),
wo_status_id INT NOT NULL,
plan_start_date DATE,
plan_finish_date DATE,
actual_finish_date DATE,
is_rework BOOLEAN DEFAULT FALSE,
rework_reason TEXT,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
update_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_wo_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id),
CONSTRAINT fk_wo_style FOREIGN KEY(style_id) REFERENCES jewelry_oltp.jewelry_style(style_id),
CONSTRAINT fk_wo_status FOREIGN KEY(wo_status_id) REFERENCES jewelry_oltp.work_order_status(wo_status_id),
CONSTRAINT chk_wo_plan_qty CHECK(plan_qty>0)
);
CREATE INDEX idx_workorder_tenant ON jewelry_oltp.work_order(tenant_id);
CREATE INDEX idx_workorder_plan_date ON jewelry_oltp.work_order(plan_start_date);
COMMENT ON TABLE jewelry_oltp.work_order IS '首饰加工工单,支持返工业务';
-- ==============================================================================
-- 7. Hierarchical Model: Jewelry Category Tree 产品分类树(3种层次模型)
-- 7.1 Adjacency List 邻接表
-- ==============================================================================
CREATE TABLE jewelry_oltp.category_adjacency (
cat_id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
parent_cat_id INT,
cat_name VARCHAR(100) NOT NULL,
sort_no INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_cat_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id),
CONSTRAINT fk_cat_parent FOREIGN KEY(parent_cat_id) REFERENCES jewelry_oltp.category_adjacency(cat_id)
);
CREATE INDEX idx_cat_adj_tenant ON jewelry_oltp.category_adjacency(tenant_id);
CREATE INDEX idx_cat_adj_parent ON jewelry_oltp.category_adjacency(parent_cat_id);
COMMENT ON TABLE jewelry_oltp.category_adjacency IS '分类树:邻接表模型,最常用,适合少量层级查询';
-- 7.2 Nested Set 嵌套集模型
CREATE TABLE jewelry_oltp.category_nested_set (
cat_id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
cat_name VARCHAR(100) NOT NULL,
lft INT NOT NULL,
rgt INT NOT NULL,
sort_no INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_cat_ns_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id)
);
CREATE INDEX idx_cat_ns_tenant ON jewelry_oltp.category_nested_set(tenant_id);
CREATE INDEX idx_cat_ns_left ON jewelry_oltp.category_nested_set(lft);
CREATE INDEX idx_cat_ns_right ON jewelry_oltp.category_nested_set(rgt);
COMMENT ON TABLE jewelry_oltp.category_nested_set IS '分类树:嵌套集模型,适合大范围子树查询,节点移动成本高';
-- 7.3 Closure Table 闭包表模型
CREATE TABLE jewelry_oltp.category_closure_node (
node_id SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
cat_name VARCHAR(100) NOT NULL,
sort_no INT DEFAULT 0,
is_active BOOLEAN DEFAULT TRUE,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_cat_cls_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id)
);
CREATE TABLE jewelry_oltp.category_closure_relation (
ancestor_id INT NOT NULL,
descendant_id INT NOT NULL,
depth INT NOT NULL,
tenant_id INT NOT NULL,
PRIMARY KEY(ancestor_id,descendant_id,tenant_id),
CONSTRAINT fk_cls_ancestor FOREIGN KEY(ancestor_id) REFERENCES jewelry_oltp.category_closure_node(node_id),
CONSTRAINT fk_cls_descendant FOREIGN KEY(descendant_id) REFERENCES jewelry_oltp.category_closure_node(node_id),
CONSTRAINT fk_cls_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id)
);
CREATE INDEX idx_cat_cls_rel_tenant ON jewelry_oltp.category_closure_relation(tenant_id);
CREATE INDEX idx_cat_cls_rel_desc ON jewelry_oltp.category_closure_relation(descendant_id);
COMMENT ON TABLE jewelry_oltp.category_closure_node IS '分类树闭包表-节点主表';
COMMENT ON TABLE jewelry_oltp.category_closure_relation IS '分类树闭包表-祖先后代关系表,查询性能优秀,维护关系开销中等';
-- ==============================================================================
-- 8. SCD2 缓慢变化维度表(珠宝产品维度SCD2)
-- ==============================================================================
CREATE TABLE jewelry_oltp.dim_jewelry_style_scd2 (
style_sk BIGSERIAL PRIMARY KEY,
style_bk BIGINT NOT NULL,
tenant_id INT NOT NULL,
style_code VARCHAR(60) NOT NULL,
style_name VARCHAR(120) NOT NULL,
metal_type_id INT,
retail_price NUMERIC(18,4),
is_on_sale BOOLEAN,
valid_start TIMESTAMP WITH TIME ZONE NOT NULL,
valid_end TIMESTAMP WITH TIME ZONE NOT NULL,
is_current BOOLEAN DEFAULT TRUE,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_scd2_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id)
);
CREATE INDEX idx_scd2_bk ON jewelry_oltp.dim_jewelry_style_scd2(style_bk);
CREATE INDEX idx_scd2_tenant ON jewelry_oltp.dim_jewelry_style_scd2(tenant_id);
CREATE INDEX idx_scd2_current ON jewelry_oltp.dim_jewelry_style_scd2(is_current);
COMMENT ON TABLE jewelry_oltp.dim_jewelry_style_scd2 IS 'SCD2缓慢变化维度,首饰款式,保留历史版本';
-- ==============================================================================
-- 9. Audit Log 审计日志表
-- ==============================================================================
CREATE TABLE jewelry_oltp.audit_log (
audit_id BIGSERIAL PRIMARY KEY,
tenant_id INT,
table_name VARCHAR(100) NOT NULL,
operation_type VARCHAR(20) NOT NULL,
record_pk TEXT,
old_data JSONB,
new_data JSONB,
db_user VARCHAR(100) DEFAULT current_user,
client_ip TEXT,
operation_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
remark TEXT,
CONSTRAINT fk_audit_tenant FOREIGN KEY(tenant_id) REFERENCES jewelry_oltp.franchisee(tenant_id)
);
CREATE INDEX idx_audit_tenant ON jewelry_oltp.audit_log(tenant_id);
CREATE INDEX idx_audit_table ON jewelry_oltp.audit_log(table_name);
CREATE INDEX idx_audit_time ON jewelry_oltp.audit_log(operation_time);
COMMENT ON TABLE jewelry_oltp.audit_log IS '全表审计日志,记录DML变更,JSONB存储变更前后数据';
-- ==============================================================================
-- 10. Data Warehouse Star / Snowflake Model 珠宝DW 星型模型
-- ==============================================================================
-- 维度表
CREATE TABLE jewelry_dw.dim_date (
date_sk INT PRIMARY KEY,
full_date DATE NOT NULL UNIQUE,
year INT NOT NULL,
month INT NOT NULL,
day INT NOT NULL,
quarter INT NOT NULL,
week_of_year INT NOT NULL,
month_name VARCHAR(20),
is_weekend BOOLEAN
);
COMMENT ON TABLE jewelry_dw.dim_date IS '日期维度表,星型模型';
CREATE TABLE jewelry_dw.dim_franchisee (
franchisee_sk SERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
franchisee_code VARCHAR(50),
franchisee_name VARCHAR(120),
is_active BOOLEAN,
valid_start TIMESTAMP WITH TIME ZONE DEFAULT now(),
valid_end TIMESTAMP WITH TIME ZONE DEFAULT 'infinity'::timestamp with time zone
);
CREATE INDEX idx_dw_fr_tenant ON jewelry_dw.dim_franchisee(tenant_id);
CREATE TABLE jewelry_dw.dim_jewelry (
jewelry_sk BIGSERIAL PRIMARY KEY,
style_bk BIGINT NOT NULL,
style_code VARCHAR(60),
style_name VARCHAR(120),
metal_name VARCHAR(100),
gem_type_name VARCHAR(100),
retail_price NUMERIC(18,4),
valid_start TIMESTAMP WITH TIME ZONE NOT NULL,
valid_end TIMESTAMP WITH TIME ZONE NOT NULL,
is_current BOOLEAN DEFAULT TRUE
);
CREATE INDEX idx_dw_jewelry_bk ON jewelry_dw.dim_jewelry(style_bk);
-- 事实表:销售事实
CREATE TABLE jewelry_dw.fact_sales (
sales_fact_sk BIGSERIAL PRIMARY KEY,
date_sk INT NOT NULL,
franchisee_sk INT NOT NULL,
jewelry_sk BIGINT NOT NULL,
order_no VARCHAR(60),
qty INT NOT NULL,
sales_amount NUMERIC(18,4) NOT NULL,
cost_amount NUMERIC(18,4) NOT NULL,
gross_profit NUMERIC(18,4),
dw_etl_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_fact_date FOREIGN KEY(date_sk) REFERENCES jewelry_dw.dim_date(date_sk),
CONSTRAINT fk_fact_fr FOREIGN KEY(franchisee_sk) REFERENCES jewelry_dw.dim_franchisee(franchisee_sk),
CONSTRAINT fk_fact_jewelry FOREIGN KEY(jewelry_sk) REFERENCES jewelry_dw.dim_jewelry(jewelry_sk)
);
CREATE INDEX idx_fact_sales_date ON jewelry_dw.fact_sales(date_sk);
CREATE INDEX idx_fact_sales_fr ON jewelry_dw.fact_sales(franchisee_sk);
CREATE INDEX idx_fact_sales_jewelry ON jewelry_dw.fact_sales(jewelry_sk);
COMMENT ON TABLE jewelry_dw.fact_sales IS '销售事实表,星型模型核心';
-- 工单事实表
CREATE TABLE jewelry_dw.fact_workorder (
wo_fact_sk BIGSERIAL PRIMARY KEY,
date_sk INT NOT NULL,
franchisee_sk INT NOT NULL,
jewelry_sk BIGINT NOT NULL,
wo_no VARCHAR(60),
plan_qty INT,
actual_qty INT,
yield_rate NUMERIC(5,4),
dw_etl_time TIMESTAMP WITH TIME ZONE DEFAULT now(),
CONSTRAINT fk_wo_fact_date FOREIGN KEY(date_sk) REFERENCES jewelry_dw.dim_date(date_sk),
CONSTRAINT fk_wo_fact_fr FOREIGN KEY(franchisee_sk) REFERENCES jewelry_dw.dim_franchisee(franchisee_sk),
CONSTRAINT fk_wo_fact_jewelry FOREIGN KEY(jewelry_sk) REFERENCES jewelry_dw.dim_jewelry(jewelry_sk)
);
CREATE INDEX idx_fact_wo_date ON jewelry_dw.fact_workorder(date_sk);
-- ==============================================================================
-- 11. ETL 增量抽取存储过程:OLTP -> DW,SCD2拉链
-- ==============================================================================
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_etl_jewelry_scd2_sync()
LANGUAGE plpgsql
AS $$
DECLARE
v_now TIMESTAMP WITH TIME ZONE := now();
BEGIN
-- 标记旧版本过期
UPDATE jewelry_oltp.dim_jewelry_style_scd2
SET valid_end = v_now, is_current = false
WHERE is_current = true
AND style_bk IN (
SELECT style_id FROM jewelry_oltp.jewelry_style
WHERE update_at > (SELECT MAX(valid_start) FROM jewelry_oltp.dim_jewelry_style_scd2)
);
-- 插入新版本SCD2
INSERT INTO jewelry_oltp.dim_jewelry_style_scd2(style_bk,tenant_id,style_code,style_name,metal_type_id,retail_price,is_on_sale,valid_start,valid_end,is_current)
SELECT style_id,tenant_id,style_code,style_name,metal_type_id,retail_price,is_on_sale,v_now,'infinity'::timestamptz,true
FROM jewelry_oltp.jewelry_style
WHERE update_at > (SELECT COALESCE(MAX(valid_start), '1970-01-01'::timestamptz) FROM jewelry_oltp.dim_jewelry_style_scd2);
RAISE NOTICE 'SCD2 Jewelry Style ETL sync finished at %',v_now;
END;
$$;
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_etl_load_dw_fact_sales()
LANGUAGE plpgsql
AS $$
DECLARE
v_etl_time TIMESTAMP WITH TIME ZONE := now();
BEGIN
INSERT INTO jewelry_dw.fact_sales(date_sk,franchisee_sk,jewelry_sk,order_no,qty,sales_amount,cost_amount,gross_profit, dw_etl_time)
SELECT
dd.date_sk,
df.franchisee_sk,
dj.jewelry_sk,
so.order_no,
soi.qty,
soi.sub_total,
soi.qty * (js.retail_price * 0.45),
soi.sub_total - (soi.qty * (js.retail_price * 0.45)),
v_etl_time
FROM jewelry_oltp.sales_order so
JOIN jewelry_oltp.sales_order_item soi ON so.order_id = soi.order_id
JOIN jewelry_oltp.jewelry_style js ON soi.style_id = js.style_id
JOIN jewelry_dw.dim_date dd ON dd.full_date = so.order_date::date
JOIN jewelry_dw.dim_franchisee df ON df.tenant_id = so.tenant_id AND df.is_active = true
JOIN jewelry_dw.dim_jewelry dj ON dj.style_bk = js.style_id AND dj.is_current = true
WHERE so.create_at > (SELECT MAX(dw_etl_time) FROM jewelry_dw.fact_sales);
RAISE NOTICE 'ETL fact_sales loaded, time:%',v_etl_time;
END;
$$;
-- ==============================================================================
-- 12. 业务存储过程:库存扣减、订单取消、工单返工、业务补偿
-- ==============================================================================
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_gem_stock_deduct(p_tenant_id INT, p_raw_id BIGINT, p_deduct_qty INT)
LANGUAGE plpgsql
AS $$
DECLARE
v_current_stock INT;
BEGIN
IF p_deduct_qty <=0 THEN
RAISE EXCEPTION '扣减数量必须大于0';
END IF;
SELECT stock_qty INTO v_current_stock FROM jewelry_oltp.gem_raw_material WHERE raw_id = p_raw_id AND tenant_id = p_tenant_id FOR UPDATE;
IF v_current_stock IS NULL THEN
RAISE EXCEPTION '原料记录不存在';
END IF;
IF v_current_stock < p_deduct_qty THEN
RAISE EXCEPTION '库存不足,当前库存:%,扣减:%',v_current_stock,p_deduct_qty;
END IF;
UPDATE jewelry_oltp.gem_raw_material
SET stock_qty = stock_qty - p_deduct_qty, update_at = now()
WHERE raw_id = p_raw_id AND tenant_id = p_tenant_id;
RAISE NOTICE '库存扣减成功,raw_id:%,扣减数量:%',p_raw_id,p_deduct_qty;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE '库存扣减失败,执行回滚';
RAISE;
END;
$$;
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_order_cancel(p_order_id BIGINT)
LANGUAGE plpgsql
AS $$
DECLARE
v_tenant_id INT;
BEGIN
SELECT tenant_id INTO v_tenant_id FROM jewelry_oltp.sales_order WHERE order_id = p_order_id FOR UPDATE;
IF v_tenant_id IS NULL THEN
RAISE EXCEPTION '订单不存在';
END IF;
UPDATE jewelry_oltp.sales_order
SET is_cancel = true, cancel_reason='业务取消',update_at=now()
WHERE order_id = p_order_id;
RAISE NOTICE '订单取消成功 order_id:%',p_order_id;
EXCEPTION WHEN OTHERS THEN
RAISE;
END;
$$;
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_workorder_rework(p_wo_id BIGINT, p_rework_reason TEXT)
LANGUAGE plpgsql
AS $$
BEGIN
UPDATE jewelry_oltp.work_order
SET is_rework=true, rework_reason=p_rework_reason, update_at=now()
WHERE wo_id = p_wo_id;
RAISE NOTICE '工单标记返工成功 wo_id:%',p_wo_id;
END;
$$;
-- ==============================================================================
-- 13. 数据质量校验 & 脏数据捕获日志表
-- ==============================================================================
CREATE TABLE jewelry_oltp.data_quality_error_log (
error_id BIGSERIAL PRIMARY KEY,
check_module VARCHAR(100),
table_name VARCHAR(100),
error_msg TEXT,
record_key TEXT,
error_data JSONB,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE INDEX idx_dq_module ON jewelry_oltp.data_quality_error_log(check_module);
CREATE INDEX idx_dq_time ON jewelry_oltp.data_quality_error_log(create_at);
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_dq_check_gem_raw()
LANGUAGE plpgsql
AS $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN SELECT raw_id, certificate_no, weight_ct, stock_qty FROM jewelry_oltp.gem_raw_material
LOOP
IF rec.certificate_no IS NULL OR TRIM(rec.certificate_no)='' THEN
INSERT INTO jewelry_oltp.data_quality_error_log(check_module,table_name,error_msg,record_key,error_data)
VALUES ('GemRawCheck','gem_raw_material','证书号为空',rec.raw_id::text,jsonb_build_object('certificate_no',rec.certificate_no));
END IF;
IF rec.weight_ct <=0 THEN
INSERT INTO jewelry_oltp.data_quality_error_log(check_module,table_name,error_msg,record_key,error_data)
VALUES ('GemRawCheck','gem_raw_material','克拉重量<=0',rec.raw_id::text,jsonb_build_object('weight_ct',rec.weight_ct));
END IF;
END LOOP;
RAISE NOTICE '宝石原料数据质量校验完成';
END;
$$;
-- ==============================================================================
-- 14. 库存告警日志表 + 触发器函数 + 触发器
-- ==============================================================================
CREATE TABLE jewelry_oltp.stock_alert_log (
alert_id BIGSERIAL PRIMARY KEY,
tenant_id INT NOT NULL,
raw_id BIGINT NOT NULL,
gem_type_name VARCHAR(100),
certificate_no VARCHAR(100),
current_stock_qty INT NOT NULL,
safety_threshold INT NOT NULL,
alert_level VARCHAR(20),
alert_msg TEXT,
create_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);
CREATE INDEX idx_stock_alert_tenant ON jewelry_oltp.stock_alert_log(tenant_id);
CREATE INDEX idx_stock_alert_time ON jewelry_oltp.stock_alert_log(create_at);
CREATE OR REPLACE FUNCTION jewelry_oltp.fn_gem_stock_low_alert()
RETURNS TRIGGER
LANGUAGE plpgsql
AS $$
DECLARE
v_safety_threshold INT := 5;
v_gem_type_name VARCHAR(100);
BEGIN
IF NEW.stock_qty <= v_safety_threshold THEN
SELECT gem_type_name INTO v_gem_type_name FROM jewelry_oltp.gemstone_type WHERE gem_type_id = NEW.gem_type_id;
INSERT INTO jewelry_oltp.stock_alert_log(
tenant_id,raw_id,gem_type_name,certificate_no,current_stock_qty,safety_threshold,alert_level,alert_msg
)
VALUES (
NEW.tenant_id,
NEW.raw_id,
v_gem_type_name,
NEW.certificate_no,
NEW.stock_qty,
v_safety_threshold,
'WARNING',
CONCAT('宝石原料【',NEW.certificate_no,'】库存不足,当前库存:',NEW.stock_qty,',安全阈值:',v_safety_threshold)
);
END IF;
RETURN NEW;
END;
$$;
CREATE TRIGGER trg_gem_raw_stock_alert
AFTER INSERT OR UPDATE OF stock_qty
ON jewelry_oltp.gem_raw_material
FOR EACH ROW
EXECUTE FUNCTION jewelry_oltp.fn_gem_stock_low_alert();
-- ==============================================================================
-- 15. BI物化视图(BI报表加速)
-- ==============================================================================
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_franchisee_sales_summary AS
SELECT
f.tenant_id,
f.franchisee_name,
COUNT(DISTINCT so.order_id) AS total_order_cnt,
SUM(so.total_amount) AS total_sales_amt,
AVG(so.total_amount) AS avg_order_amount,
COUNT(soi.item_id) AS sales_item_cnt
FROM jewelry_oltp.franchisee f
LEFT JOIN jewelry_oltp.sales_order so ON f.tenant_id = so.tenant_id
LEFT JOIN jewelry_oltp.sales_order_item soi ON so.order_id = soi.order_id
GROUP BY f.tenant_id, f.franchisee_name
ORDER BY total_sales_amt DESC;
CREATE UNIQUE INDEX idx_mv_fr_sales_tenant ON jewelry_oltp.mv_bi_franchisee_sales_summary(tenant_id);
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_monthly_sales_trend AS
SELECT
DATE_TRUNC('month',so.order_date) AS order_month,
COUNT(DISTINCT so.order_id) AS order_count,
SUM(so.total_amount) AS sales_amount,
COUNT(DISTINCT so.customer_name) AS customer_cnt
FROM jewelry_oltp.sales_order so
GROUP BY DATE_TRUNC('month',so.order_date)
ORDER BY order_month;
CREATE INDEX idx_mv_sales_month ON jewelry_oltp.mv_bi_monthly_sales_trend(order_month);
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_style_sales_rank AS
SELECT
js.style_id,
js.style_code,
js.style_name,
js.tenant_id,
SUM(soi.qty) AS sale_qty,
SUM(soi.sub_total) AS sale_amount
FROM jewelry_oltp.jewelry_style js
LEFT JOIN jewelry_oltp.sales_order_item soi ON js.style_id = soi.style_id
GROUP BY js.style_id,js.style_code,js.style_name,js.tenant_id
ORDER BY sale_amount DESC;
CREATE INDEX idx_mv_style_tenant ON jewelry_oltp.mv_bi_style_sales_rank(tenant_id);
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_gem_stock_turnover AS
WITH gem_consume AS (
SELECT
grm.tenant_id,
grm.gem_type_id,
gt.gem_type_name,
SUM(grm.purchase_price * grm.stock_qty) AS stock_total_cost,
SUM(CASE WHEN grm.stock_qty < 0 THEN 0 ELSE grm.stock_qty END) AS total_stock_qty
FROM jewelry_oltp.gem_raw_material grm
LEFT JOIN jewelry_oltp.gemstone_type gt ON grm.gem_type_id = gt.gem_type_id
GROUP BY grm.tenant_id,grm.gem_type_id,gt.gem_type_name
),
gem_monthly_consume AS (
SELECT
tenant_id,
gem_type_id,
SUM(purchase_price) AS monthly_consume_cost
FROM jewelry_oltp.gem_raw_material
WHERE update_at >= now() - INTERVAL '30 days'
GROUP BY tenant_id,gem_type_id
)
SELECT
gc.tenant_id,
gc.gem_type_name,
gc.stock_total_cost,
gc.total_stock_qty,
COALESCE(gmc.monthly_consume_cost,0) AS monthly_consume_cost,
CASE WHEN gmc.monthly_consume_cost >0 THEN ROUND(gc.stock_total_cost / gmc.monthly_consume_cost * 30,2) ELSE 9999.99 END AS stock_turnover_days
FROM gem_consume gc
LEFT JOIN gem_monthly_consume gmc ON gc.tenant_id = gmc.tenant_id AND gc.gem_type_id = gmc.gem_type_id;
CREATE INDEX idx_mv_stock_tenant ON jewelry_oltp.mv_bi_gem_stock_turnover(tenant_id);
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_workorder_yield_stat AS
SELECT
DATE_TRUNC('month',wo.plan_start_date) AS wo_month,
wo.tenant_id,
wos.status_code,
COUNT(wo.wo_id) AS wo_total_cnt,
ROUND(AVG(wo.yield_rate)*100,2) AS avg_yield_pct
FROM jewelry_oltp.work_order wo
LEFT JOIN jewelry_oltp.work_order_status wos ON wo.wo_status_id = wos.wo_status_id
GROUP BY DATE_TRUNC('month',wo.plan_start_date),wo.tenant_id,wos.status_code
ORDER BY wo_month;
CREATE INDEX idx_mv_wo_tenant ON jewelry_oltp.mv_bi_workorder_yield_stat(tenant_id);
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_franchise_dashboard AS
SELECT
f.tenant_id,
f.franchisee_name,
COALESCE(so_stat.order_cnt,0) AS order_total,
COALESCE(so_stat.sales_amt,0) AS sales_total,
COALESCE(stock_stat.gem_stock_value,0) AS gem_stock_value,
COALESCE(wo_stat.avg_yield,0) AS avg_yield_pct
FROM jewelry_oltp.franchisee f
LEFT JOIN (
SELECT tenant_id,COUNT(order_id) order_cnt,SUM(total_amount) sales_amt
FROM jewelry_oltp.sales_order GROUP BY tenant_id
) so_stat ON f.tenant_id = so_stat.tenant_id
LEFT JOIN (
SELECT tenant_id,SUM(purchase_price*stock_qty) gem_stock_value
FROM jewelry_oltp.gem_raw_material GROUP BY tenant_id
) stock_stat ON f.tenant_id = stock_stat.tenant_id
LEFT JOIN (
SELECT tenant_id,ROUND(AVG(yield_rate)*100,2) avg_yield
FROM jewelry_oltp.work_order GROUP BY tenant_id
) wo_stat ON f.tenant_id = wo_stat.tenant_id;
CREATE UNIQUE INDEX idx_mv_franchise_dash_tenant ON jewelry_oltp.mv_bi_franchise_dashboard(tenant_id);
CREATE MATERIALIZED VIEW jewelry_oltp.mv_bi_gem_low_stock_alert AS
SELECT
grm.raw_id,
grm.tenant_id,
f.franchisee_name,
gt.gem_type_name,
grm.certificate_no,
grm.weight_ct,
grm.stock_qty,
grm.purchase_price
FROM jewelry_oltp.gem_raw_material grm
JOIN jewelry_oltp.gemstone_type gt ON grm.gem_type_id = gt.gem_type_id
JOIN jewelry_oltp.franchisee f ON grm.tenant_id = f.tenant_id
WHERE grm.stock_qty <= 5
ORDER BY grm.stock_qty ASC;
CREATE INDEX idx_mv_lowstock_tenant ON jewelry_oltp.mv_bi_gem_low_stock_alert(tenant_id);
-- 物化视图刷新存储过程
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_refresh_all_bi_mv(p_concurrent BOOLEAN DEFAULT true)
LANGUAGE plpgsql
AS $$
BEGIN
IF p_concurrent THEN
REFRESH MATERIALIZED VIEW CONCURRENTLY jewelry_oltp.mv_bi_franchisee_sales_summary;
REFRESH MATERIALIZED VIEW CONCURRENTLY jewelry_oltp.mv_bi_style_sales_rank;
REFRESH MATERIALIZED VIEW CONCURRENTLY jewelry_oltp.mv_bi_gem_stock_turnover;
REFRESH MATERIALIZED VIEW CONCURRENTLY jewelry_oltp.mv_bi_franchise_dashboard;
ELSE
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_franchisee_sales_summary;
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_monthly_sales_trend;
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_style_sales_rank;
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_gem_stock_turnover;
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_workorder_yield_stat;
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_franchise_dashboard;
REFRESH MATERIALIZED VIEW jewelry_oltp.mv_bi_gem_low_stock_alert;
END IF;
RAISE NOTICE '全部BI物化视图刷新完成,并发模式:%', p_concurrent;
END;
$$;
-- ==============================================================================
-- 16. RLS 行级安全:BI物化视图租户隔离
-- ==============================================================================
DO $$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'bi_franchise_user') THEN
CREATE ROLE bi_franchise_user;
END IF;
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'bi_admin') THEN
CREATE ROLE bi_admin;
END IF;
END $$;
ALTER TABLE jewelry_oltp.mv_bi_franchisee_sales_summary ENABLE ROW LEVEL SECURITY;
ALTER TABLE jewelry_oltp.mv_bi_style_sales_rank ENABLE ROW LEVEL SECURITY;
ALTER TABLE jewelry_oltp.mv_bi_gem_stock_turnover ENABLE ROW LEVEL SECURITY;
ALTER TABLE jewelry_oltp.mv_bi_workorder_yield_stat ENABLE ROW LEVEL SECURITY;
ALTER TABLE jewelry_oltp.mv_bi_franchise_dashboard ENABLE ROW LEVEL SECURITY;
ALTER TABLE jewelry_oltp.mv_bi_gem_low_stock_alert ENABLE ROW LEVEL SECURITY;
CREATE POLICY bi_mv_tenant_policy ON jewelry_oltp.mv_bi_franchisee_sales_summary
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY bi_mv_style_tenant_policy ON jewelry_oltp.mv_bi_style_sales_rank
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY bi_mv_stock_tenant_policy ON jewelry_oltp.mv_bi_gem_stock_turnover
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY bi_mv_wo_tenant_policy ON jewelry_oltp.mv_bi_workorder_yield_stat
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY bi_mv_dash_tenant_policy ON jewelry_oltp.mv_bi_franchise_dashboard
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY bi_mv_lowstock_tenant_policy ON jewelry_oltp.mv_bi_gem_low_stock_alert
FOR SELECT USING (tenant_id = current_setting('app.tenant_id')::int);
CREATE POLICY bi_mv_admin_policy ON jewelry_oltp.mv_bi_franchisee_sales_summary
FOR SELECT TO bi_admin USING (true);
CREATE POLICY bi_mv_style_admin_policy ON jewelry_oltp.mv_bi_style_sales_rank
FOR SELECT TO bi_admin USING (true);
CREATE POLICY bi_mv_stock_admin_policy ON jewelry_oltp.mv_bi_gem_stock_turnover
FOR SELECT TO bi_admin USING (true);
CREATE POLICY bi_mv_wo_admin_policy ON jewelry_oltp.mv_bi_workorder_yield_stat
FOR SELECT TO bi_admin USING (true);
CREATE POLICY bi_mv_dash_admin_policy ON jewelry_oltp.mv_bi_franchise_dashboard
FOR SELECT TO bi_admin USING (true);
CREATE POLICY bi_mv_lowstock_admin_policy ON jewelry_oltp.mv_bi_gem_low_stock_alert
FOR SELECT TO bi_admin USING (true);
GRANT SELECT ON
jewelry_oltp.mv_bi_franchisee_sales_summary,
jewelry_oltp.mv_bi_monthly_sales_trend,
jewelry_oltp.mv_bi_style_sales_rank,
jewelry_oltp.mv_bi_gem_stock_turnover,
jewelry_oltp.mv_bi_workorder_yield_stat,
jewelry_oltp.mv_bi_franchise_dashboard,
jewelry_oltp.mv_bi_gem_low_stock_alert
TO bi_franchise_user, bi_admin;
-- ==============================================================================
-- 17. 运维PL/SQL:统计信息、碎片优化
-- ==============================================================================
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_analyze_all_tables()
LANGUAGE plpgsql
AS $$
BEGIN
ANALYZE jewelry_oltp.gem_raw_material;
ANALYZE jewelry_oltp.sales_order;
ANALYZE jewelry_oltp.sales_order_item;
ANALYZE jewelry_oltp.work_order;
ANALYZE jewelry_dw.fact_sales;
ANALYZE jewelry_dw.fact_workorder;
RAISE NOTICE '统计信息收集完成';
END;
$$;
-- ==============================================================================
-- 18. AWR压测存储过程,并发库存扣减
-- ==============================================================================
CREATE OR REPLACE PROCEDURE jewelry_oltp.sp_stock_stress_test(p_tenant_id INT, p_raw_id BIGINT, p_loop_cnt INT, p_deduct_qty INT)
LANGUAGE plpgsql
AS $$
DECLARE
i INT;
BEGIN
FOR i IN 1..p_loop_cnt LOOP
CALL jewelry_oltp.sp_gem_stock_deduct(p_tenant_id,p_raw_id,p_deduct_qty);
END LOOP;
RAISE NOTICE '压测循环执行结束,循环次数:%',p_loop_cnt;
EXCEPTION WHEN OTHERS THEN
RAISE NOTICE '压测发生异常:%',SQLERRM;
END;
$$;
-- ==============================================================================
-- 【新增:批量测试数据插入脚本】
-- 包含:码表、加盟商、宝石原料、首饰款式、销售订单、订单明细、生产工单、3套分类树、日期维度DW数据
-- ==============================================================================
-- 1.基础码表插入
INSERT INTO jewelry_oltp.gemstone_type(gem_type_code,gem_type_name,hardness,refractive_index,color_desc) VALUES
('DIAM','钻石',10.00,2.4170,'白色,D-Z色'),
('RUBY','红宝石',9.00,1.7620,'红色,鸽血红'),
('SAPP','蓝宝石',9.00,1.7620,'皇家蓝、矢车菊蓝'),
('EMERALD','祖母绿',7.50,1.5700,'翠绿色'),
('AMETH','紫水晶',7.00,1.5440,'紫色');
INSERT INTO jewelry_oltp.metal_type(metal_code,metal_name,purity_desc,unit_price) VALUES
('K18','18K黄金','75%黄金',480.0000),
('PT950','铂金PT950','95%铂',320.0000),
('AU999','足金999','99.9%黄金',605.0000),
('K18W','18K白金色','75%黄金镀铑',472.0000);
INSERT INTO jewelry_oltp.order_status(status_code,status_name,sort_no) VALUES
('CREATED','已创建',1),
('PAID','已支付',2),
('PROD','生产中',3),
('SHIPPED','已发货',4),
('CANCEL','已取消',99);
INSERT INTO jewelry_oltp.work_order_status(status_code,status_name) VALUES
('WAIT','待排产'),
('PROD','加工中'),
('QC','质检'),
('REWORK','返工'),
('FINISH','完工');
-- 2.多租户加盟商
INSERT INTO jewelry_oltp.franchisee(franchisee_code,franchisee_name,contact_person,contact_phone,address,is_active) VALUES
('FR001','深圳珠宝一号加盟店','张总','13800138001','深圳市罗湖区水贝珠宝产业园A栋',true),
('FR002','广州番禺珠宝分店','李总','13900139002','广州市番禺大罗塘珠宝城B座',true),
('FR003','杭州珠宝门店','王总','13700137003','杭州市上城区武林路',true);
-- 3.宝石原料测试数据(部分库存<=5,触发库存告警触发器)
INSERT INTO jewelry_oltp.gem_raw_material(tenant_id,gem_type_id,certificate_no,weight_ct,color_grade,clarity_grade,cut_grade,purchase_price,stock_qty,supplier_name,purchase_date) VALUES
(1,1,'GIA202600001',0.500,'D','VS1','EX',3200.0000,12,'钻石国际供应商','2026-01-10'),
(1,1,'GIA202600002',0.300,'F','VVS2','VG',1600.0000,3,'钻石国际供应商','2026-02-15'),
(1,2,'GRS202600003',1.020,'RED','SI','GOOD',4500.0000,4,'红宝石矿业','2026-03-01'),
(2,1,'GIA202600004',0.700,'G','SI1','EX',4100.0000,15,'钻石国际供应商','2026-01-20'),
(2,3,'GRS202600005',0.850,'BLUE','VS','EX',3800.0000,2,'蓝宝石矿业','2026-02-28'),
(3,4,'IGI202600006',1.200,'GREEN','SI','VG',5200.0000,8,'哥伦比亚祖母绿','2026-03-10');
-- 4.首饰款式
INSERT INTO jewelry_oltp.jewelry_style(tenant_id,style_code,style_name,metal_type_id,main_gem_type_id,retail_price,is_on_sale) VALUES
(1,'R001','经典六爪钻戒',1,1,9999.0000,true),
(1,'R002','红宝石吊坠',1,2,6888.0000,true),
(1,'R003','铂金钻石对戒',2,1,5200.0000,true),
(2,'G001','蓝宝石项链',4,3,7500.0000,true),
(2,'G002','足金素圈戒指',3,null,1800.0000,true),
(3,'H001','祖母绿耳钉',1,4,8600.0000,true);
-- 5.销售订单
INSERT INTO jewelry_oltp.sales_order(tenant_id,order_no,customer_name,customer_phone,total_amount,status_id,order_date,is_cancel,cancel_reason) VALUES
(1,'ORD20260401001','陈小姐','13611112222',9999.0000,2,'2026-04-01 10:20:00',false,null),
(1,'ORD20260401002','刘先生','13622223333',6888.0000,3,'2026-04-02 14:10:00',false,null),
(2,'ORD20260402001','赵女士','13633334444',7500.0000,4,'2026-04-03 09:15:00',false,null),
(2,'ORD20260402002','黄先生','13644445555',1800.0000,5,'2026-04-04 16:30:00',true,'客户取消订单'),
(3,'ORD20260403001','周小姐','13655556666',8600.0000,2,'2026-04-05 11:00:00',false,null);
-- 6.订单明细
INSERT INTO jewelry_oltp.sales_order_item(order_id,style_id,qty,unit_price,sub_total) VALUES
(1,1,1,9999.0000,9999.0000),