# BD 手册：FDW 反向映射 — ETL 库读取业务库维客线索

> 创建日期：2026-02-26（原 `member_birthday_manual`），2026-03-19 重写为当前版本
> 替代文档：`docs/database/_archived/BD_Manual_fdw_reverse_member_birthday.md`
> 关联 SQL：`db/fdw/setup_fdw_reverse.sql`（生产）、`db/fdw/setup_fdw_reverse_test.sql`（测试）
> 关联表文档：`docs/database/BD_Manual_member_retention_clue.md`

---

## 1. 概述

在 `etl_feiqiu`（生产）/ `test_etl_feiqiu`（测试）数据库中，通过 `postgres_fdw` 创建指向 `zqyy_app` / `test_zqyy_app` 的外部表 `fdw_app.member_retention_clue`，使 ETL DWS 任务可只读访问助教为会员记录的维客线索数据。

方向：`etl_feiqiu → zqyy_app`（与正向 FDW `setup_fdw.sql` 的 `zqyy_app → etl_feiqiu` 方向相反）。

### 数据流向

```
zqyy_app.public.member_retention_clue  （业务库源表，后端 API 写入）
        │
        │  postgres_fdw（只读）
        ▼
etl_feiqiu.fdw_app.member_retention_clue  （ETL 库外部表，DWS 任务读取）
```

### 历史沿革

| 日期 | 事件 |
|------|------|
| 2026-02-22 | 初版：`fdw_app.member_birthday_manual`（映射助教手动补录生日表） |
| 2026-02-26 | 重构：`member_birthday_manual` → `member_retention_clue`（维客线索替代单一生日方案） |
| 2026-02-27 | 业务库侧新增 `source` 列（线索来源），FDW 外部表定义未同步更新 |
| 2026-03-08 | 业务库侧 `category` 枚举对齐：`客户基础信息` → `客户基础` |

---

## 2. 变更说明

### 2.1 新增对象

| 所在库 | 对象类型 | 名称 | 说明 |
|--------|---------|------|------|
| etl_feiqiu / test_etl_feiqiu | 扩展 | `postgres_fdw` | PostgreSQL 外部数据包装器（如已安装则跳过） |
| etl_feiqiu | 外部服务器 | `zqyy_app_server` | 指向 `zqyy_app` 业务库（host/port 按环境配置） |
| test_etl_feiqiu | 外部服务器 | `test_zqyy_app_server` | 指向 `test_zqyy_app` 测试业务库 |
| etl_feiqiu / test_etl_feiqiu | 用户映射 | `etl_user → app_reader` | ETL 连接角色映射到业务库只读角色 |
| etl_feiqiu / test_etl_feiqiu | Schema | `fdw_app` | 存放来自业务库的外部表 |
| etl_feiqiu / test_etl_feiqiu | 外部表 | `fdw_app.member_retention_clue` | 映射 `public.member_retention_clue` |

### 2.2 外部表列定义

| 列名 | 类型 | 说明 |
|------|------|------|
| id | BIGINT | 自增主键 |
| member_id | BIGINT | 会员 ID |
| category | VARCHAR(20) | 线索大类（6 值枚举：客户基础/消费习惯/玩法偏好/促销偏好/社交关系/重要反馈） |
| summary | VARCHAR(200) | 摘要 |
| detail | TEXT | 详情 |
| recorded_by_assistant_id | BIGINT | 记录助教 ID |
| recorded_by_name | VARCHAR(50) | 记录助教姓名 |
| recorded_at | TIMESTAMPTZ | 记录时间 |
| site_id | BIGINT | 门店 ID |

> **注意**：业务库侧 `member_retention_clue` 已于 2026-02-27 新增 `source VARCHAR(20)` 列（线索来源：`manual` / `ai_consumption` / `ai_note`），但当前 FDW 外部表定义（`db/fdw/setup_fdw_reverse*.sql`）尚未包含此列。如 ETL 任务需要读取 `source` 字段，需更新外部表定义并重新执行部署脚本。

### 2.3 角色与权限

| 角色 | 所在库 | 用途 |
|------|--------|------|
| `etl_user` | etl_feiqiu / test_etl_feiqiu | ETL 连接角色，通过 FDW 只读访问业务库数据 |
| `app_reader` | zqyy_app / test_zqyy_app | 业务库只读角色，供 FDW 用户映射使用 |

权限配置：

| 角色 | Schema | 权限 |
|------|--------|------|
| `etl_user` | `fdw_app` | `USAGE` + `SELECT ON ALL TABLES` + `ALTER DEFAULT PRIVILEGES`（自动授权未来新增外部表） |

