init: 项目初始提交 - NeoZQYY Monorepo 完整代码

This commit is contained in:
Neo
2026-02-15 14:58:14 +08:00
commit ded6dfb9d8
769 changed files with 182616 additions and 0 deletions

21
db/README.md Normal file
View File

@@ -0,0 +1,21 @@
# db/
## 作用说明
数据库资产目录,集中管理所有数据库的 DDL、迁移脚本和种子数据。每个数据库实例对应一个子目录。
## 内部结构
- `etl_feiqiu/` — ETL 数据库(六层 schemameta/ods/dwd/core/dws/app
- `schemas/` — DDL 定义(每个 schema 一个文件)
- `migrations/` — 迁移脚本(日期前缀命名)
- `seeds/` — 种子数据
- `zqyy_app/` — 业务应用数据库(用户/权限/任务/审批)
- `schemas/` / `migrations/` / `seeds/`
- `fdw/` — PostgreSQL FDW 跨库映射配置
## Roadmap
- FDW 演进:当前为单机 localhost 映射,未来支持跨主机远程 FDW 配置
- 引入 schema diff 工具,自动检测生产与测试库结构偏差
- 考虑 FDW 性能监控与查询下推优化

78
db/etl_feiqiu/README.md Normal file
View File

@@ -0,0 +1,78 @@
# database/ — 数据库层
## 文件说明
| 文件 | 用途 |
|------|------|
| `connection.py` | 数据库连接管理(带超时的 psycopg2 封装) |
| `operations.py` | 批量操作upsert、execute、query |
| `base.py` | 数据库操作基础类 |
## DDL Schema 文件
| 文件 | Schema | 说明 |
|------|--------|------|
| `schema_ODS_doc.sql` | `billiards_ods` | ODS 层表结构(含字段注释) |
| `schema_dwd_doc.sql` | `billiards_dwd` | DWD 层表结构(维度 + 事实,含 SCD2 列) |
| `schema_dws.sql` | `billiards_dws` | DWS 层表结构(汇总表 + 配置表) |
| `schema_etl_admin.sql` | `etl_admin` | ETL 元数据(任务注册、游标、运行记录) |
| `schema_verify_perf_indexes.sql` | 各 Schema | 校验性能索引(仅索引 + ANALYZE |
## 种子脚本
| 文件 | 用途 |
|------|------|
| `seed_ods_tasks.sql` | 注册 ODS 任务到 `etl_admin.etl_task` |
| `seed_scheduler_tasks.sql` | 初始化调度任务配置 |
| `seed_dws_config.sql` | DWS 配置数据(绩效档位、等级定价、技能映射等) |
| `seed_index_parameters.sql` | 指数算法参数WBI/NCI/RS/OS/MS/ML |
## 迁移脚本
位于 `migrations/` 子目录,纯 SQL按日期前缀命名
```
migrations/
├── 20260208_relation_index_manual_ml.sql
├── 20260214_drop_ods_settlelist.sql
├── 20260214_drop_dwd_settle_list.sql
└── 20260214_drop_ods_option_name_able_site_transfer.sql
```
新增迁移时,文件名格式:`YYYYMMDD_描述.sql`
## Schema 约定
- 所有 DDL 使用 `CREATE TABLE IF NOT EXISTS`,支持幂等执行
- 表名小写蛇形,带 Schema 前缀(如 `billiards_dwd.dim_member`
- 维度表包含 SCD2 列:`scd2_start_time``scd2_end_time``scd2_is_current``scd2_version`
- ODS 表包含元数据列:`content_hash``payload``fetched_at``source_file`
- 金额字段统一 `NUMERIC(12,2)`ID 字段统一 `BIGINT`
- 不使用 ORM所有 SQL 通过 `psycopg2` 直接执行
<!--
AI_CHANGELOG:
- 日期: 2026-02-14
- Prompt: P20260214-030000 — 上下文传递续接,完成 settlelist 删除后的变更影响审查
- 直接原因: Change Impact Review 要求将新增迁移脚本同步到 database/README.md
- 变更摘要: 迁移脚本列表新增 20260214_drop_ods_settlelist.sql 条目
- 风险与验证: 纯文档变更,无运行时影响;验证方式:`ls database/migrations/` 确认文件存在
-->
<!--
AI_CHANGELOG:
- 日期: 2026-02-14
- Prompt: P20260214-040000 — "dwd_settlement_head_ex.settle_list 也没有必要保留了"
- 直接原因: 新增迁移脚本需同步到 README 迁移列表
- 变更摘要: 迁移脚本列表新增 20260214_drop_dwd_settle_list.sql
- 风险与验证: 纯文档变更验证ls database/migrations/ 确认文件存在
-->
<!--
AI_CHANGELOG:
- 日期: 2026-02-14
- Prompt: P20260214-070000 — ODS 清理(删除 option_name/able_site_transfer变更影响审查
- 直接原因: Change Impact Review 要求将新增迁移脚本同步到 database/README.md
- 变更摘要: 迁移脚本列表新增 20260214_drop_ods_option_name_able_site_transfer.sql
- 风险与验证: 纯文档变更验证ls database/migrations/ 确认文件存在
-->

View File

View File

@@ -0,0 +1,144 @@
-- =============================================================================
-- 关系指数与 ML 人工台账迁移脚本
-- 版本: 2026-02-08
-- AI_CHANGELOG [2026-02-13] 移除 INSERT 中的 source_mode 参数ML 仅用人工台账)
-- 说明:
-- 1) 新增关系指数结果表 dws_member_assistant_relation_index
-- 2) 新增 ML 人工台账宽表/窄表
-- 3) 补充 RS/OS/MS/ML 参数并下线 INTIMACY
-- =============================================================================
BEGIN;
-- -----------------------------------------------------------------------------
-- 1) 关系指数结果表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS billiards_dws.dws_member_assistant_relation_index (
relation_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
member_id BIGINT NOT NULL,
assistant_id BIGINT NOT NULL,
session_count INTEGER NOT NULL DEFAULT 0,
total_duration_minutes INTEGER NOT NULL DEFAULT 0,
basic_session_count INTEGER NOT NULL DEFAULT 0,
incentive_session_count INTEGER NOT NULL DEFAULT 0,
days_since_last_session INTEGER,
rs_f NUMERIC(14,6) NOT NULL DEFAULT 0,
rs_d NUMERIC(14,6) NOT NULL DEFAULT 0,
rs_r NUMERIC(14,6) NOT NULL DEFAULT 0,
rs_raw NUMERIC(14,6) NOT NULL DEFAULT 0,
rs_display NUMERIC(4,2) NOT NULL DEFAULT 0,
os_share NUMERIC(10,6) NOT NULL DEFAULT 0,
os_label VARCHAR(20) NOT NULL DEFAULT 'POOL',
os_rank INTEGER,
ms_f_short NUMERIC(14,6) NOT NULL DEFAULT 0,
ms_f_long NUMERIC(14,6) NOT NULL DEFAULT 0,
ms_raw NUMERIC(14,6) NOT NULL DEFAULT 0,
ms_display NUMERIC(4,2) NOT NULL DEFAULT 0,
ml_order_count INTEGER NOT NULL DEFAULT 0,
ml_allocated_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
ml_raw NUMERIC(14,6) NOT NULL DEFAULT 0,
ml_display NUMERIC(4,2) NOT NULL DEFAULT 0,
calc_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_dws_member_assistant_relation_index UNIQUE (site_id, member_id, assistant_id)
);
CREATE INDEX IF NOT EXISTS idx_dws_relation_member
ON billiards_dws.dws_member_assistant_relation_index (site_id, member_id, os_share DESC);
CREATE INDEX IF NOT EXISTS idx_dws_relation_assistant
ON billiards_dws.dws_member_assistant_relation_index (site_id, assistant_id, rs_display DESC);
CREATE INDEX IF NOT EXISTS idx_dws_relation_calc_time
ON billiards_dws.dws_member_assistant_relation_index (calc_time);
-- -----------------------------------------------------------------------------
-- 2) ML 人工台账宽表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS billiards_dws.dws_ml_manual_order_source (
source_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
biz_date DATE NOT NULL,
external_id VARCHAR(128) NOT NULL,
member_id BIGINT NOT NULL DEFAULT 0,
pay_time TIMESTAMPTZ NOT NULL,
order_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
assistant_id_1 BIGINT,
assistant_name_1 VARCHAR(128),
assistant_id_2 BIGINT,
assistant_name_2 VARCHAR(128),
assistant_id_3 BIGINT,
assistant_name_3 VARCHAR(128),
assistant_id_4 BIGINT,
assistant_name_4 VARCHAR(128),
assistant_id_5 BIGINT,
assistant_name_5 VARCHAR(128),
import_batch_no VARCHAR(64) NOT NULL,
import_file_name VARCHAR(255) NOT NULL,
import_scope_key VARCHAR(128) NOT NULL,
import_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
import_user VARCHAR(64),
row_no INTEGER NOT NULL,
remark TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_dws_ml_manual_order_source UNIQUE (site_id, external_id, import_scope_key, row_no)
);
CREATE INDEX IF NOT EXISTS idx_dws_ml_source_scope
ON billiards_dws.dws_ml_manual_order_source (site_id, biz_date);
CREATE INDEX IF NOT EXISTS idx_dws_ml_source_external
ON billiards_dws.dws_ml_manual_order_source (site_id, external_id);
-- -----------------------------------------------------------------------------
-- 3) ML 人工台账窄表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS billiards_dws.dws_ml_manual_order_alloc (
alloc_id BIGSERIAL PRIMARY KEY,
site_id BIGINT NOT NULL,
biz_date DATE NOT NULL,
external_id VARCHAR(128) NOT NULL,
member_id BIGINT NOT NULL DEFAULT 0,
pay_time TIMESTAMPTZ NOT NULL,
order_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
assistant_id BIGINT NOT NULL,
assistant_name VARCHAR(128),
share_ratio NUMERIC(14,8) NOT NULL DEFAULT 0,
allocated_amount NUMERIC(14,2) NOT NULL DEFAULT 0,
currency VARCHAR(16) NOT NULL DEFAULT 'CNY',
import_scope_key VARCHAR(128) NOT NULL,
import_batch_no VARCHAR(64) NOT NULL,
import_file_name VARCHAR(255) NOT NULL,
import_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
import_user VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT uk_dws_ml_manual_order_alloc UNIQUE (site_id, external_id, assistant_id)
);
CREATE INDEX IF NOT EXISTS idx_dws_ml_alloc_scope
ON billiards_dws.dws_ml_manual_order_alloc (site_id, biz_date);
CREATE INDEX IF NOT EXISTS idx_dws_ml_alloc_member_assistant
ON billiards_dws.dws_ml_manual_order_alloc (site_id, member_id, assistant_id);
-- -----------------------------------------------------------------------------
-- 4) 参数切换
-- -----------------------------------------------------------------------------
UPDATE billiards_dws.cfg_index_parameters
SET effective_to = DATE '2025-12-31',
updated_at = NOW()
WHERE index_type = 'INTIMACY'
AND (effective_to IS NULL OR effective_to > DATE '2025-12-31');
INSERT INTO billiards_dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
('OS', 'ownership_gap_threshold', 0.150000, '主责与次席份额差阈值', DATE '2026-01-01')
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
COMMIT;

View File

@@ -0,0 +1,14 @@
-- ============================================================
-- ODS 表与 API JSON 字段对齐迁移
-- 自动生成于 2026-02-13
-- 基于: docs/api-reference/ 文档 vs billiards_ods 实际表结构
-- 比对逻辑: camelCase → snake_case 归一化后再比较
-- ============================================================
--
-- 结论: 22 张 ODS 表全部与 API JSON 字段对齐,无需任何 ALTER 操作。
--
-- stock_goods_category_tree 的 goodsCategoryList/total 为响应包装层字段,
-- ODS 表已正确展开存储数组内的记录级字段id, category_name, pid 等),
-- 不需要将包装层字段作为列添加。
--
-- 无需执行此文件。

View File

