- .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>
12 KiB
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
- THE Area_Mapping SHALL 定义 AREA_LABEL_MAP 字典,将每个 Area_Code(hallA/hallB/hallC/vip/snooker/mahjong/ktv)映射到对应的
site_table_area_name列表 - THE Area_Mapping SHALL 存放在
packages/shared/src/neozqyy_shared/area_mapping.py中,ETL 和后端通过 import 引用 - THE Area_Mapping SHALL 定义 hall = 所有具体区域之和(不含 all),all = 所有区域之和
- WHEN 桌台的
site_table_area_name不匹配任何 AREA_LABEL_MAP 条目, THEN THE Area_Mapping SHALL 将该桌台归入一个可配置的默认区域(或排除),并在 ETL 日志中记录警告 - 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
- THE Area_Daily_Table SHALL 包含收入结构 4 项(table_fee_amount/goods_amount/assistant_pd_amount/assistant_cx_amount)和 gross_amount,其中 gross_amount = 四项之和
- THE Area_Daily_Table SHALL 包含优惠拆分 6 项(discount_groupbuy/discount_vip/discount_manual/discount_gift_card/discount_rounding/discount_other)和 discount_total,满足 Discount_Identity 恒等式
- THE Area_Daily_Table SHALL 包含 confirmed_income 字段,其值 = gross_amount - discount_total
- 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)的有效值
- WHILE area_code ≠ 'all', THE Area_Daily_Table SHALL 将现金流、卡消费、充值字段设为 0
- THE Area_Daily_Table SHALL 以
(site_id, stat_date, area_code)为唯一约束 - WHEN ETL 写入某个
(site_id, stat_date)的数据, THE Area_Daily_Table SHALL 包含 9 行(all + hall + hallA + hallB + hallC + vip + snooker + mahjong + ktv) - 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
- THE ETL_Area_Daily_Task SHALL 从 Settlement_Head 和
dim_table(scd2_is_current=1)按区域聚合收入和优惠字段 - THE ETL_Area_Daily_Task SHALL 使用 Business_Day_Cutoff(BUSINESS_DAY_START_HOUR=8)计算 stat_date
- THE ETL_Area_Daily_Task SHALL 使用 Area_Mapping 的
resolve_area_code将桌台映射到 Area_Code - THE ETL_Area_Daily_Task SHALL 采用 delete-before-insert 策略:先删除目标
(site_id, stat_date)的所有行,再插入 9 行 - THE ETL_Area_Daily_Task SHALL 从现有
dws_finance_daily_summary复用全局现金流/充值/卡消费字段填充 all 行 - THE ETL_Area_Daily_Task SHALL 仅处理
settle_type IN (1, 3)的结算单 - THE ETL_Area_Daily_Task SHALL 按每小时调度频率运行,与现有 DWS_FINANCE_DAILY 同频
- THE ETL_Area_Daily_Task SHALL 依赖 DWD_LOAD_FROM_ODS 任务完成后执行
- 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
- THE Board_Cache_Table SHALL 按
(site_id, time_range, area_code)唯一存储缓存数据 - THE Board_Cache_Table SHALL 包含 overview 板块的 8 项核心指标(occurrence/discount/discount_rate/confirmed_revenue/cash_in/cash_out/cash_balance/balance_rate)
- THE Board_Cache_Table SHALL 包含 start_date/end_date 记录当期日期范围,prev_start_date/prev_end_date 记录上期日期范围(环比用)
- THE Board_Cache_Table SHALL 包含 Data_Fingerprint 字段和 computed_at 时间戳
Requirement 5: 缓存层 ETL 任务与指纹机制
User Story: As a 数据工程师, I want 缓存层通过数据指纹自动检测数据变化并重算, so that 补录数据后缓存自动失效和更新
Acceptance Criteria
- THE ETL_Cache_Task SHALL 遍历所有 Completed_Period(lastMonth/lastWeek/lastQuarter/quarter3/half6)× 9 个 Area_Code 组合
- THE ETL_Cache_Task SHALL 对每个组合计算源数据的 Data_Fingerprint(基于日粒度行的 stat_date/gross_amount/discount_total 的 MD5 hash)
- WHEN Data_Fingerprint 与 Board_Cache_Table 中已有指纹不一致, THEN THE ETL_Cache_Task SHALL 从 Area_Daily_Table SUM 重算该组合的缓存数据并更新
- WHEN Data_Fingerprint 与 Board_Cache_Table 中已有指纹一致, THE ETL_Cache_Task SHALL 跳过该组合的重算
- THE ETL_Cache_Task SHALL 依赖 ETL_Area_Daily_Task 完成后执行
- THE ETL_Cache_Task SHALL 按每天一次调度(营业日切点后)
- 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
- WHEN time_range 为 Completed_Period, THE Finance_Board SHALL 先查 Board_Cache_Table 获取缓存数据
- WHEN Board_Cache_Table 未命中(无记录), THE Finance_Board SHALL 从 Area_Daily_Table SUM 计算结果,写入缓存后返回
- WHEN time_range 为 Current_Period, THE Finance_Board SHALL 直接从 Area_Daily_Table SUM 计算结果
- WHEN compare=1, THE Finance_Board SHALL 对上期也执行同样的缓存/日粒度查询逻辑,然后计算环比
- THE Finance_Board SHALL 将 overview 板块的数据来源从
dws_finance_daily_summary改为 Area_Daily_Table(按 area_code 过滤) - THE Finance_Board SHALL 将 revenue 板块的数据来源从 Settlement_Head 实时查询改为 Area_Daily_Table(收入+优惠+渠道全部预计算)
- WHILE area_code ≠ 'all', THE Finance_Board SHALL 对 cashflow/expense/coach_analysis 板块仍使用全局数据
- WHILE area_code ≠ 'all', THE Finance_Board SHALL 对 recharge 板块返回 null
Requirement 7: 接口契约不变(前端零改动)
User Story: As a 前端开发者, I want API 签名和返回数据结构完全不变, so that 前端无需任何改动
Acceptance Criteria
- THE Finance_Board SHALL 保持 API 签名不变:
GET /api/xcx/board/finance?time={FinanceTimeEnum}&area={AreaFilterEnum}&compare={0|1} - THE Finance_Board SHALL 保持所有 Pydantic Schema 类不变(FinanceBoardResponse/OverviewPanel/RechargePanel/RevenuePanel/CashflowPanel/ExpensePanel/CoachAnalysisPanel),不新增、不删除、不改名任何字段
- THE Finance_Board SHALL 保持 revenue.discount_items 固定 5 项(团购/会员折扣/手动调整/赠送卡/其他)
- THE Finance_Board SHALL 保持 revenue.channel_items 固定 3 项(储值卡结算冲销/现金线上支付/团购核销)
- WHEN compare=1, THE Finance_Board SHALL 返回环比字段(格式为 "X.X%"/"持平"/"新增");WHEN compare=0, THE Finance_Board SHALL 返回环比字段为 null
- 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
- THE ETL_Area_Daily_Task SHALL 将每张结算单的优惠通过
table_id → dim_table.site_table_area_name → Area_Code映射归属到对应区域 - THE ETL_Area_Daily_Task SHALL 对优惠按区域直接聚合,不做任何分摊计算
- 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
- 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
- WHEN area_code = 'all', THE Finance_Board SHALL 返回与重构前完全一致的数据(所有 6 个板块)
- THE Finance_Board SHALL 通过 144 组合全量验证(8 个 time_range × 9 个 area_code × 2 个 compare 值)
- WHEN area_code ≠ 'all', THE Finance_Board SHALL 返回的 discountRate 不出现 400%+ 的异常值
- WHEN 已完成周期被第二次请求, THE Finance_Board SHALL 从 Board_Cache_Table 命中缓存,不触发 Area_Daily_Table 的 SUM 计算