# 数据库变更：dws_finance_area_daily 增加 member_order_count 列

> 日期：2026-04-23
> 迁移脚本：[db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql](../../db/etl_feiqiu/migrations/20260423__app2a_add_member_order_count.sql)
> 相关设计文档：[docs/ai/app2_finance_multi_app_design.md](../ai/app2_finance_multi_app_design.md) · Phase B
> 涉及库：`etl_feiqiu`（六层 DWS）
> 风险等级：**低**（新增列带默认值，对历史行为无破坏）

---

## 1 · 变更说明

### 新增列

| Schema | 表 | 列名 | 类型 | 默认值 | Nullable | 说明 |
|---|---|---|---|---|---|---|
| `dws` | `dws_finance_area_daily` | `member_order_count` | `integer` | `0` | `NOT NULL` | 会员订单数（区域粒度，从 DWD 聚合 `is_member_order = true` 的订单计数） |

### 更新视图

| Schema | 视图 | 变更 |
|---|---|---|
| `app` | `v_dws_finance_area_daily` | `CREATE OR REPLACE` · 暴露 `member_order_count` 列（**追加到视图末尾**）· RLS 过滤保持 |
| `dws` | `v_dws_finance_area_daily` | `CREATE OR REPLACE` · 暴露 `member_order_count` 列（**追加到视图末尾**）· RLS 过滤保持 · 遵守双 schema 规则 |

**列顺序说明**：PostgreSQL `CREATE OR REPLACE VIEW` 仅允许在视图**末尾**追加列，不能在中间插入。因此 `member_order_count` 放在 `updated_at` 之后（而非紧邻 `order_count`）。列顺序不影响语义，后端查询按列名访问。

### 新增文档

无（本文档即为同步文档）

---

## 2 · 兼容性影响

### 对 ETL（飞球 Connector）

- **必须改造** · `apps/etl/connectors/feiqiu/loaders/dws_finance_area_daily*.py`（对应 loader）
  - 聚合 DWD 订单数据时按 `(area_code, stat_date)` 分组
  - 将 `is_member_order = true` 的订单数归入 `member_order_count` 列
  - 上线前 ETL 未改造时历史与新增数据均为 `0`（接受降级）

### 对后端 API

- **新增 · 向后兼容** · 后端查询 `app.v_dws_finance_area_daily` 选取列时增加 `member_order_count` 即可访问
- 已知消费者：`apps/backend/app/ai/prompts/app2a_finance_area_prompt.py`（新建，本设计文档 Phase C 实施）
- 其他读 `v_dws_finance_area_daily` 的代码：未读新列则无影响

### 对小程序字段映射

- 无直接影响（小程序不直接读 DWS 视图，通过后端 `/api/board/finance` 中转）
- P2 上线后，面板的"区域会员订单占比"派生字段会生效（经 app2a prompt 内部计算，不通过 board API）

### 对 FDW 跨库映射

- `etl_feiqiu.app.v_dws_finance_area_daily` 通过 FDW 映射到 `zqyy_app.etl_app` schema（若已映射），需确认 FDW 侧是否要重导（FDW foreign table 的列定义可能需同步）
- **验证项**：`zqyy_app` 端查询 `etl_app.v_dws_finance_area_daily` 是否能看到新列，若不能，需重跑 FDW 刷新脚本 `db/fdw/` 下对应 remap 脚本

---

## 3 · 回滚策略

### 回滚步骤（按顺序执行）

```sql
BEGIN;

-- 1. 先 DROP 两个 RLS 视图（否则 DROP COLUMN 会因依赖失败）
DROP VIEW IF EXISTS app.v_dws_finance_area_daily;
DROP VIEW IF EXISTS dws.v_dws_finance_area_daily;

-- 2. DROP 新列
ALTER TABLE dws.dws_finance_area_daily
  DROP COLUMN IF EXISTS member_order_count;

-- 3. 重建两个 RLS 视图（去掉 member_order_count · 从 db/etl_feiqiu/schemas/app.sql:714 复制原定义）
-- 注意：因 member_order_count 在原视图中是末尾追加，回滚时只需去掉该列即可
-- app schema
CREATE OR REPLACE VIEW app.v_dws_finance_area_daily AS
SELECT id, site_id, tenant_id, stat_date, area_code,
       table_fee_amount, goods_amount, assistant_pd_amount, assistant_cx_amount,
       gross_amount,
       discount_groupbuy, discount_vip, discount_manual,
       discount_gift_card, discount_rounding, discount_other, discount_total,
       confirmed_income,
       cash_pay_amount, cash_paper_amount, scan_pay_amount, groupbuy_pay_amount,
       recharge_cash_inflow, cash_inflow_total, cash_outflow_total, cash_balance_change,
       card_consume_total, recharge_card_consume, gift_card_consume,
       recharge_cash, first_recharge_cash, renewal_cash,
       order_count,
       created_at, updated_at
  FROM dws.dws_finance_area_daily
 WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;

-- dws schema（双 schema 规则）
CREATE OR REPLACE VIEW dws.v_dws_finance_area_daily AS
SELECT id, site_id, tenant_id, stat_date, area_code,
       table_fee_amount, goods_amount, assistant_pd_amount, assistant_cx_amount,
       gross_amount,
       discount_groupbuy, discount_vip, discount_manual,
       discount_gift_card, discount_rounding, discount_other, discount_total,
       confirmed_income,
       cash_pay_amount, cash_paper_amount, scan_pay_amount, groupbuy_pay_amount,
       recharge_cash_inflow, cash_inflow_total, cash_outflow_total, cash_balance_change,
       card_consume_total, recharge_card_consume, gift_card_consume,
       recharge_cash, first_recharge_cash, renewal_cash,
       order_count,
       created_at, updated_at
  FROM dws.dws_finance_area_daily
 WHERE site_id = (current_setting('app.current_site_id'::text))::bigint;

COMMIT;
```

