Files
Neo-ZQYY/apps/etl/pipelines/feiqiu/docs/etl_tasks/dws_tasks.md

73 KiB
Raw Blame History

DWS 层任务详解

本文档说明飞球 ETL 系统中 DWS数据服务层的所有任务。 DWS 层负责从 DWD 层读取明细数据,按业务维度聚合计算后写入汇总表, 服务于助教业绩、会员分析、财务统计、指数算法等业务场景。


概述

DWS 层共有 15 个已注册任务,按业务域分为四组:

助教业绩域5 个)

任务代码 Python 类 目标表 粒度 更新策略
DWS_ASSISTANT_DAILY AssistantDailyTask dws_assistant_daily_detail 日期+助教 delete-before-insert
DWS_ASSISTANT_MONTHLY AssistantMonthlyTask dws_assistant_monthly_summary 月份+助教 delete-before-insert
DWS_ASSISTANT_CUSTOMER AssistantCustomerTask dws_assistant_customer_stats 日期+助教+会员 delete-before-insert
DWS_ASSISTANT_SALARY AssistantSalaryTask dws_assistant_salary_calc 月份+助教 delete-before-insert
DWS_ASSISTANT_FINANCE AssistantFinanceTask dws_assistant_finance_analysis 日期+助教 delete-before-insert

会员分析域2 个)

任务代码 Python 类 目标表 粒度 更新策略
DWS_MEMBER_CONSUMPTION MemberConsumptionTask dws_member_consumption_summary 日期+会员 delete-before-insert
DWS_MEMBER_VISIT MemberVisitTask dws_member_visit_detail 日期+会员+结账单 delete-before-insert

财务统计域4 个)

任务代码 Python 类 目标表 粒度 更新策略
DWS_FINANCE_DAILY FinanceDailyTask dws_finance_daily_summary 日期 delete-before-insert
DWS_FINANCE_RECHARGE FinanceRechargeTask dws_finance_recharge_summary 日期 delete-before-insert
DWS_FINANCE_INCOME_STRUCTURE FinanceIncomeStructureTask dws_finance_income_structure 日期+收入类型 delete-before-insert
DWS_FINANCE_DISCOUNT_DETAIL FinanceDiscountDetailTask dws_finance_discount_detail 日期+折扣类型 delete-before-insert

运维任务4 个)

任务代码 Python 类 继承 说明 更新策略
DWS_BUILD_ORDER_SUMMARY DwsBuildOrderSummaryTask BaseTask 构建订单汇总中间表 delete-before-insert
DWS_RETENTION_CLEANUP DwsRetentionCleanupTask BaseDwsTask 按时间分层清理历史数据 DELETE
DWS_MV_REFRESH_FINANCE_DAILY DwsMvRefreshFinanceDailyTask BaseMvRefreshTask 刷新财务日报物化视图 REFRESH
DWS_MV_REFRESH_ASSISTANT_DAILY DwsMvRefreshAssistantDailyTask BaseMvRefreshTask 刷新助教日报物化视图 REFRESH

注册位置:orchestration/task_registry.pyDWS_BUILD_ORDER_SUMMARY 继承 BaseTask 外,其余 14 个任务均继承 BaseDwsTask(或其子类 BaseMvRefreshTask)。


BaseDwsTask 公共机制

BaseDwsTask(位于 tasks/dws/base_dws_task.py)继承自 BaseTask,为所有 DWS 层业务任务提供统一的基础设施。核心能力包括:

  1. 时间分层与窗口计算 — 按业务需要选取不同跨度的数据范围
  2. 配置缓存 — 从 DWS 配置表加载业绩档位、等级定价、奖金规则等,带 TTL 缓存
  3. 幂等更新delete-before-insert — 先删后插,保证重跑结果一致
  4. 批量写入bulk_insert / upsert — 两种落库方式,适配不同场景
  5. DWD 数据读取 — 分批迭代或直接查询 DWD 层数据
  6. SCD2 维度 as-of 取值 — 按历史生效期获取维度快照
  7. 滚动窗口统计与排名计算 — 多窗口聚合和考虑并列的排名

子类必须实现的抽象方法

def get_target_table(self) -> str:
    """返回目标表名(不含 schema如 'dws_assistant_daily_detail'"""

def get_primary_keys(self) -> List[str]:
    """返回主键字段列表,如 ['site_id', 'assistant_id', 'stat_date']"""

这两个方法被 delete_existing_data()bulk_insert()upsert() 内部调用,用于定位目标表和构建冲突检测条件。


1. 时间分层TimeLayer

TimeLayer 是一个枚举类,定义了 5 个数据筛选层级。DWS 任务根据业务需要选择合适的层级来确定查询范围。

class TimeLayer(Enum):
    LAST_2_DAYS   = "LAST_2_DAYS"      # 近 2 天
    LAST_1_MONTH  = "LAST_1_MONTH"     # 近 1 月30 天)
    LAST_3_MONTHS = "LAST_3_MONTHS"    # 近 3 月90 天)
    LAST_6_MONTHS = "LAST_6_MONTHS"    # 近 6 月(不含本月)
    ALL           = "ALL"               # 全量(从 2000-01-01 起)

各层级的日期范围计算规则

层级 起始日期 结束日期 说明
LAST_2_DAYS base_date - 1天 base_date 昨天 + 今天
LAST_1_MONTH base_date - 30天 base_date 固定 30 天窗口
LAST_3_MONTHS base_date - 90天 base_date 固定 90 天窗口
LAST_6_MONTHS 6 个月前月初 上月末 不含本月,按自然月偏移
ALL 2000-01-01 base_date 全量回溯

base_date 默认为 date.today(),可由调用方指定。

TimeWindow财务报表专用

TimeLayer 外,还提供 TimeWindow 枚举用于财务报表场景,支持更精细的时间口径:

窗口类型 说明 口径
THIS_WEEK 本周 周一起始
LAST_WEEK 上周 上周一 ~ 上周日
THIS_MONTH 本月 月初 ~ 今天
LAST_MONTH 上月 上月初 ~ 上月末
LAST_3_MONTHS_EXCL_CURRENT 前 3 个月(不含本月) 3 个月前月初 ~ 上月末
LAST_3_MONTHS_INCL_CURRENT 前 3 个月(含本月) 2 个月前月初 ~ 今天
THIS_QUARTER 本季度 季度首月 1 日 ~ 今天
LAST_QUARTER 上季度 上季度首月 1 日 ~ 上季度末
LAST_6_MONTHS 最近半年 不含本月,同 TimeLayer

环比计算

get_comparison_range(time_range) 方法自动计算上一个等长区间,用于环比分析:

当前区间: [start, end]  →  天数 = end - start + 1
环比区间: [start - 天数, start - 1]

2. 配置缓存ConfigCache

DWS 层的业务计算依赖多张配置表(绩效档位、等级定价、奖金规则等)。ConfigCache 数据类将这些配置统一加载并缓存,避免每次计算都查库。

ConfigCache 数据结构

@dataclass
class ConfigCache:
    performance_tiers: List[Dict]     # 绩效档位配置
    level_prices: List[Dict]          # 等级定价配置
    bonus_rules: List[Dict]           # 奖金规则配置
    area_categories: Dict[str, Dict]  # 区域分类映射
    skill_types: Dict[int, Dict]      # 技能类型映射
    loaded_at: datetime               # 加载时间

缓存机制

  • 类级别共享_config_cache 为类变量,同一进程内所有 DWS 任务实例共享同一份缓存
  • TTL 过期_config_cache_ttl = 3005 分钟),超时后下次访问自动重新加载
  • 强制刷新:调用 load_config_cache(force_reload=True) 可跳过 TTL 检查
  • 加载入口load_config_cache() 方法,内部依次调用 5 个私有加载方法

配置表来源

配置项 来源表 用途
performance_tiers billiards_dws.cfg_performance_tier 绩效档位(小时阈值 → 抽成/休假)
level_prices billiards_dws.cfg_assistant_level_price 助教等级单价(基础课/附加课)
bonus_rules billiards_dws.cfg_bonus_rules 奖金规则(冲刺奖金/Top 排名奖金)
area_categories billiards_dws.cfg_area_category 区域分类映射(精确/模糊/兜底)
skill_types billiards_dws.cfg_skill_type 技能 → 课程类型映射BASE/BONUS/ROOM

生效期过滤

所有配置项均支持 effective_from / effective_to 生效期字段。_filter_by_effective_date(items, effective_date) 方法按指定日期过滤,确保历史月份使用当时生效的配置版本。

配置应用方法

