# BD 手册：助教手动补录会员生日表（C2）

## 概述

为支持助教手动提交客户生日信息（上游 API 未提供时的补充渠道），在 `zqyy_app` / `test_zqyy_app` 业务库中新建 `member_birthday_manual` 表。该表通过 FDW 只读映射供 ETL DWS 任务读取，与 `dim_member.birthday`（API 来源）配合实现生日数据的双源合并。

## 变更说明

| 库 | Schema | 表 | 变更类型 | 说明 |
|----|--------|---|---------|------|
| zqyy_app / test_zqyy_app | public | member_birthday_manual | 新建表 | 助教手动补录的会员生日信息 |

### 表结构

| 列名 | 类型 | 约束 | 说明 |
|------|------|------|------|
| id | BIGSERIAL | PRIMARY KEY | 自增主键 |
| member_id | BIGINT | NOT NULL | 会员 ID |
| birthday_value | DATE | NOT NULL | 补录的生日值 |
| recorded_by_assistant_id | BIGINT | — | 提交助教 ID |
| recorded_by_name | VARCHAR(50) | — | 提交助教姓名 |
| recorded_at | TIMESTAMPTZ | NOT NULL DEFAULT NOW() | 提交时间 |
| source | VARCHAR(20) | DEFAULT 'assistant' | 数据来源标识 |
| site_id | BIGINT | NOT NULL | 门店 ID（多门店隔离，Requirements 13.1） |

### 约束与索引

| 名称 | 类型 | 列 | 说明 |
|------|------|---|------|
| member_birthday_manual_pkey | PRIMARY KEY | id | 主键 |
| uk_member_birthday_manual | UNIQUE | (member_id, recorded_by_assistant_id) | 同一助教对同一会员只保留一条记录，支持 UPSERT |
| idx_mbd_member | INDEX (btree) | member_id | 加速按会员 ID 查询 |
| idx_mbd_site_id | INDEX (btree) | site_id | 加速按门店 ID 查询 |

## 兼容性

- **ETL Connector**：后续通过 FDW 反向映射（`fdw_app.member_birthday_manual`）在 ETL 库中只读访问，DWS 任务使用 `COALESCE(手动补录值, API 值)` 合并生日数据
- **后端 API**：新增 `POST /member-birthday` 接口执行 UPSERT 写入（任务 9.4）
- **小程序**：助教端调用后端 API 提交生日，无直接数据库访问
- **现有数据**：新建表，无历史数据影响

## 回滚策略

```sql
BEGIN;
DROP TABLE IF EXISTS member_birthday_manual CASCADE;
COMMIT;
```

回滚无数据丢失风险：该表为新建表，回滚前的数据均为手动补录的生日信息，可由助教重新提交。若已建立 FDW 映射，需先在 ETL 库中删除外部表 `fdw_app.member_birthday_manual`。

## 验证步骤

```sql
-- 1. 确认表存在
SELECT table_name
  FROM information_schema.tables
 WHERE table_schema = 'public'
   AND table_name = 'member_birthday_manual';
-- 预期：1 行

-- 2. 确认唯一约束 uk_member_birthday_manual 存在
SELECT conname, contype
  FROM pg_constraint
 WHERE conrelid = 'member_birthday_manual'::regclass
   AND conname = 'uk_member_birthday_manual';
-- 预期：1 行，contype = 'u'

-- 3. 确认索引 idx_mbd_member 存在
SELECT indexname
  FROM pg_indexes
 WHERE tablename = 'member_birthday_manual'
   AND indexname = 'idx_mbd_member';
-- 预期：1 行

-- 4. 确认表注释已设置
SELECT obj_description('member_birthday_manual'::regclass, 'pg_class');
-- 预期：'助教手动补录的会员生日信息'

-- 5. 确认列结构完整
SELECT column_name, data_type, is_nullable
  FROM information_schema.columns
 WHERE table_schema = 'public'
   AND table_name = 'member_birthday_manual'
 ORDER BY ordinal_position;
-- 预期：8 列（id, member_id, birthday_value, recorded_by_assistant_id,
--        recorded_by_name, recorded_at, source, site_id）

-- 6. 确认 site_id 索引存在
SELECT indexname
  FROM pg_indexes
 WHERE tablename = 'member_birthday_manual'
   AND indexname = 'idx_mbd_site_id';
-- 预期：1 行
```

## 关联文件

- 迁移脚本：`db/zqyy_app/migrations/2026-02-22__C2_member_birthday_manual.sql`
- FDW 反向映射（生产）：`db/fdw/setup_fdw_reverse.sql` — 在 etl_feiqiu 中创建 `fdw_app.member_birthday_manual` 外部表
- FDW 反向映射（测试）：`db/fdw/setup_fdw_reverse_test.sql` — 在 test_etl_feiqiu 中创建外部表
- FDW 反向映射文档：`docs/database/BD_Manual_fdw_reverse_member_birthday.md`
- 需求文档：`.kiro/specs/etl-aggregation-fix/requirements.md` — 需求 5.1, 5.3
- 后续任务：9.3（DWS 任务生日读取优先级）、9.4（后端 API 接口）
