# P2-4 课程体系深度调研(第二轮)

> 日期:2026-05-04
> 触发:Neo 否决 H-1 调研结论(`P2-4-and-P2-7-research.md`):"课程的调研不对,不够深入,课程全部应该有 包厢课 麻将课 基础课 激励课 但应该分布在不同的表中"
> 范围:**重新调研课程体系**,把上游 SaaS 真实 skill、配置表、DWD 明细、DWS 派生列、ETL 代码、前端文案统一对齐
> 数据库:`test_etl_feiqiu`(只读 SELECT)
> 替代关系:**本文件取代 `P2-4-and-P2-7-research.md` 中的 P2-4 部分。P2-7 部分仍以原文件为准。**

---

## 一、Neo 提示的关键(逐字引用)

> "category_code 不是课程,是项目分类。课程的调研不对,不够深入,课程全部应该有 **包厢课 麻将课 基础课 激励课** 但应该分布在不同的表中。"

解读:

1. **课程 ≠ 场地分类(`category_code`)**:H-1 把 `cfg_area_category` 当成课程候选,这条路是错的(它在 `category_code` 列里映射的是 `BILLIARD/SNOOKER/MAHJONG/KTV` 等"项目分类",描述台桌而非课程)。
2. **课程至少 4 类**:基础课 / 包厢课 / 麻将课 / 激励课。
3. **分布在不同表中**:不是一张表的 4 行,是多张表的不同列、不同模块各自表达一部分。

---

## 二、表搜索全集(只读,2026-05-04)

### 2.1 schema 全表名搜索结果(命中 course/lesson/class/room/mahjong/incentive/bonus/skill)

| schema | table | 命中关键字 |
|--------|-------|-----------|
| dws | `cfg_skill_type` | skill |
| dws | `cfg_bonus_rules` | bonus |
| app | `v_cfg_bonus_rules` | bonus(RLS 视图) |

注意:**没有任何表名以 `cfg_course_*`、`dim_course_*`、`mahjong_*`、`incentive_*`、`lesson_*` 命名**。也就是说"课程"在 NeoZQYY 中**不是一个独立的实体**,而是依附在助教服务记录上的一个**类型属性**。

### 2.2 含课程相关字段的表(course/lesson/class_type/room_type/mahjong/bonus_type/incentive)

| 模块 | 表 | 字段 | 用途 |
|------|---|------|------|
| 配置 | `dws.cfg_skill_type` | `skill_id`, `skill_name`, `course_type_code`(BASE/BONUS/ROOM), `course_type_name` | skill_id → 课程类型映射 |
| 配置 | `dws.cfg_assistant_level_price` | `base_course_price`, `bonus_course_price` | 等级 × 课程类型定价 |
| DWS 明细 | `dws.dws_assistant_daily_detail` | `base_*`、`bonus_*`、`room_*`(共 12 列:count/seconds/hours/ledger_amount × 3 类型) | 助教日级三维拆分 |
| DWS 工资 | `dws.dws_assistant_salary_calc` | `base_hours`、`bonus_hours`、`room_hours`、`base_income`、`bonus_income`、`room_income` | 工资按 3 类计算 |
| DWS 财务 | `dws.dws_assistant_finance_analysis` | `revenue_base`、`revenue_bonus`、`revenue_room`、`room_service_count`、`room_service_hours` | 助教营收按 3 类拆分 |
| DWS 关系 | `dws.dws_member_assistant_intimacy` | `basic_session_count`、`incentive_session_count` | 会员-助教亲密度按 2 类拆分 |
| DWS 关系 | `dws.dws_member_assistant_relation_index` | `basic_session_count`、`incentive_session_count` | 关系指数按 2 类拆分 |
| DWS 区域 | `dws.dws_coach_area_hours` | `base_hours`、`bonus_hours`、`room_hours`、`*_service_count` | 助教区域工时按 3 类 |
| DWD 明细 | `dwd.dwd_assistant_service_log` | `skill_id`, `skill_name`(原始字符串) | 单笔服务原始 skill |
| ODS 上游 | `ods.assistant_service_records` | `skill_id`, `skillname`(飞球 SaaS 原始字段) | 来自飞球 API |