@@ -0,0 +1,72 @@
-- =============================================================================
-- 迁移移除旧版指数RECALL / INTIMACY及 ML last-touch 备用参数
-- 日期: 2026-02-13
-- AI_CHANGELOG [2026-02-13] 新建迁移DROP recall/intimacy 表DELETE 旧版参数/分位点/调度任务
-- 原因: 旧版 RecallIndexTask / IntimacyIndexTask 已被 WBI+NCI / RelationIndexTask 替代
-- ML 仅使用人工台账,不再需要 last-touch 备用路径
-- =============================================================================
BEGIN;
-- 1. 删除旧版指数参数
DELETE FROM billiards_dws.cfg_index_parameters
WHERE index_type IN ('RECALL', 'INTIMACY');
-- 2. 删除 ML 已废弃参数
DELETE FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'ML'
AND param_name IN ('source_mode', 'recharge_attribute_hours');
-- 3. 删除旧版指数分位点历史
DELETE FROM billiards_dws.cfg_index_percentile_history
WHERE index_type IN ('RECALL', 'INTIMACY');
-- 4. 删除调度器中的旧版任务
DELETE FROM etl_admin.etl_task
WHERE task_code IN ('DWS_RECALL_INDEX', 'DWS_INTIMACY_INDEX');
-- 5. 删除旧版 recall 表(数据已由 WBI+NCI 替代)
DROP TABLE IF EXISTS billiards_dws.dws_member_recall_index CASCADE;
-- 6. 删除旧版 intimacy 表(数据已由 RelationIndexTask 替代)
DROP TABLE IF EXISTS billiards_dws.dws_member_assistant_intimacy CASCADE;
COMMIT;
-- =============================================================================
-- 验证
-- =============================================================================
DO $
DECLARE
recall_params INTEGER;
intimacy_params INTEGER;
ml_legacy INTEGER;
recall_table BOOLEAN;
intimacy_table BOOLEAN;
BEGIN
SELECT COUNT(*) INTO recall_params
FROM billiards_dws.cfg_index_parameters WHERE index_type = 'RECALL';
SELECT COUNT(*) INTO intimacy_params
FROM billiards_dws.cfg_index_parameters WHERE index_type = 'INTIMACY';
SELECT COUNT(*) INTO ml_legacy
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'ML' AND param_name IN ('source_mode', 'recharge_attribute_hours');
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'billiards_dws' AND table_name = 'dws_member_recall_index'
) INTO recall_table;
SELECT EXISTS(
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'billiards_dws' AND table_name = 'dws_member_assistant_intimacy'
) INTO intimacy_table;
RAISE NOTICE 'RECALL 参数残留: % (应为 0)', recall_params;
RAISE NOTICE 'INTIMACY 参数残留: % (应为 0)', intimacy_params;
RAISE NOTICE 'ML 废弃参数残留: % (应为 0)', ml_legacy;
RAISE NOTICE 'recall 表存在: % (应为 false)', recall_table;
RAISE NOTICE 'intimacy 表存在: % (应为 false)', intimacy_table;
END $;

View File

@@ -0,0 +1,12 @@
-- 迁移:删除 DWD 层 dwd_settlement_head_ex 的 settle_list 列
-- 原因settle_list 存储结算明细 JSON与 ODS 层 payload 中的 settleList 完全重复
-- ODS 层 settlelist 列已在 20260214_drop_ods_settlelist.sql 中删除
-- DWD 层该列同样冗余,结算明细可随时从 ODS payload 提取
-- 回滚ALTER TABLE billiards_dwd.dwd_settlement_head_ex ADD COLUMN settle_list JSONB;
-- UPDATE billiards_dwd.dwd_settlement_head_ex e
-- SET settle_list = o.payload->'settleList'
-- FROM billiards_ods.settlement_records o
-- WHERE e.order_settle_id = o.id;
-- Prompt: P20260214-040000
ALTER TABLE billiards_dwd.dwd_settlement_head_ex DROP COLUMN IF EXISTS settle_list;

View File

@@ -0,0 +1,23 @@
-- 迁移:删除 ODS 层两个冗余列
-- 日期2026-02-14
-- Prompt-IDP20260214-070000
-- 原因option_namestore_goods_sales_records和 able_site_transfermember_stored_value_cards
-- 在 API JSON 响应中不存在ODS 中全部为 NULL0 条非空数据),属于冗余列
-- 回滚ALTER TABLE billiards_ods.store_goods_sales_records ADD COLUMN option_name TEXT;
-- ALTER TABLE billiards_ods.member_stored_value_cards ADD COLUMN able_site_transfer INTEGER;
-- CHANGE: intent=删除 API 中不存在的冗余 ODS 列; assumptions=两列全 NULL 无数据丢失风险; Prompt=P20260214-070000
BEGIN;
ALTER TABLE billiards_ods.store_goods_sales_records DROP COLUMN IF EXISTS option_name;
ALTER TABLE billiards_ods.member_stored_value_cards DROP COLUMN IF EXISTS able_site_transfer;
COMMIT;
-- AI_CHANGELOG:
-- - 日期: 2026-02-14
-- - Prompt: P20260214-070000 — ODS 清理与文档标注5 项任务)
-- - 直接原因: option_name 和 able_site_transfer 在 API JSON 中不存在ODS 全 NULL需删除冗余列
-- - 变更摘要: 新建迁移脚本DROP COLUMN 两张表各一列
-- - 风险与验证: 已执行成功验证information_schema 查询确认列不存在

View File

@@ -0,0 +1,34 @@
-- 迁移:删除 ODS 层 settlement_records / recharge_settlements 的 settlelist jsonb 列
-- 原因settlelist 与 payload 列数据重复payload 存储完整 API 响应 JSON已包含 settleList 对象)
-- 日期2026-02-14
-- Prompt-ID: P20260214-023000
--
-- 前置条件:确认 DWD 加载逻辑已修改为从 payload 提取 settleList而非直接读 settlelist 列)
-- 回滚:见文件末尾 ROLLBACK 部分
-- CHANGE: intent=删除冗余 settlelist 列,减少存储浪费
-- assumptions=payload 列已包含完整 settleList 数据DWD 加载已改为从 payload 提取
-- edge_cases=历史数据中 payload 为 NULL 的行将丢失 settleList 信息
-- prompt=P20260214-023000
-- 1) settlement_records
ALTER TABLE billiards_ods.settlement_records DROP COLUMN IF EXISTS settlelist;
-- 2) recharge_settlements
ALTER TABLE billiards_ods.recharge_settlements DROP COLUMN IF EXISTS settlelist;
-- 验证 SQL
-- SELECT column_name FROM information_schema.columns
-- WHERE table_schema = 'billiards_ods'
-- AND table_name IN ('settlement_records', 'recharge_settlements')
-- AND column_name = 'settlelist';
-- 预期结果0 行
-- ============================================================
-- ROLLBACK回滚
-- ============================================================
-- ALTER TABLE billiards_ods.settlement_records ADD COLUMN settlelist jsonb;
-- ALTER TABLE billiards_ods.recharge_settlements ADD COLUMN settlelist jsonb;
-- 注意:回滚后列数据为 NULL需从 payload 中重新提取:
-- UPDATE billiards_ods.settlement_records SET settlelist = payload->'settleList' WHERE payload IS NOT NULL;
-- UPDATE billiards_ods.recharge_settlements SET settlelist = payload->'settleList' WHERE payload IS NOT NULL;

View File

View File

@@ -0,0 +1,214 @@
-- =============================================================================
-- app schema DDL — 面向外部访问的视图/函数 + RLS 策略
-- 说明:以视图封装 DWS/Core 层数据,所有视图启用 RLS以 site_id 过滤
-- 不存储实际数据,仅做访问层
-- =============================================================================
CREATE SCHEMA IF NOT EXISTS app;
SET search_path TO app;
-- -----------------------------------------------------------------------------
-- 应用角色(供 FDW 和外部应用使用)
-- -----------------------------------------------------------------------------
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'app_reader') THEN
CREATE ROLE app_reader;
END IF;
END
$$;
GRANT USAGE ON SCHEMA app TO app_reader;
GRANT USAGE ON SCHEMA core TO app_reader;
GRANT USAGE ON SCHEMA dws TO app_reader;
-- =============================================================================
-- 第一部分:基于 Core 层的视图
-- =============================================================================
-- -----------------------------------------------------------------------------
-- 1. v_site — 门店视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_site AS
SELECT
s.site_id,
s.tenant_id,
s.shop_name,
s.site_label,
s.shop_status
FROM core.dim_site s;
COMMENT ON VIEW app.v_site IS '门店视图:封装 core.dim_site供外部应用访问。';
-- -----------------------------------------------------------------------------
-- 2. v_member — 会员视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_member AS
SELECT
m.member_id,
m.system_member_id,
m.tenant_id,
m.register_site_id AS site_id,
m.mobile,
m.nickname,
m.member_card_grade_name,
m.status
FROM core.dim_member m;
COMMENT ON VIEW app.v_member IS '会员视图:封装 core.dim_member以 register_site_id 作为 site_id。';
-- -----------------------------------------------------------------------------
-- 3. v_assistant — 助教视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_assistant AS
SELECT
a.assistant_id,
a.tenant_id,
a.site_id,
a.real_name,
a.nickname,
a.mobile,
a.level,
a.assistant_status,
a.leave_status
FROM core.dim_assistant a;
COMMENT ON VIEW app.v_assistant IS '助教视图:封装 core.dim_assistant。';
-- =============================================================================
-- 第二部分:基于 DWS 层的汇总视图
-- =============================================================================
-- -----------------------------------------------------------------------------
-- 4. v_assistant_daily — 助教日明细视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_assistant_daily AS
SELECT
d.id,
d.site_id,
d.assistant_id,
d.stat_date,
d.total_service_hours,
d.total_service_count,
d.total_revenue,
d.basic_hours,
d.extra_hours,
d.tip_hours,
d.created_at
FROM dws.dws_assistant_daily_detail d;
COMMENT ON VIEW app.v_assistant_daily IS '助教日明细视图:封装 dws.dws_assistant_daily_detail。';
-- -----------------------------------------------------------------------------
-- 5. v_finance_daily — 财务日报视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_finance_daily AS
SELECT
f.id,
f.site_id,
f.stat_date,
f.total_revenue,
f.table_fee_revenue,
f.goods_revenue,
f.assistant_revenue,
f.recharge_revenue,
f.total_orders,
f.total_customers,
f.created_at
FROM dws.dws_finance_daily_summary f;
COMMENT ON VIEW app.v_finance_daily IS '财务日报视图:封装 dws.dws_finance_daily_summary。';
-- -----------------------------------------------------------------------------
-- 6. v_member_consumption — 会员消费汇总视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_member_consumption AS
SELECT
mc.id,
mc.site_id,
mc.member_id,
mc.total_visits,
mc.total_consumption,
mc.last_visit_date,
mc.first_visit_date,
mc.avg_consumption,
mc.created_at
FROM dws.dws_member_consumption_summary mc;
COMMENT ON VIEW app.v_member_consumption IS '会员消费汇总视图:封装 dws.dws_member_consumption_summary。';
-- -----------------------------------------------------------------------------
-- 7. v_order_summary — 订单汇总视图
-- -----------------------------------------------------------------------------
CREATE OR REPLACE VIEW app.v_order_summary AS
SELECT
os.site_id,
os.order_settle_id,
os.order_trade_no,
os.member_id,
os.total_amount,
os.actual_amount,
os.settle_time,
os.pay_status,
os.created_at
FROM dws.dws_order_summary os;
COMMENT ON VIEW app.v_order_summary IS '订单汇总视图:封装 dws.dws_order_summary。';
-- =============================================================================
-- 第三部分RLS 策略
-- 说明:所有视图基于 site_id 隔离,通过会话变量 app.current_site_id 过滤
-- 使用方式SET app.current_site_id = '2790685415443269';
-- =============================================================================
-- 对视图底层表启用 RLS视图本身不支持 RLS需在底层表上设置
-- 由于 app schema 的视图引用 core/dws 表RLS 需在源表上配置
-- core 层 RLS
ALTER TABLE core.dim_site ENABLE ROW LEVEL SECURITY;
ALTER TABLE core.dim_member ENABLE ROW LEVEL SECURITY;
ALTER TABLE core.dim_assistant ENABLE ROW LEVEL SECURITY;
ALTER TABLE core.dim_table ENABLE ROW LEVEL SECURITY;
ALTER TABLE core.fact_settlement ENABLE ROW LEVEL SECURITY;
ALTER TABLE core.fact_payment ENABLE ROW LEVEL SECURITY;
-- core 层策略
CREATE POLICY site_isolation_dim_site ON core.dim_site
FOR SELECT TO app_reader
USING (site_id = current_setting('app.current_site_id')::bigint);
CREATE POLICY site_isolation_dim_member ON core.dim_member
FOR SELECT TO app_reader
USING (register_site_id = current_setting('app.current_site_id')::bigint);
CREATE POLICY site_isolation_dim_assistant ON core.dim_assistant
FOR SELECT TO app_reader
USING (site_id = current_setting('app.current_site_id')::bigint);
CREATE POLICY site_isolation_dim_table ON core.dim_table
FOR SELECT TO app_reader
USING (site_id = current_setting('app.current_site_id')::bigint);
CREATE POLICY site_isolation_fact_settlement ON core.fact_settlement
FOR SELECT TO app_reader
USING (site_id = current_setting('app.current_site_id')::bigint);
CREATE POLICY site_isolation_fact_payment ON core.fact_payment
FOR SELECT TO app_reader
USING (site_id = current_setting('app.current_site_id')::bigint);
-- =============================================================================
-- 第四部分:授权
-- =============================================================================
-- 授予 app_reader 对 core 表的 SELECT 权限
GRANT SELECT ON ALL TABLES IN SCHEMA core TO app_reader;
GRANT SELECT ON ALL TABLES IN SCHEMA app TO app_reader;
-- 授予对 dws 表的 SELECT 权限(视图需要)
GRANT SELECT ON ALL TABLES IN SCHEMA dws TO app_reader;
-- 设置默认权限(未来新建的表自动授权)
ALTER DEFAULT PRIVILEGES IN SCHEMA core GRANT SELECT ON TABLES TO app_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA app GRANT SELECT ON TABLES TO app_reader;
ALTER DEFAULT PRIVILEGES IN SCHEMA dws GRANT SELECT ON TABLES TO app_reader;

