# P12：赠送卡矩阵细分数据 — gift-card-breakdown

> 优先级：P1（BOARD-3 财务看板功能缺陷修复）
> 来源：RNS1 系列审计遗留项 P1-6
> 预估工作量：中
> 依赖：无新增依赖（DWD 层数据已就绪）

---

## 背景

BOARD-3 财务看板的「预收资产」板块包含一个赠送卡 3×4 矩阵（3 行：新增/消费/余额；4 列：合计/酒水卡/台费卡/抵用券）。当前矩阵除余额行的 total 列外，所有细分单元格均返回 0。

根因：DWS 层 `dws_finance_recharge_summary` 只存储赠送卡总额（`recharge_gift`、`gift_card_balance`），未按卡类型拆分。而 DWD 层 `dim_member_card_account` 已通过 `card_type_id` 区分三种赠送卡类型，数据源完备。

---

## 需求（Requirements）

### 用户故事

1. 作为门店管理者，我需要在财务看板中看到赠送卡按用途（酒水/台费/抵用券）拆分的余额、新增、消费数据，以便了解各类赠送卡的使用情况和资金分布。

### 验收标准

- AC1：`dws_finance_recharge_summary` 新增 6 个字段（3 种卡类型 × 余额+新增），ETL 任务正确填充
- AC2：BOARD-3 赠送卡矩阵「余额」行的 liquor/table_fee/voucher 列显示正确数值
- AC3：赠送卡矩阵「新增」行显示各类型赠送卡的新增金额（按充值订单的 `point_amount` 拆分）
- AC4：赠送卡矩阵「消费」行显示各类型赠送卡的消费金额（如 DWD 层可追溯）
- AC5：环比数据正确计算（如启用 compare 参数）
- AC6：RLS 视图自动包含新字段（`CREATE OR REPLACE VIEW` 使用 `SELECT *` 或显式列名）
- AC7：FDW 外部表通过 `IMPORT FOREIGN SCHEMA` 自动同步
- AC8：`GET /api/xcx/board/finance` 接口返回的 `gift_rows` 矩阵包含正确的细分数据（非全 0）
- AC9：小程序 `board-finance` 页面赠送卡矩阵正确渲染后端返回的细分数据（替换 mock）

---

## 数据源分析

### DWD 层现有数据

`dwd.dim_member_card_account` 通过 `card_type_id` 区分卡类型：

| card_type_id | 类型 | 当前记录数 | 当前余额 |
|---|---|---|---|
| 2793249295533893 | 储值卡 | 421 | 128,918.32 |
| 2791990152417157 | 台费卡（赠送） | 343 | 246,267.50 |
| 2793266846533445 | 活动抵用券（赠送） | 118 | 24,978.70 |
| 2794699703437125 | 酒水卡（赠送） | 49 | 3,708.95 |
| 2791987095408517 | 年卡 | 7 | 7.00 |
| 2793306611533637 | 月卡 | 12 | 4,938.00 |

ETL 任务 `FinanceRechargeTask._extract_card_balances()` 已硬编码三个赠送卡 ID：
```python
GIFT_CARD_TYPE_IDS = [2791990152417157, 2793266846533445, 2794699703437125]
```

### 矩阵数据需求

| 行 | 数据来源 | 说明 |
|---|---|---|
| 余额 | `dim_member_card_account` 按 `card_type_id` 分组 | 当日末快照，已有数据 |
| 新增 | `dwd_recharge_order.point_amount` 按会员卡类型拆分 | 需要 JOIN `dim_member_card_account` 确定卡类型 |
| 消费 | `dwd_settlement_head.gift_card_amount` | 总额已有，但无法按卡类型拆分（结算单不记录具体使用哪种赠送卡） |

### 关键约束

- **消费行拆分可能不可行**：`dwd_settlement_head.gift_card_amount` 是赠送卡消费总额，飞球上游 API 不提供按卡类型拆分的消费明细。需确认是否有 `dwd_settlement_head_ex` 或其他扩展表包含此信息。
- **新增行拆分**：充值订单 `dwd_recharge_order` 的 `tenant_member_card_id` 可关联 `dim_member_card_account.tenant_member_id`，从而确定充值到哪种卡类型。
- **card_type_id 硬编码**：当前 ETL 已硬编码，本次扩展沿用同一套 ID。后续可考虑迁移至配置表（feiqiu-data-rules 规则 6 精神）。

---

## 设计要点

### DDL 变更（`dws_finance_recharge_summary`）

新增 6 个字段：

