Files
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

12 KiB
Raw Permalink Blame History

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_TaskDWS_FINANCE_AREA_DAILY ETL 任务,从 DWD 层按区域聚合计算日粒度数据
  • ETL_Cache_TaskDWS_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.pyETL 和后端通过 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_tablescd2_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 计算