View File

@@ -0,0 +1,161 @@
-- =============================================================================
-- core schema DDL — 统一维度/事实最小字段集层
-- 说明:从 DWD 维度表和事实表提取跨系统共享的核心字段
-- 仅包含 ID、名称、状态、site_id 等最小字段集
-- 第一版保持精简,后续按需扩展
-- =============================================================================
CREATE SCHEMA IF NOT EXISTS core;
SET search_path TO core;
-- -----------------------------------------------------------------------------
-- 1. dim_site — 门店维度(核心字段)
-- 来源dwd.dim_site14 字段 → 6 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dim_site (
site_id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
shop_name TEXT NOT NULL,
site_label TEXT,
shop_status INTEGER,
site_id_alias BIGINT GENERATED ALWAYS AS (site_id) STORED
);
COMMENT ON TABLE core.dim_site IS '门店维度核心表:仅保留跨系统共享的最小字段集。';
COMMENT ON COLUMN core.dim_site.site_id IS '门店 ID主键';
COMMENT ON COLUMN core.dim_site.tenant_id IS '租户/品牌 ID。';
COMMENT ON COLUMN core.dim_site.shop_name IS '门店名称。';
COMMENT ON COLUMN core.dim_site.site_label IS '门店标签(如 A/B 店)。';
COMMENT ON COLUMN core.dim_site.shop_status IS '门店状态枚举。';
-- site_id_alias 用于 RLS 策略中统一使用 site_id 过滤
-- -----------------------------------------------------------------------------
-- 2. dim_member — 会员维度(核心字段)
-- 来源dwd.dim_member12 字段 → 8 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dim_member (
member_id BIGINT PRIMARY KEY,
system_member_id BIGINT,
tenant_id BIGINT NOT NULL,
register_site_id BIGINT NOT NULL,
mobile TEXT,
nickname TEXT,
member_card_grade_name TEXT,
status INTEGER
);
COMMENT ON TABLE core.dim_member IS '会员维度核心表:仅保留跨系统共享的最小字段集。';
COMMENT ON COLUMN core.dim_member.member_id IS '会员 ID主键';
COMMENT ON COLUMN core.dim_member.system_member_id IS '系统级会员 ID跨门店统一';
COMMENT ON COLUMN core.dim_member.tenant_id IS '租户/品牌 ID。';
COMMENT ON COLUMN core.dim_member.register_site_id IS '注册门店 ID等同 site_id';
COMMENT ON COLUMN core.dim_member.mobile IS '手机号。';
COMMENT ON COLUMN core.dim_member.nickname IS '昵称/姓名。';
COMMENT ON COLUMN core.dim_member.member_card_grade_name IS '会员卡等级名称。';
COMMENT ON COLUMN core.dim_member.status IS '会员状态。';
-- -----------------------------------------------------------------------------
-- 3. dim_assistant — 助教维度(核心字段)
-- 来源dwd.dim_assistant16 字段 → 9 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dim_assistant (
assistant_id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
site_id BIGINT NOT NULL,
real_name TEXT NOT NULL,
nickname TEXT,
mobile TEXT,
level INTEGER,
assistant_status INTEGER,
leave_status INTEGER
);
COMMENT ON TABLE core.dim_assistant IS '助教维度核心表:仅保留跨系统共享的最小字段集。';
COMMENT ON COLUMN core.dim_assistant.assistant_id IS '助教 ID主键';
COMMENT ON COLUMN core.dim_assistant.tenant_id IS '租户/品牌 ID。';
COMMENT ON COLUMN core.dim_assistant.site_id IS '门店 ID。';
COMMENT ON COLUMN core.dim_assistant.real_name IS '真实姓名。';
COMMENT ON COLUMN core.dim_assistant.nickname IS '昵称。';
COMMENT ON COLUMN core.dim_assistant.mobile IS '手机号。';
COMMENT ON COLUMN core.dim_assistant.level IS '助教等级。';
COMMENT ON COLUMN core.dim_assistant.assistant_status IS '助教状态。';
COMMENT ON COLUMN core.dim_assistant.leave_status IS '离职状态。';
-- -----------------------------------------------------------------------------
-- 4. dim_table — 台桌维度(核心字段)
-- 来源dwd.dim_table8 字段 → 5 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dim_table (
table_id BIGINT PRIMARY KEY,
site_id BIGINT NOT NULL,
table_name TEXT NOT NULL,
site_table_area_name TEXT,
table_price NUMERIC(18,2)
);
COMMENT ON TABLE core.dim_table IS '台桌维度核心表:仅保留跨系统共享的最小字段集。';
COMMENT ON COLUMN core.dim_table.table_id IS '台桌 ID主键';
COMMENT ON COLUMN core.dim_table.site_id IS '门店 ID。';
COMMENT ON COLUMN core.dim_table.table_name IS '台桌名称。';
COMMENT ON COLUMN core.dim_table.site_table_area_name IS '区域名称。';
COMMENT ON COLUMN core.dim_table.table_price IS '台桌单价。';
-- -----------------------------------------------------------------------------
-- 5. dim_goods_category — 商品分类维度(核心字段)
-- 来源dwd.dim_goods_category10 字段 → 5 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS dim_goods_category (
category_id BIGINT PRIMARY KEY,
tenant_id BIGINT NOT NULL,
category_name TEXT NOT NULL,
parent_id BIGINT,
level INTEGER
);
COMMENT ON TABLE core.dim_goods_category IS '商品分类维度核心表。';
-- -----------------------------------------------------------------------------
-- 6. fact_settlement — 结算事实(核心字段)
-- 来源dwd.dwd_settlement_head约 30 字段 → 12 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS fact_settlement (
order_settle_id BIGINT PRIMARY KEY,
site_id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
order_trade_no BIGINT,
member_id BIGINT,
total_amount NUMERIC(18,2),
actual_amount NUMERIC(18,2),
discount_amount NUMERIC(18,2),
pay_status INTEGER,
settle_time TIMESTAMPTZ,
created_at TIMESTAMPTZ,
updated_at TIMESTAMPTZ
);
COMMENT ON TABLE core.fact_settlement IS '结算事实核心表:仅保留跨系统共享的最小字段集。';
COMMENT ON COLUMN core.fact_settlement.order_settle_id IS '结账单 ID主键';
COMMENT ON COLUMN core.fact_settlement.site_id IS '门店 ID。';
COMMENT ON COLUMN core.fact_settlement.tenant_id IS '租户 ID。';
COMMENT ON COLUMN core.fact_settlement.total_amount IS '应收总额。';
COMMENT ON COLUMN core.fact_settlement.actual_amount IS '实收金额。';
COMMENT ON COLUMN core.fact_settlement.discount_amount IS '优惠金额。';
-- -----------------------------------------------------------------------------
-- 7. fact_payment — 支付事实(核心字段)
-- 来源dwd.dwd_payment约 12 字段 → 7 字段)
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS fact_payment (
payment_id BIGINT PRIMARY KEY,
site_id BIGINT NOT NULL,
order_settle_id BIGINT,
pay_type INTEGER,
pay_amount NUMERIC(18,2),
pay_time TIMESTAMPTZ,
status INTEGER
);
COMMENT ON TABLE core.fact_payment IS '支付事实核心表。';
-- -----------------------------------------------------------------------------
-- 索引
-- -----------------------------------------------------------------------------
CREATE INDEX IF NOT EXISTS idx_core_member_site ON core.dim_member (register_site_id);
CREATE INDEX IF NOT EXISTS idx_core_assistant_site ON core.dim_assistant (site_id);
CREATE INDEX IF NOT EXISTS idx_core_table_site ON core.dim_table (site_id);
CREATE INDEX IF NOT EXISTS idx_core_settlement_site ON core.fact_settlement (site_id);
CREATE INDEX IF NOT EXISTS idx_core_settlement_time ON core.fact_settlement (settle_time);
CREATE INDEX IF NOT EXISTS idx_core_payment_site ON core.fact_payment (site_id);

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
-- 文件说明meta 调度元数据 DDL独立文件便于初始化任务单独执行
-- 包含任务注册表、游标表、运行记录表;字段注释使用中文。
CREATE SCHEMA IF NOT EXISTS meta;
CREATE TABLE IF NOT EXISTS meta.etl_task (
task_id BIGSERIAL PRIMARY KEY,
task_code TEXT NOT NULL,
store_id BIGINT NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
cursor_field TEXT,
window_minutes_default INT DEFAULT 30,
overlap_seconds INT DEFAULT 600,
page_size INT DEFAULT 200,
retry_max INT DEFAULT 3,
params JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (task_code, store_id)
);
COMMENT ON TABLE meta.etl_task IS '任务注册表:调度依据的任务清单(与 task_registry 中的任务码对应)。';
COMMENT ON COLUMN meta.etl_task.task_code IS '任务编码,需与代码中的任务码一致。';
COMMENT ON COLUMN meta.etl_task.store_id IS '门店/租户粒度,区分多门店执行。';
COMMENT ON COLUMN meta.etl_task.enabled IS '是否启用此任务。';
COMMENT ON COLUMN meta.etl_task.cursor_field IS '增量游标字段名(可选)。';
COMMENT ON COLUMN meta.etl_task.window_minutes_default IS '默认时间窗口(分钟)。';
COMMENT ON COLUMN meta.etl_task.overlap_seconds IS '窗口重叠秒数,用于防止遗漏。';
COMMENT ON COLUMN meta.etl_task.page_size IS '默认分页大小。';
COMMENT ON COLUMN meta.etl_task.retry_max IS 'API重试次数上限。';
COMMENT ON COLUMN meta.etl_task.params IS '任务级自定义参数 JSON。';
COMMENT ON COLUMN meta.etl_task.created_at IS '创建时间。';
COMMENT ON COLUMN meta.etl_task.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS meta.etl_cursor (
cursor_id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES meta.etl_task(task_id) ON DELETE CASCADE,
store_id BIGINT NOT NULL,
last_start TIMESTAMPTZ,
last_end TIMESTAMPTZ,
last_id BIGINT,
last_run_id BIGINT,
extra JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (task_id, store_id)
);
COMMENT ON TABLE meta.etl_cursor IS '任务游标表:记录每个任务/门店的增量窗口及最后 run。';
COMMENT ON COLUMN meta.etl_cursor.task_id IS '关联 etl_task.task_id。';
COMMENT ON COLUMN meta.etl_cursor.store_id IS '门店/租户粒度。';
COMMENT ON COLUMN meta.etl_cursor.last_start IS '上次窗口开始时间(含重叠偏移)。';
COMMENT ON COLUMN meta.etl_cursor.last_end IS '上次窗口结束时间。';
COMMENT ON COLUMN meta.etl_cursor.last_id IS '上次处理的最大主键/游标值(可选)。';
COMMENT ON COLUMN meta.etl_cursor.last_run_id IS '上次运行ID对应 etl_run.run_id。';
COMMENT ON COLUMN meta.etl_cursor.extra IS '附加游标信息 JSON。';
COMMENT ON COLUMN meta.etl_cursor.created_at IS '创建时间。';
COMMENT ON COLUMN meta.etl_cursor.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS meta.etl_run (
run_id BIGSERIAL PRIMARY KEY,
run_uuid TEXT NOT NULL,
task_id BIGINT NOT NULL REFERENCES meta.etl_task(task_id) ON DELETE CASCADE,
store_id BIGINT NOT NULL,
status TEXT NOT NULL,
started_at TIMESTAMPTZ DEFAULT now(),
ended_at TIMESTAMPTZ,
window_start TIMESTAMPTZ,
window_end TIMESTAMPTZ,
window_minutes INT,
overlap_seconds INT,
fetched_count INT DEFAULT 0,
loaded_count INT DEFAULT 0,
updated_count INT DEFAULT 0,
skipped_count INT DEFAULT 0,
error_count INT DEFAULT 0,
unknown_fields INT DEFAULT 0,
export_dir TEXT,
log_path TEXT,
request_params JSONB DEFAULT '{}'::jsonb,
manifest JSONB DEFAULT '{}'::jsonb,
error_message TEXT,
extra JSONB DEFAULT '{}'::jsonb
);
COMMENT ON TABLE meta.etl_run IS '运行记录表:记录每次任务执行的窗口、状态、计数与日志路径。';
COMMENT ON COLUMN meta.etl_run.run_uuid IS '本次调度的唯一标识。';
COMMENT ON COLUMN meta.etl_run.task_id IS '关联 etl_task.task_id。';
COMMENT ON COLUMN meta.etl_run.store_id IS '门店/租户粒度。';
COMMENT ON COLUMN meta.etl_run.status IS '运行状态SUCC/FAIL/PARTIAL 等)。';
COMMENT ON COLUMN meta.etl_run.started_at IS '开始时间。';
COMMENT ON COLUMN meta.etl_run.ended_at IS '结束时间。';
COMMENT ON COLUMN meta.etl_run.window_start IS '本次窗口开始时间。';
COMMENT ON COLUMN meta.etl_run.window_end IS '本次窗口结束时间。';
COMMENT ON COLUMN meta.etl_run.window_minutes IS '窗口跨度(分钟)。';
COMMENT ON COLUMN meta.etl_run.overlap_seconds IS '窗口重叠秒数。';
COMMENT ON COLUMN meta.etl_run.fetched_count IS '抓取/读取的记录数。';
COMMENT ON COLUMN meta.etl_run.loaded_count IS '插入的记录数。';
COMMENT ON COLUMN meta.etl_run.updated_count IS '更新的记录数。';
COMMENT ON COLUMN meta.etl_run.skipped_count IS '跳过的记录数。';
COMMENT ON COLUMN meta.etl_run.error_count IS '错误记录数。';
COMMENT ON COLUMN meta.etl_run.unknown_fields IS '未知字段计数(清洗阶段)。';
COMMENT ON COLUMN meta.etl_run.export_dir IS '抓取/导出目录。';
COMMENT ON COLUMN meta.etl_run.log_path IS '日志路径。';
COMMENT ON COLUMN meta.etl_run.request_params IS '请求参数 JSON。';
COMMENT ON COLUMN meta.etl_run.manifest IS '运行产出清单/统计 JSON。';
COMMENT ON COLUMN meta.etl_run.error_message IS '错误信息(若失败)。';
COMMENT ON COLUMN meta.etl_run.extra IS '附加字段,保留扩展。';

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,105 @@
-- 文件说明etl_admin 调度元数据 DDL独立文件便于初始化任务单独执行
-- 包含任务注册表、游标表、运行记录表;字段注释使用中文。
CREATE SCHEMA IF NOT EXISTS etl_admin;
CREATE TABLE IF NOT EXISTS etl_admin.etl_task (
task_id BIGSERIAL PRIMARY KEY,
task_code TEXT NOT NULL,
store_id BIGINT NOT NULL,
enabled BOOLEAN DEFAULT TRUE,
cursor_field TEXT,
window_minutes_default INT DEFAULT 30,
overlap_seconds INT DEFAULT 600,
page_size INT DEFAULT 200,
retry_max INT DEFAULT 3,
params JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (task_code, store_id)
);
COMMENT ON TABLE etl_admin.etl_task IS '任务注册表:调度依据的任务清单(与 task_registry 中的任务码对应)。';
COMMENT ON COLUMN etl_admin.etl_task.task_code IS '任务编码,需与代码中的任务码一致。';
COMMENT ON COLUMN etl_admin.etl_task.store_id IS '门店/租户粒度,区分多门店执行。';
COMMENT ON COLUMN etl_admin.etl_task.enabled IS '是否启用此任务。';
COMMENT ON COLUMN etl_admin.etl_task.cursor_field IS '增量游标字段名(可选)。';
COMMENT ON COLUMN etl_admin.etl_task.window_minutes_default IS '默认时间窗口(分钟)。';
COMMENT ON COLUMN etl_admin.etl_task.overlap_seconds IS '窗口重叠秒数,用于防止遗漏。';
COMMENT ON COLUMN etl_admin.etl_task.page_size IS '默认分页大小。';
COMMENT ON COLUMN etl_admin.etl_task.retry_max IS 'API重试次数上限。';
COMMENT ON COLUMN etl_admin.etl_task.params IS '任务级自定义参数 JSON。';
COMMENT ON COLUMN etl_admin.etl_task.created_at IS '创建时间。';
COMMENT ON COLUMN etl_admin.etl_task.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS etl_admin.etl_cursor (
cursor_id BIGSERIAL PRIMARY KEY,
task_id BIGINT NOT NULL REFERENCES etl_admin.etl_task(task_id) ON DELETE CASCADE,
store_id BIGINT NOT NULL,
last_start TIMESTAMPTZ,
last_end TIMESTAMPTZ,
last_id BIGINT,
last_run_id BIGINT,
extra JSONB DEFAULT '{}'::jsonb,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
UNIQUE (task_id, store_id)
);
COMMENT ON TABLE etl_admin.etl_cursor IS '任务游标表:记录每个任务/门店的增量窗口及最后 run。';
COMMENT ON COLUMN etl_admin.etl_cursor.task_id IS '关联 etl_task.task_id。';
COMMENT ON COLUMN etl_admin.etl_cursor.store_id IS '门店/租户粒度。';
COMMENT ON COLUMN etl_admin.etl_cursor.last_start IS '上次窗口开始时间(含重叠偏移)。';
COMMENT ON COLUMN etl_admin.etl_cursor.last_end IS '上次窗口结束时间。';
COMMENT ON COLUMN etl_admin.etl_cursor.last_id IS '上次处理的最大主键/游标值(可选)。';
COMMENT ON COLUMN etl_admin.etl_cursor.last_run_id IS '上次运行ID对应 etl_run.run_id。';
COMMENT ON COLUMN etl_admin.etl_cursor.extra IS '附加游标信息 JSON。';
COMMENT ON COLUMN etl_admin.etl_cursor.created_at IS '创建时间。';
COMMENT ON COLUMN etl_admin.etl_cursor.updated_at IS '更新时间。';
CREATE TABLE IF NOT EXISTS etl_admin.etl_run (
run_id BIGSERIAL PRIMARY KEY,
run_uuid TEXT NOT NULL,
task_id BIGINT NOT NULL REFERENCES etl_admin.etl_task(task_id) ON DELETE CASCADE,
store_id BIGINT NOT NULL,
status TEXT NOT NULL,
started_at TIMESTAMPTZ DEFAULT now(),
ended_at TIMESTAMPTZ,
window_start TIMESTAMPTZ,
window_end TIMESTAMPTZ,
window_minutes INT,
overlap_seconds INT,
fetched_count INT DEFAULT 0,
loaded_count INT DEFAULT 0,
updated_count INT DEFAULT 0,
skipped_count INT DEFAULT 0,
error_count INT DEFAULT 0,
unknown_fields INT DEFAULT 0,
export_dir TEXT,
log_path TEXT,
request_params JSONB DEFAULT '{}'::jsonb,
manifest JSONB DEFAULT '{}'::jsonb,
error_message TEXT,
extra JSONB DEFAULT '{}'::jsonb
);
COMMENT ON TABLE etl_admin.etl_run IS '运行记录表:记录每次任务执行的窗口、状态、计数与日志路径。';
COMMENT ON COLUMN etl_admin.etl_run.run_uuid IS '本次调度的唯一标识。';
COMMENT ON COLUMN etl_admin.etl_run.task_id IS '关联 etl_task.task_id。';
COMMENT ON COLUMN etl_admin.etl_run.store_id IS '门店/租户粒度。';
COMMENT ON COLUMN etl_admin.etl_run.status IS '运行状态SUCC/FAIL/PARTIAL 等)。';
COMMENT ON COLUMN etl_admin.etl_run.started_at IS '开始时间。';
COMMENT ON COLUMN etl_admin.etl_run.ended_at IS '结束时间。';
COMMENT ON COLUMN etl_admin.etl_run.window_start IS '本次窗口开始时间。';
COMMENT ON COLUMN etl_admin.etl_run.window_end IS '本次窗口结束时间。';
COMMENT ON COLUMN etl_admin.etl_run.window_minutes IS '窗口跨度(分钟)。';
COMMENT ON COLUMN etl_admin.etl_run.overlap_seconds IS '窗口重叠秒数。';
COMMENT ON COLUMN etl_admin.etl_run.fetched_count IS '抓取/读取的记录数。';
COMMENT ON COLUMN etl_admin.etl_run.loaded_count IS '插入的记录数。';
COMMENT ON COLUMN etl_admin.etl_run.updated_count IS '更新的记录数。';
COMMENT ON COLUMN etl_admin.etl_run.skipped_count IS '跳过的记录数。';
COMMENT ON COLUMN etl_admin.etl_run.error_count IS '错误记录数。';
COMMENT ON COLUMN etl_admin.etl_run.unknown_fields IS '未知字段计数(清洗阶段)。';
COMMENT ON COLUMN etl_admin.etl_run.export_dir IS '抓取/导出目录。';
COMMENT ON COLUMN etl_admin.etl_run.log_path IS '日志路径。';
COMMENT ON COLUMN etl_admin.etl_run.request_params IS '请求参数 JSON。';
COMMENT ON COLUMN etl_admin.etl_run.manifest IS '运行产出清单/统计 JSON。';
COMMENT ON COLUMN etl_admin.etl_run.error_message IS '错误信息(若失败)。';
COMMENT ON COLUMN etl_admin.etl_run.extra IS '附加字段,保留扩展。';