| 字段名 | 类型 | 说明 |
|---|---|---|
| `gift_liquor_balance` | NUMERIC(14,2) | 酒水卡余额（当日末） |
| `gift_table_fee_balance` | NUMERIC(14,2) | 台费卡余额（当日末） |
| `gift_voucher_balance` | NUMERIC(14,2) | 抵用券余额（当日末） |
| `gift_liquor_recharge` | NUMERIC(14,2) | 酒水卡新增充值（赠送部分） |
| `gift_table_fee_recharge` | NUMERIC(14,2) | 台费卡新增充值（赠送部分） |
| `gift_voucher_recharge` | NUMERIC(14,2) | 抵用券新增充值（赠送部分） |

> 消费行拆分字段暂不新增（待确认上游数据可行性）。消费行 total 可通过 `gift_card_balance` 变化量 + 新增量反推（`consumed = prev_balance + recharge - current_balance`），但精度依赖余额快照的连续性。

### ETL 任务修改

修改 `FinanceRechargeTask._extract_card_balances()`：
- 将 `gift_balance` 拆分为 3 个细分余额
- 新增 `_extract_gift_recharge_breakdown()` 方法，通过 `dwd_recharge_order JOIN dim_member_card_account` 按卡类型拆分赠送金额

### 后端接口修改

接口路径：`GET /api/xcx/board/finance`（BOARD-3 财务看板）
- 路由：`apps/backend/app/routers/xcx_board.py` → `get_finance_board()`
- 权限：`view_board_finance`

涉及文件与修改点：

| 文件 | 修改点 |
|---|---|
| `apps/backend/app/services/fdw_queries.py` | `get_finance_recharge()` — SQL 新增 6 个字段的 SUM，填充 `gift_rows` 矩阵的余额行和新增行细分数据 |
| `apps/backend/app/services/fdw_queries.py` | `_empty_recharge_data()` — 空默认值同步新增字段 |
| `apps/backend/app/services/board_service.py` | `_build_recharge()` — 环比计算已覆盖 `gift_rows` 所有 cell，无需额外修改（自动适配） |
| `apps/backend/app/schemas/xcx_board.py` | `GiftCell` / `GiftRow` / `RechargePanel` — schema 无需修改（已预留 `liquor`/`table_fee`/`voucher` 字段） |

> 消费行：如果无法直接拆分，使用 `余额变化量反推法` 或保持 total only。

### 小程序页面影响

页面路径：`apps/miniprogram/miniprogram/pages/board-finance/`

| 文件 | 当前状态 | 修改点 |
|---|---|---|
| `board-finance.wxml` (L316-370) | 赠送卡矩阵已渲染 `recharge.giftRows`，3 行 × 4 列（酒水卡/台费卡/抵用券） | 无需修改（模板已就绪，数据绑定字段已对齐） |
| `board-finance.ts` | 当前使用 mock 数据（`giftRows` 硬编码） | 联调时替换 mock 为真实 API 调用（非本 SPEC 范围，属于联调阶段） |
| `board-finance.wxss` (L643-717) | 赠送卡表格样式已完成 | 无需修改 |

字段映射关系（后端 → 小程序）：

| 后端字段 | 小程序绑定 |
|---|---|
| `GiftRow.total.value` | `item.total` |
| `GiftRow.liquor.value` | `item.wine` |
| `GiftRow.table_fee.value` | `item.table` |
| `GiftRow.voucher.value` | `item.coupon` |
| `GiftRow.*.compare` | `item.*Compare` |

> 小程序页面模板和样式已在 RNS1 阶段完成，本次只需后端返回正确数据即可自动渲染。联调阶段需将 mock 数据替换为真实 API 调用。

### 管理后台

admin-web 无涉及赠送卡/充值统计的页面，不受影响。

### RLS 视图 & FDW

- `app.v_dws_finance_recharge_summary` 使用 `CREATE OR REPLACE VIEW`，需要更新列列表
- FDW 使用 `IMPORT FOREIGN SCHEMA`，需要重新导入（幂等脚本已支持）

---

## 数据流向

```
DWD 层
├─ dwd_recharge_order.point_amount ──→ JOIN dim_member_card_account ──→ 按 card_type_id 拆分赠送金额
├─ dim_member_card_account.balance ──→ 按 card_type_id 分组求和 ──→ 3 种赠送卡余额
└─ dwd_settlement_head.gift_card_amount ──→ 总额（无法按卡类型拆分）

    ↓ ETL（FinanceRechargeTask）

DWS 层：dws_finance_recharge_summary（+6 字段）

    ↓ RLS 视图 + FDW

业务库：app.v_dws_finance_recharge_summary

    ↓ fdw_queries.get_finance_recharge()

FastAPI：GET /api/xcx/board/finance → RechargePanel.gift_rows

    ↓ 小程序渲染

board-finance 页面：赠送卡矩阵 3×4 显示正确数值
```