方法 功能 匹配逻辑
get_performance_tier(hours, is_new_hire, date) 按有效小时数匹配绩效档位 遍历档位,找 min_hours ≤ hours < max_hours 的首个匹配
get_performance_tier_by_id(tier_id, date) 按档位 ID 直接获取 精确匹配 tier_id
get_level_price(level_code, date) 获取助教等级单价 level_code 匹配
get_course_type(skill_id) 技能 → 课程类型 skill_types 映射,默认 BASE
get_area_category(area_name) 区域名 → 分类 精确匹配 → 模糊匹配 → 兜底 OTHER
calculate_sprint_bonus(hours, date) 冲刺奖金 不累计,取满足阈值的最高档
calculate_top_rank_bonus(rank, date) Top 排名奖金 第 1/2/3 名分别对应配置金额,>3 返回 0

3. 幂等更新策略delete-before-insert

DWS 层的主要更新策略是 delete-before-insert:在写入新数据前,先按日期范围和门店 ID 删除已有数据,再批量插入。这保证了任务重跑(幂等)时不会产生重复数据。

delete_existing_data()

def delete_existing_data(
    self,
    context: TaskContext,
    date_col: str = "stat_date",
    extra_conditions: Optional[Dict[str, Any]] = None
) -> int:

执行逻辑:

  1. get_target_table() 获取目标表名,拼接 billiards_dws. schema 前缀
  2. 构建 WHERE 条件:
    • site_id = {context.store_id}(门店隔离)
    • {date_col} >= {window_start} AND {date_col} <= {window_end}(日期范围)
    • 可选的 extra_conditions(如按助教 ID 过滤)
  3. 执行 DELETE FROM ... WHERE ...
  4. 返回删除行数

典型调用模式(子类 load 方法中):

def load(self, transformed, context):
    # 1. 先删除当前窗口内的旧数据
    deleted = self.delete_existing_data(context, date_col="stat_date")
    # 2. 再批量插入新数据
    inserted = self.bulk_insert(transformed)
    return {"deleted": deleted, "inserted": inserted}

4. 批量写入方法

BaseDwsTask 提供两种写入方法,子类根据场景选择:

bulk_insert() — 纯插入

def bulk_insert(
    self,
    rows: List[Dict[str, Any]],
    columns: Optional[List[str]] = None
) -> int:
  • 目标表由 get_target_table() 确定,自动拼接 billiards_dws. 前缀
  • columnsNone,从第一行的 keys 自动推断
  • 逐行执行 INSERT INTO ... VALUES (...)
  • 返回插入行数
  • 适用场景:配合 delete_existing_data() 使用,先删后插

upsert() — 插入或更新

def upsert(
    self,
    rows: List[Dict[str, Any]],
    columns: Optional[List[str]] = None,
    update_columns: Optional[List[str]] = None
) -> Tuple[int, int]:
  • 利用 PostgreSQL 的 INSERT ... ON CONFLICT (...) DO UPDATE SET ... 语法
  • 冲突检测键由 get_primary_keys() 提供
  • update_columnsNone,自动排除主键列和 created_at 后取剩余列
  • 更新时自动追加 updated_at = NOW()
  • 返回 (inserted, updated) 元组
  • 适用场景:不适合先删后插的场景(如需保留 created_at 等元数据)

两种策略对比

特性 delete-before-insert + bulk_insert upsert
幂等性 先删后插,天然幂等 ON CONFLICT 保证幂等
性能 批量删除 + 批量插入,适合大范围重算 逐行判断冲突,适合小批量增量
元数据保留 删除后 created_at 会重置 仅更新指定列
主要使用者 大多数 DWS 汇总任务 少数需要保留历史元数据的场景

5. DWD 数据读取

iter_dwd_rows() — 分批迭代

def iter_dwd_rows(
    self, table_name, columns, start_date, end_date,
    date_col="created_at", where_clause="", order_by="", batch_size=1000
) -> Iterator[List[Dict[str, Any]]]:
  • LIMIT/OFFSET 分批读取 billiards_dwd.{table_name}
  • 默认按 date_col ASC 排序,每批 1000 行
  • 自动构建日期范围 WHERE 条件,支持追加自定义 where_clause
  • 以生成器方式 yield 每批数据,适合处理大数据量

query_dwd() — 直接查询

def query_dwd(self, sql, params=None) -> List[Dict[str, Any]]:
  • 直接执行任意 SQL返回字典列表
  • 适合复杂聚合查询或多表 JOIN 场景

6. SCD2 维度 as-of 取值

DWS 汇总计算涉及历史月份时,不能直接使用维度表的"当前版本",需要按生效期取历史快照。

get_assistant_level_asof(assistant_id, asof_date)

查询 billiards_dwd.dim_assistant 表,按 scd2_start_time ≤ asof_datescd2_end_time IS NULL 或 > asof_date 条件取最近一条记录,返回助教在指定日期的等级信息(level_codelevel_name)。

get_member_card_balance_asof(member_id, asof_date)

查询 billiards_dwd.dim_member_card_account 表,按 SCD2 生效期取会员在指定日期的卡余额,区分现金卡(card_type_id = 2793249295533893)和赠送卡(台费卡/活动抵用券/酒水卡),返回 cash_balancegift_balancetotal_balance


7. 辅助计算方法

滚动窗口统计

calculate_rolling_stats(base_date, entity_id, entity_type, stat_sql, windows) 按预定义的窗口天数列表(默认 [7, 10, 15, 30, 60, 90])执行统计 SQL返回各窗口的聚合结果键名格式为 {指标}_{天数}d

排名计算(考虑并列)

calculate_rank_with_ties(values) 接收 (entity_id, score) 列表,按分数降序排名。并列时共享同一排名,下一名跳过(如 2 个第 1 名,下一个是第 3 名)。返回 (entity_id, rank, dense_rank) 元组列表。

其他工具方法

方法 功能
is_new_hire_in_month(hire_date, stat_month) 判断是否为当月新入职(月 1 日后入职)
is_guest(member_id) 判断是否为散客(member_id 为 0 或 None
safe_decimal(value, default) 安全转换为 Decimal,异常返回默认值
safe_int(value, default) 安全转换为 int,异常返回默认值
seconds_to_hours(seconds) 秒 → 小时(Decimal 精度)
hours_to_seconds(hours) 小时 → 秒
get_month_first_day(dt) 获取月第一天
get_month_last_day(dt) 获取月最后一天
get_comparison_range(time_range) 计算环比区间

助教业绩域

助教业绩域包含 5 个任务,围绕助教的日度服务明细、月度汇总与排名、客户关系、工资计算、收支分析展开。数据流向为:

dwd_assistant_service_log ──┬──► DWS_ASSISTANT_DAILY日度明细
dwd_assistant_trash_event ──┘         │
                                      ▼
                              DWS_ASSISTANT_MONTHLY月度汇总+档位+排名)
                                      │
                                      ▼
                              DWS_ASSISTANT_SALARY工资计算
                                      │
dwd_assistant_service_log ────► DWS_ASSISTANT_FINANCE收支分析◄── dws_assistant_salary_calc
dwd_assistant_service_log ────► DWS_ASSISTANT_CUSTOMER客户关系统计

DWS_ASSISTANT_DAILY — 助教日度业绩明细

属性
任务代码 DWS_ASSISTANT_DAILY
Python 类 AssistantDailyTasktasks/dws/assistant_daily_task.py
目标表 billiards_dws.dws_assistant_daily_detail
主键 site_id, assistant_id, stat_date
粒度 日期 + 助教
更新策略 delete-before-insert按日期窗口
更新频率 每小时增量更新

数据来源

来源表 Schema 用途
dwd_assistant_service_log billiards_dwd 助教服务流水(主数据源)
dwd_assistant_trash_event billiards_dwd 废除记录(排除无效业绩)
dim_assistant billiards_dwd 助教维度SCD2获取当日等级
cfg_skill_type billiards_dws 技能 → 课程类型映射

聚合维度与输出字段

(assistant_id, service_date) 聚合,输出以下字段:

字段分组 字段 说明
标识 site_id, tenant_id, assistant_id, assistant_nickname, stat_date 门店、助教、日期
等级 assistant_level_code, assistant_level_name SCD2 as-of 取值,取统计日当日生效的等级
服务次数 total_service_count, base_service_count, bonus_service_count, room_service_count 总/基础课/附加课/包厢课
计费秒数 total_seconds, base_seconds, bonus_seconds, room_seconds 原始秒数
计费小时 total_hours, base_hours, bonus_hours, room_hours 秒数 ÷ 3600Decimal 精度
计费金额 total_ledger_amount, base_ledger_amount, bonus_ledger_amount, room_ledger_amount 台账金额
去重统计 unique_customers, unique_tables 去重客户数(排除散客)、去重台桌数
废除统计 trashed_seconds, trashed_count 被废除的秒数和次数

