# DWS 数据层实施任务计划 v1.0 > **版本**: 1.0 > **创建日期**: 2026-02-01 > **项目路径**: `C:\dev\LLTQ\ETL\feiqiu-ETL` > **目标**: 在已完成的 DWD 层数据基础上,完成 DWS(数据汇总层)的数据库设计与 ETL 实现 --- ## 一、项目背景与现状分析 ### 1.1 已有基础设施 | 层级 | 状态 | Schema | 说明 | |------|------|--------|------| | ODS(操作数据层) | ✅ 完成 | `billiards_ods` | 原始数据落地,20+ 张表 | | DWD(明细数据层) | ✅ 完成 | `billiards_dwd` | 维度表 9 张 + 事实表 12 张 | | DWS(汇总数据层) | 🔄 部分完成 | `billiards_dws` | 仅有 `dws_order_summary` 1 张 | ### 1.2 现有代码架构 ``` etl_billiards/ ├── tasks/ │ ├── base_task.py # BaseTask 基类(E/T/L 模板方法) │ ├── init_dws_schema_task.py # DWS Schema 初始化任务 │ └── dws_build_order_summary_task.py # 订单汇总表构建任务 ├── database/ │ └── schema_dws.sql # DWS DDL(当前仅 dws_order_summary) ├── scripts/ │ └── build_dws_order_summary.py # 订单汇总构建脚本 └── docs/ ├── dwd_main_tables_dictionary.md # DWD 数据字典 └── 财务页面需求.md # 财务需求原型 ``` ### 1.3 核心 DWD 表依赖关系 **维度表**: - `dim_assistant`: 助教维度(level 字段标识等级:初级/中级/高级/星级) - `dim_member`: 会员维度 - `dim_table`: 台桌维度 - `dim_site`: 门店维度 **事实表**: - `dwd_assistant_service_log`: 助教服务记录(核心:基础课+附加课统计) - `dwd_assistant_trash_event`: 助教废除记录(影响业绩有效性) - `dwd_settlement_head`: 结算单头表 - `dwd_table_fee_log`: 台费流水 - `dwd_store_goods_sale`: 商品销售 - `dwd_recharge_order`: 充值订单 - `dwd_member_balance_change`: 会员余额变动 - `dwd_payment`: 支付记录 - `dwd_refund`: 退款记录 --- ## 二、需求分析与表设计 ### 2.1 数据分层策略(时间分区) 根据需求,将数据按更新时间分为 4 层以优化查询效率: | 分层 | 时间范围 | 用途 | 实现方式 | |------|----------|------|----------| | L1(热数据) | 最近 2 天 | 实时监控/当日报表 | 物化视图 + 定时刷新 | | L2(近期数据) | 最近 1 个月 | 周报/月报 | 分区表 | | L3(中期数据) | 最近 3 个月 | 季度分析 | 分区表 | | L4(全量数据) | 历史全部 | 年度汇总/深度分析 | 原始表 | **实现机制**: 1. 使用 PostgreSQL 表分区(Range Partition by 月份) 2. 通过视图自动路由到对应时间层 3. 配套清理任务定期归档/删除过期数据 ### 2.2 DWS 表清单(完整设计) #### 2.2.1 配置表(系统设置) | 表名 | 类型 | 说明 | |------|------|------| | `cfg_performance_tier` | 配置 | 助教绩效档位配置(6档:0-5档) | | `cfg_assistant_level_price` | 配置 | 助教等级定价(初级/中级/高级/星级) | | `cfg_bonus_rules` | 配置 | 奖金规则配置(冲刺奖/Top3奖) | | `cfg_tier_effective_period` | 配置 | 档位配置生效时间范围 | #### 2.2.2 助教维度汇总表 | 表名 | 类型 | 主要维度 | 说明 | |------|------|----------|------| | `dws_assistant_monthly_summary` | 汇总 | 助教×月份 | 月度业绩汇总(课时/收入/档位) | | `dws_assistant_daily_detail` | 明细 | 助教×日期 | 日度业绩明细(便于月中进度追踪) | | `dws_assistant_customer_stats` | 汇总 | 助教×时间窗口 | 服务客户统计(7/10/15/30/60/90天) | | `dws_assistant_salary_calc` | 计算 | 助教×月份 | 月度工资计算结果 | #### 2.2.3 客户维度汇总表 | 表名 | 类型 | 主要维度 | 说明 | |------|------|----------|------| | `dws_member_consumption_summary` | 汇总 | 会员×时间窗口 | 消费情况统计(7/10/15/30/60/90天) | | `dws_member_visit_detail` | 明细 | 会员×订单 | 来店消费明细(含服务详情) | #### 2.2.4 财务维度汇总表 | 表名 | 类型 | 主要维度 | 说明 | |------|------|----------|------| | `dws_finance_daily_summary` | 汇总 | 门店×日期 | 日度财务汇总 | | `dws_finance_income_structure` | 汇总 | 门店×日期×区域 | 收入结构(按区域/类型) | | `dws_finance_discount_detail` | 明细 | 门店×日期 | 优惠明细(团购/大客户/赠送卡等) | | `dws_finance_recharge_summary` | 汇总 | 门店×日期 | 充值与预收汇总 | | `dws_finance_expense_summary` | 汇总 | 门店×日期 | 支出结构汇总 | | `dws_assistant_finance_analysis` | 汇总 | 门店×日期×等级 | 助教收支分析 | #### 2.2.5 现有表保留 | 表名 | 状态 | 说明 | |------|------|------| | `dws_order_summary` | ✅ 保留 | 订单级汇总(已实现) | --- ## 三、表结构详细设计 ### 3.1 配置表 DDL #### 3.1.1 `cfg_performance_tier` - 绩效档位配置 ```sql -- ============================================================ -- 表名: cfg_performance_tier -- 用途: 助教绩效档位配置表,定义6个档位的阈值与分成规则 -- 业务规则: -- - 过档后,所有时长按新档位进行计算 -- - 总业绩小时数 = 基础课 + 附加课 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.cfg_performance_tier ( tier_id SERIAL PRIMARY KEY, -- 档位主键 tier_code INTEGER NOT NULL UNIQUE, -- 档位代码:0-5 tier_name VARCHAR(50) NOT NULL, -- 档位名称 tier_reason VARCHAR(100), -- 档位设计原因 min_hours NUMERIC(10,2) NOT NULL, -- 最小小时数阈值(含) max_hours NUMERIC(10,2), -- 最大小时数阈值(不含),NULL表示无上限 base_deduction NUMERIC(10,2) NOT NULL, -- 基础课抽成(元/小时) bonus_ratio NUMERIC(5,4) NOT NULL, -- 附加课/打赏课抽成比例(如0.50表示50%) vacation_days INTEGER, -- 次月休假天数,NULL表示休假自由 effective_from DATE NOT NULL DEFAULT '2026-01-01', -- 生效开始日期 effective_to DATE DEFAULT '9999-12-31', -- 生效结束日期 is_active BOOLEAN DEFAULT TRUE, -- 是否启用 created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); -- 初始化默认档位配置(2026年标准) COMMENT ON TABLE billiards_dws.cfg_performance_tier IS '助教绩效档位配置表:定义各档位的业绩阈值与分成规则'; COMMENT ON COLUMN billiards_dws.cfg_performance_tier.tier_code IS '档位代码:0=淘汰压力,1=及格档,2=良好档,3=优秀档,4=卓越加速档,5=冠军加速档'; COMMENT ON COLUMN billiards_dws.cfg_performance_tier.base_deduction IS '基础课球房抽成金额(元/小时),助教实得=客户单价-此值'; COMMENT ON COLUMN billiards_dws.cfg_performance_tier.bonus_ratio IS '附加课/打赏课球房抽成比例,助教实得=收费*(1-此值)'; ``` #### 3.1.2 `cfg_assistant_level_price` - 助教等级定价 ```sql -- ============================================================ -- 表名: cfg_assistant_level_price -- 用途: 助教等级对应的客户收费标准 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.cfg_assistant_level_price ( price_id SERIAL PRIMARY KEY, level_code INTEGER NOT NULL, -- 等级代码(对应dim_assistant.level) level_name VARCHAR(20) NOT NULL, -- 等级名称:初级/中级/高级/星级 base_price NUMERIC(10,2) NOT NULL, -- 基础课客户收费(元/小时) bonus_price NUMERIC(10,2) NOT NULL DEFAULT 190, -- 附加课客户收费(元/小时),统一190 effective_from DATE NOT NULL DEFAULT '2026-01-01', effective_to DATE DEFAULT '9999-12-31', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(level_code, effective_from) ); COMMENT ON TABLE billiards_dws.cfg_assistant_level_price IS '助教等级定价表:定义各等级对客户的收费标准'; COMMENT ON COLUMN billiards_dws.cfg_assistant_level_price.level_code IS '等级代码:1=初级,2=中级,3=高级,4=星级,8=助教管理'; ``` #### 3.1.3 `cfg_bonus_rules` - 奖金规则配置 ```sql -- ============================================================ -- 表名: cfg_bonus_rules -- 用途: 冲刺奖、Top3奖等额外奖金规则配置 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.cfg_bonus_rules ( rule_id SERIAL PRIMARY KEY, rule_type VARCHAR(20) NOT NULL, -- 规则类型:SPRINT(冲刺奖)/TOP_RANK(排名奖) rule_name VARCHAR(50) NOT NULL, -- 规则名称 condition_type VARCHAR(20) NOT NULL, -- 条件类型:HOURS_GTE(时长>=)/RANK_EQ(排名=) condition_value NUMERIC(10,2) NOT NULL, -- 条件值 bonus_amount NUMERIC(10,2) NOT NULL, -- 奖金金额(元) priority INTEGER DEFAULT 0, -- 优先级(同类型取高优先级) is_stackable BOOLEAN DEFAULT FALSE, -- 是否可叠加 effective_from DATE NOT NULL DEFAULT '2026-01-01', effective_to DATE DEFAULT '9999-12-31', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now() ); COMMENT ON TABLE billiards_dws.cfg_bonus_rules IS '奖金规则配置表:定义冲刺奖和Top3排名奖'; COMMENT ON COLUMN billiards_dws.cfg_bonus_rules.is_stackable IS '是否可叠加:FALSE表示同类型奖金取高不叠加'; ``` ### 3.2 助教维度汇总表 DDL #### 3.2.1 `dws_assistant_monthly_summary` - 月度业绩汇总 ```sql -- ============================================================ -- 表名: dws_assistant_monthly_summary -- 用途: 助教月度业绩汇总表,核心统计表 -- 更新策略: 每日增量更新当月数据,月初全量刷新上月 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_assistant_monthly_summary ( summary_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, -- 门店ID assistant_id BIGINT NOT NULL, -- 助教ID stat_month DATE NOT NULL, -- 统计月份(每月1日) -- 基础课统计 base_service_count INTEGER DEFAULT 0, -- 基础课服务次数 base_service_seconds INTEGER DEFAULT 0, -- 基础课服务秒数 base_service_hours NUMERIC(10,2) DEFAULT 0, -- 基础课服务小时数(精确到分钟) base_income_original NUMERIC(12,2) DEFAULT 0, -- 基础课原始收入(客户支付) base_income_deduction NUMERIC(12,2) DEFAULT 0, -- 基础课球房抽成 base_income_actual NUMERIC(12,2) DEFAULT 0, -- 基础课助教实得 -- 附加课统计 bonus_service_count INTEGER DEFAULT 0, -- 附加课服务次数 bonus_service_seconds INTEGER DEFAULT 0, -- 附加课服务秒数 bonus_service_hours NUMERIC(10,2) DEFAULT 0, -- 附加课服务小时数 bonus_income_original NUMERIC(12,2) DEFAULT 0, -- 附加课原始收入 bonus_income_ratio_ded NUMERIC(12,2) DEFAULT 0, -- 附加课球房按比例抽成 bonus_income_actual NUMERIC(12,2) DEFAULT 0, -- 附加课助教实得 -- 汇总统计 total_service_count INTEGER DEFAULT 0, -- 总服务次数 total_service_hours NUMERIC(10,2) DEFAULT 0, -- 总服务小时数 total_income_original NUMERIC(12,2) DEFAULT 0, -- 总原始收入 total_income_actual NUMERIC(12,2) DEFAULT 0, -- 总助教实得 -- 废除影响 trashed_service_count INTEGER DEFAULT 0, -- 被废除服务次数 trashed_service_seconds INTEGER DEFAULT 0, -- 被废除服务秒数 trashed_amount NUMERIC(12,2) DEFAULT 0, -- 被废除金额 -- 档位信息 tier_code INTEGER, -- 当前档位代码(0-5) tier_name VARCHAR(50), -- 档位名称 tier_base_deduction NUMERIC(10,2), -- 档位基础课抽成 tier_bonus_ratio NUMERIC(5,4), -- 档位附加课抽成比例 -- 奖金 sprint_bonus NUMERIC(10,2) DEFAULT 0, -- 冲刺奖金额 rank_bonus NUMERIC(10,2) DEFAULT 0, -- 排名奖金额 other_bonus NUMERIC(10,2) DEFAULT 0, -- 其他奖金 total_bonus NUMERIC(10,2) DEFAULT 0, -- 奖金合计 -- 最终工资 final_salary NUMERIC(12,2) DEFAULT 0, -- 最终应发工资 -- 助教快照信息 assistant_level INTEGER, -- 助教等级 assistant_level_name VARCHAR(20), -- 等级名称 entry_date DATE, -- 入职日期 is_new_employee BOOLEAN DEFAULT FALSE, -- 是否本月新入职 work_days_in_month INTEGER, -- 本月在职天数 -- 排名信息 monthly_rank INTEGER, -- 当月排名 -- 元数据 created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, assistant_id, stat_month) ); -- 索引优化 CREATE INDEX IF NOT EXISTS idx_dws_asst_monthly_site_month ON billiards_dws.dws_assistant_monthly_summary(site_id, stat_month); CREATE INDEX IF NOT EXISTS idx_dws_asst_monthly_assistant ON billiards_dws.dws_assistant_monthly_summary(assistant_id, stat_month); CREATE INDEX IF NOT EXISTS idx_dws_asst_monthly_month ON billiards_dws.dws_assistant_monthly_summary(stat_month); COMMENT ON TABLE billiards_dws.dws_assistant_monthly_summary IS '助教月度业绩汇总表:统计每位助教每月的业绩、收入、档位、工资'; COMMENT ON COLUMN billiards_dws.dws_assistant_monthly_summary.base_service_hours IS '基础课服务小时数,精确到分钟(秒数/3600)'; COMMENT ON COLUMN billiards_dws.dws_assistant_monthly_summary.tier_code IS '根据total_service_hours计算的档位,参照cfg_performance_tier'; ``` #### 3.2.2 `dws_assistant_daily_detail` - 日度业绩明细 ```sql -- ============================================================ -- 表名: dws_assistant_daily_detail -- 用途: 助教日度业绩明细,用于月中进度追踪 -- 更新策略: 每小时增量更新 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_assistant_daily_detail ( detail_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, assistant_id BIGINT NOT NULL, stat_date DATE NOT NULL, -- 统计日期 -- 基础课日统计 base_service_count INTEGER DEFAULT 0, base_service_seconds INTEGER DEFAULT 0, base_service_hours NUMERIC(10,4) DEFAULT 0, base_income_original NUMERIC(12,2) DEFAULT 0, -- 附加课日统计 bonus_service_count INTEGER DEFAULT 0, bonus_service_seconds INTEGER DEFAULT 0, bonus_service_hours NUMERIC(10,4) DEFAULT 0, bonus_income_original NUMERIC(12,2) DEFAULT 0, -- 汇总 total_service_count INTEGER DEFAULT 0, total_service_hours NUMERIC(10,4) DEFAULT 0, total_income_original NUMERIC(12,2) DEFAULT 0, -- 废除 trashed_count INTEGER DEFAULT 0, trashed_seconds INTEGER DEFAULT 0, trashed_amount NUMERIC(12,2) DEFAULT 0, -- 累计(当月至今) mtd_total_hours NUMERIC(10,2) DEFAULT 0, -- Month-To-Date 累计小时数 mtd_tier_code INTEGER, -- 累计后档位 -- 服务客户统计 unique_customers INTEGER DEFAULT 0, -- 当日服务不同客户数 created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, assistant_id, stat_date) ); CREATE INDEX IF NOT EXISTS idx_dws_asst_daily_site_date ON billiards_dws.dws_assistant_daily_detail(site_id, stat_date); CREATE INDEX IF NOT EXISTS idx_dws_asst_daily_assistant ON billiards_dws.dws_assistant_daily_detail(assistant_id, stat_date); COMMENT ON TABLE billiards_dws.dws_assistant_daily_detail IS '助教日度业绩明细表:便于实时追踪月中业绩进度'; ``` #### 3.2.3 `dws_assistant_customer_stats` - 服务客户统计 ```sql -- ============================================================ -- 表名: dws_assistant_customer_stats -- 用途: 助教服务客户统计(多时间窗口) -- 更新策略: 每日凌晨全量刷新 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_assistant_customer_stats ( stats_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, assistant_id BIGINT NOT NULL, stat_date DATE NOT NULL, -- 统计基准日期 window_days INTEGER NOT NULL, -- 时间窗口天数:7/10/15/30/60/90 -- 客户统计 unique_customers INTEGER DEFAULT 0, -- 服务不同客户数 total_service_count INTEGER DEFAULT 0, -- 总服务次数 total_service_hours NUMERIC(10,2) DEFAULT 0, -- 总服务小时数 -- 按课程类型 base_customers INTEGER DEFAULT 0, -- 基础课客户数 base_service_count INTEGER DEFAULT 0, bonus_customers INTEGER DEFAULT 0, -- 附加课客户数 bonus_service_count INTEGER DEFAULT 0, -- 收入 total_income NUMERIC(12,2) DEFAULT 0, -- 客户复购 repeat_customers INTEGER DEFAULT 0, -- 复购客户数(服务>=2次) repeat_rate NUMERIC(5,4) DEFAULT 0, -- 复购率 created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, assistant_id, stat_date, window_days) ); CREATE INDEX IF NOT EXISTS idx_dws_asst_cust_window ON billiards_dws.dws_assistant_customer_stats(assistant_id, stat_date, window_days); COMMENT ON TABLE billiards_dws.dws_assistant_customer_stats IS '助教服务客户统计表:支持7/10/15/30/60/90天多时间窗口'; ``` ### 3.3 客户维度汇总表 DDL #### 3.3.1 `dws_member_consumption_summary` - 消费情况统计 ```sql -- ============================================================ -- 表名: dws_member_consumption_summary -- 用途: 会员消费情况统计(多时间窗口) -- 更新策略: 每日凌晨全量刷新 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_member_consumption_summary ( summary_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, member_id BIGINT NOT NULL, stat_date DATE NOT NULL, window_days INTEGER NOT NULL, -- 7/10/15/30/60/90 -- 到店统计 visit_count INTEGER DEFAULT 0, -- 到店次数 visit_days INTEGER DEFAULT 0, -- 到店天数 -- 台费消费 table_fee_count INTEGER DEFAULT 0, -- 开台次数 table_fee_seconds INTEGER DEFAULT 0, -- 台费时长(秒) table_fee_hours NUMERIC(10,2) DEFAULT 0, -- 台费小时数 table_fee_amount NUMERIC(12,2) DEFAULT 0, -- 台费金额 -- 助教服务 assistant_service_count INTEGER DEFAULT 0, -- 助教服务次数 assistant_service_hours NUMERIC(10,2) DEFAULT 0, -- 助教服务小时数 assistant_service_amt NUMERIC(12,2) DEFAULT 0, -- 助教服务金额 unique_assistants INTEGER DEFAULT 0, -- 服务过的不同助教数 -- 商品消费 goods_order_count INTEGER DEFAULT 0, -- 商品订单数 goods_item_count INTEGER DEFAULT 0, -- 商品项数 goods_amount NUMERIC(12,2) DEFAULT 0, -- 商品金额 -- 汇总金额 total_original_amount NUMERIC(12,2) DEFAULT 0, -- 原始消费总额 total_discount_amount NUMERIC(12,2) DEFAULT 0, -- 优惠总额 total_actual_amount NUMERIC(12,2) DEFAULT 0, -- 实际支付总额 -- 支付方式 cash_payment NUMERIC(12,2) DEFAULT 0, -- 现金支付 online_payment NUMERIC(12,2) DEFAULT 0, -- 线上支付 card_payment NUMERIC(12,2) DEFAULT 0, -- 储值卡支付 coupon_payment NUMERIC(12,2) DEFAULT 0, -- 团购券支付 -- 客户价值指标 avg_visit_amount NUMERIC(10,2) DEFAULT 0, -- 单次到店平均消费 avg_visit_duration_min INTEGER DEFAULT 0, -- 单次平均停留时长(分钟) created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, member_id, stat_date, window_days) ); CREATE INDEX IF NOT EXISTS idx_dws_member_cons_window ON billiards_dws.dws_member_consumption_summary(member_id, stat_date, window_days); CREATE INDEX IF NOT EXISTS idx_dws_member_cons_site ON billiards_dws.dws_member_consumption_summary(site_id, stat_date, window_days); COMMENT ON TABLE billiards_dws.dws_member_consumption_summary IS '会员消费汇总表:支持7/10/15/30/60/90天多时间窗口'; ``` ### 3.4 财务维度汇总表 DDL #### 3.4.1 `dws_finance_daily_summary` - 日度财务汇总 ```sql -- ============================================================ -- 表名: dws_finance_daily_summary -- 用途: 门店日度财务汇总(核心财务报表数据源) -- 更新策略: 每小时增量更新 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_finance_daily_summary ( summary_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, stat_date DATE NOT NULL, -- ========== 经营链:发生额 → 优惠 → 确认收入 ========== -- 发生额(正价) gross_revenue NUMERIC(14,2) DEFAULT 0, -- 发生额/正价总计 gross_table_fee NUMERIC(14,2) DEFAULT 0, -- 台费发生额 gross_assistant_base NUMERIC(14,2) DEFAULT 0, -- 助教基础课发生额 gross_assistant_bonus NUMERIC(14,2) DEFAULT 0, -- 助教激励课发生额 gross_goods NUMERIC(14,2) DEFAULT 0, -- 食品酒水发生额 -- 优惠明细 discount_total NUMERIC(14,2) DEFAULT 0, -- 优惠总额 discount_groupbuy NUMERIC(14,2) DEFAULT 0, -- 团购优惠 discount_vip NUMERIC(14,2) DEFAULT 0, -- 大客户优惠 discount_gift_card NUMERIC(14,2) DEFAULT 0, -- 赠送卡抵扣(台桌卡+酒水卡+抵用券) discount_manual NUMERIC(14,2) DEFAULT 0, -- 手动调整 discount_free NUMERIC(14,2) DEFAULT 0, -- 免单 discount_rounding NUMERIC(14,2) DEFAULT 0, -- 抹零 -- 成交/确认收入 confirmed_revenue NUMERIC(14,2) DEFAULT 0, -- 成交/确认收入(不含充值) -- ========== 支付方式构成 ========== payment_stored_card NUMERIC(14,2) DEFAULT 0, -- 储值卡结算冲销 payment_cash_online NUMERIC(14,2) DEFAULT 0, -- 现金/线上支付 payment_groupbuy_deal NUMERIC(14,2) DEFAULT 0, -- 团购核销确认收入(成交价) -- ========== 现金流 ========== -- 现金流入 cash_inflow_consume NUMERIC(14,2) DEFAULT 0, -- 消费现金流入(现金+线上+平台回款-退款) cash_inflow_recharge NUMERIC(14,2) DEFAULT 0, -- 充值到账(首充+续费) cash_inflow_total NUMERIC(14,2) DEFAULT 0, -- 现金流入合计 -- 现金支出(需外部输入或其他系统对接) cash_outflow_total NUMERIC(14,2) DEFAULT 0, -- 现金支出合计 cash_outflow_rent NUMERIC(14,2) DEFAULT 0, -- 房租 cash_outflow_utility NUMERIC(14,2) DEFAULT 0, -- 水电 cash_outflow_purchase NUMERIC(14,2) DEFAULT 0, -- 进货成本 cash_outflow_assistant NUMERIC(14,2) DEFAULT 0, -- 助教分成 cash_outflow_salary NUMERIC(14,2) DEFAULT 0, -- 固定人员工资 cash_outflow_platform NUMERIC(14,2) DEFAULT 0, -- 平台服务费 cash_outflow_other NUMERIC(14,2) DEFAULT 0, -- 其他费用 -- 现金结余 cash_balance NUMERIC(14,2) DEFAULT 0, -- 现金结余 cash_balance_rate NUMERIC(5,4) DEFAULT 0, -- 结余率 -- ========== 充值与预收 ========== recharge_first NUMERIC(14,2) DEFAULT 0, -- 首充金额 recharge_renew NUMERIC(14,2) DEFAULT 0, -- 续费金额 recharge_total NUMERIC(14,2) DEFAULT 0, -- 充值合计 recharge_gift NUMERIC(14,2) DEFAULT 0, -- 充值赠送金额 -- 储值卡 stored_card_consume NUMERIC(14,2) DEFAULT 0, -- 储值卡消耗 stored_card_balance NUMERIC(14,2) DEFAULT 0, -- 储值卡余额(截止当日) -- 赠送卡 gift_card_new NUMERIC(14,2) DEFAULT 0, -- 赠送卡新增 gift_card_consume NUMERIC(14,2) DEFAULT 0, -- 赠送卡消耗 gift_card_balance NUMERIC(14,2) DEFAULT 0, -- 赠送卡余额 -- ========== 订单统计 ========== order_count INTEGER DEFAULT 0, -- 订单数 order_avg_amount NUMERIC(10,2) DEFAULT 0, -- 客单价 member_order_count INTEGER DEFAULT 0, -- 会员订单数 member_order_rate NUMERIC(5,4) DEFAULT 0, -- 会员订单占比 -- ========== 退款统计 ========== refund_count INTEGER DEFAULT 0, -- 退款笔数 refund_amount NUMERIC(14,2) DEFAULT 0, -- 退款金额 created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, stat_date) ); CREATE INDEX IF NOT EXISTS idx_dws_finance_daily_site_date ON billiards_dws.dws_finance_daily_summary(site_id, stat_date); CREATE INDEX IF NOT EXISTS idx_dws_finance_daily_date ON billiards_dws.dws_finance_daily_summary(stat_date); COMMENT ON TABLE billiards_dws.dws_finance_daily_summary IS '门店日度财务汇总表:核心财务报表数据源'; COMMENT ON COLUMN billiards_dws.dws_finance_daily_summary.gross_revenue IS '发生额/正价:按各项目正价计算的理论销售额'; COMMENT ON COLUMN billiards_dws.dws_finance_daily_summary.confirmed_revenue IS '成交/确认收入:扣除优惠后的营业收入,不含充值'; ``` #### 3.4.2 `dws_finance_income_structure` - 收入结构(按区域) ```sql -- ============================================================ -- 表名: dws_finance_income_structure -- 用途: 收入结构按区域/类型细分 -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_finance_income_structure ( structure_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, stat_date DATE NOT NULL, -- 分类维度 income_category VARCHAR(20) NOT NULL, -- 收入大类:TABLE/ASSISTANT_BASE/ASSISTANT_BONUS/GOODS area_name VARCHAR(50), -- 区域名称:A区/B区/C区/团建区/麻将区/NULL(不分区) -- 金额 gross_amount NUMERIC(14,2) DEFAULT 0, -- 发生额 discount_amount NUMERIC(14,2) DEFAULT 0, -- 优惠金额 confirmed_amount NUMERIC(14,2) DEFAULT 0, -- 确认收入 -- 数量/时长 item_count INTEGER DEFAULT 0, -- 项目数/次数 duration_seconds INTEGER DEFAULT 0, -- 时长(秒) duration_hours NUMERIC(10,2) DEFAULT 0, -- 时长(小时) created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, stat_date, income_category, area_name) ); COMMENT ON TABLE billiards_dws.dws_finance_income_structure IS '收入结构表:按大类和区域细分的收入明细'; ``` #### 3.4.3 `dws_assistant_finance_analysis` - 助教收支分析 ```sql -- ============================================================ -- 表名: dws_assistant_finance_analysis -- 用途: 助教收支分析(财务视角) -- ============================================================ CREATE TABLE IF NOT EXISTS billiards_dws.dws_assistant_finance_analysis ( analysis_id BIGSERIAL PRIMARY KEY, site_id BIGINT NOT NULL, stat_date DATE NOT NULL, assistant_level INTEGER, -- 助教等级(NULL表示全部) assistant_level_name VARCHAR(20), -- 基础课 base_customer_payment NUMERIC(14,2) DEFAULT 0, -- 客户支付 base_shop_commission NUMERIC(14,2) DEFAULT 0, -- 球房抽成 base_hours NUMERIC(10,2) DEFAULT 0, -- 小时数 base_avg_commission NUMERIC(10,2) DEFAULT 0, -- 均小时抽成 -- 激励课 bonus_customer_payment NUMERIC(14,2) DEFAULT 0, bonus_shop_commission NUMERIC(14,2) DEFAULT 0, bonus_hours NUMERIC(10,2) DEFAULT 0, bonus_avg_commission NUMERIC(10,2) DEFAULT 0, -- 汇总 total_customer_payment NUMERIC(14,2) DEFAULT 0, total_shop_commission NUMERIC(14,2) DEFAULT 0, total_hours NUMERIC(10,2) DEFAULT 0, -- 助教数量 assistant_count INTEGER DEFAULT 0, created_at TIMESTAMPTZ DEFAULT now(), updated_at TIMESTAMPTZ DEFAULT now(), UNIQUE(site_id, stat_date, assistant_level) ); COMMENT ON TABLE billiards_dws.dws_assistant_finance_analysis IS '助教收支分析表:财务视角的助教业绩与抽成统计'; ``` --- ## 四、技术实现方案 ### 4.1 代码架构设计 ``` etl_billiards/ ├── tasks/ │ ├── dws/ # DWS 任务模块(新增) │ │ ├── __init__.py │ │ ├── base_dws_task.py # DWS任务基类 │ │ ├── cfg_init_task.py # 配置表初始化任务 │ │ ├── assistant_monthly_task.py # 助教月度汇总任务 │ │ ├── assistant_daily_task.py # 助教日度明细任务 │ │ ├── assistant_customer_task.py # 助教客户统计任务 │ │ ├── member_consumption_task.py # 会员消费统计任务 │ │ ├── finance_daily_task.py # 财务日度汇总任务 │ │ ├── finance_structure_task.py # 收入结构任务 │ │ └── assistant_finance_task.py # 助教收支分析任务 │ └── init_dws_schema_task.py # 更新:执行完整DWS DDL ├── database/ │ ├── schema_dws.sql # 更新:完整DWS DDL │ └── seed_dws_config.sql # 新增:配置表初始数据 ├── scripts/ │ └── build_dws_*.py # DWS 构建脚本 └── docs/ └── dws_tables_dictionary.md # 新增:DWS 数据字典 ``` ### 4.2 类设计(OOP) ```python # ============================================================ # 文件: tasks/dws/base_dws_task.py # 用途: DWS 任务基类,封装通用逻辑 # ============================================================ class BaseDwsTask(BaseTask): """ DWS 层任务基类 特性: 1. 支持增量/全量两种刷新模式 2. 支持 delete-before-insert 策略(幂等性保障) 3. 内置时间窗口处理逻辑 """ def __init__(self, config, db_connection, api_client, logger): super().__init__(config, db_connection, api_client, logger) self.target_schema = "billiards_dws" self.source_schema = "billiards_dwd" @property def target_table(self) -> str: """子类需实现:返回目标表名""" raise NotImplementedError @property def delete_before_insert(self) -> bool: """是否在插入前删除同范围数据,默认True""" return True def get_delete_sql(self, context: TaskContext) -> tuple[str, list]: """生成删除SQL,子类可覆盖""" raise NotImplementedError def get_insert_sql(self, context: TaskContext) -> tuple[str, dict]: """生成插入SQL,子类需实现""" raise NotImplementedError ``` ### 4.3 核心算法实现 #### 4.3.1 档位计算算法 ```python def calculate_tier(total_hours: float, is_new_employee: bool, entry_date: date, stat_month: date, work_days: int) -> dict: """ 计算助教档位 规则: 1. 过档后,所有时长按新档位计算 2. 新入职助教按 日均*30 折算定档 3. 25日后入职最高定3档 Args: total_hours: 总业绩小时数 is_new_employee: 是否本月新入职 entry_date: 入职日期 stat_month: 统计月份 work_days: 本月在职天数 Returns: {tier_code, tier_name, base_deduction, bonus_ratio} """ effective_hours = total_hours max_tier = 5 if is_new_employee and work_days < 30: # 新入职折算:日均 * 30 daily_avg = total_hours / max(work_days, 1) effective_hours = daily_avg * 30 # 25日后入职最高3档 if entry_date.day >= 25: max_tier = 3 # 查询档位配置 tier = query_tier_config(effective_hours, max_tier) return tier ``` #### 4.3.2 工资计算算法 ```python def calculate_salary(assistant_data: dict, tier: dict, level_price: dict, bonus_rules: list) -> dict: """ 计算助教月度工资 公式(以中级助教185小时为例): 基础课收入 = 基础课时 * (客户单价 - 档位抽成) 附加课收入 = 附加课原价 * (1 - 档位抽成比例) 冲刺奖 = 达到阈值取最高 排名奖 = 按排名取值 最终工资 = 基础课收入 + 附加课收入 + 冲刺奖 + 排名奖 """ # 基础课收入 base_income = ( assistant_data['base_hours'] * (level_price['base_price'] - tier['base_deduction']) ) # 附加课收入 bonus_income = ( assistant_data['bonus_income_original'] * (1 - tier['bonus_ratio']) ) # 冲刺奖(取高不叠加) sprint_bonus = calculate_sprint_bonus( assistant_data['total_hours'], bonus_rules ) # 排名奖 rank_bonus = calculate_rank_bonus( assistant_data['monthly_rank'], bonus_rules ) final_salary = base_income + bonus_income + sprint_bonus + rank_bonus return { 'base_income': base_income, 'bonus_income': bonus_income, 'sprint_bonus': sprint_bonus, 'rank_bonus': rank_bonus, 'final_salary': final_salary } ``` ### 4.4 SQL 构建示例 #### 4.4.1 助教月度汇总 SQL ```sql -- 助教月度业绩汇总构建SQL WITH -- 1. 有效服务记录(排除废除) valid_services AS ( SELECT asl.site_id, asl.site_assistant_id AS assistant_id, DATE_TRUNC('month', asl.start_use_time)::date AS stat_month, asl.skill_name, -- 判断课程类型:基础课/附加课 CASE WHEN asl.skill_name IN ('超休', '激励', '打赏') THEN 'BONUS' ELSE 'BASE' END AS service_type, asl.income_seconds, asl.ledger_amount, asl.projected_income, asl.tenant_member_id FROM billiards_dwd.dwd_assistant_service_log asl WHERE COALESCE(asl.is_delete, 0) = 0 AND asl.start_use_time >= %(start_date)s AND asl.start_use_time < %(end_date)s -- 排除已废除的记录 AND NOT EXISTS ( SELECT 1 FROM billiards_dwd.dwd_assistant_trash_event ate WHERE ate.assistant_service_id = asl.assistant_service_id ) ), -- 2. 按助教+月份+类型汇总 service_summary AS ( SELECT site_id, assistant_id, stat_month, service_type, COUNT(*) AS service_count, SUM(income_seconds) AS service_seconds, SUM(income_seconds) / 3600.0 AS service_hours, SUM(ledger_amount) AS income_original, COUNT(DISTINCT tenant_member_id) AS unique_customers FROM valid_services GROUP BY site_id, assistant_id, stat_month, service_type ), -- 3. 废除统计 trash_summary AS ( SELECT site_id, assistant_id, -- 需要从关联表获取 DATE_TRUNC('month', create_time)::date AS stat_month, COUNT(*) AS trashed_count, SUM(charge_minutes_raw * 60) AS trashed_seconds, SUM(abolish_amount) AS trashed_amount FROM billiards_dwd.dwd_assistant_trash_event WHERE create_time >= %(start_date)s AND create_time < %(end_date)s GROUP BY site_id, assistant_id, DATE_TRUNC('month', create_time) ), -- 4. 助教维度信息 assistant_info AS ( SELECT assistant_id, site_id, level, CASE level WHEN 1 THEN '初级' WHEN 2 THEN '中级' WHEN 3 THEN '高级' WHEN 4 THEN '星级' WHEN 8 THEN '助教管理' ELSE '未知' END AS level_name, entry_time::date AS entry_date FROM billiards_dwd.dim_assistant WHERE SCD2_is_current = 1 ) -- 5. 最终汇总 SELECT ss_base.site_id, ss_base.assistant_id, ss_base.stat_month, -- 基础课 COALESCE(ss_base.service_count, 0) AS base_service_count, COALESCE(ss_base.service_seconds, 0) AS base_service_seconds, COALESCE(ss_base.service_hours, 0) AS base_service_hours, COALESCE(ss_base.income_original, 0) AS base_income_original, -- 附加课 COALESCE(ss_bonus.service_count, 0) AS bonus_service_count, COALESCE(ss_bonus.service_seconds, 0) AS bonus_service_seconds, COALESCE(ss_bonus.service_hours, 0) AS bonus_service_hours, COALESCE(ss_bonus.income_original, 0) AS bonus_income_original, -- 汇总 COALESCE(ss_base.service_count, 0) + COALESCE(ss_bonus.service_count, 0) AS total_service_count, COALESCE(ss_base.service_hours, 0) + COALESCE(ss_bonus.service_hours, 0) AS total_service_hours, COALESCE(ss_base.income_original, 0) + COALESCE(ss_bonus.income_original, 0) AS total_income_original, -- 废除 COALESCE(ts.trashed_count, 0) AS trashed_service_count, COALESCE(ts.trashed_seconds, 0) AS trashed_service_seconds, COALESCE(ts.trashed_amount, 0) AS trashed_amount, -- 助教信息 ai.level AS assistant_level, ai.level_name AS assistant_level_name, ai.entry_date FROM service_summary ss_base LEFT JOIN service_summary ss_bonus ON ss_base.site_id = ss_bonus.site_id AND ss_base.assistant_id = ss_bonus.assistant_id AND ss_base.stat_month = ss_bonus.stat_month AND ss_bonus.service_type = 'BONUS' LEFT JOIN trash_summary ts ON ss_base.site_id = ts.site_id AND ss_base.assistant_id = ts.assistant_id AND ss_base.stat_month = ts.stat_month LEFT JOIN assistant_info ai ON ss_base.assistant_id = ai.assistant_id WHERE ss_base.service_type = 'BASE' ; ``` --- ## 五、实施任务清单 ### 5.1 阶段一:基础设施(预计 1-2 天) | 序号 | 任务 | 优先级 | 依赖 | 产出物 | |------|------|--------|------|--------| | 1.1 | 更新 `schema_dws.sql` DDL | P0 | - | DDL 文件 | | 1.2 | 创建 `seed_dws_config.sql` 初始数据 | P0 | 1.1 | 种子数据文件 | | 1.3 | 更新 `InitDwsSchemaTask` | P0 | 1.1,1.2 | 任务代码 | | 1.4 | 创建 `BaseDwsTask` 基类 | P0 | - | 基类代码 | | 1.5 | 注册新任务到 `task_registry.py` | P0 | 1.3,1.4 | 注册代码 | ### 5.2 阶段二:配置表与助教维度(预计 2-3 天) | 序号 | 任务 | 优先级 | 依赖 | 产出物 | |------|------|--------|------|--------| | 2.1 | 实现 `CfgInitTask` 配置初始化 | P0 | 1.* | 任务代码 | | 2.2 | 实现 `AssistantDailyTask` 日度明细 | P0 | 1.4 | 任务代码 | | 2.3 | 实现 `AssistantMonthlyTask` 月度汇总 | P0 | 2.2 | 任务代码 | | 2.4 | 实现档位计算与工资计算模块 | P0 | 2.3 | 算法模块 | | 2.5 | 实现 `AssistantCustomerTask` 客户统计 | P1 | 2.2 | 任务代码 | ### 5.3 阶段三:客户维度(预计 1-2 天) | 序号 | 任务 | 优先级 | 依赖 | 产出物 | |------|------|--------|------|--------| | 3.1 | 实现 `MemberConsumptionTask` | P0 | 1.4 | 任务代码 | | 3.2 | 实现多时间窗口查询视图 | P1 | 3.1 | SQL 视图 | ### 5.4 阶段四:财务维度(预计 2-3 天) | 序号 | 任务 | 优先级 | 依赖 | 产出物 | |------|------|--------|------|--------| | 4.1 | 实现 `FinanceDailySummaryTask` | P0 | 1.4 | 任务代码 | | 4.2 | 实现 `FinanceIncomeStructureTask` | P0 | 4.1 | 任务代码 | | 4.3 | 实现 `AssistantFinanceTask` | P0 | 2.3 | 任务代码 | | 4.4 | 实现充值与预收统计逻辑 | P1 | 4.1 | 模块代码 | ### 5.5 阶段五:数据分层与维护(预计 1-2 天) | 序号 | 任务 | 优先级 | 依赖 | 产出物 | |------|------|--------|------|--------| | 5.1 | 实现分区表管理任务 | P1 | 1.1 | 任务代码 | | 5.2 | 实现数据清理/归档任务 | P1 | 5.1 | 任务代码 | | 5.3 | 配置定时调度 | P1 | 2-4.* | 调度配置 | ### 5.6 阶段六:文档与测试(预计 1-2 天) | 序号 | 任务 | 优先级 | 依赖 | 产出物 | |------|------|--------|------|--------| | 6.1 | 编写 `dws_tables_dictionary.md` | P0 | 1-5.* | 数据字典 | | 6.2 | 更新 `README.md` | P0 | 1-5.* | README | | 6.3 | 编写单元测试 | P1 | 2-4.* | 测试代码 | | 6.4 | 编写集成测试 | P1 | 6.3 | 测试代码 | --- ## 六、风险与注意事项 ### 6.1 数据一致性 1. **废除记录处理**:计算助教业绩时必须排除已废除的服务记录 2. **课程类型判断**:根据 `skill_name` 字段区分基础课和附加课,需确认枚举值完整性 3. **新入职折算**:需要准确获取入职日期和在职天数 ### 6.2 性能优化 1. **索引设计**:所有汇总表按常用查询维度建立复合索引 2. **增量更新**:日常运行采用增量模式,仅全量刷新月初或异常恢复场景 3. **分区策略**:大表按月份分区,历史数据可归档 ### 6.3 编码注意 1. **中文注释**:所有 SQL 和 Python 代码需要详尽的中文注释 2. **UTF-8 编码**:确保文件编码为 UTF-8,DDL 中使用 `COMMENT ON` 添加中文说明 3. **日志输出**:关键步骤输出中文日志便于排查 ### 6.4 业务规则确认 以下内容需与业务方确认: 1. 附加课识别规则:`skill_name` 的具体枚举值(超休/激励/打赏等) 2. 助教等级映射:`dim_assistant.level` 的值域(1=初级/2=中级/3=高级/4=星级?) 3. 充值提成规则:当前需求文档为空,需补充 4. 赠送卡分类:台费卡/酒水卡/抵用券的区分字段 --- ## 七、验收标准 ### 7.1 功能验收 - [ ] 所有 DWS 表可正常建表、查询 - [ ] 助教月度汇总数据准确(与手工计算对比) - [ ] 档位计算逻辑符合业务规则 - [ ] 工资计算结果正确 - [ ] 客户统计支持多时间窗口 - [ ] 财务汇总数据与 DWD 明细一致 ### 7.2 文档验收 - [ ] `dws_tables_dictionary.md` 完整记录所有表和字段 - [ ] `README.md` 包含 DWS 使用说明 - [ ] 代码注释覆盖率 > 80% ### 7.3 性能验收 - [ ] 日增量刷新 < 5 分钟 - [ ] 月全量刷新 < 30 分钟 - [ ] 常用查询响应 < 1 秒 --- ## 八、附录 ### A. 配置表初始数据 ```sql -- 绩效档位初始数据 INSERT INTO billiards_dws.cfg_performance_tier (tier_code, tier_name, tier_reason, min_hours, max_hours, base_deduction, bonus_ratio, vacation_days) VALUES (0, '0档 淘汰压力', '淘汰压力', 0, 100, 28, 0.50, 3), (1, '1档 及格档', '重点激励', 100, 130, 18, 0.40, 4), (2, '2档 良好档', '重点激励', 130, 160, 15, 0.38, 4), (3, '3档 优秀档', '优秀标准', 160, 190, 13, 0.35, 5), (4, '4档 卓越加速档', '高端人才倾斜', 190, 220, 10, 0.33, 6), (5, '5档 冠军加速档', '高端人才倾斜', 220, NULL, 8, 0.30, NULL); -- 助教等级定价初始数据 INSERT INTO billiards_dws.cfg_assistant_level_price (level_code, level_name, base_price, bonus_price) VALUES (1, '初级', 98, 190), (2, '中级', 108, 190), (3, '高级', 118, 190), (4, '星级', 138, 190), (8, '助教管理', 0, 0); -- 奖金规则初始数据 INSERT INTO billiards_dws.cfg_bonus_rules (rule_type, rule_name, condition_type, condition_value, bonus_amount, priority, is_stackable) VALUES ('SPRINT', '冲刺奖-190小时', 'HOURS_GTE', 190, 300, 1, FALSE), ('SPRINT', '冲刺奖-220小时', 'HOURS_GTE', 220, 800, 2, FALSE), ('TOP_RANK', 'Top1奖金', 'RANK_EQ', 1, 1000, 1, TRUE), ('TOP_RANK', 'Top2奖金', 'RANK_EQ', 2, 600, 2, TRUE), ('TOP_RANK', 'Top3奖金', 'RANK_EQ', 3, 400, 3, TRUE); ``` ### B. CLI 命令示例 ```bash # 初始化 DWS Schema python -m cli.main --pipeline-flow INGEST_ONLY --tasks INIT_DWS_SCHEMA # 初始化配置数据 python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_INIT_CONFIG # 构建助教日度明细(指定日期) python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_ASSISTANT_DAILY \ --window-start "2026-01-01" --window-end "2026-01-31" # 构建助教月度汇总 python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_ASSISTANT_MONTHLY \ --window-start "2026-01-01" --window-end "2026-02-01" # 构建财务日度汇总 python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_FINANCE_DAILY \ --window-start "2026-01-01" --window-end "2026-01-31" # 一键刷新所有 DWS 表 python -m cli.main --pipeline-flow INGEST_ONLY --tasks DWS_REFRESH_ALL ``` --- **文档版本历史** | 版本 | 日期 | 作者 | 变更说明 | |------|------|------|----------| | 1.0 | 2026-02-01 | AI Assistant | 初始版本 |