View File

@@ -0,0 +1,173 @@
SET client_encoding TO "UTF8";
-- ============================================================================
-- 校验性能索引ODS / DWD
-- ----------------------------------------------------------------------------
-- 用途:
-- 1) 加速校验查询(主键查找、窗口扫描、当前版本扫描)。
-- 2) 保持数据语义不变(仅添加索引 + ANALYZE不改写业务数据
--
-- 注意事项:
-- 1) 本脚本具有幂等性(`CREATE INDEX IF NOT EXISTS`)。
-- 2) 如有严格的在线 DDL 要求,请手动使用 `CREATE INDEX CONCURRENTLY`
-- 在维护安全模式下执行(不可在事务块内运行)。
-- ============================================================================
DO $$
DECLARE
rec RECORD;
pk_cols TEXT[];
pk_cols_sql TEXT;
idx_name TEXT;
BEGIN
FOR rec IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'billiards_ods'
AND table_type = 'BASE TABLE'
LOOP
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'billiards_ods'
AND table_name = rec.table_name
AND column_name = 'fetched_at'
) THEN
idx_name := left(format('idx_%s_vfy_fetched_at', rec.table_name), 50)
|| '_' || substr(md5(rec.table_name || '_vfy_fetched_at'), 1, 8);
EXECUTE format(
'CREATE INDEX IF NOT EXISTS %I ON billiards_ods.%I (fetched_at)',
idx_name, rec.table_name
);
SELECT array_agg(kcu.column_name ORDER BY kcu.ordinal_position)
INTO pk_cols
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.table_schema = kcu.table_schema
AND tc.table_name = kcu.table_name
AND tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'billiards_ods'
AND tc.table_name = rec.table_name
AND tc.constraint_type = 'PRIMARY KEY';
IF pk_cols IS NOT NULL AND coalesce(array_length(pk_cols, 1), 0) <= 3 THEN
SELECT string_agg(format('%I', c), ', ')
INTO pk_cols_sql
FROM unnest(pk_cols) AS c;
idx_name := left(format('idx_%s_vfy_fetched_pk', rec.table_name), 50)
|| '_' || substr(md5(rec.table_name || '_vfy_fetched_pk'), 1, 8);
EXECUTE format(
'CREATE INDEX IF NOT EXISTS %I ON billiards_ods.%I (fetched_at, %s)',
idx_name, rec.table_name, pk_cols_sql
);
END IF;
END IF;
END LOOP;
END
$$;
DO $$
DECLARE
rec RECORD;
tcol TEXT;
pk_cols TEXT[];
pk_cols_sql TEXT;
idx_name TEXT;
time_candidates TEXT[] := ARRAY[
'pay_time',
'create_time',
'start_use_time',
'scd2_start_time',
'calc_time',
'order_date',
'fetched_at'
];
BEGIN
FOR rec IN
SELECT table_name
FROM information_schema.tables
WHERE table_schema = 'billiards_dwd'
AND table_type = 'BASE TABLE'
LOOP
SELECT array_agg(kcu.column_name ORDER BY kcu.ordinal_position)
INTO pk_cols
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.table_schema = kcu.table_schema
AND tc.table_name = kcu.table_name
AND tc.constraint_name = kcu.constraint_name
WHERE tc.table_schema = 'billiards_dwd'
AND tc.table_name = rec.table_name
AND tc.constraint_type = 'PRIMARY KEY';
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'billiards_dwd'
AND table_name = rec.table_name
AND column_name = 'scd2_is_current'
) AND pk_cols IS NOT NULL
AND coalesce(array_length(pk_cols, 1), 0) BETWEEN 1 AND 4 THEN
SELECT string_agg(format('%I', c), ', ')
INTO pk_cols_sql
FROM unnest(pk_cols) AS c;
idx_name := left(format('idx_%s_vfy_pk_current', rec.table_name), 50)
|| '_' || substr(md5(rec.table_name || '_vfy_pk_current'), 1, 8);
EXECUTE format(
'CREATE INDEX IF NOT EXISTS %I ON billiards_dwd.%I (%s, scd2_is_current)',
idx_name, rec.table_name, pk_cols_sql
);
END IF;
FOREACH tcol IN ARRAY time_candidates
LOOP
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'billiards_dwd'
AND table_name = rec.table_name
AND column_name = tcol
) THEN
idx_name := left(format('idx_%s_vfy_%s', rec.table_name, tcol), 50)
|| '_' || substr(md5(rec.table_name || '_vfy_' || tcol), 1, 8);
EXECUTE format(
'CREATE INDEX IF NOT EXISTS %I ON billiards_dwd.%I (%I)',
idx_name, rec.table_name, tcol
);
IF pk_cols IS NOT NULL AND coalesce(array_length(pk_cols, 1), 0) <= 3 THEN
SELECT string_agg(format('%I', c), ', ')
INTO pk_cols_sql
FROM unnest(pk_cols) AS c;
idx_name := left(format('idx_%s_vfy_%s_pk', rec.table_name, tcol), 50)
|| '_' || substr(md5(rec.table_name || '_vfy_' || tcol || '_pk'), 1, 8);
EXECUTE format(
'CREATE INDEX IF NOT EXISTS %I ON billiards_dwd.%I (%I, %s)',
idx_name, rec.table_name, tcol, pk_cols_sql
);
END IF;
END IF;
END LOOP;
END LOOP;
END
$$;
DO $$
DECLARE
rec RECORD;
BEGIN
FOR rec IN
SELECT table_schema, table_name
FROM information_schema.tables
WHERE table_schema IN ('billiards_ods', 'billiards_dwd')
AND table_type = 'BASE TABLE'
LOOP
EXECUTE format('ANALYZE %I.%I', rec.table_schema, rec.table_name);
END LOOP;
END
$$;

