# Implementation Plan: 财务看板 DWS 区域维度重构

## Overview

将财务看板的优惠数据从全局 DWS 取数改为按区域日粒度预计算，分 4 个阶段实施：基础设施层（共享映射 + DDL）→ ETL 层（两个新任务）→ 后端层（查询改造 + 缓存逻辑）→ 收尾（联调、DDL 合并、文档、审计）。每个阶段末尾设检查点，确保增量验证。

## Tasks

- [x] 1. 共享区域映射配置与属性测试
  - [x] 1.1 创建 `packages/shared/src/neozqyy_shared/area_mapping.py`
    - 定义 `AREA_LABEL_MAP` 字典（7 个具体区域 → 物理名称列表）
    - 定义 `SPECIFIC_AREA_CODES`、`ALL_AREA_CODES` 常量
    - 构建 `_REVERSE_MAP` 反向映射
    - 实现 `resolve_area_code(area_name)` 和 `get_area_labels(area_code)` 函数
    - _Requirements: 1.1, 1.2, 1.3, 1.5_

  - [x] 1.2 编写属性测试：区域映射 round-trip
    - **Property 1: 区域映射 round-trip**
    - 生成器：从 `AREA_LABEL_MAP` 所有值列表中随机选取 `area_name`
    - 验证：`resolve_area_code(area_name)` 返回正确 `area_code`，且 `area_name in get_area_labels(result)`
    - **验证: 需求 1.1, 1.5**

  - [x] 1.3 编写属性测试：未知区域名称返回 None
    - **Property 2: 未知区域名称返回 None**
    - 生成器：`st.text()` 生成随机字符串，过滤掉已知 area_name
    - 验证：`resolve_area_code(unknown_name)` 返回 `None`
    - **验证: 需求 1.4**

  - [x] 1.4 编写单元测试：area_mapping 边界条件
    - 测试文件：`tests/test_area_mapping_unit.py`
    - 覆盖：空字符串、None、大小写敏感、特殊字符、hall/all 的 get_area_labels 返回 None
    - _Requirements: 1.4, 1.5_

- [x] 2. DDL：创建 dws_finance_area_daily 表与 RLS 视图
  - [x] 2.1 编写 DDL 迁移脚本
    - 创建 `dws.dws_finance_area_daily` 表（含收入 5 字段、优惠 7 字段、confirmed_income、现金流 8 字段、卡消费 3 字段、充值 3 字段、order_count）
    - 添加 UNIQUE 约束 `(site_id, stat_date, area_code)`
    - 创建 RLS 视图 `v_dws_finance_area_daily`（`WHERE site_id = current_setting('app.current_site_id')::bigint`）
    - _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_

- [x] 3. 检查点 — 基础设施层验证
  - 确保 area_mapping 属性测试和单元测试通过：`cd C:\Project\NeoZQYY && pytest tests/test_area_mapping_props.py tests/test_area_mapping_unit.py -v`
  - 确保 DDL 迁移脚本语法正确
  - ask the user if questions arise.