### 回滚前置条件

- 确认无正在运行的 ETL loader 写入 `member_order_count`
- 确认 `apps/backend/app/ai/prompts/app2a_finance_area_prompt.py` 已 `git revert`（否则查询缺列会报错）
- 确认 FDW 映射同步回滚（若 FDW foreign table 显式列出 member_order_count，需重建 foreign table）

### 数据回填回滚

- 新列默认 0 · 无需数据回填即可回滚
- 若已执行过 `scripts/ops/backfill_area_member_order.py`（未来脚本），回滚后历史数据会丢失 member_order_count 值 · 但 DWD 原始订单数据在 `dwd.dwd_orders` 保留 · 可从 DWD 重新回填

---

## 4 · 验证 SQL

### 验证 1 · 列存在性

```sql
SELECT column_name,
       data_type,
       column_default,
       is_nullable
  FROM information_schema.columns
 WHERE table_schema = 'dws'
   AND table_name = 'dws_finance_area_daily'
   AND column_name = 'member_order_count';

-- 期望：1 行 · integer · 0 · NO
```

### 验证 2 · 双 schema RLS 视图暴露新列

```sql
SELECT table_schema, table_name, column_name
  FROM information_schema.columns
 WHERE table_name = 'v_dws_finance_area_daily'
   AND column_name = 'member_order_count'
 ORDER BY table_schema;

-- 期望：2 行 · (app, v_dws_finance_area_daily, member_order_count) + (dws, v_dws_finance_area_daily, member_order_count)
```

### 验证 3 · 全店 = 区域和（ETL loader 改造完成后执行）

```sql
-- 设置测试门店 ID
SET app.current_site_id = 2790685415443269;

-- 比较最近 7 天 全店会员订单数 vs 区域会员订单数之和
SELECT s.site_id,
       s.stat_date,
       s.member_order_count                    AS full_store_member_orders,
       COALESCE(SUM(a.member_order_count), 0)  AS sum_area_member_orders,
       s.member_order_count - COALESCE(SUM(a.member_order_count), 0) AS diff
  FROM app.v_dws_finance_daily_summary s
  LEFT JOIN app.v_dws_finance_area_daily a
    ON s.site_id = a.site_id
   AND s.stat_date = a.stat_date
 WHERE s.stat_date >= CURRENT_DATE - INTERVAL '7 days'
 GROUP BY s.site_id, s.stat_date, s.member_order_count
HAVING s.member_order_count <> COALESCE(SUM(a.member_order_count), 0);

-- 期望：0 行（全店会员订单数 = 所有区域之和）
-- 注：若 ETL loader 尚未改造，此 SQL 返回若干行属正常降级状态（区域侧全为 0）
```

### 验证 4 · RLS 过滤正常工作

```sql
-- 未设置 app.current_site_id 时应返回空
RESET app.current_site_id;
SELECT COUNT(*) FROM app.v_dws_finance_area_daily;
-- 期望：0 行 或 报错 "current_setting not found"

-- 设置后应返回该门店数据
SET app.current_site_id = 2790685415443269;
SELECT site_id, COUNT(*) FROM app.v_dws_finance_area_daily GROUP BY site_id;
-- 期望：1 行（仅 site_id = 2790685415443269）
```

---

## 5 · 关联变更

| 关联项 | 状态 | 说明 |
|---|---|---|
| ETL loader 改造 | ⏳ 待实施 · Phase B2 | `apps/etl/connectors/feiqiu/loaders/dws_finance_area_daily*.py` 按 `is_member_order` 聚合 |
| `apps/backend/app/ai/prompts/app2a_finance_area_prompt.py` | ⏳ 待实施 · Phase C | 消费新列计算区域级会员占比 |
| FDW 跨库映射刷新 | ⏳ 待确认 | 若 `zqyy_app` 端通过 FDW 访问 `etl_feiqiu.app.v_dws_finance_area_daily`，需确认 foreign table 是否需重建 |
| DWS 快照 DDL 同步 | ⏳ 待执行 | 本迁移合并后运行 `PYTHONUTF8=1 python tools/db/gen_consolidated_ddl.py` 刷新 `docs/database/ddl/` 快照 |

---

## 6 · 变更记录

| 日期 | 操作 | 执行人 |
|---|---|---|
| 2026-04-23 | 迁移脚本产出 | Claude + Neo |
| 待定 | 测试库执行 + 验证 SQL 通过 | Neo |
| 待定 | 生产库执行 | Neo |