---

## 待确认项

1. **消费行拆分**：`dwd_settlement_head_ex` 或其他表是否包含赠送卡消费的卡类型明细？如果没有，消费行只能显示 total，细分列保持 0 或使用反推法。
2. **card_type_id 2791987095408517 和 2793306611533637**：已确认为年卡和月卡，不纳入赠送卡矩阵。
3. **环比需求**：赠送卡矩阵的每个 cell 是否需要环比数据（compare 字段）？当前 schema `GiftCell` 已预留 `compare: str | None`。

---

## 任务清单

### 数据层（ETL + DDL）

- [ ] T1：DDL 迁移 — `dws_finance_recharge_summary` 新增 6 个字段
  - 迁移脚本：`db/etl_feiqiu/migrations/2026-xx-xx_add_gift_breakdown_fields.sql`
  - DDL 基线同步：`docs/database/ddl/etl_feiqiu__dws.sql`
- [ ] T2：ETL 修改 — `_extract_card_balances()` 拆分赠送卡余额
  - 文件：`apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py`
  - 返回值新增 `gift_liquor_balance`、`gift_table_fee_balance`、`gift_voucher_balance`
- [ ] T3：ETL 新增 — `_extract_gift_recharge_breakdown()` 拆分赠送卡新增
  - 文件：同 T2
  - SQL：`dwd_recharge_order JOIN dim_member_card_account` 按 `card_type_id` 分组
- [ ] T4：ETL transform — `transform()` 方法写入新增 6 个字段到 record dict

### 数据库视图层

- [ ] T5：RLS 视图更新 — `app.v_dws_finance_recharge_summary` 重建
  - 迁移脚本：`db/etl_feiqiu/migrations/` 或 `db/zqyy_app/migrations/`
- [ ] T6：FDW 外部表同步 — `IMPORT FOREIGN SCHEMA` 重新导入

### 后端接口层

- [ ] T7：后端修改 — `fdw_queries.get_finance_recharge()`
  - 文件：`apps/backend/app/services/fdw_queries.py`
  - SQL 新增 6 个字段的 SUM
  - 填充 `gift_rows` 矩阵：余额行 + 新增行的 liquor/table_fee/voucher
- [ ] T8：后端修改 — `_empty_recharge_data()` 空默认值同步

### 小程序页面层

- [ ] T9：小程序联调 — `board-finance.ts` 替换 mock 数据为真实 API 调用
  - 文件：`apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts`
  - 当前状态：mock 数据硬编码，模板和样式已就绪
  - 注意：字段映射 `liquor→wine`、`table_fee→table`、`voucher→coupon` 需在数据转换层处理

### 验证与文档

- [ ] T10：验证 — 跑数后对比 `dim_member_card_account` 余额与 DWS 汇总值
- [ ] T11：BD 手册更新 — `BD_manual_dws_finance_recharge_summary.md`

### 涉及文件汇总

| 模块 | 文件路径 | 操作 |
|---|---|---|
| DDL | `db/etl_feiqiu/migrations/2026-xx-xx_add_gift_breakdown_fields.sql` | 新增 |
| DDL 基线 | `docs/database/ddl/etl_feiqiu__dws.sql` | 修改 |
| ETL | `apps/etl/connectors/feiqiu/tasks/dws/finance_recharge_task.py` | 修改 |
| RLS 视图 | `db/etl_feiqiu/migrations/` 或 `db/zqyy_app/migrations/` | 新增 |
| 后端 | `apps/backend/app/services/fdw_queries.py` | 修改 |
| 小程序 | `apps/miniprogram/miniprogram/pages/board-finance/board-finance.ts` | 修改 |
| Schema | `apps/backend/app/schemas/xcx_board.py` | 无需修改（已预留） |
| 路由 | `apps/backend/app/routers/xcx_board.py` | 无需修改 |
| 服务 | `apps/backend/app/services/board_service.py` | 无需修改（环比自动适配） |
| BD 手册 | `apps/etl/connectors/feiqiu/docs/database/DWS/main/BD_manual_dws_finance_recharge_summary.md` | 修改 |