核心业务逻辑

  1. 课程类型分类:通过 skill_id 查询 cfg_skill_type 映射,分为 BASE(基础课)、BONUS(附加课)、ROOM(包厢课),未匹配默认 BASE
  2. 废除记录排除:以 assistant_service_id 为键构建废除索引,被废除的服务记录不计入有效业绩(服务次数、时长、金额),但单独统计 trashed_secondstrashed_count
  3. 助教等级 SCD2 取值:调用 get_assistant_level_asof(assistant_id, service_date) 获取统计日当日生效的等级版本,而非当前最新版本
  4. 散客过滤unique_customers 统计时排除 member_id 为 0 或 None 的散客
  5. 客户/台桌去重:无论服务记录是否被废除,客户和台桌均参与去重统计

DWS_ASSISTANT_MONTHLY — 助教月度业绩汇总

属性
任务代码 DWS_ASSISTANT_MONTHLY
Python 类 AssistantMonthlyTasktasks/dws/assistant_monthly_task.py
目标表 billiards_dws.dws_assistant_monthly_summary
主键 site_id, assistant_id, stat_month
粒度 月份 + 助教
更新策略 delete-before-insert按月份
更新频率 每日更新当月数据

数据来源

来源表 Schema 用途
dws_assistant_daily_detail billiards_dws 日度明细(按月聚合)
dwd_assistant_service_log billiards_dwd 月度去重客户/台桌(直接从 DWD 去重,避免日度求和失真)
dim_assistant billiards_dwd 助教维度(入职日期、当前等级)
cfg_performance_tier billiards_dws 绩效档位配置

聚合维度与输出字段

(assistant_id, stat_month) 聚合,输出以下字段:

字段分组 字段 说明
标识 site_id, tenant_id, assistant_id, assistant_nickname, stat_month 门店、助教、月份
等级 assistant_level_code, assistant_level_name 月末 SCD2 as-of 取值
入职 hire_date, is_new_hire 入职日期、是否当月新入职
工作天数 work_days COUNT(DISTINCT stat_date)
服务次数 total_service_count, base_service_count, bonus_service_count, room_service_count 月度累计
时长 total_hours, base_hours, bonus_hours, room_hours, effective_hours, trashed_hours 月度累计小时数
金额 total_ledger_amount, base_ledger_amount, bonus_ledger_amount, room_ledger_amount 月度累计金额
去重统计 unique_customers, unique_tables 月度去重(从 DWD 直接去重)
平均时长 avg_service_seconds 总秒数 ÷ 总服务次数
档位 tier_id, tier_code, tier_name 匹配的绩效档位
排名 rank_by_hours, rank_with_ties 按有效业绩小时数排名(考虑并列)

核心业务逻辑

1. 有效业绩计算

effective_hours = total_hours - trashed_hours

其中 trashed_hours 由日度明细的 trashed_seconds 累加后转换为小时。

2. 新入职判断

调用 is_new_hire_in_month(hire_date, stat_month):入职日期在当月 1 日 0 点之后即视为新入职。

3. 档位匹配

  • 正常助教:以 effective_hours 匹配 cfg_performance_tier,找 min_hours ≤ hours < max_hours 的首个档位
  • 新入职助教:先按日均折算 30 天(effective_hours / work_days × 30),再匹配档位
  • 新人封顶规则:当同时满足以下条件时,档位不超过配置的最大等级(默认 2 档):
    • 统计月份 ≥ 封顶规则生效月(配置项 dws.monthly.new_hire_cap_effective_from,默认 2026-03-01
    • 入职日期晚于封顶日(配置项 dws.monthly.new_hire_cap_day,默认当月 25 日)

4. 排名逻辑

effective_hours 降序排名,使用 calculate_rank_with_ties() 方法:

  • 并列时共享同一排名,下一名跳过(如 2 个第 1 名,下一个是第 3 名)
  • rank_by_hoursrank_with_ties 均使用此排名结果
  • 排名结果用于后续 DWS_ASSISTANT_SALARY 的 Top3 奖金计算

5. 月度去重客户/台桌

unique_customersunique_tables 优先使用从 dwd_assistant_service_log 直接按月去重的结果(_extract_monthly_uniques),而非日度明细的简单求和,避免跨日重复计数导致失真。

6. 月份过滤调度

  • 默认仅处理当月;月初前 N 天(配置项 dws.monthly.prev_month_grace_days,默认 5可同时处理上月
  • 配置 dws.monthly.allow_history = True 可处理全部历史月份
  • 配置 dws.monthly.history_months 可指定回溯月数

DWS_ASSISTANT_CUSTOMER — 助教-客户关系统计

属性
任务代码 DWS_ASSISTANT_CUSTOMER
Python 类 AssistantCustomerTasktasks/dws/assistant_customer_task.py
目标表 billiards_dws.dws_assistant_customer_stats
主键 site_id, assistant_id, member_id, stat_date
粒度 统计日期 + 助教 + 会员
更新策略 delete-before-insert按统计日期
更新频率 每日更新

数据来源

来源表 Schema 用途
dwd_assistant_service_log billiards_dwd 助教服务流水(全量,用于累计和滚动窗口统计)
dim_member billiards_dwd 会员维度(昵称、手机号)
dim_assistant billiards_dwd 助教维度(当前有效记录)

聚合维度与输出字段

(assistant_id, member_id) 聚合,以 stat_date(窗口结束日期)为统计基准日,输出以下字段:

字段分组 字段 说明
标识 site_id, tenant_id, assistant_id, assistant_nickname, member_id, member_nickname, member_mobile, stat_date 助教、会员、统计日期
全量累计 first_service_date, last_service_date, total_service_count, total_service_hours, total_service_amount 首次/最近服务日期、累计次数/时长/金额
滚动窗口 service_count_{N}d, service_hours_{N}d, service_amount_{N}dN = 7/10/15/30/60/90 各窗口的服务次数、时长、金额
活跃度 days_since_last, is_active_7d, is_active_30d 距最近服务天数、近 7/30 天是否活跃

核心业务逻辑

  1. 散客排除member_id 为 0 或 None 的散客不进入此表统计
  2. 滚动窗口:在单条 SQL 中通过 CASE WHEN service_date >= stat_date - INTERVAL '{N-1} days' 实现 6 个窗口7/10/15/30/60/90 天)的并行计算
  3. 活跃度判定is_active_7d = 近 7 天服务次数 > 0is_active_30d = 近 30 天服务次数 > 0
  4. HAVING 过滤:仅保留最近 90 天内有服务记录的助教-客户对,避免输出过多历史冷数据
  5. 手机号脱敏member_mobile 输出时中间 4 位替换为 ****(如 138****1234

DWS_ASSISTANT_SALARY — 助教工资计算

属性
任务代码 DWS_ASSISTANT_SALARY
Python 类 AssistantSalaryTasktasks/dws/assistant_salary_task.py
目标表 billiards_dws.dws_assistant_salary_calc
主键 site_id, assistant_id, salary_month
粒度 月份 + 助教
更新策略 delete-before-insert按月份
更新频率 月初计算上月工资

数据来源

来源表 Schema 用途
dws_assistant_monthly_summary billiards_dws 月度业绩汇总(有效小时数、档位、排名)
dws_assistant_recharge_commission billiards_dws 充值提成Excel 导入)
cfg_performance_tier billiards_dws 绩效档位(抽成比例、假期天数)
cfg_assistant_level_price billiards_dws 等级定价(客户支付价格)
cfg_bonus_rules billiards_dws 奖金规则冲刺奖金、Top 排名奖金)

输出字段

字段分组 字段 说明
标识 site_id, tenant_id, assistant_id, assistant_nickname, salary_month 门店、助教、工资月份
等级与档位 assistant_level_code, assistant_level_name, hire_date, is_new_hire, tier_id, tier_code, tier_name, rank_with_ties 等级、档位、排名
时长 effective_hours, base_hours, bonus_hours, room_hours 有效小时数(来自月度汇总)
定价信息 base_course_price, bonus_course_price, base_deduction, bonus_deduction_ratio 客户支付价格、球房抽成
收入明细 base_income, bonus_income, room_income, total_course_income 各课程类型收入
奖金明细 sprint_bonus, top_rank_bonus, recharge_commission, other_bonus, total_bonus 各类奖金
应发工资 gross_salary 课时收入 + 奖金合计
假期 vacation_days, vacation_unlimited 档位对应的假期天数
备注 calc_notes 计算备注(新入职、档位、奖金等)

工资计算公式

应发工资 = 课时收入 + 奖金合计

课时收入 = 基础课收入 + 附加课收入 + 包厢课收入
奖金合计 = 冲刺奖金 + Top3排名奖金 + 充值提成 + 其他奖金

基础课收入

基础课收入 = base_hours × (base_course_price - base_deduction)
  • base_course_price:客户支付价格,按助教等级区分(初级 98 / 中级 108 / 高级 118 / 星级 138 元/小时)
  • base_deduction:专业课抽成(元/小时),由档位配置决定,球房从每小时扣除
  • 示例:中级助教 170 小时3 档(抽成 13 元)→ 170 × (108 - 13) = 16,150 元

