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>
This commit is contained in:
141
docs/specs/board-finance-dws-area-refactor/requirements.md
Normal file
141
docs/specs/board-finance-dws-area-refactor/requirements.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# 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_Code(hallA/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 = 所有具体区域之和(不含 all),all = 所有区域之和
|
||||
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_Cutoff(BUSINESS_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_Period(lastMonth/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_Period(month/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_Identity:discount_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 计算
|
||||
Reference in New Issue
Block a user