- [x] 4. ETL：DWS_FINANCE_AREA_DAILY 任务与属性测试
  - [x] 4.1 创建 `apps/etl/connectors/feiqiu/tasks/dws/finance_area_daily.py`
    - 继承 `FinanceBaseTask`，实现 `get_task_code`/`get_target_table`/`get_primary_keys`
    - `extract`：从 `dwd_settlement_head` + `dim_table`（`scd2_is_current=1`）提取当天结算单（`settle_type IN (1,3)`，按 `BUSINESS_DAY_START_HOUR` 切点），同时从 `dws_finance_daily_summary` 提取全局现金流/充值/卡消费
    - `transform`：使用 `resolve_area_code` 映射区域，按区域聚合收入和优惠字段，构建 9 行（hallA~ktv + hall + all），非 all 行现金流/卡消费/充值字段为 0
    - `load`：delete-before-insert（按 `site_id + stat_date` 删除后插入 9 行，单事务）
    - `discount_gift_card` 使用赠送卡消费金额口径（与现有 ETL 一致）
    - _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 8.1, 8.2_

  - [x] 4.2 编写属性测试：日粒度行数学恒等式
    - **Property 3: 日粒度行数学恒等式**
    - 生成器：随机生成结算单列表（金额用 `st.decimals`，area_name 从已知+未知混合）
    - 验证：transform 输出的每行满足 gross_amount = 四项之和、discount_total = 六项之和、confirmed_income = gross - discount
    - **验证: 需求 2.1, 2.2, 2.3, 8.3**

  - [x] 4.3 编写属性测试：非 all 区域现金流为零
    - **Property 4: 非 all 区域现金流/卡消费/充值为零**
    - 生成器：随机结算单列表 + 全局现金流数据
    - 验证：transform 输出中 `area_code ≠ 'all'` 的行，所有现金流/卡消费/充值字段 = 0
    - **验证: 需求 2.5**

  - [x] 4.4 编写属性测试：ETL 输出完整性与聚合正确性
    - **Property 5: ETL 输出完整性与聚合正确性**
    - 生成器：随机结算单列表
    - 验证：输出恰好 9 行，all 行收入/优惠 = hallA~ktv 之和，hall 行 = hallA~ktv 之和
    - **验证: 需求 2.7, 2.8, 8.4**

  - [x] 4.5 编写属性测试：ETL 幂等性
    - **Property 6: ETL 幂等性（delete-before-insert）**
    - 生成器：随机结算单列表
    - 验证：对同一输入运行两次 transform，两次输出完全相同
    - **验证: 需求 3.4**

  - [x] 4.6 编写属性测试：settle_type 过滤
    - **Property 7: settle_type 过滤**
    - 生成器：包含不同 settle_type 值的结算单列表
    - 验证：仅 `settle_type IN (1, 3)` 的记录影响输出金额
    - **验证: 需求 3.6**

  - [x] 4.7 编写单元测试：ETL transform 边界条件
    - 测试文件：`apps/etl/connectors/feiqiu/tests/unit/test_finance_area_daily.py`
    - 覆盖：discount_gift_card 口径验证、营业日切点边界、未知区域名称处理、空结算单输入
    - _Requirements: 3.1, 3.2, 3.9_

- [x] 5. DDL：创建 dws_finance_board_cache 表与 RLS 视图
  - [x] 5.1 编写 DDL 迁移脚本
    - 创建 `dws.dws_finance_board_cache` 表（overview 8 项 + 日期范围 + 指纹 + 元数据）
    - 添加 UNIQUE 约束 `(site_id, time_range, area_code)`
    - 创建 RLS 视图 `v_dws_finance_board_cache`
    - _Requirements: 4.1, 4.2, 4.3, 4.4_

- [x] 6. ETL：DWS_FINANCE_BOARD_CACHE 任务与属性测试
  - [x] 6.1 创建 `apps/etl/connectors/feiqiu/tasks/dws/finance_board_cache.py`
    - 继承 `BaseDwsTask`
    - `extract`：遍历 5 个已完成周期 × 9 个区域 = 45 组合，从 `dws_finance_area_daily` 读取日粒度行
    - `transform`：实现 `compute_fingerprint`（MD5），与缓存表对比，标记需重算的组合
    - `load`：对需重算的组合从日粒度表 SUM 后 upsert 到缓存表（`ON CONFLICT DO UPDATE`）
    - _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7_

  - [x] 6.2 编写属性测试：数据指纹确定性与缓存失效
    - **Property 8: 数据指纹确定性与缓存失效**
    - 生成器：随机日粒度行列表
    - 验证：相同输入产生相同指纹；修改任意行的 gross_amount 或 discount_total 后指纹变化
    - **验证: 需求 5.2, 5.3, 5.4**

  - [x] 6.3 编写属性测试：当期周期不写入缓存
    - **Property 9: 当期周期不写入缓存**
    - 生成器：随机 time_range 从 {month, week, quarter}
    - 验证：ETL 缓存任务不为当期 time_range 写入缓存记录
    - **验证: 需求 5.7**

  - [x] 6.4 编写单元测试：缓存任务边界条件
    - 测试文件：`apps/etl/connectors/feiqiu/tests/unit/test_finance_board_cache.py`
    - 覆盖：指纹变化检测、空数据处理、upsert 幂等性
    - _Requirements: 5.2, 5.3, 5.4_