---

## 3. 与正向 FDW 的对比

| 维度 | 正向 FDW（`setup_fdw.sql`） | 反向 FDW（`setup_fdw_reverse.sql`） |
|------|---------------------------|-------------------------------------|
| 执行位置 | `zqyy_app` 业务库 | `etl_feiqiu` ETL 库 |
| 数据方向 | 业务库读取 ETL 数据 | ETL 库读取业务库数据 |
| 目标 Schema | `fdw_etl`（35 张外部表） | `fdw_app`（1 张外部表） |
| 导入方式 | `IMPORT FOREIGN SCHEMA app`（批量） | `CREATE FOREIGN TABLE`（逐表定义） |
| 消费方 | 后端 API | ETL DWS 任务 |
| 文档 | `BD_Manual_fdw_etl_setup.md` | 本文档 |

---

## 4. 兼容性影响

| 组件 | 影响 |
|------|------|
| ETL DWS 任务 | 可通过 `fdw_app.member_retention_clue` 只读访问维客线索数据。当前无 DWS 任务直接消费此表（原 `member_birthday_manual` 的生日读取逻辑已移除，生日仅从 `dim_member.birthday` 读取） |
| 后端 API | 无影响。后端直接写入 `zqyy_app.public.member_retention_clue`，不经过 FDW |
| 小程序 | 无影响。小程序通过后端 API 间接访问 |
| 管理后台 | 无影响 |
| 正向 FDW（`fdw_etl`） | 无影响。两个方向的 FDW 配置完全独立 |
| 业务库 `member_retention_clue` 表 | 无影响。FDW 为只读映射，不修改源表 |

### 幂等性说明

`CREATE FOREIGN TABLE IF NOT EXISTS` 确保重复执行不会报错。如需更新列定义（如添加 `source` 列），需先 `DROP FOREIGN TABLE` 再重建，或使用 `ALTER FOREIGN TABLE ADD COLUMN`。

---

## 5. 回滚策略

### 5.1 完整回滚（按逆序执行）

```sql
-- 在 etl_feiqiu（生产）中执行：
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user;
REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user;
REVOKE USAGE ON SCHEMA fdw_app FROM etl_user;
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
DROP SCHEMA IF EXISTS fdw_app CASCADE;
DROP USER MAPPING IF EXISTS FOR etl_user SERVER zqyy_app_server;
DROP SERVER IF EXISTS zqyy_app_server CASCADE;
-- 注意：如果其他外部表也使用 postgres_fdw，不要执行 DROP EXTENSION
```

```sql
-- 在 test_etl_feiqiu（测试）中执行：
ALTER DEFAULT PRIVILEGES IN SCHEMA fdw_app REVOKE SELECT ON TABLES FROM etl_user;
REVOKE SELECT ON ALL TABLES IN SCHEMA fdw_app FROM etl_user;
REVOKE USAGE ON SCHEMA fdw_app FROM etl_user;
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
DROP SCHEMA IF EXISTS fdw_app CASCADE;
DROP USER MAPPING IF EXISTS FOR etl_user SERVER test_zqyy_app_server;
DROP SERVER IF EXISTS test_zqyy_app_server CASCADE;
```

### 5.2 仅删除外部表（保留 FDW 基础设施）

```sql
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
```

回滚无数据丢失风险：外部表不存储数据，仅为远程表的映射定义。

---

## 6. 验证 SQL

以下 SQL 在 ETL 库（`etl_feiqiu` 或 `test_etl_feiqiu`）中执行：