附加课收入

附加课收入 = bonus_hours × bonus_course_price × (1 - bonus_deduction_ratio)
  • bonus_course_price:附加课客户支付价格(固定 190 元/小时)
  • bonus_deduction_ratio:打赏课抽成比例,由档位配置决定
  • 示例15 小时3 档(抽成比例 0.35)→ 15 × 190 × (1 - 0.35) = 1,852.5 元

包厢课收入

包厢课收入 = room_hours × (room_course_price - base_deduction)
  • room_course_price:包厢课统一价格(配置项 dws.salary.room_course_price,默认 138 元/小时)

冲刺奖金

调用 calculate_sprint_bonus(effective_hours, salary_month),按 cfg_bonus_rules 配置表匹配:

  • 不累计,取满足有效小时数阈值的最高档奖金

Top3 排名奖金

调用 calculate_top_rank_bonus(rank, salary_month),仅排名前 3 的助教获得:

  • 第 1 名1,000 元
  • 第 2 名600 元
  • 第 3 名400 元
  • 并列排名均可获得对应奖金

充值提成

dws_assistant_recharge_commission 表读取,按 assistant_id 汇总当月提成金额。

SCD2 口径

等级定价使用 get_level_price(level_code, salary_month) 按月份取历史生效值,确保历史月份使用当时的定价版本。

运行调度

  • 默认仅在月初前 N 天运行(配置项 dws.salary.run_days,默认 5超过则跳过
  • 配置 dws.salary.allow_out_of_cycle = True 可强制运行
  • 工资月份判定月初day ≤ 5计算上月工资否则计算当月调整场景

DWS_ASSISTANT_FINANCE — 助教收支分析

属性
任务代码 DWS_ASSISTANT_FINANCE
Python 类 AssistantFinanceTasktasks/dws/assistant_finance_task.py
目标表 billiards_dws.dws_assistant_finance_analysis
主键 site_id, stat_date, assistant_id
粒度 日期 + 助教
更新策略 delete-before-insert按日期
更新频率 每日更新

数据来源

来源表 Schema 用途
dwd_assistant_service_log billiards_dwd 助教服务流水(日度收入)
cfg_skill_type billiards_dws 技能 → 课程类型映射(收入分类)
dws_assistant_salary_calc billiards_dws 工资计算结果(月度成本)
dws_assistant_daily_detail billiards_dws 日度明细(计算月度工作天数)

输出字段

字段分组 字段 说明
标识 site_id, tenant_id, stat_date, assistant_id, assistant_nickname 门店、日期、助教
收入 revenue_total, revenue_base, revenue_bonus, revenue_room 日度总收入及按课程类型拆分
成本 cost_daily 日均成本
利润 gross_profit, gross_margin 毛利润、毛利率
服务量 service_count, service_hours, room_service_count, room_service_hours, unique_customers 服务次数、时长、包厢服务、去重客户数

核心业务逻辑

1. 日度收入计算

dwd_assistant_service_log(DATE(start_use_time), site_assistant_id) 聚合:

  • revenue_totalSUM(ledger_amount)
  • revenue_base / revenue_bonus / revenue_room:按 cfg_skill_type.course_type_code 分类汇总
  • service_hoursSUM(income_seconds) / 3600.0
  • unique_customersCOUNT(DISTINCT tenant_member_id)(排除 ≤ 0

2. 日均成本计算

cost_daily = gross_salary / work_days
  • gross_salary:从 dws_assistant_salary_calc 取对应月份的应发工资
  • work_days:从 dws_assistant_daily_detail 按月统计 COUNT(DISTINCT stat_date),默认 20 天

3. 毛利润与毛利率

gross_profit = revenue_total - cost_daily
gross_margin = gross_profit / revenue_total    revenue_total > 0 时)
             = 0                                revenue_total = 0 时)

4. 依赖关系

此任务依赖 DWS_ASSISTANT_SALARYDWS_ASSISTANT_DAILY 的输出数据,应在这两个任务完成后运行。


会员分析域

会员分析域包含 2 个任务,围绕会员的消费行为汇总和到店明细展开。数据流向为:

dwd_settlement_head ──────────┬──► DWS_MEMBER_CONSUMPTION消费汇总+分层)
dim_member ───────────────────┤
dim_member_card_account ──────┘

dwd_settlement_head ──────────┬──► DWS_MEMBER_VISIT到店明细
dwd_assistant_service_log ────┤
dwd_table_fee_log ────────────┤
dim_member ───────────────────┤
dim_table ────────────────────┘

DWS_MEMBER_CONSUMPTION — 会员消费汇总

属性
任务代码 DWS_MEMBER_CONSUMPTION
Python 类 MemberConsumptionTasktasks/dws/member_consumption_task.py
目标表 billiards_dws.dws_member_consumption_summary
主键 site_id, member_id, stat_date
粒度 统计日期 + 会员
更新策略 delete-before-insert按统计日期
更新频率 每日更新

数据来源

来源表 Schema 用途
dwd_settlement_head billiards_dwd 结账单头表(消费金额、台费、商品、助教费用)
dim_member billiards_dwd 会员维度SCD2 当前版本,昵称、手机号、卡等级、注册日期、累计充值)
dim_member_card_account billiards_dwd 会员卡账户SCD2 当前版本,卡余额)

聚合维度与输出字段

(member_id) 聚合全量消费记录,以 stat_date(窗口结束日期)为统计基准日,输出以下字段:

字段分组 字段 说明
标识 site_id, tenant_id, member_id, stat_date 门店、会员、统计日期
会员信息 member_nickname, member_mobile, card_grade_name, register_date 昵称、脱敏手机号、卡等级、注册日期
全量累计 first_consume_date, last_consume_date, total_visit_count, total_consume_amount, total_recharge_amount, total_table_fee, total_goods_amount, total_assistant_amount 首次/最近消费日期、累计到店次数、累计消费金额、累计充值金额、累计台费、累计商品金额、累计助教费用
滚动窗口(次数) visit_count_7d, visit_count_10d, visit_count_15d, visit_count_30d, visit_count_60d, visit_count_90d 各窗口到店次数
滚动窗口(金额) consume_amount_7d, consume_amount_10d, consume_amount_15d, consume_amount_30d, consume_amount_60d, consume_amount_90d 各窗口消费金额
卡余额 cash_card_balance, gift_card_balance, total_card_balance 储值卡(现金卡)余额、赠送卡余额、总余额
活跃度 days_since_last, is_active_7d, is_active_30d, is_active_90d 距最近消费天数、近 7/30/90 天是否活跃
客户分层 customer_tier 分层标签(高价值/中等/低活跃/流失)

核心业务逻辑

1. 散客排除

member_id 为 0 或 None 的散客不进入此表统计。SQL 层面和 transform 阶段均做过滤。

2. 消费统计来源

dwd_settlement_headmember_id 聚合,消费金额拆分为:

  • consume_money:总消费金额
  • table_charge_money:台费
  • goods_money:商品金额
  • assistant_pd_money + assistant_cx_money:助教费用(专业课 + 陪练课合计)

3. 滚动窗口

在单条 SQL 中通过 CASE WHEN consume_date >= stat_date - INTERVAL '{N-1} days' 实现 6 个窗口7/10/15/30/60/90 天)的并行计算,同时统计到店次数和消费金额。

4. 卡余额区分

dim_member_card_accountcard_type_id 区分卡类型:

卡类型 card_type_id 归入字段
储值卡(现金卡) 2793249295533893 cash_card_balance
台费卡 2791990152417157 gift_card_balance
活动抵用券 2793266846533445 gift_card_balance
酒水卡 2794699703437125 gift_card_balance

total_card_balance = cash_card_balance + gift_card_balance

仅取 SCD2 当前版本(scd2_is_current = 1)且未删除(is_delete = 0)的记录。同一会员可能有多张卡,余额按类型累加。

5. 活跃度判定

  • days_since_laststat_date - last_consume_date 的天数差,无消费记录时为 NULL
  • is_active_7d:近 7 天到店次数 > 0
  • is_active_30d:近 30 天到店次数 > 0
  • is_active_90d:近 90 天到店次数 > 0

6. 客户分层规则

按以下优先级判定 customer_tier

分层 条件 说明
高价值 90 天内消费 ≥ 3 次 消费金额 ≥ 1000 元 高频高额客户
中等 30 天内有消费 近期活跃客户
低活跃 90 天内有消费但 30 天内无消费 有消费但频率下降
流失 90 天内无消费 长期未到店

判定顺序为从上到下,命中即返回。

7. 手机号脱敏

member_mobile 输出时中间 4 位替换为 ****(如 138****1234),长度不足 7 位时原样输出。


DWS_MEMBER_VISIT — 会员到店明细

