Files
Neo-ZQYY/docs/specs/board-finance-dws-area-refactor/requirements.md
Neo 70324d8542 chore: 文档与 IDE 配置整理
- .kiro/specs/ → docs/specs/(41 个历史需求 spec 迁移,移除 .config.kiro)
- CLAUDE.md 三层拆分:根文件精简 + apps/backend/CLAUDE.md + .claude/commands/
- 新增 /spec-close、/pre-change 两个工作流命令
- DDL 基线刷新(从测试库重新导出 11 个文件,dws 35→38 表,biz 18→21 表)
- BD_Manual → BD_manual 命名统一(48 个文件)
- 修复 3 处文档与数据库不一致(auth.users.status 默认值、scheduled_tasks 字段、RLS 视图数)
- 新增 BD_manual_public_rbac_tables.md(public schema 8 张 RBAC/工作流表)
- 合并 biz.trigger_jobs 文档(10→12 字段,归档独立文档)
- docs/database/README.md 索引更新

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-06 00:02:37 +08:00

142 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Requirements Document
## Introduction
财务看板 DWS 区域维度重构。当前财务看板在 area≠all 时,优惠数据仍从全局 DWS 表取数,导致区域级优惠占比严重失真(如 B区优惠占比 417.9%)。本次重构新建区域日粒度原子层表 `dws_finance_area_daily` 和已完成周期缓存层表 `dws_finance_board_cache`,将优惠按结算单桌台区域直接聚合,后端查询改为先查缓存、未命中从日粒度表 SUM同时保持 API 签名和返回结构完全不变(前端零改动)。
## Glossary
- **Finance_Board**:财务看板,后端 API `/api/xcx/board/finance` 返回的 6 个板块数据overview/recharge/revenue/cashflow/expense/coach_analysis
- **Area_Daily_Table**:区域日粒度原子层表 `dws_finance_area_daily`,按 `(site_id, stat_date, area_code)` 存储每日每区域的收入、优惠、现金流等预计算数据
- **Board_Cache_Table**:看板缓存层表 `dws_finance_board_cache`,缓存已完成周期的聚合结果,按 `(site_id, time_range, area_code)` 唯一
- **Area_Code**:区域编码枚举,取值为 all/hall/hallA/hallB/hallC/vip/snooker/mahjong/ktv 共 9 个值
- **Area_Mapping**:区域映射配置,将 `dim_table.site_table_area_name`(如 "A区"、"VIP包厢")映射到 Area_Code 的共享配置
- **ETL_Area_Daily_Task**DWS_FINANCE_AREA_DAILY ETL 任务,从 DWD 层按区域聚合计算日粒度数据
- **ETL_Cache_Task**DWS_FINANCE_BOARD_CACHE ETL 任务,基于数据指纹机制维护已完成周期的缓存
- **Data_Fingerprint**:数据指纹,对源数据关键字段计算 MD5 hash用于检测已完成周期的数据是否因补录而变化
- **Business_Day_Cutoff**:营业日切点,`BUSINESS_DAY_START_HOUR=8`,当日 08:00 前的结算单归属前一营业日
- **Settlement_Head**:结算单主表 `dwd_settlement_head`,每张结算单对应一张桌台,通过 `table_id` 关联 `dim_table` 获取区域归属
- **Discount_Identity**:优惠拆分恒等式,`discount_total = discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other`
- **Completed_Period**:已完成周期,指 lastMonth/lastWeek/lastQuarter/quarter3/half6 等时间范围,数据不再变化(除补录外)
- **Current_Period**:当期周期,指 month/week/quarter 等时间范围,数据每天在变,不缓存
## Requirements
### Requirement 1: 共享区域映射配置
**User Story:** As a 开发者, I want 区域映射配置集中在 `packages/shared/` 中维护, so that ETL 和后端共用同一份映射,避免不一致
#### Acceptance Criteria
1. THE Area_Mapping SHALL 定义 AREA_LABEL_MAP 字典,将每个 Area_CodehallA/hallB/hallC/vip/snooker/mahjong/ktv映射到对应的 `site_table_area_name` 列表
2. THE Area_Mapping SHALL 存放在 `packages/shared/src/neozqyy_shared/area_mapping.py`ETL 和后端通过 import 引用
3. THE Area_Mapping SHALL 定义 hall = 所有具体区域之和(不含 allall = 所有区域之和
4. WHEN 桌台的 `site_table_area_name` 不匹配任何 AREA_LABEL_MAP 条目, THEN THE Area_Mapping SHALL 将该桌台归入一个可配置的默认区域(或排除),并在 ETL 日志中记录警告
5. THE Area_Mapping SHALL 提供 `resolve_area_code(area_name: str) -> str` 函数,输入 `site_table_area_name` 返回对应的 Area_Code
### Requirement 2: 区域日粒度原子层表
**User Story:** As a 数据工程师, I want 按 `(site_id, stat_date, area_code)` 粒度预计算财务数据, so that 后端查询可以按区域过滤而不依赖全局 DWS 表
#### Acceptance Criteria
1. THE Area_Daily_Table SHALL 包含收入结构 4 项table_fee_amount/goods_amount/assistant_pd_amount/assistant_cx_amount和 gross_amount其中 gross_amount = 四项之和
2. THE Area_Daily_Table SHALL 包含优惠拆分 6 项discount_groupbuy/discount_vip/discount_manual/discount_gift_card/discount_rounding/discount_other和 discount_total满足 Discount_Identity 恒等式
3. THE Area_Daily_Table SHALL 包含 confirmed_income 字段,其值 = gross_amount - discount_total
4. WHILE area_code = 'all', THE Area_Daily_Table SHALL 包含现金流字段cash_pay_amount/cash_paper_amount/scan_pay_amount/groupbuy_pay_amount/recharge_cash_inflow/cash_inflow_total/cash_outflow_total/cash_balance_change的有效值
5. WHILE area_code ≠ 'all', THE Area_Daily_Table SHALL 将现金流、卡消费、充值字段设为 0
6. THE Area_Daily_Table SHALL 以 `(site_id, stat_date, area_code)` 为唯一约束
7. WHEN ETL 写入某个 `(site_id, stat_date)` 的数据, THE Area_Daily_Table SHALL 包含 9 行all + hall + hallA + hallB + hallC + vip + snooker + mahjong + ktv
8. THE Area_Daily_Table SHALL 中 all 行的收入和优惠字段 = 各具体区域行hallA~ktv对应字段之和
### Requirement 3: 区域日粒度 ETL 任务
**User Story:** As a 数据工程师, I want ETL 任务自动从 DWD 层按区域聚合计算日粒度数据, so that Area_Daily_Table 保持最新
#### Acceptance Criteria
1. THE ETL_Area_Daily_Task SHALL 从 Settlement_Head 和 `dim_table``scd2_is_current=1`)按区域聚合收入和优惠字段
2. THE ETL_Area_Daily_Task SHALL 使用 Business_Day_CutoffBUSINESS_DAY_START_HOUR=8计算 stat_date
3. THE ETL_Area_Daily_Task SHALL 使用 Area_Mapping 的 `resolve_area_code` 将桌台映射到 Area_Code
4. THE ETL_Area_Daily_Task SHALL 采用 delete-before-insert 策略:先删除目标 `(site_id, stat_date)` 的所有行,再插入 9 行
5. THE ETL_Area_Daily_Task SHALL 从现有 `dws_finance_daily_summary` 复用全局现金流/充值/卡消费字段填充 all 行
6. THE ETL_Area_Daily_Task SHALL 仅处理 `settle_type IN (1, 3)` 的结算单
7. THE ETL_Area_Daily_Task SHALL 按每小时调度频率运行,与现有 DWS_FINANCE_DAILY 同频
8. THE ETL_Area_Daily_Task SHALL 依赖 DWD_LOAD_FROM_ODS 任务完成后执行
9. THE ETL_Area_Daily_Task SHALL 确保 `discount_gift_card` 使用赠送卡消费金额口径(与现有 ETL 一致),而非结算单的 `gift_card_amount`
### Requirement 4: 看板缓存层表
**User Story:** As a 后端开发者, I want 已完成周期的聚合结果被缓存, so that 重复查询不需要每次从日粒度表 SUM
#### Acceptance Criteria
1. THE Board_Cache_Table SHALL 按 `(site_id, time_range, area_code)` 唯一存储缓存数据
2. THE Board_Cache_Table SHALL 包含 overview 板块的 8 项核心指标occurrence/discount/discount_rate/confirmed_revenue/cash_in/cash_out/cash_balance/balance_rate
3. THE Board_Cache_Table SHALL 包含 start_date/end_date 记录当期日期范围prev_start_date/prev_end_date 记录上期日期范围(环比用)
4. THE Board_Cache_Table SHALL 包含 Data_Fingerprint 字段和 computed_at 时间戳
### Requirement 5: 缓存层 ETL 任务与指纹机制
**User Story:** As a 数据工程师, I want 缓存层通过数据指纹自动检测数据变化并重算, so that 补录数据后缓存自动失效和更新
#### Acceptance Criteria
1. THE ETL_Cache_Task SHALL 遍历所有 Completed_PeriodlastMonth/lastWeek/lastQuarter/quarter3/half6× 9 个 Area_Code 组合
2. THE ETL_Cache_Task SHALL 对每个组合计算源数据的 Data_Fingerprint基于日粒度行的 stat_date/gross_amount/discount_total 的 MD5 hash
3. WHEN Data_Fingerprint 与 Board_Cache_Table 中已有指纹不一致, THEN THE ETL_Cache_Task SHALL 从 Area_Daily_Table SUM 重算该组合的缓存数据并更新
4. WHEN Data_Fingerprint 与 Board_Cache_Table 中已有指纹一致, THE ETL_Cache_Task SHALL 跳过该组合的重算
5. THE ETL_Cache_Task SHALL 依赖 ETL_Area_Daily_Task 完成后执行
6. THE ETL_Cache_Task SHALL 按每天一次调度(营业日切点后)
7. WHILE time_range 为 Current_Periodmonth/week/quarter, THE ETL_Cache_Task SHALL 不写入缓存(当期数据每天在变)
### Requirement 6: 后端查询改造
**User Story:** As a 后端开发者, I want 后端查询逻辑改为先查缓存再查日粒度表, so that overview 和 revenue 板块支持按区域正确过滤
#### Acceptance Criteria
1. WHEN time_range 为 Completed_Period, THE Finance_Board SHALL 先查 Board_Cache_Table 获取缓存数据
2. WHEN Board_Cache_Table 未命中(无记录), THE Finance_Board SHALL 从 Area_Daily_Table SUM 计算结果,写入缓存后返回
3. WHEN time_range 为 Current_Period, THE Finance_Board SHALL 直接从 Area_Daily_Table SUM 计算结果
4. WHEN compare=1, THE Finance_Board SHALL 对上期也执行同样的缓存/日粒度查询逻辑,然后计算环比
5. THE Finance_Board SHALL 将 overview 板块的数据来源从 `dws_finance_daily_summary` 改为 Area_Daily_Table按 area_code 过滤)
6. THE Finance_Board SHALL 将 revenue 板块的数据来源从 Settlement_Head 实时查询改为 Area_Daily_Table收入+优惠+渠道全部预计算)
7. WHILE area_code ≠ 'all', THE Finance_Board SHALL 对 cashflow/expense/coach_analysis 板块仍使用全局数据
8. WHILE area_code ≠ 'all', THE Finance_Board SHALL 对 recharge 板块返回 null
### Requirement 7: 接口契约不变(前端零改动)
**User Story:** As a 前端开发者, I want API 签名和返回数据结构完全不变, so that 前端无需任何改动
#### Acceptance Criteria
1. THE Finance_Board SHALL 保持 API 签名不变:`GET /api/xcx/board/finance?time={FinanceTimeEnum}&area={AreaFilterEnum}&compare={0|1}`
2. THE Finance_Board SHALL 保持所有 Pydantic Schema 类不变FinanceBoardResponse/OverviewPanel/RechargePanel/RevenuePanel/CashflowPanel/ExpensePanel/CoachAnalysisPanel不新增、不删除、不改名任何字段
3. THE Finance_Board SHALL 保持 revenue.discount_items 固定 5 项(团购/会员折扣/手动调整/赠送卡/其他)
4. THE Finance_Board SHALL 保持 revenue.channel_items 固定 3 项(储值卡结算冲销/现金线上支付/团购核销)
5. WHEN compare=1, THE Finance_Board SHALL 返回环比字段(格式为 "X.X%"/"持平"/"新增"WHEN compare=0, THE Finance_Board SHALL 返回环比字段为 null
6. WHILE area_code ≠ 'all', THE Finance_Board SHALL 保持 overview.occurrence/discount/confirmedRevenue = revenue 板块的对应值(后端覆盖逻辑保留)
### Requirement 8: 优惠按区域正确归属
**User Story:** As a 门店管理员, I want 查看某区域的优惠数据时看到的是该区域实际发生的优惠, so that 优惠占比反映真实业务情况
#### Acceptance Criteria
1. THE ETL_Area_Daily_Task SHALL 将每张结算单的优惠通过 `table_id → dim_table.site_table_area_name → Area_Code` 映射归属到对应区域
2. THE ETL_Area_Daily_Task SHALL 对优惠按区域直接聚合,不做任何分摊计算
3. FOR ALL Area_Code 值, THE Area_Daily_Table SHALL 满足 Discount_Identitydiscount_total = discount_groupbuy + discount_vip + discount_manual + discount_gift_card + discount_rounding + discount_other
4. WHILE area_code = 'all', THE Area_Daily_Table SHALL 中 discount_total = 各具体区域hallA~ktv的 discount_total 之和
### Requirement 9: 回归测试与全量验证
**User Story:** As a 开发者, I want 重构后 area=all 的数据与现有逻辑完全一致, so that 确保重构不引入新的数据偏差
#### Acceptance Criteria
1. WHEN area_code = 'all', THE Finance_Board SHALL 返回与重构前完全一致的数据(所有 6 个板块)
2. THE Finance_Board SHALL 通过 144 组合全量验证8 个 time_range × 9 个 area_code × 2 个 compare 值)
3. WHEN area_code ≠ 'all', THE Finance_Board SHALL 返回的 discountRate 不出现 400%+ 的异常值
4. WHEN 已完成周期被第二次请求, THE Finance_Board SHALL 从 Board_Cache_Table 命中缓存,不触发 Area_Daily_Table 的 SUM 计算