```sql
-- 1. 确认 postgres_fdw 扩展已安装
SELECT extname, extversion
  FROM pg_extension
 WHERE extname = 'postgres_fdw';
-- 预期：1 行

-- 2. 确认外部服务器存在
-- 生产：
SELECT srvname, srvoptions FROM pg_foreign_server WHERE srvname = 'zqyy_app_server';
-- 测试：
SELECT srvname, srvoptions FROM pg_foreign_server WHERE srvname = 'test_zqyy_app_server';
-- 预期：1 行，srvoptions 包含正确的 host/dbname/port

-- 3. 确认用户映射存在
SELECT um.umid, r.rolname AS local_role, s.srvname, um.umoptions
  FROM pg_user_mappings um
  JOIN pg_foreign_server s ON s.srvname = um.srvname
  JOIN pg_roles r ON r.rolname = um.usename
 WHERE s.srvname IN ('zqyy_app_server', 'test_zqyy_app_server')
   AND r.rolname = 'etl_user';
-- 预期：1 行

-- 4. 确认 fdw_app schema 存在
SELECT schema_name
  FROM information_schema.schemata
 WHERE schema_name = 'fdw_app';
-- 预期：1 行

-- 5. 确认外部表列结构完整（当前 9 列）
SELECT column_name, data_type
  FROM information_schema.columns
 WHERE table_schema = 'fdw_app'
   AND table_name = 'member_retention_clue'
 ORDER BY ordinal_position;
-- 预期：9 行（id, member_id, category, summary, detail,
--              recorded_by_assistant_id, recorded_by_name, recorded_at, site_id）

-- 6. 确认外部表在 information_schema.foreign_tables 中注册
SELECT foreign_table_schema, foreign_table_name, foreign_server_name
  FROM information_schema.foreign_tables
 WHERE foreign_table_schema = 'fdw_app'
   AND foreign_table_name = 'member_retention_clue';
-- 预期：1 行

-- 7. 确认 etl_user 对 fdw_app 有 USAGE 权限
SELECT has_schema_privilege('etl_user', 'fdw_app', 'USAGE') AS has_usage;
-- 预期：true

-- 8. 确认外部表可读取（需业务库侧表已存在且网络连通）
SELECT COUNT(*) FROM fdw_app.member_retention_clue;
-- 预期：返回行数（可能为 0）

-- 9. 确认 ALTER DEFAULT PRIVILEGES 已设置
SELECT n.nspname AS schema_name, d.defaclacl AS default_acl
  FROM pg_default_acl d
  JOIN pg_namespace n ON n.oid = d.defaclnamespace
 WHERE n.nspname = 'fdw_app';
-- 预期：1 行，default_acl 包含 etl_user 的 SELECT 权限
```

---

## 7. 已知差异与待办

| 项目 | 状态 | 说明 |
|------|------|------|
| `source` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `source VARCHAR(20) NOT NULL DEFAULT 'manual'`（2026-02-27），FDW 外部表定义未包含。当前无 ETL 任务需要此字段，但未来如需读取线索来源需先更新外部表 |
| `is_hidden` 列缺失 | ⚠️ 待同步 | 业务库侧已有 `is_hidden BOOLEAN NOT NULL DEFAULT false`（2026-03-20，NS4 迁移），FDW 外部表定义未包含。如 ETL 任务需要过滤隐藏线索需先更新外部表 |
| DWS 任务消费 | 📋 待规划 | 原 `member_birthday_manual` 的 DWS 消费逻辑已移除。维客线索的 DWS 聚合任务尚未规划 |

### source 列同步方法（备用）

如需同步 `source` 列，在 ETL 库中执行：

```sql
-- 方法 1：ALTER 追加列（推荐，无需重建）
ALTER FOREIGN TABLE fdw_app.member_retention_clue
  ADD COLUMN source VARCHAR(20);

-- 方法 2：DROP + 重建（完整重置）
DROP FOREIGN TABLE IF EXISTS fdw_app.member_retention_clue;
CREATE FOREIGN TABLE fdw_app.member_retention_clue (
    id                       BIGINT,
    member_id                BIGINT,
    category                 VARCHAR(20),
    summary                  VARCHAR(200),
    detail                   TEXT,
    recorded_by_assistant_id BIGINT,
    recorded_by_name         VARCHAR(50),
    recorded_at              TIMESTAMPTZ,
    site_id                  BIGINT,
    source                   VARCHAR(20)
) SERVER zqyy_app_server
  OPTIONS (schema_name 'public', table_name 'member_retention_clue');
```

同步后需更新 `db/fdw/setup_fdw_reverse.sql` 和 `db/fdw/setup_fdw_reverse_test.sql` 中的外部表定义。

---

## 8. 关联文件

| 文件 | 说明 |
|------|------|
| `db/fdw/setup_fdw_reverse.sql` | 生产环境部署脚本（在 `etl_feiqiu` 中执行） |
| `db/fdw/setup_fdw_reverse_test.sql` | 测试环境部署脚本（在 `test_etl_feiqiu` 中执行） |
| `docs/database/BD_Manual_member_retention_clue.md` | 业务库侧 `member_retention_clue` 表结构文档 |
| `docs/database/BD_Manual_fdw_etl_setup.md` | 正向 FDW 配置文档（方向相反） |
| `docs/database/BD_Manual_app_schema_rls_views.md` | ETL 库 `app` Schema RLS 视图层文档 |
| `db/zqyy_app/migrations/2026-02-26__refactor_birthday_to_retention_clue.sql` | 业务库侧建表迁移脚本 |