属性
任务代码 DWS_MEMBER_VISIT
Python 类 MemberVisitTasktasks/dws/member_visit_task.py
目标表 billiards_dws.dws_member_visit_detail
主键 site_id, member_id, order_settle_id
粒度 会员 + 结账单(每次到店一条记录)
更新策略 delete-before-insertvisit_date 日期窗口)
更新频率 每日增量更新

数据来源

来源表 Schema 用途
dwd_settlement_head billiards_dwd 结账单头表(消费金额、支付方式)
dwd_assistant_service_log billiards_dwd 助教服务流水(服务时长、金额)
dwd_table_fee_log billiards_dwd 台费流水(真实台桌使用秒数)
dim_member billiards_dwd 会员维度SCD2 当前版本,昵称、手机号、生日)
dim_table billiards_dwd 台桌维度SCD2 当前版本,台桌名称、区域名称)
cfg_area_category billiards_dws 区域分类映射(通过 ConfigCache 加载)

聚合维度与输出字段

以每笔结账单(order_settle_id)为粒度,输出以下字段:

字段分组 字段 说明
标识 site_id, tenant_id, member_id, order_settle_id, visit_date, visit_time 门店、会员、结账单号、到店日期、到店时间
会员信息 member_nickname, member_mobile, member_birthday 昵称、脱敏手机号、生日
台桌信息 table_id, table_name, area_name, area_category 台桌 ID、台桌名称、区域名称、区域分类
消费金额 table_fee, goods_amount, assistant_amount, total_consume, total_discount, actual_pay 台费、商品金额、助教费用、总消费、总优惠、实付金额
支付方式 cash_pay, cash_card_pay, gift_card_pay, groupbuy_pay 现金/在线支付、储值卡支付、赠送卡支付、团购券支付
时长 table_duration_min, assistant_duration_min 台桌使用时长(分钟)、助教服务时长(分钟)
助教服务 assistant_services JSON 格式的助教服务明细

核心业务逻辑

1. 散客排除

SQL 层面通过 member_id IS NOT NULL AND member_id != 0 过滤transform 阶段通过 is_guest() 二次过滤。

2. 消费金额拆分

dwd_settlement_head 直接读取各金额字段:

  • table_feetable_charge_money(台费)
  • goods_amountgoods_money(商品金额)
  • assistant_amountassistant_pd_money + assistant_cx_money(专业课 + 陪练课助教费用合计)
  • total_consumeconsume_money(总消费金额)
  • actual_paypay_amount(实付金额)

3. 总优惠计算

total_discount = adjust_amount + member_discount_amount + rounding_amount
  • adjust_amount:手动调整金额
  • member_discount_amount:会员折扣金额
  • rounding_amount:抹零金额

4. 支付方式拆分

字段 来源字段 说明
cash_pay pay_amount 现金/在线支付
cash_card_pay balance_amount 储值卡(现金卡)支付
gift_card_pay gift_card_amount 赠送卡支付
groupbuy_pay coupon_amount 团购券支付

5. 台桌使用时长

dwd_table_fee_logorder_settle_id 聚合 SUM(real_table_use_seconds) 获取真实台费秒数,转换为分钟(整除 60。仅取未删除记录is_delete = 0)。

6. 助教服务时长

dwd_assistant_service_logorder_settle_id 关联,汇总所有助教的 income_seconds 后转换为分钟(整除 60。仅取未删除记录is_delete = 0)。

7. 助教服务明细JSON

每笔结账单关联的助教服务以 JSON 数组格式存储在 assistant_services 字段中,每个元素包含:

[
  {
    "assistant_id": 12345,
    "nickname": "张教练",
    "duration_min": 60,
    "amount": 108.00
  }
]
JSON 字段 来源 说明
assistant_id site_assistant_id 助教 ID
nickname nickname 助教昵称
duration_min income_seconds // 60 服务时长(分钟)
amount ledger_amount 台账金额

无助教服务时 assistant_servicesNULL

8. 区域分类

通过 ConfigCache 加载 cfg_area_category 配置,调用 get_area_category(area_name) 将台桌区域名称映射为分类标签。匹配逻辑:精确匹配 → 模糊匹配 → 兜底 OTHER

9. 手机号脱敏

DWS_MEMBER_CONSUMPTION 相同,中间 4 位替换为 ****


财务统计域

财务统计域包含 4 个任务,围绕门店的日度财务汇总、充值统计、收入结构分析和优惠明细展开。数据流向为:

dwd_settlement_head ──────────┬──► DWS_FINANCE_DAILY财务日报
dwd_groupbuy_redemption ──────┤
dwd_recharge_order ───────────┤
dwd_member_balance_change ────┤
dws_finance_expense_summary ──┤
dws_platform_settlement ──────┘

dwd_recharge_order ───────────┬──► DWS_FINANCE_RECHARGE充值统计
dim_member_card_account ──────┘

dwd_settlement_head ──────────┬──► DWS_FINANCE_INCOME_STRUCTURE收入结构
dwd_table_fee_log ────────────┤
dwd_assistant_service_log ────┤
dim_table ────────────────────┤
cfg_area_category ────────────┘

dwd_settlement_head ──────────┬──► DWS_FINANCE_DISCOUNT_DETAIL折扣明细
dwd_groupbuy_redemption ──────┤
dwd_member_balance_change ────┘

DWS_FINANCE_DAILY — 财务日报

属性
任务代码 DWS_FINANCE_DAILY
Python 类 FinanceDailyTasktasks/dws/finance_daily_task.py
目标表 billiards_dws.dws_finance_daily_summary
主键 site_id, stat_date
粒度 日期
更新策略 delete-before-insert按日期窗口
更新频率 每小时更新当日数据

数据来源

来源表 Schema 用途
dwd_settlement_head billiards_dwd 结账单头表(发生额、支付、优惠)
dwd_groupbuy_redemption billiards_dwd 团购核销(团购实付金额)
dwd_recharge_order billiards_dwd 充值订单(首充/续充、现金/赠送)
dwd_member_balance_change billiards_dwd 余额变动(赠送卡消费)
dws_finance_expense_summary billiards_dws 支出汇总Excel 导入,按月分摊到日)
dws_platform_settlement billiards_dws 平台回款/服务费Excel 导入)

输出字段

字段分组 字段 说明
标识 site_id, tenant_id, stat_date 门店、统计日期
发生额 gross_amount, table_fee_amount, goods_amount, assistant_pd_amount, assistant_cx_amount 正价总额及按类型拆分(台费/商品/专业课/陪练课)
优惠 discount_total, discount_groupbuy, discount_vip, discount_gift_card, discount_manual, discount_rounding, discount_other 优惠合计及按类型拆分
确认收入 confirmed_income 发生额 - 优惠合计
现金流入 cash_inflow_total, cash_pay_amount, groupbuy_pay_amount, platform_settlement_amount, recharge_cash_inflow 现金流入合计及来源拆分
现金流出 cash_outflow_total, platform_fee_amount 现金流出合计(支出 + 平台费用)
现金净变动 cash_balance_change 流入 - 流出
卡消费 card_consume_total, cash_card_consume, gift_card_consume 储值卡消费 + 赠送卡消费
充值统计 recharge_count, recharge_total, recharge_cash, recharge_gift, first_recharge_count, first_recharge_amount, renewal_count, renewal_amount 充值笔数/金额、首充/续充拆分
订单统计 order_count, member_order_count, guest_order_count, avg_order_amount 总订单数、会员/散客订单数、客单价

核心业务逻辑

1. 发生额(正价)

gross_amount = table_charge_money + goods_money + assistant_pd_money + assistant_cx_money

dwd_settlement_headDATE(pay_time) 聚合分别统计台费、商品、专业课PD、陪练课CX四类收入。

2. 团购优惠计算

团购实付金额 = pl_coupon_sale_amount > 0 ? pl_coupon_sale_amount : groupbuy_redemption.ledger_unit_price
团购优惠 = coupon_amount - 团购实付金额
  • coupon_amount:团购抵消台费金额(结账单字段)
  • 团购实付金额优先取 pl_coupon_sale_amount(平台券销售金额),否则取 dwd_groupbuy_redemption.ledger_unit_price
  • 团购优惠为负时置 0

3. 大客户优惠拆分

手动调整金额(adjust_amount)中,通过配置项 dws.discount.big_customer_member_idsdws.discount.big_customer_order_ids 标记的订单归入大客户优惠,其余归入其他优惠:

discount_other = adjust_amount - big_customer_amount    (负值置 0

4. 赠送卡消费

dwd_member_balance_change 提取赠送卡消费(from_type = 1change_amount < 0),按卡类型过滤:

  • 台费卡(card_type_id = 2791990152417157
  • 酒水卡(card_type_id = 2794699703437125
  • 活动抵用券(card_type_id = 2793266846533445

ABS(change_amount) 汇总为当日赠送卡消费总额。

5. 优惠合计与确认收入

discount_total = discount_groupbuy + discount_vip + discount_gift_card + discount_manual + discount_rounding
confirmed_income = gross_amount - discount_total

6. 现金流计算

cash_inflow_total = cash_pay_amount + platform_inflow + recharge_cash_inflow
cash_outflow_total = expense_amount + platform_fee_amount
cash_balance_change = cash_inflow_total - cash_outflow_total
  • platform_inflow:优先取 platform_settlement_amount(平台回款),为 0 时取 groupbuy_pay_amount
  • platform_fee_amountcommission_amount + service_fee(平台佣金 + 服务费)
  • expense_amount:月度支出按日均分摊(月总额 ÷ 当月天数)

7. 支出分摊逻辑

支出数据来自 dws_finance_expense_summaryExcel 导入),以月为粒度。任务按当月天数均分到每日:

daily_expense = expense_amount / days_in_month

8. 卡消费统计

cash_card_consume = recharge_card_amount + balance_amount    (储值卡支付)
gift_card_consume = 赠送卡消费总额                            (来自余额变动)
card_consume_total = cash_card_consume + gift_card_consume

DWS_FINANCE_RECHARGE — 充值统计

属性
任务代码 DWS_FINANCE_RECHARGE
Python 类 FinanceRechargeTasktasks/dws/finance_recharge_task.py
目标表 billiards_dws.dws_finance_recharge_summary
主键 site_id, stat_date
粒度 日期
更新策略 delete-before-insert按日期窗口
更新频率 每日更新

数据来源

来源表 Schema 用途
dwd_recharge_order billiards_dwd 充值订单(首充/续充、现金/赠送)
dim_member_card_account billiards_dwd 会员卡账户SCD2 当前版本,卡余额快照)

输出字段

字段分组 字段 说明
标识 site_id, tenant_id, stat_date 门店、统计日期
充值汇总 recharge_count, recharge_total, recharge_cash, recharge_gift 充值笔数、总额(现金+赠送)、现金部分、赠送部分
首充 first_recharge_count, first_recharge_cash, first_recharge_gift, first_recharge_total 首充笔数、现金、赠送、总额
续充 renewal_count, renewal_cash, renewal_gift, renewal_total 续充笔数、现金、赠送、总额
会员统计 recharge_member_count, new_member_count 当日充值去重会员数、首充新会员数
卡余额快照 total_card_balance, cash_card_balance, gift_card_balance 全店卡余额总计、储值卡余额、赠送卡余额

核心业务逻辑

1. 首充/续充区分

通过 dwd_recharge_order.is_first 字段区分:

  • is_first = 1:首充(会员首次充值)
  • is_first = 0NULL:续充

每笔充值金额拆分为:

充值总额 = pay_money现金部分+ gift_money赠送部分

2. 会员去重统计

  • recharge_member_countCOUNT(DISTINCT member_id),当日充值的去重会员数
  • new_member_countCOUNT(DISTINCT CASE WHEN is_first = 1 THEN member_id END),当日首充的去重新会员数

3. 卡余额快照

dim_member_card_account 取 SCD2 当前版本(scd2_is_current = 1)且未删除(is_delete = 0)的记录,按 card_type_id 分类汇总:

卡类型 card_type_id 归入字段
储值卡(现金卡) 2793249295533893 cash_card_balance
台费卡 2791990152417157 gift_card_balance
酒水卡 2794699703437125 gift_card_balance
活动抵用券 2793266846533445 gift_card_balance
total_card_balance = cash_card_balance + gift_card_balance

注意:卡余额为窗口结束日的全量快照,而非按日变化值。窗口内所有日期共享同一份余额数据。


DWS_FINANCE_INCOME_STRUCTURE — 收入结构分析

属性
任务代码 DWS_FINANCE_INCOME_STRUCTURE
Python 类 FinanceIncomeStructureTasktasks/dws/finance_income_task.py
目标表 billiards_dws.dws_finance_income_structure
主键 site_id, stat_date, structure_type, category_code
粒度 日期 + 结构类型 + 分类代码
更新策略 delete-before-insert按日期窗口
更新频率 每日更新

数据来源

来源表 Schema 用途
dwd_settlement_head billiards_dwd 结账单头表(按收入类型汇总)
dwd_table_fee_log billiards_dwd 台费流水(按区域汇总台费收入)
dwd_assistant_service_log billiards_dwd 助教服务流水(按区域汇总助教收入)
dim_table billiards_dwd 台桌维度SCD2获取区域名称
cfg_area_category billiards_dws 区域分类映射(通过 ConfigCache 加载)

输出字段

字段 说明
site_id, tenant_id, stat_date 门店、统计日期
structure_type 结构类型:INCOME_TYPE(按收入类型)或 AREA(按区域)
category_code 分类代码(见下方分类定义)
category_name 分类名称(中文)
income_amount 收入金额
income_ratio 收入占比(保留 4 位小数)
order_count 关联订单数
duration_minutes 使用时长(分钟),仅 AREA 类型有值

两种分析维度

维度 1按收入类型structure_type = 'INCOME_TYPE'

dwd_settlement_headpay_time::DATE 聚合,仅统计已结账订单(settle_status = 1),每日展开为 4 条记录:

category_code category_name 来源字段 说明
TABLE_FEE 台费收入 table_charge_money 台桌使用费
GOODS 商品收入 goods_money 商品销售
ASSISTANT_BASE 助教基础课 assistant_pd_money 专业课PD=陪打)
ASSISTANT_BONUS 助教附加课 assistant_cx_money 附加课CX=超休/促销)

占比计算:income_ratio = 该类型金额 / 当日四类收入总和

维度 2按区域structure_type = 'AREA'

通过 CTE 合并台费流水和助教服务流水,关联 dim_table 获取 site_table_area_name,再通过 get_area_category(area_name) 映射到分类代码。

区域映射逻辑(与 DWS_MEMBER_VISIT 相同):精确匹配 → 模糊匹配 → 兜底 OTHER

相同 category_code 的不同区域名称会被合并聚合。每条记录额外输出 duration_minutes(台费秒数 + 助教服务秒数,转换为分钟)。

占比计算:income_ratio = 该区域金额 / 当日所有区域收入总和

核心业务逻辑

1. 自定义 load 方法

此任务未使用 BaseDwsTask.delete_existing_data() + bulk_insert(),而是自行实现 DELETE + INSERT SQL直接操作 billiards_dws.dws_finance_income_structure 表,逐行插入并自动设置 created_atupdated_at

2. 区域收入来源

区域维度的收入同时包含台费收入(dwd_table_fee_log.ledger_amount)和助教收入(dwd_assistant_service_log.ledger_amount),通过 UNION ALL 合并后按 (stat_date, area_name) 聚合。


DWS_FINANCE_DISCOUNT_DETAIL — 折扣明细统计

属性
任务代码 DWS_FINANCE_DISCOUNT_DETAIL
Python 类 FinanceDiscountDetailTasktasks/dws/finance_discount_task.py
目标表 billiards_dws.dws_finance_discount_detail
主键 site_id, stat_date, discount_type_code
粒度 日期 + 折扣类型
更新策略 delete-before-insert按日期窗口
更新频率 每日更新

数据来源

来源表 Schema 用途
dwd_settlement_head billiards_dwd 结账单头表(团购、手动调整、会员折扣、抹零)
dwd_groupbuy_redemption billiards_dwd 团购核销(团购实付金额)
dwd_member_balance_change billiards_dwd 余额变动(赠送卡消费,按卡类型拆分)

输出字段