---

## 三、4 类课程的实际分布

> **核心结论**:NeoZQYY 中"课程"以三个不同抽象层、四个不同模块各自表达,不存在统一的课程表。

### 3.1 基础课(BASE) — 全链路一等公民

| 层 | 实例 |
|---|------|
| ODS | `assistant_service_records.skillname='基础课'`,9 095 条有效订单(2025-09-16 ~ 2026-04-23) |
| 配置 | `cfg_skill_type` 映射 `skill_id ∈ {2790683529513797(基础课), 2791903611396869(台球基础陪打)}` → `course_type_code='BASE'`(`course_type_name='基础课'`) |
| 代码 | `base_dws_task.py::CourseType.BASE` |
| DWS 列 | `dws_assistant_daily_detail.base_service_count/base_hours/base_seconds/base_ledger_amount`(已写入,2026 年 1-4 月有数据) |
| 工资 | `dws_assistant_salary_calc.base_hours / base_income`(按 `cfg_assistant_level_price.base_course_price` × 等级 计价,初/中/高/星 = 98/108/118/138 元) |
| 关系指数 | `dws_member_assistant_intimacy.basic_session_count`(注意:这里是 `basic` 不是 `base`,命名不一致) |
| 前端文案 | "基础课"(performance/coach-detail/task-list/customer-detail) |
| 别名 | "陪打"、"PD"、`assistant_pd_money` |

### 3.2 包厢课(ROOM) — 设计存在但**实际未生效**

| 层 | 实例 |
|---|------|
| ODS | `skillname='包厢课'`,**174 条有效订单**(2026-01-06 ~ 2026-04-26) |
| 配置 | `cfg_skill_type` 中"包厢课"(`skill_id=3039912271463941`)和"包厢服务"(`skill_id=2807440316432198`)**被映射为 `course_type_code='BASE'`**(注释说"包厢服务:归入基础课统计,统一按 138 元/小时计价";"包厢课:与'包厢服务'同类") |
| 代码 | `base_dws_task.py::CourseType.ROOM` 枚举存在;`get_course_type()` 中 `if code == 'ROOM': return CourseType.ROOM` 分支存在,**但永不命中**(因配置表无 ROOM 行) |
| DWS 列 | `dws_assistant_daily_detail.room_service_count/room_hours/room_seconds/room_ledger_amount` 列存在,**全 0**(2942 行均为 0) |
| 工资 | `dws_assistant_salary_calc.room_hours / room_income` 列存在,**全 0**(10 个月全 0) |
| 财务 | `dws_assistant_finance_analysis.revenue_room` SQL 中 `WHEN ... = 'ROOM'` 分支存在,**永不命中** |
| 现状 | 174 条飞球原始包厢课订单**实际被合并进 BASE**(因为 cfg_skill_type 把它们都标成 BASE) |
| 文档 | `CLAUDE.md` 写"包厢课统一 138 元/小时(`dws.salary.room_course_price`)";`assistant_finance_task.py:146` 注释写 ROOM 分类 |

**这是 H-1 漏掉的最关键发现**:**ROOM 是设计预留 + 死代码组合**,不是 H-1 说的"纯死代码可移除"。
174 条真实包厢订单存在,Neo 心目中"包厢课要独立"是合理需求,只是配置表没把它们标出来。

### 3.3 麻将课(MAHJONG) — **数据库与代码层完全空白**