- [x] 7. 检查点 — ETL 层验证
  - 运行 ETL 属性测试：`cd C:\Project\NeoZQYY && pytest tests/test_finance_area_daily_props.py tests/test_finance_board_cache_props.py -v`
  - 运行 ETL 单元测试：`cd apps/etl/connectors/feiqiu && pytest tests/unit/test_finance_area_daily.py tests/unit/test_finance_board_cache.py -v`
  - 确保 Property 3-9 全部通过
  - ask the user if questions arise.

- [x] 8. 后端：改造 fdw_queries.py 查询函数与属性测试
  - [x] 8.1 新增/改造 `apps/backend/app/fdw_queries.py` 中的查询函数
    - 新增 `get_finance_overview_area(conn, site_id, start_date, end_date, area_code)` — 从 `v_dws_finance_area_daily` 按 area_code 聚合 overview 8 项指标
    - 新增 `get_finance_revenue_area(conn, site_id, start_date, end_date, area_code)` — 从 `v_dws_finance_area_daily` 按 area_code 聚合 revenue 板块数据
    - 新增 `get_finance_board_cache(conn, site_id, time_range, area_code)` — 查询 `v_dws_finance_board_cache` 缓存
    - 新增 `set_finance_board_cache(conn, site_id, time_range, area_code, data)` — 写入/更新缓存
    - 使用 `SET LOCAL app.current_site_id` 保证 RLS 隔离
    - _Requirements: 6.1, 6.2, 6.3, 6.5, 6.6_

  - [x] 8.2 编写属性测试：查询路由正确性
    - **Property 10: 查询路由正确性**
    - 生成器：随机查询参数（time_range、area_code）+ mock 数据库返回
    - 验证：已完成周期+缓存存在→返回缓存；缓存不存在→SUM+写缓存；当期→直接 SUM 不查缓存
    - **验证: 需求 6.1, 6.2, 6.3, 9.4**

  - [x] 8.3 编写属性测试：区域过滤行为
    - **Property 11: 区域过滤行为**
    - 生成器：随机 area_code ≠ 'all' + mock 数据
    - 验证：recharge 返回 null；cashflow/expense/coach_analysis 使用全局数据
    - **验证: 需求 6.7, 6.8**

  - [x] 8.4 编写属性测试：revenue 固定项数
    - **Property 12: revenue 固定项数**
    - 生成器：随机查询参数 + mock 数据
    - 验证：discount_items 恰好 5 项，channel_items 恰好 3 项
    - **验证: 需求 7.3, 7.4**

  - [x] 8.5 编写属性测试：area≠all 时 overview 覆盖逻辑
    - **Property 13: area≠all 时 overview 覆盖逻辑**
    - 生成器：随机 area_code ≠ 'all' + mock revenue 数据
    - 验证：overview.occurrence = revenue.total_occurrence，overview.discount = revenue.discount_total，overview.confirmed_revenue = revenue.confirmed_total
    - **验证: 需求 7.6**

  - [x] 8.6 编写单元测试：fdw_queries 查询正确性
    - 测试文件：`apps/backend/tests/unit/test_fdw_queries_area.py`
    - 覆盖：SQL 正确性、area_code 过滤、缓存命中/未命中、RLS 隔离
    - _Requirements: 6.1, 6.2, 6.5, 6.6_