字段 说明
site_id, tenant_id, stat_date 门店、统计日期
discount_type_code 折扣类型代码(见下方分类定义)
discount_type_name 折扣类型名称(中文)
discount_amount 折扣金额
discount_ratio 折扣占比(保留 4 位小数)
usage_count 使用次数
affected_orders 影响订单数(当前等于 usage_count

折扣类型分类

每日展开为最多 8 条记录,覆盖以下折扣类型:

discount_type_code discount_type_name 数据来源 计算逻辑
GROUPBUY 团购优惠 dwd_settlement_head + dwd_groupbuy_redemption coupon_amount - 团购实付金额(负值置 0
VIP 会员折扣 dwd_settlement_head.member_discount_amount 直接取绝对值
ROUNDING 抹零 dwd_settlement_head.rounding_amount 直接取绝对值
GIFT_CARD_TABLE 台费卡抵扣 dwd_member_balance_changecard_type_id = 2791990152417157 消费金额绝对值
GIFT_CARD_DRINK 酒水卡抵扣 dwd_member_balance_changecard_type_id = 2794699703437125 消费金额绝对值
GIFT_CARD_COUPON 活动抵用券抵扣 dwd_member_balance_changecard_type_id = 2793266846533445 消费金额绝对值
BIG_CUSTOMER 大客户优惠 dwd_settlement_head.adjust_amount(配置标记) 按配置的会员/订单 ID 匹配
OTHER 其他优惠 dwd_settlement_head.adjust_amount(剩余部分) adjust_amount - big_customer_amount(负值置 0

核心业务逻辑

1. 团购优惠计算

DWS_FINANCE_DAILY 相同的逻辑:

团购实付 = pl_coupon_sale_amount > 0 ? pl_coupon_sale_amount : groupbuy_redemption.ledger_unit_price
团购优惠 = coupon_amount - 团购实付

仅统计 coupon_amount > 0 的已结账订单(settle_status = 1)。

2. 赠送卡消费拆分

DWS_FINANCE_DAILY 不同,此任务将赠送卡消费按卡类型拆分为 3 条独立记录(台费卡/酒水卡/活动抵用券),而非合并为一个总额。数据来源相同:dwd_member_balance_changefrom_type = 1change_amount < 0 的记录。

3. 大客户优惠拆分

手动调整金额(adjust_amount)通过配置项拆分:

  • dws.discount.big_customer_member_ids:大客户会员 ID 列表
  • dws.discount.big_customer_order_ids:大客户订单 ID 列表

匹配到的订单调整金额归入 BIG_CUSTOMER,其余归入 OTHER。若两个配置项均为空,则所有手动调整归入 OTHER

4. 占比计算

discount_ratio = 该类型折扣金额 / 当日所有类型折扣金额总和

5. 自定义 load 方法

DWS_FINANCE_INCOME_STRUCTURE 类似,此任务自行实现 DELETE + INSERT SQL逐行插入并自动设置 created_atupdated_at


运维任务

运维任务包含 4 个任务,负责订单汇总中间表构建、历史数据清理和物化视图刷新。这些任务不直接产出业务报表,而是为其他 DWS 任务和下游查询提供基础设施支撑。

dwd_settlement_head ──────────┐
dwd_table_fee_log ────────────┤
dwd_assistant_service_log ────┼──► DWS_BUILD_ORDER_SUMMARY订单汇总中间表
dwd_store_goods_sale ─────────┤
dwd_groupbuy_redemption ──────┤
dwd_refund / dwd_refund_ex ───┘

dws_*(所有 DWS 汇总表)──────► DWS_RETENTION_CLEANUP历史数据清理

dws_finance_daily_summary ────► DWS_MV_REFRESH_FINANCE_DAILY财务日报物化视图刷新
dws_assistant_daily_detail ───► DWS_MV_REFRESH_ASSISTANT_DAILY助教日报物化视图刷新

DWS_BUILD_ORDER_SUMMARY — 订单汇总中间表构建

属性
任务代码 DWS_BUILD_ORDER_SUMMARY
Python 类 DwsBuildOrderSummaryTasktasks/utility/dws_build_order_summary_task.py
继承 BaseTask(非 BaseDwsTask
目标表 billiards_dws.dws_order_summary
主键 site_id, order_settle_id
粒度 订单(每笔结账单一条记录)
更新策略 delete-before-insert + ON CONFLICT DO UPDATEupsert

用途

构建订单级别的汇总中间表 dws_order_summary,将分散在多张 DWD 事实表中的订单信息(台费、助教费、商品、团购、退款等)合并为一张宽表。该中间表可供下游报表查询和指数计算直接使用,避免每次都执行多表 JOIN。

数据来源

来源表 Schema 用途
dwd_settlement_head billiards_dwd 结账单头表(基础订单信息、支付金额、优惠金额)
dwd_table_fee_log billiards_dwd 台费流水(真实台费金额)
dwd_assistant_service_log billiards_dwd 助教服务流水(助教服务金额)
dwd_store_goods_sale billiards_dwd 商品销售明细(商品数量、金额)
dwd_groupbuy_redemption billiards_dwd 团购核销(团购金额)
dwd_refund / dwd_refund_ex billiards_dwd 退款记录(退款金额)

输出字段

字段分组 字段 说明
标识 site_id, order_settle_id, order_trade_no, order_date, tenant_id 门店、结账单号、交易号、订单日期、租户
会员 member_id, member_flag, recharge_order_flag 会员 ID、是否绑定会员、是否充值订单
商品 item_count, total_item_quantity 商品种类数、商品总数量
费用明细 table_fee_amount, assistant_service_amount, goods_amount, group_amount 台费、助教费、商品金额、团购金额
优惠 total_coupon_deduction, member_discount_amount, manual_discount_amount 团购抵扣、会员折扣、手动调整
金额汇总 order_original_amount, order_final_amount 订单原价、实付金额
支付方式 stored_card_deduct, external_paid_amount, total_paid_amount 储值卡抵扣、外部支付、总支付
台账流水 book_table_flow, book_assistant_flow, book_goods_flow, book_group_flow, book_order_flow 台费/助教/商品/团购/订单台账流水
有效消费 order_effective_consume_cash, order_effective_recharge_cash, order_effective_flow 有效消费现金、有效充值现金、有效流水
退款 refund_amount, net_income 退款金额、净收入

核心业务逻辑

1. SQL CTE 多表合并

任务通过一条大型 SQLSQL_BUILD_SUMMARY)完成所有计算,使用 6 个 CTE 分别从不同事实表聚合数据,最终通过 LEFT JOIN 合并到订单粒度:

  • base:从 dwd_settlement_head 提取订单基础信息
  • table_fee:从 dwd_table_fee_logorder_settle_id 聚合台费
  • assistant_fee:从 dwd_assistant_service_logorder_settle_id 聚合助教费
  • goods_fee:从 dwd_store_goods_saleorder_settle_id 聚合商品数量和金额
  • group_fee:从 dwd_groupbuy_redemptionorder_settle_id 聚合团购金额
  • refunds:从 dwd_refund + dwd_refund_exrelate_id(关联订单)聚合退款金额

2. 金额优先级

台费、助教费、商品金额优先取明细表(dwd_table_fee_log / dwd_assistant_service_log / dwd_store_goods_sale)的聚合值,若明细表无数据则回退到结账单头表(dwd_settlement_head)的汇总字段:

COALESCE(tf.table_fee_amount, b.settle_table_fee_amount)
COALESCE(af.assistant_service_amount, b.settle_assistant_service_amount)
COALESCE(gf.goods_amount, b.settle_goods_amount)

3. 订单原价计算

order_original_amount = total_paid_amount + total_coupon_deduction + member_discount_amount + manual_discount_amount

即实付金额加上所有优惠金额,还原订单原始价格。

4. 外部支付金额

external_paid_amount = MAX(total_paid_amount - stored_card_deduct, 0)

总支付减去储值卡抵扣部分,即通过现金/在线支付的金额。

5. 净收入

net_income = total_paid_amount - refund_amount

6. 充值订单标记

recharge_order_flag = (consume_money = 0 AND pay_amount > 0)

消费金额为 0 但有支付金额的订单标记为充值订单。

配置参数

配置项 类型 默认值 说明
dws.order_summary.full_refresh bool False 全量刷新模式(忽略日期窗口,处理全部数据)
dws.order_summary.site_id int/None app.store_id 指定门店 ID设为 null 时处理所有门店
dws.order_summary.start_date date/None 窗口起始日期 手动指定起始日期(覆盖窗口计算)
dws.order_summary.end_date date/None 窗口结束日期 手动指定结束日期(覆盖窗口计算)
dws.order_summary.delete_before_insert bool True 是否在插入前先删除旧数据

执行模式

  • 增量模式(默认):按时间窗口处理,先 DELETE 窗口内旧数据,再 INSERT ... ON CONFLICT DO UPDATE
  • 全量刷新full_refresh=True
    • site_idnull:执行 TRUNCATE TABLE 清空全表后重建
    • site_id 有值DELETE 该门店全部数据后重建
  • 分段执行:支持 build_window_segments 窗口分段,大时间范围自动拆分为多段依次执行

前置依赖

目标表 billiards_dws.dws_order_summary 必须已存在,否则抛出 RuntimeError。需先运行 INIT_DWS_SCHEMA 任务创建表结构。


DWS_RETENTION_CLEANUP — 时间分层清理

属性
任务代码 DWS_RETENTION_CLEANUP
Python 类 DwsRetentionCleanupTasktasks/dws/retention_cleanup_task.py
继承 BaseDwsTask
目标表 多张 DWS 表(按配置)
更新策略 DELETE按日期截断删除历史数据

用途

按配置的时间分层范围,对 DWS 层的汇总表执行历史数据清理。用于控制 DWS 表的数据量增长,删除超出保留期的历史记录。该任务默认不启用,需通过配置显式开启。

默认清理表列表

任务内置了 14 张 DWS 表的清理定义,每张表指定了对应的日期列:

目标表 日期列 说明
dws_assistant_daily_detail stat_date 助教日度明细
dws_assistant_monthly_summary stat_month 助教月度汇总
dws_assistant_customer_stats stat_date 助教-客户关系
dws_assistant_salary_calc salary_month 助教工资
dws_assistant_recharge_commission commission_month 充值提成
dws_assistant_finance_analysis stat_date 助教收支分析
dws_member_consumption_summary stat_date 会员消费汇总
dws_member_visit_detail visit_date 会员到店明细
dws_finance_daily_summary stat_date 财务日报
dws_finance_income_structure stat_date 收入结构
dws_finance_discount_detail stat_date 折扣明细
dws_finance_recharge_summary stat_date 充值统计
dws_finance_expense_summary expense_month 支出汇总
dws_platform_settlement settlement_date 平台结算

配置参数

配置项 类型 默认值 说明
dws.retention.enabled bool False 是否启用清理(必须显式设为 true
dws.retention.layer str "ALL" 默认清理层级TimeLayer 枚举值)
dws.retention.tables str 全部 14 张表 需要清理的表名列表(逗号分隔),为空时使用全部默认表
dws.retention.table_layers str/dict {} 表级别的层级覆盖JSON 格式),可为特定表指定不同的保留期

配置示例

# .env 配置
DWS_RETENTION_ENABLED=true
DWS_RETENTION_LAYER=LAST_3_MONTHS
DWS_RETENTION_TABLES=dws_finance_daily_summary,dws_assistant_daily_detail
DWS_RETENTION_TABLE_LAYERS={"dws_finance_expense_summary":"ALL"}

上述配置含义:

  • 启用清理功能
  • 默认保留近 3 个月数据,删除更早的记录
  • 仅清理 dws_finance_daily_summarydws_assistant_daily_detail 两张表
  • dws_finance_expense_summary 使用 ALL 层级(即不清理,保留全部数据)

核心业务逻辑

1. 启用检查

任务执行时首先检查 dws.retention.enabled 配置,未启用则直接跳过,不执行任何删除操作。

2. 层级解析与截断日期计算

根据配置的 TimeLayer 层级,调用 get_time_layer_range(layer, base_date) 计算时间范围取范围的起始日期作为截断点cutoff。早于截断点的数据将被删除。

层级 保留范围 截断点(以 2026-03-15 为例)
LAST_2_DAYS 近 2 天 2026-03-14
LAST_1_MONTH 近 30 天 2026-02-13
LAST_3_MONTHS 近 90 天 2025-12-15
LAST_6_MONTHS 近 6 个月 2025-09-01月初
ALL 全量保留 不清理(跳过)

3. 月度列截断对齐

对于日期列为月度粒度的表(stat_monthsalary_monthcommission_monthexpense_month),截断日期自动对齐到月初(day=1),避免删除当月部分数据。

4. 表级层级覆盖

通过 dws.retention.table_layers 配置,可为特定表指定不同于默认层级的保留期。例如支出汇总表(月度 Excel 导入)可设为 ALL 永久保留,而日度明细表设为 LAST_3_MONTHS

5. 清理 SQL

对每张目标表执行:

DELETE FROM billiards_dws.{table}
WHERE site_id = {store_id}
  AND {date_col} < {cutoff}

按门店隔离,仅删除当前门店的历史数据。

6. 执行结果

返回每张表的删除行数明细和总删除行数:

{
  "counts": {"cleaned": 1500},
  "extra": {
    "details": [
      {"table": "dws_finance_daily_summary", "deleted": 800, "cutoff": "2025-12-15"},
      {"table": "dws_assistant_daily_detail", "deleted": 700, "cutoff": "2025-12-15"}
    ]
  }
}

DWS_MV_REFRESH_FINANCE_DAILY / DWS_MV_REFRESH_ASSISTANT_DAILY — 物化视图分层刷新

这两个任务共享同一个基类 BaseMvRefreshTask,仅在基表名称上有所不同。

任务信息

属性 DWS_MV_REFRESH_FINANCE_DAILY DWS_MV_REFRESH_ASSISTANT_DAILY
任务代码 DWS_MV_REFRESH_FINANCE_DAILY DWS_MV_REFRESH_ASSISTANT_DAILY
Python 类 DwsMvRefreshFinanceDailyTask DwsMvRefreshAssistantDailyTask
继承 BaseMvRefreshTaskBaseDwsTask BaseMvRefreshTaskBaseDwsTask
基表 dws_finance_daily_summary dws_assistant_daily_detail
日期列 stat_date stat_date
更新策略 REFRESH MATERIALIZED VIEW REFRESH MATERIALIZED VIEW

用途

按时间分层刷新 PostgreSQL 物化视图。物化视图预先按不同时间范围L1-L4聚合基表数据供前端查询直接使用避免每次查询都执行全表扫描。该任务默认不启用,需通过配置显式开启。

分层机制L1-L4

每张基表对应最多 4 个物化视图,按时间范围从小到大排列:

层级 后缀 对应 TimeLayer 时间范围 视图命名示例
L1 _l1 LAST_2_DAYS 近 2 天 mv_dws_finance_daily_summary_l1
L2 _l2 LAST_1_MONTH 近 30 天 mv_dws_finance_daily_summary_l2
L3 _l3 LAST_3_MONTHS 近 90 天 mv_dws_finance_daily_summary_l3
L4 _l4 LAST_6_MONTHS 近 6 个月 mv_dws_finance_daily_summary_l4

视图命名规则:mv_{基表名}_{层级后缀}

配置参数

配置项 类型 默认值 说明
dws.mv.enabled bool False 是否启用物化视图刷新(必须显式设为 true
dws.mv.tables str None 需要刷新的基表列表(逗号分隔),为空时回退到 dws.retention.tables
dws.mv.layers str None 显式指定刷新的层级列表(逗号分隔,如 LAST_2_DAYS,LAST_1_MONTH
dws.mv.table_layers str/dict None 表级别的层级覆盖JSON 格式),为空时回退到 dws.retention.table_layers
dws.mv.refresh_concurrently bool False 是否使用 CONCURRENTLY 关键字刷新(不阻塞读查询,但需要唯一索引)

层级解析优先级

任务按以下优先级确定需要刷新哪些层级:

  1. 显式配置dws.mv.layers):直接指定层级列表,最高优先级
  2. 表级覆盖dws.mv.table_layersdws.retention.table_layers):按基表名查找对应层级,刷新该层级及其以下所有层级
  3. 默认层级dws.retention.layer):使用保留清理的层级配置,刷新该层级及其以下所有层级
  4. 全部刷新:以上均未配置时,刷新 L1-L4 全部 4 个层级