| 层 | 实例 |
|---|------|
| ODS | `assistant_service_records.skillname` 中**没有"麻将课"**(只有"基础课"/"附加课"/"包厢课"/NULL 共 4 种) |
| 飞球上游 | 飞球 SaaS **没有"麻将"作为助教 skill** |
| 配置 | `cfg_skill_type` 没有 MAHJONG 行 |
| 代码 | `CourseType` 枚举**没有 MAHJONG**(只有 BASE/BONUS/ROOM) |
| DWS 列 | 没有 `mahjong_*` 列 |
| 文档 | 仅在 `cfg_area_category.category_code='MAHJONG'` 中出现(场地分类,**不是课程**) |
| 前端 | 没有"麻将课"文案 |

**结论**:"麻将课"在 NeoZQYY **完全不存在**。它是 Neo 在业务规划层提出的概念,**当前所有表、代码、配置、上游都没有对应的载体**。

可能的语义:店铺用麻将台桌时,助教提供的服务可能也叫"麻将课"。但飞球助教 skill 当前不区分这种,落到 ODS 时 `skillname` 仍是"基础课"。

### 3.4 激励课(INCENTIVE) — **实际就是 BONUS,有别名混乱**

| 层 | 实例 |
|---|------|
| ODS | `skillname='附加课'`,985 条有效订单 |
| 配置 | `cfg_skill_type` `skill_id ∈ {2790683529513798(附加课), 2807440316432197(台球超休服务)}` → `course_type_code='BONUS'`,`course_type_name='附加课'`,`description='附加课:超休/激励课,固定 190 元/小时'` |
| 代码 | `CourseType.BONUS`;`relation_index_task.py:268` `is_incentive = course_type == CourseType.BONUS` |
| DWS 列 | `bonus_service_count/bonus_hours/bonus_seconds/bonus_ledger_amount`(已写入,2 942 行中 574 行有 bonus) |
| 关系 | `incentive_session_count`(BD 手册写"附加课服务次数",但列名用 `incentive`) |
| 工资 | `bonus_income`(单价 190,`bonus_deduction_ratio` 35%~50%) |
| 前端文案 | **"激励课" / "超休" / "打赏课" 三种说法在前端都用** — `performance.ts` 和 `performance-records.ts` 中明确写 `'超休': 'incentive', '激励课': 'incentive', '打赏课': 'incentive'`(三个上游别名都映射到 `incentive` CSS 标签) |
| BD-DWD-DOC | 称为"助教激励课正价 / `assistant_cx_money`",规则 2 强制要求 `assistant_cx_money` 表示 BONUS |
| 别名总集 | "附加课"(配置/ODS) / "激励课"(前端文案/Neo 用语) / "超休"(用户口语) / "打赏课"(用户口语) / `BONUS`(代码枚举) / `cx`(SQL/费用字段) |

**结论**:"激励课"和"BONUS"和"附加课"和"超休"是**同一个东西**,只是叫法不一致。Neo 提的"激励课"不是新维度,只是更口语化的命名。

---

## 四、4 类是否真的"分布在不同表中"

回答 Neo 的核心判断:**部分正确,但分布的不是"4 类"而是"多种抽象组合"**。

| 类别 | 是否独立存在 | 在哪些表/列体现 |
|------|------------|----------------|
| 基础课(BASE) | 是,完整 | cfg_skill_type、dwd_assistant_service_log.skill_id、dws.*.base_*(daily/salary/finance/coach_area) + intimacy.basic_session_count |
| 激励课(BONUS) | 是,完整 | 同上替换为 bonus_*、intimacy.incentive_session_count |
| 包厢课(ROOM) | **半残**:DWD 有 174 条原始订单,DWS 列预留但全 0,代码 ROOM 分支永不命中 | dws.*.room_*(daily/salary/finance/coach_area)全 0;cfg_skill_type 把它们标成 BASE 而不是 ROOM |
| 麻将课(MAHJONG) | **不存在**:仅在台桌分类中有 MAHJONG 区(`cfg_area_category`),从未作为课程类型落库 | 没有任何相关列、配置、代码 |

所以"分布在不同表"只对前两类成立(基础课和激励课确实在 daily/salary/finance/intimacy/relation_index 多表多列各自展开)。
**包厢课是半成品,麻将课是空白**。

---