View File

@@ -0,0 +1,33 @@
-- =============================================================================
-- 创建测试数据库 test_etl_feiqiu
-- 结构与生产 etl_feiqiu 完全一致(六层 schemameta/ods/dwd/core/dws/app
-- 用途:开发和测试环境,避免影响生产数据
-- =============================================================================
-- 1. 创建数据库(需要以超级用户或有 CREATEDB 权限的角色执行)
-- 如果数据库已存在,先手动 DROP 或跳过此步
CREATE DATABASE test_etl_feiqiu
WITH ENCODING = 'UTF8'
LC_COLLATE = 'zh_CN.UTF-8'
LC_CTYPE = 'zh_CN.UTF-8'
TEMPLATE = template0;
-- 2. 连接到 test_etl_feiqiu 后,依次执行以下脚本初始化六层 schema
-- 每个脚本包含 CREATE SCHEMA IF NOT EXISTS + 该层所有表定义
--
-- 在 psql 中执行:
-- \c test_etl_feiqiu
-- \i ../schemas/meta.sql
-- \i ../schemas/ods.sql
-- \i ../schemas/dwd.sql
-- \i ../schemas/core.sql
-- \i ../schemas/dws.sql
-- \i ../schemas/app.sql
--
-- 3. 如需加载种子数据,执行 seeds 目录下的脚本:
-- \i ../seeds/*.sql
-- 注意事项:
-- - 生产 schema 变更后需同步在测试库执行相同的迁移脚本Requirements 9.4
-- - 迁移脚本位于 ../migrations/ 目录,按日期前缀顺序执行
-- - app schema 的 RLS 策略在测试库中同样生效,测试时需设置 app.current_site_id

View File

View File

@@ -0,0 +1,389 @@
-- =============================================================================
-- DWS 配置表初始数据
-- 版本: v3.0
-- 创建日期: 2026-02-01
-- 描述: 初始化配置表数据,包含绩效档位、等级定价、奖金规则、区域分类、技能映射
-- =============================================================================
-- NOTE: 当前数据库 cfg_* 配置表为空(以数据库现状为准)
-- 下方默认配置仅作参考,已整体注释
/*
-- =============================================================================
-- 1. cfg_performance_tier - 绩效档位配置(含历史口径)
-- 数据来源DWS 数据库处理需求.md
-- 旧方案历史口径至2026-02-28:
-- 0档 淘汰压力 H <100 28 50% 3
-- 1档 及格档(重点激励) 100≤ H <130 18 40% 4
-- 2档 良好档(重点激励) 130≤ H <160 15 38% 4
-- 3档 优秀档 160≤ H <190 13 35% 5
-- 4档 卓越加速档(高端人才倾斜) 190≤ H <220 10 33% 6
-- 5档 冠军加速档(高端人才倾斜) H ≥220 8 30% 休假自由
-- 新方案2026-03-01起:
-- 0档 淘汰压力 H <120 28 50% 3
-- 1档 及格档 120≤ H <150 18 40% 4
-- 2档 良好档 150≤ H <180 13 35% 5
-- 3档 优秀档 180≤ H <210 10 30% 6
-- 4档 销冠竞争 H ≥210 8 25% 休假自由
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_performance_tier RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_performance_tier (
tier_code, tier_name, tier_level,
min_hours, max_hours,
base_deduction, bonus_deduction_ratio, vacation_days, vacation_unlimited,
is_new_hire_tier, effective_from, effective_to, description
) VALUES
-- 旧方案至2026-02-28
-- 0档 淘汰压力: H<100, 专业课抽成28元/小时, 打赏课抽成50%, 休假3天
('T0', '0档-淘汰压力', 0,
0, 100,
28.00, 0.50, 3, FALSE,
FALSE, '2000-01-01', '2026-02-28',
'旧方案H<100专业课抽成28元/小时打赏课抽成50%休假3天'),
-- 1档 及格档: 100≤H<130, 专业课抽成18元/小时, 打赏课抽成40%, 休假4天
('T1', '1档-及格档', 1,
100, 130,
18.00, 0.40, 4, FALSE,
FALSE, '2000-01-01', '2026-02-28',
'旧方案100≤H<130专业课抽成18元/小时打赏课抽成40%休假4天'),
-- 2档 良好档: 130≤H<160, 专业课抽成15元/小时, 打赏课抽成38%, 休假4天
('T2', '2档-良好档', 2,
130, 160,
15.00, 0.38, 4, FALSE,
FALSE, '2000-01-01', '2026-02-28',
'旧方案130≤H<160专业课抽成15元/小时打赏课抽成38%休假4天'),
-- 3档 优秀档: 160≤H<190, 专业课抽成13元/小时, 打赏课抽成35%, 休假5天
('T3', '3档-优秀档', 3,
160, 190,
13.00, 0.35, 5, FALSE,
FALSE, '2000-01-01', '2026-02-28',
'旧方案160≤H<190专业课抽成13元/小时打赏课抽成35%休假5天'),
-- 4档 卓越加速档: 190≤H<220, 专业课抽成10元/小时, 打赏课抽成33%, 休假6天
('T4', '4档-卓越加速档', 4,
190, 220,
10.00, 0.33, 6, FALSE,
FALSE, '2000-01-01', '2026-02-28',
'旧方案190≤H<220专业课抽成10元/小时打赏课抽成33%休假6天'),
-- 5档 冠军加速档: H≥220, 专业课抽成8元/小时, 打赏课抽成30%, 休假自由
('T5', '5档-冠军加速档', 5,
220, NULL,
8.00, 0.30, 0, TRUE,
FALSE, '2000-01-01', '2026-02-28',
'旧方案H≥220专业课抽成8元/小时打赏课抽成30%,休假自由'),
-- 新方案2026-03-01起
-- 0档 淘汰压力: H<120, 专业课抽成28元/小时, 打赏课抽成50%, 休假3天
('T0', '0档-淘汰压力', 0,
0, 120,
28.00, 0.50, 3, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案H<120专业课抽成28元/小时打赏课抽成50%休假3天'),
-- 1档 及格档: 120≤H<150, 专业课抽成18元/小时, 打赏课抽成40%, 休假4天
('T1', '1档-及格档', 1,
120, 150,
18.00, 0.40, 4, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案120≤H<150专业课抽成18元/小时打赏课抽成40%休假4天'),
-- 2档 良好档: 150≤H<180, 专业课抽成13元/小时, 打赏课抽成35%, 休假5天
('T2', '2档-良好档', 2,
150, 180,
13.00, 0.35, 5, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案150≤H<180专业课抽成13元/小时打赏课抽成35%休假5天'),
-- 3档 优秀档: 180≤H<210, 专业课抽成10元/小时, 打赏课抽成30%, 休假6天
('T3', '3档-优秀档', 3,
180, 210,
10.00, 0.30, 6, FALSE,
FALSE, '2026-03-01', '9999-12-31',
'新方案180≤H<210专业课抽成10元/小时打赏课抽成30%休假6天'),
-- 4档 销冠竞争: H≥210, 专业课抽成8元/小时, 打赏课抽成25%, 休假自由
('T4', '4档-销冠竞争', 4,
210, NULL,
8.00, 0.25, 0, TRUE,
FALSE, '2026-03-01', '9999-12-31',
'新方案H≥210专业课抽成8元/小时打赏课抽成25%,休假自由');
-- =============================================================================
-- 2. cfg_assistant_level_price - 助教等级定价
-- 说明:
-- - level_code 来自 dim_assistant.assistant_level
-- - 8=助教管理, 10=初级, 20=中级, 30=高级, 40=星级
-- - 价格为客户支付价格(对外价格),助教收入=客户支付-档位抽成
-- - 包厢课基础课统一138元/小时(不随等级变化)
-- - 数据来源DWS 数据库处理需求.md
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_assistant_level_price RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_assistant_level_price (
level_code, level_name,
base_course_price, bonus_course_price,
effective_from, effective_to, description
) VALUES
-- 初级助教基础课对客户收费98元/小时
(10, '初级',
98.00, 190.00,
'2000-01-01', '9999-12-31',
'初级助教基础课98元/附加课190元/时(客户支付价格)'),
-- 中级助教基础课对客户收费108元/小时
(20, '中级',
108.00, 190.00,
'2000-01-01', '9999-12-31',
'中级助教基础课108元/附加课190元/时(客户支付价格)'),
-- 高级助教基础课对客户收费118元/小时
(30, '高级',
118.00, 190.00,
'2000-01-01', '9999-12-31',
'高级助教基础课118元/附加课190元/时(客户支付价格)'),
-- 星级助教基础课对客户收费138元/小时
(40, '星级',
138.00, 190.00,
'2000-01-01', '9999-12-31',
'星级助教基础课138元/附加课190元/时(客户支付价格)'),
-- 助教管理level_code=8通常不参与客户服务计费此处设置默认值
(8, '助教管理',
98.00, 190.00,
'2000-01-01', '9999-12-31',
'助教管理:不参与客户服务计费,默认按初级价格');
-- =============================================================================
-- 3. cfg_bonus_rules - 奖金规则配置
-- 说明:
-- - SPRINT: 冲刺奖金历史口径至2026-02-28
-- - TOP_RANK: Top3排名奖金2026-03-01起
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_bonus_rules RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_bonus_rules (
rule_type, rule_code, rule_name,
threshold_hours, rank_position, bonus_amount,
is_cumulative, priority,
effective_from, effective_to, description
) VALUES
-- 冲刺奖金: H>=190 得300元历史口径
('SPRINT', 'SPRINT_190', '冲刺奖金190',
190.00, NULL, 300.00,
FALSE, 1,
'2000-01-01', '2026-02-28',
'历史口径业绩≥190小时获得300元冲刺奖金不累计'),
-- 冲刺奖金: H>=220 得800元历史口径优先级更高覆盖190档
('SPRINT', 'SPRINT_220', '冲刺奖金220',
220.00, NULL, 800.00,
FALSE, 2,
'2000-01-01', '2026-02-28',
'历史口径业绩≥220小时获得800元冲刺奖金覆盖190档'),
-- Top1排名奖金: 1000元2026-03-01起
('TOP_RANK', 'TOP_1', 'Top1排名奖金',
NULL, 1, 1000.00,
FALSE, 0,
'2026-03-01', '9999-12-31',
'月度排名第一获得1000元并列都算'),
-- Top2排名奖金: 600元2026-03-01起
('TOP_RANK', 'TOP_2', 'Top2排名奖金',
NULL, 2, 600.00,
FALSE, 0,
'2026-03-01', '9999-12-31',
'月度排名第二获得600元并列都算'),
-- Top3排名奖金: 400元2026-03-01起
('TOP_RANK', 'TOP_3', 'Top3排名奖金',
NULL, 3, 400.00,
FALSE, 0,
'2026-03-01', '9999-12-31',
'月度排名第三获得400元并列都算');
-- =============================================================================
-- 4. cfg_area_category - 台区分类映射
-- 说明:
-- - 将 dim_table.site_table_area_name 映射到财务报表区域分类
-- - 映射规则: 精确匹配 > 模糊匹配 > 默认兜底
-- - 数据来源: BD_manual_dim_table.md 中的 site_table_area_name 实际分布
-- 分类设计:
-- - BILLIARD: 台球散台A区/B区/C区/TV台
-- - BILLIARD_VIP: 台球VIP包厢
-- - SNOOKER: 斯诺克区
-- - MAHJONG: 麻将区
-- - KTV: K歌/KTV
-- - SPECIAL: 特殊(补时长等)
-- - OTHER: 其他
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_area_category RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_area_category (
source_area_name, category_code, category_name,
match_type, match_priority, is_active, description
) VALUES
-- ============ 台球散台区(精确匹配)============
('A区', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台A区18台- 中八/追分'),
('B区', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台B区15台- 中八/追分'),
('C区', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台C区6台- 中八/追分'),
('TV台', 'BILLIARD', '台球散台',
'EXACT', 10, TRUE, '台球散台TV台1台- 中八/追分'),
-- ============ 台球VIP包厢精确匹配============
('VIP包厢', 'BILLIARD_VIP', '台球VIP',
'EXACT', 10, TRUE, '台球VIPVIP包厢4台- V1-V4中八, V5斯诺克'),
-- ============ 斯诺克区(精确匹配)============
('斯诺克区', 'SNOOKER', '斯诺克',
'EXACT', 10, TRUE, '斯诺克斯诺克区4台'),
-- ============ 麻将区(精确匹配)============
('麻将房', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌麻将房5台'),
('M7', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌M72台'),
('M8', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌M81台'),
('666', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌6662台'),
('发财', 'MAHJONG', '麻将棋牌',
'EXACT', 10, TRUE, '麻将棋牌发财1台'),
-- ============ KTV/K包精确匹配============
('K包', 'KTV', 'K歌娱乐',
'EXACT', 10, TRUE, 'K歌娱乐K包4台'),
('k包活动区', 'KTV', 'K歌娱乐',
'EXACT', 10, TRUE, 'K歌娱乐k包活动区2台'),
('幸会158', 'KTV', 'K歌娱乐',
'EXACT', 10, TRUE, 'K歌娱乐幸会1582台'),
-- ============ 特殊区域(精确匹配)============
('补时长', 'SPECIAL', '补时长',
'EXACT', 10, TRUE, '特殊补时长7台- 用于时长补录'),
-- ============ 模糊匹配规则(优先级较低)============
('%VIP%', 'BILLIARD_VIP', '台球VIP',
'LIKE', 50, TRUE, '模糊匹配:包含"VIP"的区域'),
('%斯诺克%', 'SNOOKER', '斯诺克',
'LIKE', 50, TRUE, '模糊匹配:包含"斯诺克"的区域'),
('%麻将%', 'MAHJONG', '麻将棋牌',
'LIKE', 50, TRUE, '模糊匹配:包含"麻将"的区域'),
('%K包%', 'KTV', 'K歌娱乐',
'LIKE', 50, TRUE, '模糊匹配:包含"K包"的区域'),
('%KTV%', 'KTV', 'K歌娱乐',
'LIKE', 50, TRUE, '模糊匹配:包含"KTV"的区域'),
-- ============ 默认兜底(优先级最低)============
('DEFAULT', 'OTHER', '其他',
'DEFAULT', 999, TRUE, '兜底规则:无法匹配的区域归入其他');
-- =============================================================================
-- 5. cfg_skill_type - 技能→课程类型映射
-- 说明:
-- - 将 skill_id 映射到课程类型
-- - 基础课/陪打: skill_id = 2791903611396869
-- - 附加课/超休: skill_id = 2807440316432197
-- - 避免依赖 skill_name 文本匹配
-- =============================================================================
TRUNCATE TABLE billiards_dws.cfg_skill_type RESTART IDENTITY CASCADE;
INSERT INTO billiards_dws.cfg_skill_type (
skill_id, skill_name,
course_type_code, course_type_name,
is_active, description
) VALUES
-- 基础课/陪打
(2791903611396869, '台球基础陪打',
'BASE', '基础课',
TRUE, '基础课:陪打服务,按助教等级计价'),
-- 附加课/超休
(2807440316432197, '台球超休服务',
'BONUS', '附加课',
TRUE, '附加课:超休/激励课固定190元/小时'),
-- 包厢课(如有)
(2807440316432198, '包厢服务',
'BASE', '基础课',
TRUE, '包厢服务归入基础课统计统一按138元/小时计价');
-- =============================================================================
-- 6. 优惠类型配置(用于财务优惠明细分析)
-- 说明: 定义各类优惠的代码和名称,便于后续分析
-- =============================================================================
-- 此配置作为代码常量使用,不单独建表
-- GROUPBUY - 团购优惠
-- VIP - 会员折扣
-- GIFT_CARD - 赠送卡抵扣
-- MANUAL - 手动调整
-- ROUNDING - 抹零
-- BIG_CUSTOMER - 大客户优惠(待抽样分析确认)
-- OTHER - 其他优惠
-- =============================================================================
-- 7. 支出类型配置用于Excel导入
-- 说明: 定义各类支出的代码和名称
-- =============================================================================
-- 此配置作为代码常量使用,不单独建表
-- RENT - 房租
-- UTILITY - 水电费
-- PROPERTY - 物业费
-- SALARY - 工资
-- REIMBURSE - 报销
-- PLATFORM_FEE - 平台服务费
-- OTHER - 其他支出
-- =============================================================================
-- 8. 平台类型配置用于Excel导入
-- 说明: 定义各平台的代码和名称
-- =============================================================================
-- 此配置作为代码常量使用,不单独建表
-- MEITUAN - 美团
-- DOUYIN - 抖音
-- DIANPING - 大众点评
-- OTHER - 其他平台
-- =============================================================================
-- 验证数据插入
-- =============================================================================
DO $$
DECLARE
v_tier_count INTEGER;
v_price_count INTEGER;
v_bonus_count INTEGER;
v_area_count INTEGER;
v_skill_count INTEGER;
BEGIN
SELECT COUNT(*) INTO v_tier_count FROM billiards_dws.cfg_performance_tier;
SELECT COUNT(*) INTO v_price_count FROM billiards_dws.cfg_assistant_level_price;
SELECT COUNT(*) INTO v_bonus_count FROM billiards_dws.cfg_bonus_rules;
SELECT COUNT(*) INTO v_area_count FROM billiards_dws.cfg_area_category;
SELECT COUNT(*) INTO v_skill_count FROM billiards_dws.cfg_skill_type;
RAISE NOTICE '配置数据初始化完成:';
RAISE NOTICE ' - cfg_performance_tier: % 条', v_tier_count;
RAISE NOTICE ' - cfg_assistant_level_price: % 条', v_price_count;
RAISE NOTICE ' - cfg_bonus_rules: % 条', v_bonus_count;
RAISE NOTICE ' - cfg_area_category: % 条', v_area_count;
RAISE NOTICE ' - cfg_skill_type: % 条', v_skill_count;
END;
$$;
*/

View File

@@ -0,0 +1,190 @@
-- =============================================================================
-- 指数算法参数初始化脚本
-- 版本: v3.0
-- 创建日期: 2026-02-13
-- 描述: 仅保留 RS / OS / MS / ML / NCI / WBI 指数参数(已移除 RECALL / INTIMACY
-- AI_CHANGELOG [2026-02-13] 移除 RECALL/INTIMACY 参数及 ML 废弃参数source_mode/recharge_attribute_hours
-- =============================================================================
-- 清理旧版指数参数
DELETE FROM billiards_dws.cfg_index_parameters WHERE index_type IN ('RECALL', 'INTIMACY');
-- 清理 ML 已废弃参数
DELETE FROM billiards_dws.cfg_index_parameters WHERE index_type = 'ML' AND param_name IN ('source_mode', 'recharge_attribute_hours');
INSERT INTO billiards_dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
('NCI', 'active_new_penalty', 0.200000, 'active-new suppression multiplier', DATE '2026-02-06'),
('NCI', 'active_new_recency_days', 7.000000, 'active-new recency window (days)', DATE '2026-02-06'),
('NCI', 'active_new_visit_threshold_14d', 2.000000, 'active-new threshold in 14d visits', DATE '2026-02-06'),
('NCI', 'amount_base_M0', 300.000000, 'spend log base M0', DATE '2026-02-06'),
('NCI', 'balance_base_B0', 500.000000, 'balance log base B0', DATE '2026-02-06'),
('NCI', 'compression_mode', 0.000000, 'compression mode', DATE '2026-02-06'),
('NCI', 'enable_stop_high_balance_exception', 0.000000, 'enable high-balance STOP exception', DATE '2026-02-06'),
('NCI', 'ewma_alpha', 0.200000, 'EWMA alpha', DATE '2026-02-06'),
('NCI', 'h_recharge', 7.000000, 'recharge decay half-life (days)', DATE '2026-02-06'),
('NCI', 'high_balance_threshold', 1000.000000, 'high-balance threshold', DATE '2026-02-06'),
('NCI', 'lookback_days_recency', 60.000000, 'recency lookback window (days)', DATE '2026-02-06'),
('NCI', 'new_days_threshold', 30.000000, 'new member days threshold', DATE '2026-02-06'),
('NCI', 'new_recharge_max_visits', 10.000000, 'max visits for new-recharge grouping', DATE '2026-02-06'),
('NCI', 'new_visit_threshold', 2.000000, 'new member visit threshold', DATE '2026-02-06'),
('NCI', 'no_touch_days_new', 3.000000, 'no-touch threshold (days)', DATE '2026-02-06'),
('NCI', 'percentile_lower', 5.000000, 'lower percentile', DATE '2026-02-06'),
('NCI', 'percentile_upper', 95.000000, 'upper percentile', DATE '2026-02-06'),
('NCI', 'recharge_recent_days', 14.000000, 'recent recharge window (days)', DATE '2026-02-06'),
('NCI', 'salvage_end', 60.000000, 'salvage decay end day', DATE '2026-02-06'),
('NCI', 'salvage_start', 30.000000, 'salvage decay start day', DATE '2026-02-06'),
('NCI', 't2_target_days', 7.000000, 'second-visit target window (days)', DATE '2026-02-06'),
('NCI', 'use_smoothing', 1.000000, 'enable smoothing', DATE '2026-02-06'),
('NCI', 'value_w_bal', 0.800000, 'value weight for balance', DATE '2026-02-06'),
('NCI', 'value_w_spend', 1.000000, 'value weight for spend', DATE '2026-02-06'),
('NCI', 'visit_lookback_days', 180.000000, 'visit history lookback (days)', DATE '2026-02-06'),
('NCI', 'w_need', 1.600000, 'need weight', DATE '2026-02-06'),
('NCI', 'w_re', 0.800000, 'recharge pressure weight', DATE '2026-02-06'),
('NCI', 'w_value', 1.000000, 'value weight', DATE '2026-02-06'),
('NCI', 'w_welcome', 1.000000, 'welcome-stage weight', DATE '2026-02-06'),
('NCI', 'welcome_window_days', 3.000000, 'welcome outreach window for first touch (days)', DATE '2026-02-06'),
('WBI', 'amount_base_M0', 300.000000, 'spend log base M0', DATE '2026-02-06'),
('WBI', 'balance_base_B0', 500.000000, 'balance log base B0', DATE '2026-02-06'),
('WBI', 'compression_mode', 0.000000, 'compression mode', DATE '2026-02-06'),
('WBI', 'enable_stop_high_balance_exception', 0.000000, 'enable high-balance STOP exception', DATE '2026-02-06'),
('WBI', 'ewma_alpha', 0.200000, 'EWMA alpha', DATE '2026-02-06'),
('WBI', 'h_recharge', 7.000000, 'recharge decay half-life (days)', DATE '2026-02-06'),
('WBI', 'high_balance_threshold', 1000.000000, 'high-balance threshold', DATE '2026-02-06'),
('WBI', 'lookback_days_recency', 60.000000, 'recency lookback window (days)', DATE '2026-02-06'),
('WBI', 'new_days_threshold', 30.000000, 'new member days threshold', DATE '2026-02-06'),
('WBI', 'new_recharge_max_visits', 10.000000, 'max visits for new-recharge grouping', DATE '2026-02-06'),
('WBI', 'new_visit_threshold', 2.000000, 'new member visit threshold', DATE '2026-02-06'),
('WBI', 'overdue_alpha', 2.000000, 'overdue fallback alpha', DATE '2026-02-06'),
('WBI', 'overdue_weight_blend_min_samples', 8.000000, 'minimum samples to fully trust weighted overdue CDF', DATE '2026-02-07'),
('WBI', 'overdue_weight_halflife_days', 30.000000, 'overdue weighted-CDF interval half-life (days)', DATE '2026-02-07'),
('WBI', 'percentile_lower', 5.000000, 'lower percentile', DATE '2026-02-06'),
('WBI', 'percentile_upper', 95.000000, 'upper percentile', DATE '2026-02-06'),
('WBI', 'recency_gate_days', 14.000000, 'recency suppression gate center (days)', DATE '2026-02-06'),
('WBI', 'recency_gate_slope_days', 3.000000, 'recency suppression slope (days)', DATE '2026-02-06'),
('WBI', 'recency_hard_floor_days', 14.000000, 'hard floor for winback recency (days)', DATE '2026-02-06'),
('WBI', 'recharge_recent_days', 14.000000, 'recent recharge window (days)', DATE '2026-02-06'),
('WBI', 'use_smoothing', 1.000000, 'enable smoothing', DATE '2026-02-06'),
('WBI', 'value_w_bal', 1.000000, 'value weight for balance', DATE '2026-02-06'),
('WBI', 'value_w_spend', 1.000000, 'value weight for spend', DATE '2026-02-06'),
('WBI', 'visit_lookback_days', 180.000000, 'visit history lookback (days)', DATE '2026-02-06'),
('WBI', 'w_drop', 1.000000, 'drop weight', DATE '2026-02-06'),
('WBI', 'w_over', 2.000000, 'overdue weight', DATE '2026-02-06'),
('WBI', 'w_re', 0.400000, 'recharge pressure weight', DATE '2026-02-06'),
('WBI', 'w_value', 1.200000, 'value weight', DATE '2026-02-06')
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- 关系指数RS/OS/MS/ML参数
-- 生效时间:北京时间 2026-01-01按数据库日期管理
-- =============================================================================
INSERT INTO billiards_dws.cfg_index_parameters
(index_type, param_name, param_value, description, effective_from)
VALUES
-- RS关系强度
('RS', 'lookback_days', 60.000000, '服务行为回溯窗口(天)', DATE '2026-01-01'),
('RS', 'session_merge_hours', 4.000000, '会话合并阈值(小时)', DATE '2026-01-01'),
('RS', 'incentive_weight', 1.500000, '激励课权重', DATE '2026-01-01'),
('RS', 'halflife_session', 14.000000, '会话半衰期(天)', DATE '2026-01-01'),
('RS', 'halflife_last', 10.000000, '最近一次服务半衰期(天)', DATE '2026-01-01'),
('RS', 'weight_f', 1.000000, '频次项权重', DATE '2026-01-01'),
('RS', 'weight_d', 0.700000, '时长项权重', DATE '2026-01-01'),
('RS', 'gate_alpha', 0.600000, '最近服务门控指数', DATE '2026-01-01'),
('RS', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-01-01'),
('RS', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-01-01'),
('RS', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-01-01'),
('RS', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-01-01'),
('RS', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-01-01'),
-- OS归属份额
('OS', 'min_rs_raw_for_ownership', 0.050000, '参与归属计算的最小RS_raw', DATE '2026-01-01'),
('OS', 'min_total_rs_raw', 0.100000, '形成稳定归属的最小sum_rs', DATE '2026-01-01'),
('OS', 'ownership_main_threshold', 0.600000, '主责阈值', DATE '2026-01-01'),
('OS', 'ownership_comanage_threshold', 0.350000, '共管阈值', DATE '2026-01-01'),
('OS', 'ownership_gap_threshold', 0.150000, '主责与次席份额差阈值', DATE '2026-01-01'),
('OS', 'eps', 0.000001, '数值稳定项', DATE '2026-01-01'),
-- MS升温动量
('MS', 'lookback_days', 60.000000, '服务行为回溯窗口(天)', DATE '2026-01-01'),
('MS', 'session_merge_hours', 4.000000, '会话合并阈值(小时)', DATE '2026-01-01'),
('MS', 'incentive_weight', 1.500000, '激励课权重', DATE '2026-01-01'),
('MS', 'halflife_short', 7.000000, '短期半衰期(天)', DATE '2026-01-01'),
('MS', 'halflife_long', 30.000000, '长期半衰期(天)', DATE '2026-01-01'),
('MS', 'eps', 0.000001, '数值稳定项', DATE '2026-01-01'),
('MS', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-01-01'),
('MS', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-01-01'),
('MS', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-01-01'),
('MS', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-01-01'),
('MS', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-01-01'),
-- ML付费关联
('ML', 'lookback_days', 60.000000, '充值行为回溯窗口(天)', DATE '2026-01-01'),
('ML', 'amount_base', 500.000000, '金额压缩基准', DATE '2026-01-01'),
('ML', 'halflife_recharge', 21.000000, '充值半衰期(天)', DATE '2026-01-01'),
('ML', 'percentile_lower', 5.000000, '展示分下分位', DATE '2026-01-01'),
('ML', 'percentile_upper', 95.000000, '展示分上分位', DATE '2026-01-01'),
('ML', 'compression_mode', 1.000000, '压缩模式0=none,1=log1p,2=asinh', DATE '2026-01-01'),
('ML', 'use_smoothing', 1.000000, '是否启用分位平滑', DATE '2026-01-01'),
('ML', 'ewma_alpha', 0.200000, 'EWMA平滑系数', DATE '2026-01-01')
ON CONFLICT (index_type, param_name, effective_from) DO UPDATE SET
param_value = EXCLUDED.param_value,
description = EXCLUDED.description,
updated_at = NOW();
-- =============================================================================
-- 验证
-- =============================================================================
DO $$
DECLARE
rs_count INTEGER;
os_count INTEGER;
ms_count INTEGER;
ml_count INTEGER;
nci_count INTEGER;
wbi_count INTEGER;
BEGIN
SELECT COUNT(*) INTO rs_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'RS';
SELECT COUNT(*) INTO os_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'OS';
SELECT COUNT(*) INTO ms_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'MS';
SELECT COUNT(*) INTO ml_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'ML';
SELECT COUNT(*) INTO nci_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'NCI';
SELECT COUNT(*) INTO wbi_count
FROM billiards_dws.cfg_index_parameters
WHERE index_type = 'WBI';
RAISE NOTICE 'RS 参数数量: %', rs_count;
RAISE NOTICE 'OS 参数数量: %', os_count;
RAISE NOTICE 'MS 参数数量: %', ms_count;
RAISE NOTICE 'ML 参数数量: %', ml_count;
RAISE NOTICE '新客转化参数数量: %', nci_count;
RAISE NOTICE '唤回指数参数数量: %', wbi_count;
END $$;
SELECT
index_type,
param_name,
param_value,
description,
effective_from
FROM billiards_dws.cfg_index_parameters
ORDER BY index_type, param_name, effective_from;

View File

@@ -0,0 +1,41 @@
-- 将新的 ODS 任务注册到 etl_admin.etl_task按需替换 store_id
-- 使用方式(示例):
-- psql "$PG_DSN" -f etl_billiards/database/seed_ods_tasks.sql
-- 或在 psql 中直接执行本文件内容。
WITH target_store AS (
SELECT 2790685415443269::bigint AS store_id -- TODO: 替换为实际 store_id
),
task_codes AS (
SELECT unnest(ARRAY[
-- Must match tasks/ods_tasks.py (ENABLED_ODS_CODES)
'ODS_ASSISTANT_ACCOUNT',
'ODS_ASSISTANT_LEDGER',
'ODS_ASSISTANT_ABOLISH',
'ODS_SETTLEMENT_RECORDS',
'ODS_TABLE_USE',
'ODS_PAYMENT',
'ODS_REFUND',
'ODS_PLATFORM_COUPON',
'ODS_MEMBER',
'ODS_MEMBER_CARD',
'ODS_MEMBER_BALANCE',
'ODS_RECHARGE_SETTLE',
'ODS_GROUP_PACKAGE',
'ODS_GROUP_BUY_REDEMPTION',
'ODS_INVENTORY_STOCK',
'ODS_INVENTORY_CHANGE',
'ODS_TABLES',
'ODS_GOODS_CATEGORY',
'ODS_STORE_GOODS',
'ODS_STORE_GOODS_SALES',
'ODS_TABLE_FEE_DISCOUNT',
'ODS_TENANT_GOODS',
'ODS_SETTLEMENT_TICKET'
]) AS task_code
)
INSERT INTO etl_admin.etl_task (task_code, store_id, enabled)
SELECT t.task_code, s.store_id, TRUE
FROM task_codes t CROSS JOIN target_store s
ON CONFLICT (task_code, store_id) DO UPDATE
SET enabled = EXCLUDED.enabled;

View File

@@ -0,0 +1,54 @@
-- Seed scheduler-compatible tasks into etl_admin.etl_task.
-- AI_CHANGELOG [2026-02-13] 移除 DWS_RECALL_INDEX/DWS_INTIMACY_INDEX 任务种子
--
-- Notes:
-- - These task_code values must match orchestration/task_registry.py.
-- - ODS_* tasks are intentionally excluded here because they don't follow the
-- BaseTask(cursor_data) scheduler interface in this repo version.
--
-- Usage (example):
-- psql "%PG_DSN%" -f etl_billiards/database/seed_scheduler_tasks.sql
--
WITH target_store AS (
SELECT 2790685415443269::bigint AS store_id -- TODO: replace with your store_id
),
task_codes AS (
SELECT unnest(ARRAY[
'ASSISTANT_ABOLISH',
'ASSISTANTS',
'COUPON_USAGE',
'CHECK_CUTOFF',
'DWD_LOAD_FROM_ODS',
'DWD_QUALITY_CHECK',
'INIT_DWD_SCHEMA',
'INIT_DWS_SCHEMA',
'INIT_ODS_SCHEMA',
'INVENTORY_CHANGE',
'LEDGER',
'MANUAL_INGEST',
'MEMBERS',
'MEMBERS_DWD',
'ODS_JSON_ARCHIVE',
'ORDERS',
'PACKAGES_DEF',
'PAYMENTS',
'PAYMENTS_DWD',
'PRODUCTS',
'REFUNDS',
'TABLE_DISCOUNT',
'TABLES',
'TICKET_DWD',
'TOPUPS',
'DWS_BUILD_ORDER_SUMMARY',
'DWS_WINBACK_INDEX',
'DWS_NEWCONV_INDEX',
'DWS_RELATION_INDEX',
'DWS_ML_MANUAL_IMPORT'
]) AS task_code
)
INSERT INTO etl_admin.etl_task (task_code, store_id, enabled)
SELECT t.task_code, s.store_id, TRUE
FROM task_codes t CROSS JOIN target_store s
ON CONFLICT (task_code, store_id) DO UPDATE
SET enabled = EXCLUDED.enabled,
updated_at = now();

0
db/fdw/.gitkeep Normal file
View File

64
db/fdw/setup_fdw.sql Normal file
View File

@@ -0,0 +1,64 @@
-- =============================================================================
-- FDW 映射配置 — 在 zqyy_app 数据库中执行
-- 用途:通过 postgres_fdw 将 etl_feiqiu.app schema 只读映射到 zqyy_app
-- 使业务后端无需直接连接 ETL 数据库即可读取汇总/维度数据。
-- 前提etl_feiqiu 数据库已部署 app schema 及 app_reader 角色(见 app.sql
-- Requirements: 8.3, 8.4, 8.5
-- =============================================================================
-- -----------------------------------------------------------------------------
-- 1. 安装 postgres_fdw 扩展
-- -----------------------------------------------------------------------------
CREATE EXTENSION IF NOT EXISTS postgres_fdw;
-- -----------------------------------------------------------------------------
-- 2. 创建外部服务器(指向 etl_feiqiu 数据库)
-- 部署时按实际环境替换 host / port
-- -----------------------------------------------------------------------------
CREATE SERVER IF NOT EXISTS etl_feiqiu_server
FOREIGN DATA WRAPPER postgres_fdw
OPTIONS (host 'localhost', dbname 'etl_feiqiu', port '5432');
-- -----------------------------------------------------------------------------
-- 3. 创建用户映射(只读角色)
-- app_user = zqyy_app 侧的应用连接角色
-- app_reader = etl_feiqiu 侧的只读角色(在 app.sql 中已创建)
-- 密码占位符 '***',部署时替换为真实凭据
-- -----------------------------------------------------------------------------
CREATE USER MAPPING IF NOT EXISTS FOR app_user
SERVER etl_feiqiu_server
OPTIONS (user 'app_reader', password '***');
-- -----------------------------------------------------------------------------
-- 4. 创建目标 schema存放外部表
-- -----------------------------------------------------------------------------
CREATE SCHEMA IF NOT EXISTS fdw_etl;
-- -----------------------------------------------------------------------------
-- 5. 导入 etl_feiqiu.app schema 的所有外部表到 fdw_etl
-- 映射为只读zqyy_app 不存储 ETL 数据副本Requirements 8.4
-- -----------------------------------------------------------------------------
IMPORT FOREIGN SCHEMA app
FROM SERVER etl_feiqiu_server
INTO fdw_etl;
-- -----------------------------------------------------------------------------
-- 6. 授权:允许 app_user 访问 fdw_etl schema 及其外部表
-- -----------------------------------------------------------------------------
GRANT USAGE ON SCHEMA fdw_etl TO app_user;
GRANT SELECT ON ALL TABLES IN SCHEMA fdw_etl TO app_user;
-- 未来新导入的外部表自动授权
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_etl GRANT SELECT ON TABLES TO app_user;
-- =============================================================================
-- 回滚脚本(按逆序执行)
-- =============================================================================
-- ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_etl REVOKE SELECT ON TABLES FROM app_user;
-- REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_etl FROM app_user;
-- REVOKE USAGE ON SCHEMA fdw_etl FROM app_user;
-- DROP SCHEMA IF EXISTS fdw_etl CASCADE;
-- DROP USER MAPPING IF EXISTS FOR app_user SERVER etl_feiqiu_server;
-- DROP SERVER IF EXISTS etl_feiqiu_server CASCADE;
-- DROP EXTENSION IF EXISTS postgres_fdw;

View File

@@ -0,0 +1,107 @@
-- =============================================================================
-- 从 LLZQ-test 迁移测试数据到新结构
-- 源数据库LLZQ-test旧测试库包含 billiards_ods/billiards_dwd/billiards_dws/etl_admin
-- 目标数据库test_etl_feiqiu新六层 schema+ test_zqyy_app业务应用库
--
-- 前置条件:
-- 1. 已执行 db/etl_feiqiu/scripts/create_test_db.sql 创建 test_etl_feiqiu
-- 2. 已执行 db/zqyy_app/scripts/create_test_db.sql 创建 test_zqyy_app
-- 3. 当前用户对 LLZQ-test 有 SELECT 权限,对目标库有 INSERT 权限
--
-- 执行方式:
-- psql -h <host> -U <user> -f migrate_test_data.sql
--
-- 注意:需要根据实际表映射关系调整,以下为参考模板
-- =============================================================================
-- ---------------------------------------------------------------------------
-- 第一部分etl_admin → meta调度/游标/运行记录)
-- ---------------------------------------------------------------------------
-- 迁移运行记录
INSERT INTO test_etl_feiqiu.meta.etl_run_log
SELECT * FROM dblink(
'dbname=LLZQ-test',
'SELECT * FROM etl_admin.etl_run_log'
) AS t(
-- 根据 meta.sql 中 etl_run_log 的实际列定义填充
-- 示例run_id bigint, task_code text, started_at timestamptz, ...
);
-- 迁移游标状态
INSERT INTO test_etl_feiqiu.meta.etl_cursor_state
SELECT * FROM dblink(
'dbname=LLZQ-test',
'SELECT * FROM etl_admin.etl_cursor_state'
) AS t(
-- 根据 meta.sql 中 etl_cursor_state 的实际列定义填充
);
-- ---------------------------------------------------------------------------
-- 第二部分billiards_ods → odsODS 原始数据)
-- schema 名从 billiards_ods 改为 ods表结构不变
-- ---------------------------------------------------------------------------
-- 会员档案
INSERT INTO test_etl_feiqiu.ods.member_profiles
SELECT * FROM dblink(
'dbname=LLZQ-test',
'SELECT * FROM billiards_ods.member_profiles'
) AS t(
-- 根据 ods.sql 中 member_profiles 的实际列定义填充
);
-- 会员余额变动
INSERT INTO test_etl_feiqiu.ods.member_balance_changes
SELECT * FROM dblink(
'dbname=LLZQ-test',
'SELECT * FROM billiards_ods.member_balance_changes'
) AS t(
-- 根据 ods.sql 中 member_balance_changes 的实际列定义填充
);
-- ... 对 ods schema 中的每张表重复上述模式 ...
-- 完整表清单参见 db/etl_feiqiu/schemas/ods.sql 中的 CREATE TABLE 语句
-- ---------------------------------------------------------------------------
-- 第三部分billiards_dwd → dwdDWD 明细数据)
-- schema 名从 billiards_dwd 改为 dwd保留 main+EX 拆分
-- ---------------------------------------------------------------------------
-- ... 对 dwd schema 中的每张表执行类似迁移 ...
-- 完整表清单参见 db/etl_feiqiu/schemas/dwd.sql 中的 CREATE TABLE 语句
-- ---------------------------------------------------------------------------
-- 第四部分billiards_dws → dwsDWS 汇总数据)
-- schema 名从 billiards_dws 改为 dws表结构不变
-- ---------------------------------------------------------------------------
-- ... 对 dws schema 中的每张表执行类似迁移 ...
-- 完整表清单参见 db/etl_feiqiu/schemas/dws.sql 中的 CREATE TABLE 语句
-- ---------------------------------------------------------------------------
-- 第五部分core schema新增层无历史数据可迁移
-- core 层为新增的统一最小字段集层,需从 dwd 数据重新生成
-- ---------------------------------------------------------------------------
-- 如需填充 core 层测试数据,可从已迁移的 dwd 数据中提取:
-- INSERT INTO test_etl_feiqiu.core.dim_member (member_id, name, mobile, status, site_id)
-- SELECT member_id, name, mobile, status, site_id
-- FROM test_etl_feiqiu.dwd.dim_member_main;
-- ---------------------------------------------------------------------------
-- 第六部分app schema视图层无需迁移数据
-- app 层仅包含视图和 RLS 策略,数据来自底层 schema
-- ---------------------------------------------------------------------------
-- 无需数据迁移,视图会自动引用底层表数据
-- ---------------------------------------------------------------------------
-- 迁移完成后验证
-- ---------------------------------------------------------------------------
-- 验证各 schema 表行数是否与源库一致
-- SELECT schemaname, relname, n_live_tup
-- FROM pg_stat_user_tables
-- WHERE schemaname IN ('meta', 'ods', 'dwd', 'dws', 'core')
-- ORDER BY schemaname, relname;

View File

View File

View File

@@ -0,0 +1,102 @@
-- =============================================================================
-- zqyy_app 数据库 Schema DDL
-- 业务应用数据库用户管理、RBAC 权限、任务管理、审批流程
-- 所有业务表包含 site_id 字段以支持多门店隔离Requirements 13.1
-- =============================================================================
-- ---------------------------------------------------------------------------
-- users 表:用户账户(微信 OpenID、手机号、昵称
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS users (
id BIGSERIAL PRIMARY KEY,
wx_openid TEXT UNIQUE,
mobile TEXT,
nickname TEXT,
status INT DEFAULT 1, -- 1=启用, 0=禁用
site_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_users_site_id ON users (site_id);
CREATE INDEX IF NOT EXISTS idx_users_mobile ON users (mobile);
-- ---------------------------------------------------------------------------
-- roles 表:角色定义
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name TEXT UNIQUE NOT NULL,
description TEXT,
site_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_roles_site_id ON roles (site_id);
-- ---------------------------------------------------------------------------
-- permissions 表:权限定义(全局,不需要 site_id
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS permissions (
id SERIAL PRIMARY KEY,
resource TEXT NOT NULL, -- 资源标识,如 'order', 'member'
action TEXT NOT NULL, -- 操作标识,如 'read', 'write', 'delete'
description TEXT,
UNIQUE (resource, action)
);
-- ---------------------------------------------------------------------------
-- user_roles 关联表:用户-角色多对多
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS user_roles (
user_id BIGINT REFERENCES users(id) ON DELETE CASCADE,
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
site_id BIGINT NOT NULL,
PRIMARY KEY (user_id, role_id)
);
CREATE INDEX IF NOT EXISTS idx_user_roles_site_id ON user_roles (site_id);
-- ---------------------------------------------------------------------------
-- role_permissions 关联表:角色-权限多对多
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS role_permissions (
role_id INT REFERENCES roles(id) ON DELETE CASCADE,
permission_id INT REFERENCES permissions(id) ON DELETE CASCADE,
PRIMARY KEY (role_id, permission_id)
);
-- ---------------------------------------------------------------------------
-- tasks 表:任务管理(含审批流)
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tasks (
id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
status TEXT DEFAULT 'pending', -- pending / approved / rejected / completed
assignee_id BIGINT REFERENCES users(id),
creator_id BIGINT REFERENCES users(id),
site_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_tasks_site_id ON tasks (site_id);
CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks (status);
CREATE INDEX IF NOT EXISTS idx_tasks_assignee_id ON tasks (assignee_id);
-- ---------------------------------------------------------------------------
-- approvals 表:审批记录
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS approvals (
id BIGSERIAL PRIMARY KEY,
task_id BIGINT REFERENCES tasks(id) ON DELETE CASCADE,
approver_id BIGINT REFERENCES users(id),
status TEXT DEFAULT 'pending', -- pending / approved / rejected
comment TEXT,
site_id BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX IF NOT EXISTS idx_approvals_site_id ON approvals (site_id);
CREATE INDEX IF NOT EXISTS idx_approvals_task_id ON approvals (task_id);

View File

@@ -0,0 +1,25 @@
-- =============================================================================
-- 创建测试数据库 test_zqyy_app
-- 结构与生产 zqyy_app 完全一致(用户/权限/任务/审批)
-- 用途:开发和测试环境,避免影响生产数据
-- =============================================================================
-- 1. 创建数据库(需要以超级用户或有 CREATEDB 权限的角色执行)
-- 如果数据库已存在,先手动 DROP 或跳过此步
CREATE DATABASE test_zqyy_app
WITH ENCODING = 'UTF8'
LC_COLLATE = 'zh_CN.UTF-8'
LC_CTYPE = 'zh_CN.UTF-8'
TEMPLATE = template0;
-- 2. 连接到 test_zqyy_app 后,执行初始化脚本
-- 包含所有表定义users/roles/permissions/user_roles/role_permissions/tasks/approvals
--
-- 在 psql 中执行:
-- \c test_zqyy_app
-- \i ../schemas/init.sql
-- 注意事项:
-- - 生产 schema 变更后需同步在测试库执行相同的迁移脚本Requirements 9.4
-- - 迁移脚本位于 ../migrations/ 目录,按日期前缀顺序执行
-- - 所有业务表包含 site_id 字段,测试时注意设置正确的 site_id 值

View File