- [x] 9. 后端：改造 board_service.py 缓存查询逻辑
  - [x] 9.1 改造 `apps/backend/app/board_service.py` 的 `get_finance_board` 函数
    - 新增缓存查询逻辑：已完成周期先查 `get_finance_board_cache`，命中直接返回
    - 缓存未命中：从日粒度表 SUM 计算，写入缓存后返回
    - 当期周期：直接从日粒度表 SUM，不查缓存
    - `_build_overview` 改为调用 `get_finance_overview_area`（传入 area_code）
    - `_build_revenue` 改为调用 `get_finance_revenue_area`（传入 area_code）
    - `_build_cashflow` 不变（始终用全局数据）
    - `area≠all` 时 overview 覆盖逻辑保留（occurrence/discount/confirmedRevenue = revenue 对应值）
    - `area≠all` 时 recharge 返回 null
    - compare=1 时对上期执行同样缓存/日粒度逻辑
    - _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 7.1, 7.2, 7.5, 7.6_

  - [x] 9.2 编写属性测试：area=all 回归一致性
    - **Property 14: area=all 回归一致性**
    - 生成器：随机日期范围 + mock 新旧逻辑数据
    - 验证：area=all 时新逻辑的 overview 8 项指标与旧逻辑完全一致
    - **验证: 需求 9.1**

  - [x] 9.3 编写单元测试：board_service 改造
    - 测试文件：`apps/backend/tests/unit/test_board_service_area.py`
    - 覆盖：缓存命中/未命中路径、覆盖逻辑、环比计算、降级行为（无数据返回全零）
    - _Requirements: 6.1, 6.2, 6.3, 7.6, 9.1_

- [x] 10. 检查点 — 后端层验证
  - 运行后端属性测试：`cd C:\Project\NeoZQYY && pytest tests/test_board_service_props.py -v`
  - 运行后端单元测试：`cd apps/backend && pytest tests/unit/test_fdw_queries_area.py tests/unit/test_board_service_area.py -v`
  - 确保 Property 10-14 全部通过
  - ask the user if questions arise.

- [x] 11. 历史数据回填脚本
  - [x] 11.1 编写回填脚本 `scripts/ops/backfill_finance_area_daily.py`
    - 对已有日期范围批量调用 `FinanceAreaDailyTask.transform` 逻辑
    - 支持指定 site_id 和日期范围参数
    - 回填完成后触发 `DWS_FINANCE_BOARD_CACHE` 重算所有已完成周期缓存
    - _Requirements: 2.7, 3.4, 5.1_

- [x] 12. 前后端联调与集成验证
  - [x] 12.1 启动后端服务，使用测试库验证各端点完整请求-响应链路
    - 使用真实 FDW 连接验证 SQL 查询正确性
    - 验证 JSON 响应结构与 Schema 定义一致（camelCase 序列化）
    - 验证数据隔离（`SET LOCAL app.current_site_id`）在真实请求中生效
    - _Requirements: 7.1, 7.2, 9.1_
  - [x] 12.2 运行 144 组合全量验证脚本
    - 执行 `scripts/ops/validate_board_finance.py`（8 time_range × 9 area_code × 2 compare）
    - 确认 area=all 时所有板块数据与重构前完全一致（回归测试）
    - 确认 area≠all 时 discountRate 不出现 400%+ 异常值
    - 确认已完成周期第二次请求命中缓存
    - _Requirements: 9.1, 9.2, 9.3, 9.4_
  - [x] 12.3 前端联调验证
    - 确认小程序财务看板页能正确调用 API 并渲染数据（前端零改动）
    - 验证空数据/降级场景下前端不崩溃
    - 如前端页面尚未开发，记录待联调清单供后续任务使用
    - _Requirements: 7.1, 7.2_

- [x] 13. 数据库变更审计与 DDL 合并
  - [x] 13.1 审计本次实现中对数据库的所有改动
    - 检查新建表（dws_finance_area_daily、dws_finance_board_cache）、RLS 视图、FDW 映射变更
    - _Requirements: 2.6, 4.1_
  - [x] 13.2 执行迁移脚本到测试库
    - 验证新表和索引已正确创建（使用 BD 手册中的验证 SQL）
    - _Requirements: 2.6, 4.1_
  - [x] 13.3 合并到主 DDL 基线文件
    - ETL 库 → `docs/database/ddl/etl_feiqiu__dws.sql`
    - FDW → `db/fdw/` 对应文件
    - _Requirements: 2.6, 4.1_
  - [x] 13.4 编写回滚脚本（逆序 DROP TABLE/VIEW）
    - _Requirements: 2.6, 4.1_