## 五、与 H-1 调研对照(必须修正的错误)

H-1 报告(`P2-4-and-P2-7-research.md` 第 87-115 行)说:

| H-1 结论 | 实际情况 | 修正 |
|---------|---------|------|
| "cfg_skill_type 只有 2 行" | **6 行**(基础课/附加课各 3 个 skill_id 别名都已建好) | H-1 漏掉了 SKill_id 4/5/6(2026-03-24 通过 `add_missing_cfg_skill_type.sql` 补齐的"基础课/附加课/包厢课"原始 skill) |
| "ROOM 是死代码,所有 ROOM 分支永不命中" | 命中是真的不命中(配置无 ROOM 行),但**DWD 有 174 条真实包厢订单**正在被错误合并进 BASE | 不是"死代码可移除",而是"配置缺失导致包厢数据被吞" |
| "包厢课在 NeoZQYY 不需要" | Neo 明确要求"包厢课"独立 | 需要补齐(改 cfg_skill_type 把包厢类 skill 标成 ROOM) |
| 完全没提"麻将课" | Neo 提的 4 类之一 | 需在产品层决策"是否新增麻将课作为第 4 类" |
| 没区分"激励课 / 附加课 / 超休 / 打赏课"的别名问题 | 前端 4 套文案,后端 BONUS,配置"附加课",上游 SaaS"附加课/超休服务" | 应在"统一术语"层面收口 |

H-1 漏掉的 5 个关键点:

1. **174 条飞球包厢课订单已落 ODS / DWD,被错误归入 BASE**(因 cfg_skill_type 把它们标成 BASE)
2. **room_* 列在 4 张 DWS 表(daily/salary/finance/coach_area)同时预留,全 0**
3. **`assistant_finance_task.py:146` 写了 `WHEN ... = 'ROOM'` 的 SQL 分支**(永不命中,但代码意图清晰)
4. **关系指数模块用 `basic_session_count` / `incentive_session_count` 命名,不与 `base_*` / `bonus_*` 对齐**(同概念两套命名)
5. **前端文案 4 个别名("激励课"/"超休"/"打赏课"/"附加课")混用,没有统一术语表**

---

## 六、修正后的 P2-4 选项(给 Neo 决策)

### 选项 A:统一术语 + 启用 ROOM(最小改动,贴近现状)

1. 把 `cfg_skill_type` 中"包厢课"和"包厢服务"两行的 `course_type_code` 从 `BASE` 改为 `ROOM`
2. 重跑 dws_assistant_daily_detail / salary_calc / finance_analysis / coach_area_hours,让 174 条订单进入 `room_*` 列
3. 决定 `room_course_price` 真实值(目前 CLAUDE.md 写 138 元/小时,但 cfg_assistant_level_price 没有 room_price 列 — 需要补字段或借用现有规则)
4. 统一前端"激励课"术语:performance.ts 已经做了别名映射,但其他页面要全检
5. **不引入麻将课**

适用场景:Neo 只是想看到包厢课独立,不想全面铺开。
代价:1 次 cfg_skill_type 数据修改 + 1 次回填 + 前端术语清理 + 价格字段补齐。

### 选项 B:课程类型完整化(4 类全开,新增 MAHJONG)

1. `cfg_skill_type.course_type_code` CHECK 约束扩展支持 `BASE/BONUS/ROOM/MAHJONG`
2. `dws_assistant_daily_detail` / `salary_calc` / `coach_area_hours` 增加 `mahjong_*` 列(count/seconds/hours/ledger_amount)
3. `cfg_assistant_level_price` 增加 `room_course_price`、`mahjong_course_price` 两列
4. 关系指数表增加 `room_session_count`、`mahjong_session_count`(以及把 `basic_session_count` 重命名为 `base_session_count` 对齐)
5. ETL 4 任务全部改写
6. 飞球上游目前没有"麻将课" skill 数据 — 需要店铺侧手动在飞球后台新建 skill,或在 NeoZQYY 侧用台桌区域(area_category=MAHJONG)反推
7. 前端 task-list / performance / coach-detail / customer-detail / board-coach 全部增加第 3、4 类展示