"该层级及其以下"的含义:若配置为 LAST_3_MONTHSL3则刷新 L1、L2、L3 三个层级。

启用条件

任务启用需同时满足:

  1. dws.mv.enabled = true
  2. 当前基表在允许列表中(dws.mv.tablesdws.retention.tables 包含该基表名),或两个列表均为空(不限制)

核心业务逻辑

1. 视图存在性检查

刷新前通过 SELECT to_regclass(...) 检查物化视图是否存在。不存在的视图会被跳过并记录警告日志,不会导致任务失败。

2. 刷新 SQL

-- 普通刷新(阻塞读查询)
REFRESH MATERIALIZED VIEW billiards_dws.mv_dws_finance_daily_summary_l1;

-- 并发刷新(不阻塞读查询,需要唯一索引)
REFRESH MATERIALIZED VIEW CONCURRENTLY billiards_dws.mv_dws_finance_daily_summary_l1;

是否使用 CONCURRENTLYdws.mv.refresh_concurrently 配置控制。

3. 执行结果

返回刷新的视图数量和明细:

{
  "counts": {"refreshed": 3},
  "extra": {
    "details": [
      {"view": "mv_dws_finance_daily_summary_l1", "layer": "LAST_2_DAYS"},
      {"view": "mv_dws_finance_daily_summary_l2", "layer": "LAST_1_MONTH"},
      {"view": "mv_dws_finance_daily_summary_l3", "layer": "LAST_3_MONTHS"}
    ]
  }
}

配置示例

# .env 配置
DWS_MV_ENABLED=true
DWS_MV_REFRESH_CONCURRENTLY=false
DWS_MV_LAYERS=LAST_2_DAYS,LAST_1_MONTH,LAST_3_MONTHS

# 或通过 retention 配置联动
DWS_RETENTION_ENABLED=true
DWS_RETENTION_LAYER=LAST_3_MONTHS
# 物化视图刷新会自动使用 retention 的层级配置,刷新 L1/L2/L3

与 DWS_RETENTION_CLEANUP 的配置联动

物化视图刷新任务与保留清理任务共享部分配置:

  • dws.mv.tables 为空时,回退到 dws.retention.tables 确定需要刷新的基表
  • dws.mv.table_layers 为空时,回退到 dws.retention.table_layers 确定表级层级
  • dws.retention.layer 作为默认层级的最终回退

这种设计使得两个任务可以通过统一的 retention 配置体系联动控制,也可以通过 dws.mv.* 配置独立覆盖。