- [x] 14. BD 手册更新
  - [x] 14.1 创建 BD 手册
    - ETL 库 → `apps/etl/connectors/feiqiu/docs/database/dws/main/BD_manual_dws_finance_area_daily.md`
    - ETL 库 → `apps/etl/connectors/feiqiu/docs/database/dws/main/BD_manual_dws_finance_board_cache.md`
    - FDW → `docs/database/BD_Manual_fdw_finance_area.md`
    - 每份手册包含：字段明细、约束与索引、验证 SQL（≥3 条）、兼容性影响、回滚策略
    - 记录变更原因、影响范围
    - _Requirements: 2.1, 2.6, 4.1, 4.4_

- [x] 15. 文档同步更新
  - [x] 15.1 更新 ETL 任务文档
    - 在 `docs/etl_tasks/` 新增 DWS_FINANCE_AREA_DAILY 和 DWS_FINANCE_BOARD_CACHE 任务文档
    - _Requirements: 3.7, 3.8, 5.5, 5.6_
  - [x] 15.2 更新后端 README
    - 在 `apps/backend/README.md` 更新 board_service 和 fdw_queries 模块摘要
    - _Requirements: 6.5, 6.6_
  - [x] 15.3 更新文档地图
    - 在 `docs/DOCUMENTATION-MAP.md` 新增本次模块条目（BD 手册、ETL 任务文档）
    - _Requirements: 2.6, 4.1_

- [x] 16. 变更审计收口
  - [x] 16.1 触发审计子代理（audit-writer）执行完整审计流程
    - 确认 `.kiro/state/.audit_state.json` 中 `audit_required` 已标记
    - 审计子代理自动完成：变更审计记录（docs/audit/changes/）、AI_CHANGELOG、CHANGE 标记注释
    - 高风险路径：`tasks/`（ETL）、`apps/backend/app/`（后端）、`packages/shared/`（共享包）、`db/`（数据库）
    - _Requirements: 全部_
  - [x] 16.2 验证审计产物完整性
    - 确认 `docs/audit/changes/<YYYY-MM-DD>__board-finance-dws-area-refactor.md` 已生成
    - 确认涉及的高风险文件均有 AI_CHANGELOG 条目
    - 确认逻辑变更处有 CHANGE 标记注释（含日期、Prompt、直接原因）
    - _Requirements: 全部_
  - [x] 16.3 刷新审计一览表
    - 执行 `python scripts/audit/gen_audit_dashboard.py`
    - 确认新记录出现在 `docs/audit/audit_dashboard.md`
    - _Requirements: 全部_

- [x] 17. 最终检查点 — 全量验证
  - 运行 Monorepo 属性测试：`cd C:\Project\NeoZQYY && pytest tests/ -v`
  - 运行 ETL 单元测试：`cd apps/etl/connectors/feiqiu && pytest tests/unit -v`
  - 运行后端单元测试：`cd apps/backend && pytest tests/ -v`
  - 确保所有属性测试（Property 1-14）和单元测试全部通过
  - 确保 DDL 迁移已合并到主基线
  - 确保 BD 手册已同步更新
  - 确保后端 README、文档地图均已更新
  - 确保变更审计记录已生成、AI_CHANGELOG 已写入、审计一览表已刷新
  - ask the user if questions arise.

## 备注

- 标记 `*` 的子任务为可选（属性测试/单元测试），可跳过以加速 MVP
- 每个任务引用了具体的需求编号以确保可追溯性
- 属性测试验证通用正确性属性（Property 1-14），单元测试验证具体边界条件
- 检查点任务确保增量验证，避免问题累积
- 本 spec 为跨系统类（ETL + 后端 + DB + 共享包），收尾阶段覆盖步骤 1-6
- 设计文档使用 Python，所有实现和测试均使用 Python