适用场景:Neo 真的要按 4 类做长期产品规划。
代价:大改动,4 张 DWS 表 schema + 5 个 ETL 任务 + 前端多页 + 飞球上游配合。

### 选项 C:三层澄清(暂不增数据列,先消除认知冲突)

1. **不动 schema**,只产出权威术语对照表归档 `docs/_overview/04c-glossary-courses.md`
2. 明确:"激励课 = 附加课 = 超休 = 打赏课 = BONUS"是同一概念
3. 明确:"包厢课"目前合并在 BASE,需要后续单独建模(选项 A)
4. 明确:"麻将课"目前不存在,需要后续业务决策(选项 B 或不做)
5. 把现有 `room_*` 列 + `CourseType.ROOM` 枚举 + `mahjong_*` 概念 全部标记为"预留,未启用"
6. 修正 BD 手册中"附加课"/"激励课"不一致的注释

适用场景:Neo 只是想确认"我提的 4 类系统当前实际分布",不一定要立刻改实现。
代价:零代码改动,只写文档。

---

## 七、给 Neo 的决策清单

| 编号 | 决策点 | 选项 | 影响范围 |
|-----|--------|------|---------|
| D1 | "包厢课"是否独立成第 3 类 | (a)合并 BASE 不变 (b)启用 ROOM(改 cfg_skill_type+回填) (c)不动 schema 只标记 | DWS 4 表回填 + 前端展示 |
| D2 | "麻将课"是否新增为第 4 类 | (a)不引入 (b)新增 MAHJONG(大改) | DWS 列新增 + 前端 + 飞球上游配合 |
| D3 | "激励课/附加课/超休/打赏课" 是否统一术语 | (a)前端统一为"激励课" (b)统一为"附加课" (c)保持现状 | 前端 5+ 页面 + BD 手册 + 后端 schema 注释 |
| D4 | `room_*` / `CourseType.ROOM` 当前状态 | (a)启用并回填 (b)删除并清理 (c)保留预留并文档化 | 4 张 DWS 表 + ETL 4 任务 + 后端服务 |
| D5 | 关系指数模块 `basic_session_count` vs DWS 通用 `base_*` 命名不一致 | (a)统一命名(改列名) (b)在文档说明同义 (c)保持现状 | 关系表迁移 + 后端 schema |
| D6 | `cfg_assistant_level_price.room_course_price` 缺失 | (a)增加列 (b)用单一常量 138 写死 (c)等 D1 决策后再处理 | cfg_assistant_level_price 表结构 |

---

## 八、命名一致性问题(派生发现)

| 概念 | DWS 主流命名 | 关系指数命名 | 前端 CSS | 后端字段 | 上游 |
|-----|------------|-------------|---------|---------|------|
| 基础课 | `base_*` | `basic_session_count` | `course-tag--base` | `assistant_pd_money`(PD) | "基础课"/"台球基础陪打" |
| 激励课 | `bonus_*` | `incentive_session_count` | `course-tag--incentive` | `assistant_cx_money`(CX) | "附加课"/"台球超休服务" |
| 包厢课 | `room_*`(全 0) | 无 | 无 | 无 | "包厢课"/"包厢服务" |
| 麻将课 | 无 | 无 | 无 | 无 | 无 |

5 套命名混用是高熵问题,长期容易踩坑。建议在 D3/D5 决策时一并收口。

---

## 九、实际 ODS / DWD 课程数据样本(2026-05-04 测试库,脱敏)

ODS 上游 `assistant_service_records` skillname 实际分布(有效订单,`is_trash=0 AND is_delete=0`):

| skillname | 订单数 | 首次日期 | 最近日期 |
|-----------|--------|----------|----------|
| 基础课 | 9 095 | 2025-09-16 | 2026-04-23 |
| (NULL) | 7 382 | 2025-07-21 | 2025-11-30 |
| 附加课 | 985 | 2025-09-18 | 2026-04-18 |
| 包厢课 | 174 | 2026-01-06 | 2026-04-26 |

注意:NULL 7 382 条主要在 2025-07~11 期间,2025-12 起 skillname 全部填充。说明飞球 SaaS 在 2025-12 之后才补全 skillname 字段 — 这是另一个数据质量问题。

cfg_skill_type 全量(6 行):

| id | skill_id | skill_name | course_type_code | description |
|---|----------|-----------|------------------|-------------|
| 1 | 2791903611396869 | 台球基础陪打 | BASE | 基础课:陪打服务,按助教等级计价 |
| 2 | 2807440316432197 | 台球超休服务 | BONUS | 附加课:超休/激励课,固定 190 元/小时 |
| 3 | 2807440316432198 | 包厢服务 | BASE | 包厢服务:归入基础课统计,统一按 138 元/小时计价 |
| 4 | 2790683529513797 | 基础课 | BASE | 基础课:飞球系统原始课程类型,与"台球基础陪打"同类 |
| 5 | 2790683529513798 | 附加课 | BONUS | 附加课:飞球系统原始课程类型,与"台球超休服务"同类 |
| 6 | 3039912271463941 | 包厢课 | BASE | 包厢课:飞球系统原始课程类型,与"包厢服务"同类 |

**注意**:第 3、6 行注释里写"包厢服务/包厢课"但 `course_type_code` 是 `BASE` — 这是**配置数据有意为之的合并**,不是 bug。
要让 ROOM 列生效,需要把它们改成 `ROOM` + 给 `cfg_assistant_level_price` 加 `room_course_price` 列(或使用现有的 BASE 价 + 一个 ROOM 专属规则)。

DWS `dws_assistant_daily_detail` 整体统计(2 942 行):

| 指标 | base | bonus | room |
|-----|------|-------|------|
| 非零行数 | 2 833 | 574 | **0** |
| 累计服务次数 | 5 237 | 645 | **0** |
| 累计金额 | 1 546 983.80 | 301 910.00 | **0.00** |

DWS `dws_assistant_salary_calc` 月度统计(10 个月):**room_hours / room_income 全部为 0.00**。

DWS `dws_member_assistant_intimacy` 整体统计(219 行):

| 指标 | 数值 |
|------|------|
| 总会话 | 698 |
| basic_session_count | 698 |
| incentive_session_count | **0** |

注意:亲密度模块的 incentive_session_count 也全 0。这与 daily_detail 的 bonus(574 行非零)不一致,值得在选项 A 决策后再深查 `relation_index_task.py` 的归类口径。

---

## 十、总结(给主流程)

1. **课程实际分布**:不是 H-1 说的"BASE+BONUS 两类",也不是 Neo 提的"4 类独立分布",而是 **"BASE + BONUS 完整,ROOM 半残空表,MAHJONG 空白"**。
2. **5 处分布载体**:`cfg_skill_type`(配置)、`cfg_assistant_level_price`(定价)、`dws_assistant_daily_detail/salary_calc/finance_analysis/coach_area_hours`(派生汇总)、`dws_member_assistant_intimacy/relation_index`(关系)、`dwd_assistant_service_log` + `ods.assistant_service_records`(原始)。
3. **关键 H-1 错误**:漏报 cfg_skill_type 实际 6 行(不是 2 行)、漏报 174 条真实包厢课订单被错误合并进 BASE、未提"激励课"4 套别名混用、未识别关系指数模块的命名错位。
4. **不存在统一课程表**:NeoZQYY 用"skill_id → course_type_code"映射 + DWS 派生列展开的方式表达课程,没有 `dim_course` / `cfg_course` 这种独立维度。
5. **决策清单 6 项**:D1(包厢课独立) / D2(麻将课新增) / D3(术语统一) / D4(ROOM 处置) / D5(关系指数命名) / D6(包厢定价字段)。
