Files
Neo-ZQYY/docs/_overview/04c-feedback/P2-4-and-P2-7-research.md
Neo 509cf43284 chore(docs): Wave 0 调研产出 + P0/P1/P2 反馈调研
建立项目级标杆文档 docs/_overview/ 作为产品全景索引,
解决"PRD 零碎、文档膨胀、跨子系统调研无入口"的问题。

主要内容:
- 00-index 总索引 + 维护协议 + 与 CLAUDE.md 关系
- 01-product-overview 产品全景脑图(6 角色 / 6 子系统 / 数据流 /
  7 业务概念 / 8+1 AI 矩阵 / 22 术语)
- 02a-miniprogram-page-matrix 小程序 21 页业务指纹
- 02b-adminweb-page-matrix admin-web 19 路由业务指纹
- 03-test-spec 测试规范 (L1-L5 分层 + 走查模板 + 75-95 case 估算)
- 04-doc-conflicts 39 条冲突索引(P0×8 / P1×13 / P2×13 + 5 子项)
- 04a/b/c-conflicts-*-detail 业务故事卡(7 字段:关联/逻辑/影响/选项/判定)
- 05-orphan-pages-cleanup admin-web 6 孤儿页面处置(1 归档 + 4 保留)
- WAVES-MASTER-PLAN.md 全 Wave 主计划(0-5,共 22-32 工作日)
- WAVE-1-KICKOFF.md Wave 1 实施 kickoff
- GLOBAL-DECISION-DASHBOARD.md 全局决策仪表板

反馈调研产物:
- 04a-feedback/ P0 两轮反馈(8+8 项决策 + D-1/2/3 + F-1/2 子代理产出)
- 04b-feedback/ P1 两轮反馈(13+1+5 项 + E-1/2/3/4 + G-1/2 子代理产出)
- 04c-feedback/ P2 反馈(13 项 + 5 子项 + H-1/2/3 子代理产出)
- NEO-DECISIONS-LOG 累积决策记录

关键追加发现 8 处 D Bug(原蓝本 0):
- P0-3 看板沙箱接入(Wave 1 W1-T1)
- P0-5 致命 1 (4 处 fdw_etl 残留, 已修 commit 17f045a)
- P0-5 致命 2 (JWT aud 缺失, 已修 commit 17f045a)
- P0-6 clearAllTasks 守卫 (Wave 3)
- P0-8 DBViewer 黑名单漏 (已修 commit 17f045a)
- P1-3 task-detail 跳转传 task_id 而非 customer_id
- P2-7 board-finance 隐式 null
- 2 个独立 Bug (page_context.created_at + ClueCategory 字典)

参考: docs/_overview/00-index.md
2026-05-04 07:38:28 +08:00

426 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# P2-4 课程体系 + P2-7 看板切换限制 调研
> 日期:2026-05-04
> 触发:Neo 在 04c 反馈中提出
> 调研环境:测试库 `test_etl_feiqiu`(只 SELECT)+ ETL/后端/小程序源码
> 子代理:无(主流程)
---
## 一、P2-4 课程体系实证
### 1.1 数据库层(SQL 结果)
#### A. cfg_skill_type — 课程类型主映射(skill_id → BASE/BONUS/ROOM)
实测共 6 行(`SELECT * FROM dws.cfg_skill_type ORDER BY skill_type_id`):
| skill_type_id | skill_id | skill_name | course_type_code | course_type_name | description |
|---|---|---|---|---|---|
| 1 | 2791903611396869 | 台球基础陪打 | **BASE** | 基础课 | 按助教等级计价 |
| 2 | 2807440316432197 | 台球超休服务 | **BONUS** | 附加课 | 固定 190 元/小时 |
| 3 | 2807440316432198 | 包厢服务 | **BASE** | 基础课 | 包厢服务:**归入基础课统计**,统一 138 元/小时 |
| 4 | 2790683529513797 | 基础课 | **BASE** | 基础课 | 飞球系统原始课程类型 |
| 5 | 2790683529513798 | 附加课 | **BONUS** | 附加课 | 飞球系统原始课程类型 |
| 6 | 3039912271463941 | 包厢课 | **BASE** | 基础课 | 飞球系统原始课程类型(2026-03-24 补录) |
> **关键事实**:数据库里**只有 2 个课程类型代码**:`BASE` / `BONUS`,**没有 `ROOM`**。
> "包厢服务/包厢课"两个 skill 都被标记为 `BASE`。
> 这与 BD 手册 `BD_manual_cfg_skill_type.md`(写有 ROOM 枚举)不一致 — **数据库种子数据是真相**。
#### B. cfg_assistant_level_price — 助教等级单价(只区分 BASE/BONUS,无 ROOM)
实测 5 行:
| level_code | level_name | base_course_price | bonus_course_price |
|---|---|---|---|
| 10 | 初级 | 98.00 | 190.00 |
| 20 | 中级 | 108.00 | 190.00 |
| 30 | 高级 | 118.00 | 190.00 |
| 40 | 星级 | 138.00 | 190.00 |
| 8 | 助教管理 | 98.00 | 190.00 |
> 表本身**只有 base/bonus 两列**,包厢课没有独立列,全靠代码层硬编码 138 元(`dws.salary.room_course_price`)。
#### C. dws_assistant_daily_detail / monthly_summary — 服务计数把"包厢"独立成第 3 类
实测字段(`information_schema.columns`):
```text
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
total_ledger_amount / base_ledger_amount / bonus_ledger_amount / room_ledger_amount
```
> **结论**:配置层只有 2 类(BASE/BONUS),但 DWS 汇总层把 `room_*` 单独抽出 → 是**派生第 3 个统计维度**,不是配置数据维度。
#### D. dws_assistant_order_contribution — **没有 course_type 字段**
字段总览(14 列):`contribution_id, site_id, tenant_id, assistant_id, assistant_nickname, stat_date, order_gross_revenue, order_net_revenue, time_weighted_revenue, time_weighted_net_revenue, order_count, total_service_seconds, created_at, updated_at`
> **没有按课程类型拆分**。该表是助教订单营收聚合,与课程分类无关。
#### E. cfg_area_category / dws_assistant_project_tag — **完全独立的"项目分类"体系**
```text
cfg_area_category 6 行: BILLIARD/SNOOKER/MAHJONG/KTV/SPECIAL/OTHER
dws_assistant_project_tag 实测 4 大项目: BILLIARD(158)/KTV(120)/MAHJONG(112)/SNOOKER(106)
```
> 这是**与课程类型平行的另一个分类体系**:从台桌名映射到台球/斯诺克/麻将/K歌/补时长/其他。
> 字段名 `category_code`,**不复用 course_type 字段**。
#### F. 全库扫描:**没有任何"二级嵌套课程表"**
```sql
SELECT table_schema, table_name FROM information_schema.tables
WHERE table_name ILIKE '%course%';
-- 结果: 0 行
```
course_type 概念只通过 `cfg_skill_type.course_type_code` 这一个字段存在,**不存在父子层级、不存在 dim_course、不存在 cfg_course_*`**。
---
### 1.2 后端层映射
#### A. ETL `apps/etl/connectors/feiqiu/tasks/dws/base_dws_task.py`
```python
class CourseType(Enum):
BASE = "BASE"
BONUS = "BONUS"
ROOM = "ROOM" # 包厢课 — 代码层定义,但数据库没有 ROOM 行,实际走默认 BASE 分支
def get_course_type(self, skill_id: int) -> CourseType:
skill_config = config.skill_types.get(skill_id)
if skill_config:
code = skill_config.get('course_type_code', 'BASE')
if code == 'BONUS': return CourseType.BONUS
if code == 'ROOM': return CourseType.ROOM # 永不命中
return CourseType.BASE
return CourseType.BASE
```
> **关键**:`CourseType.ROOM` 是代码层枚举,但 cfg_skill_type 里没有 `course_type_code='ROOM'` 的行。
> 所有"包厢"相关的 skill 实际返回 `CourseType.BASE`。
#### B. ETL `apps/etl/connectors/feiqiu/tasks/dws/assistant_daily_task.py:313-321`
```python
course_type = self.get_course_type(skill_id) if skill_id else CourseType.BASE
is_base = course_type == CourseType.BASE
is_bonus = course_type == CourseType.BONUS
is_room = course_type == CourseType.ROOM # 永远 False
```
> 但是 daily/monthly 表却有 `base/bonus/room` 三列。**这说明 room_* 三列实测一直是 0**(或被其他逻辑填充)。
> 需进一步用 SQL 验证(本次未跑该样本数据 SELECT,留作下一步)。
> **暂判定**:`room_*` 三列在配置正确的情况下应为 0,实际"包厢服务"全部累加进 `base_*` 三列。
#### C. ETL `apps/etl/connectors/feiqiu/tasks/dws/assistant_salary_task.py:31-36`
工资计算公式:
```python
# 基础课收入 = base_hours × (base_price - base_deduction)
# 附加课收入 = bonus_hours × bonus_price × (1 - bonus_deduction_ratio)
# 包厢课收入 = room_hours × (room_course_price - base_deduction) # room_course_price=138 配置
total_course_income = base_income + bonus_income + room_income
```
> 工资公式**显式按 3 类计算**。但因 `room_hours` 在数据流中始终为 0,实际只有 base+bonus 两路。
> 这是**代码层为未来留接口**,但当前数据流上**等价于两类**。
#### D. ETL `coach_area_hours_task.py:36`
```python
"包厢课": "room",
```
唯一一处把"包厢课" skill_name 显式映射到 `room` 标签的地方(用于 area_hours 统计)。
#### E. 后端 `apps/backend/app/routers/xcx_*` 没有发现 `course_type` 直接字段返回
- `xcx_board.py` 中 BOARD-1 助教看板返回字段不包含课程类型(返回 perf/salary/sv/task 四维度数据)
- `apps/miniprogram/miniprogram/services/api.ts:221` 有 `courseType: string`,这是后端 PERF-2 接口返回(performance-records / coach-service-records 用的同一接口形式)
> **真实 API 响应里 courseType 直接是中文字符串**(基础课/包厢课/激励课),**不是枚举代码**。
---
### 1.3 前端层使用
#### 三个页面的课程标签映射(完全一致)
`performance.ts:17-23` / `performance-records.ts:18-24` / `coach-service-records.ts:20-26`,COURSE_TAG_MAP 定义:
```typescript
const COURSE_TAG_MAP: Record<string, string> = {
'陪打': 'basic', '基础课': 'basic',
'包厢': 'room', '包厢课': 'room',
'超休': 'incentive', '激励课': 'incentive', '打赏课': 'incentive',
}
function courseTagClass(courseType: string): string {
return COURSE_TAG_MAP[courseType] || 'basic'
}
```
WXSS:三个页面都定义 `.course-tag--basic` / `.course-tag--room` / `.course-tag--incentive` 三个 CSS 类(performance.wxss:699-712 等)。
> **前端 3 类:basic / room / incentive**,与后端 ETL 的 BASE / BONUS / ROOM 一一对应:
| 后端 CourseType | 中文(API courseType) | 前端 CSS class |
|---|---|---|
| BASE | 陪打 / 基础课 | basic |
| ROOM(代码层未命中) | 包厢 / 包厢课 | room |
| BONUS | 超休 / 激励课 / 打赏课 | incentive |
#### 不一致点:service-record-card 组件的 typeClass
`service-record-card.ts:13-16`:
```typescript
/** 课程标签文字,如 "基础课" "包厢课" "打赏课" */
courseLabel: { type: String, value: '' },
/** 课程样式 class 后缀:basic / vip / tip / recharge */
typeClass: { type: String, value: 'basic' },
```
> **此组件用 `vip/tip/recharge`,而非 `room/incentive`**。被 task-detail / customer-service-records 引用。
> 这是**第三套 CSS 命名(并非配置/统计维度差异),只是组件局部 class)**,会和上面三页的 `room/incentive` 形成视觉割裂。
#### board-finance.wxml:101 还有:
```xml
{ value: 'vip', text: '台球包厢' }
```
> 这里 vip 是**区域筛选枚举**(area filter),不是课程标签 — 与 service-record-card 的 vip 含义不同,容易混淆。
---
### 1.4 结论:两套 / 两级 / 同源
**答案:A. 两套独立体系 + B. 两级隐含口径,但 NOT 严格父子嵌套。**
具体拆分:
| 体系 | 来源 | 字段 | 取值 | 用途 |
|---|---|---|---|---|
| **课程类型(course_type)** | `cfg_skill_type` | `course_type_code` | BASE / BONUS(数据库实际只有这 2 个) | 助教薪资 / 服务次数 / 工时统计 |
| **项目分类(category_code)** | `cfg_area_category` | `category_code` | BILLIARD / SNOOKER / MAHJONG / KTV / SPECIAL / OTHER | 区域偏好 / 项目标签 / 客户分群 / 财务分区 |
**两个体系完全独立**,通过 `dwd_assistant_service_log.skill_id` 的不同关联路径分别落到不同 DWS 表:
- `skill_id` → `cfg_skill_type` → `course_type_code` → 服务次数三分类
- `dim_table.area_name` → `cfg_area_category` → `category_code` → 项目标签
**额外的"派生第三类":**
- 在 `dws_assistant_daily_detail` / `dws_assistant_monthly_summary` 中,**包厢被从 BASE 中拆出来**,形成 `base_* / bonus_* / room_*` 三列。
- 但**配置数据(cfg_skill_type)里没有 ROOM 这个 course_type_code**,所以代码层 `is_room = course_type == CourseType.ROOM` **永远为 False**,实际 room_* 三列**一直是 0 或失效**。
- 这是**代码层到配置层的脱节**,需要 Neo 决定:
- **方案甲**:补一行 `cfg_skill_type` 让"包厢服务/包厢课"的 course_type_code 改为 `ROOM` → 启用三分类
- **方案乙**:接受现状,**前端把 room/包厢标签合并到 basic**,删除 `course-tag--room` CSS,workspaceWidth 仍按 138 元/小时计费
- **方案丙**:改前端文案,让"包厢"在 UI 上显示为基础课分支(让用户感知两类即可)
**"看似两级"的错觉来源**:
- 工资计算公式确实有 3 个变量:`base_income / bonus_income / room_income`,但 room_income 实际为 0(因 `room_hours` 为 0)
- 前端 3 个 CSS class:basic/room/incentive 配合 3 种颜色,看起来像"两级"
- BD 手册 `BD_manual_cfg_skill_type.md` 写了 ROOM 枚举,但**与生产数据冲突**
---
### 1.5 修正后的 P2-4 推荐选项
> 原 P2-4 推荐方案(假设)调整方向:
#### 推荐:方案 B(对齐当前生产现状)
1. **数据层不动**:`cfg_skill_type` 保持 BASE/BONUS 两类(尊重现状)
2. **代码层清理**:
- 移除 `CourseType.ROOM` 枚举(或注释为 deprecated)
- 删除 `room_course_price` 配置,所有包厢服务统一走 base 价格(`cfg_assistant_level_price.base_course_price`)
- 但**注意**:目前包厢统一 138 元 = 星级 base 价,初级/中级助教做包厢按各自 base 价 → **会改变工资**,**需求需 Neo 决策**
3. **DWS 表瘦身**:
- `dws_assistant_daily_detail` / `monthly_summary` 删除 `room_*` 三列(或保留为兼容字段一直填 0)
4. **前端三页统一**:删除 `course-tag--room` CSS 与 COURSE_TAG_MAP 中的"包厢/包厢课" key,统一映射到 basic
5. **service-record-card 命名清理**:把 `vip/tip` 重命名为 `basic/incentive`,与三页一致
6. **BD 手册修正**:`BD_manual_cfg_skill_type.md` 删除 ROOM 枚举说明
#### 备选:方案 A(启用 ROOM 三分类,贴合飞球原始数据)
1. **数据层补 cfg_skill_type 行**:把 skill_id=2807440316432198(包厢服务)和 3039912271463941(包厢课)改为 `ROOM`
2. **配置层补 cfg_assistant_level_price**:增加 `room_course_price` 字段(可选,目前用代码配置)
3. **代码层不动**(已有 ROOM 分支)
4. **前端不动**(已有 room CSS)
5. **影响**:
- 历史 DWS 数据需要回填(2026-03-24 之前所有"包厢"被错误标 BASE)
- 工资重算口径:包厢按 138 元统一 vs 各等级 base 价的差异
#### 我建议:**方案 B**(不动数据,清理代码与前端冗余),原因:
- 当前生产数据稳定且符合用户感知("包厢就是基础课的一种")
- 改 cfg_skill_type 会触发 ETL 全量回算,风险大于收益
- 命名一致性问题(service-record-card vip/tip)是更迫切的真问题
---
## 二、P2-7 看板切换限制全调研
### 2.1 board-coach 全部组合矩阵
#### TIME_OPTIONS(6 项) × SORT_OPTIONS(6 项)= 36 个组合
| time \ sort | perf_desc | perf_asc | salary_desc | salary_asc | sv_desc | task_desc |
|---|---|---|---|---|---|---|
| month | OK | OK | OK | OK | OK | OK |
| quarter | OK | OK | OK | OK | OK | OK |
| last_month | OK | OK | OK | OK | OK | OK |
| last_3m | OK | OK | OK | OK | OK | OK |
| last_quarter | OK | OK | OK | OK | OK | OK |
| **last_6m** | OK | OK | OK | OK | **400 报错** | OK |
**唯一非法组合**:`time=last_6m + sort=sv_desc` → 后端 `board_service.py:258-262` 抛 `HTTPException(400, "最近6个月不支持客源储值排序")`。
#### SKILL_OPTIONS(5 项)× 上述 6×6:无任何额外组合限制
#### 现状(双向都没拦截)
- **前端**:TIME_OPTIONS 第 6 项的 `text` 为 `"最近6个月(不含本月,不支持客源储值最高)"`,**仅文字提示**,filter-dropdown 没有任何 disabled 状态
- **后端**:仅服务端 if 校验 → 返回 HTTP 400
- **用户体验路径**:用户先选 sv_desc(成功),再切到 last_6m(切换瞬间触发 loadData → 后端报错 → 前端 toast/console.error)
### 2.2 board-finance 类似问题(扫描结果)
#### TIME_OPTIONS:8 项
`month / lastMonth / week / lastWeek / quarter3 / quarter / lastQuarter / half6`
#### AreaFilterEnum:9 项
`all / hall / hallA / hallB / hallC / vip / snooker / mahjong / ktv`
#### **隐式组合限制(后端 `board_service.py:707-760`)**
`area ≠ all` 时,**后端直接把 3 个板块返回 null**:
| 板块 | area=all | area≠all |
|---|---|---|
| overview(经营一览) | 完整 | 完整 |
| recharge(预收资产) | 完整 | **null** |
| revenue(应计收入) | 完整 | 完整(部分子项不同) |
| cashflow(现金流入) | 完整 | **null** |
| expense(现金流出) | 完整 | **null** |
| coach_analysis(助教分析) | 完整 | 完整 |
> **前端没有任何提示**,也没有把 area 选项灰掉。用户切到"台球包厢"等区域 → 页面 3 个板块凭空消失,用户无法理解为什么。
#### **额外限制:`isCurrentMonthFilter` 函数(line 16-18)**
```typescript
function isCurrentMonthFilter(selectedTime: string): boolean {
return selectedTime === 'month' && new Date().getDate() <= 5
}
```
> 月初 5 号前选"本月"会有特殊提示,但不阻塞操作。属于隐性约束。
### 2.3 board-customer 类似问题(扫描结果)
- DIMENSION_OPTIONS:8 项(recall/potential/balance/recharge/recent/spend60/freq60/loyal)
- PROJECT_OPTIONS:5 项(ALL/BILLIARD/SNOOKER/MAHJONG/KTV)
- **未扫到任何组合限制**(代码 grep 无 disabled / 不支持 / HTTPException 命中)
- 后端 `board_service.py:584-640` 也只有 `if not query_fn_name: raise 400`(参数本身非法)
> board-customer 暂无组合限制问题。
### 2.4 修正后的 P2-7 实施清单
#### 修正核心:Neo 要求"双向禁止/变灰"
> 原选项 B(只在 sv_desc 选中时禁用 last_6m)需要扩展为:
> - 选 last_6m → sv_desc 选项变灰禁选
> - 选 sv_desc → last_6m 选项变灰禁选
#### 改动点 1:board-coach 双向禁用(P2-7 主要诉求)
**文件**:`apps/miniprogram/miniprogram/pages/board-coach/board-coach.ts` + `.wxml`
**ts 改动**(SORT_OPTIONS / TIME_OPTIONS 加 `disabled` 字段,onSortChange / onTimeChange 联动重算):
```typescript
const SORT_OPTIONS = [
{ value: 'perf_desc', text: '定档业绩最高' },
{ value: 'perf_asc', text: '定档业绩最低' },
{ value: 'salary_desc', text: '工资最高' },
{ value: 'salary_asc', text: '工资最低' },
{ value: 'sv_desc', text: '客源储值最高', disabledWhen: ['last_6m'] }, // 新增
{ value: 'task_desc', text: '任务完成最多' },
]
const TIME_OPTIONS = [
...
{ value: 'last_6m', text: '最近6个月(不含本月)', disabledWhen: ['sv_desc'] }, // 提示文字简化
]
// 联动:onSortChange / onTimeChange 触发后,recompute disabled 标记并 setData
// 如果当前组合已经非法(用户先选 sv,再点 last_6m),应阻止切换并 toast
```
**wxml 改动**:filter-dropdown 组件需支持 `disabled` 数组 prop,被禁选项渲染为灰色不可点。
**filter-dropdown 组件改动**:增加 `disabledValues: string[]` prop,内部渲染时给禁用项加 `option--disabled` 样式并阻止 tap。
#### 改动点 2:board-finance 隐式 null 板块的可见性提示(扫描发现)
**问题**:area≠all 时,recharge/cashflow/expense 三个板块凭空消失,无任何提示。
**改动方案两选一**:
- **方案 a(强提示)**:在 board-finance.wxml 每个板块外层加 `wx:if="{{recharge}}"`,并在板块位置渲染占位 `view`:`"该指标暂不支持按区域拆分,请切换到全部区域查看"`
- **方案 b(过滤源头)**:在 area 选项 dropdown 上,把"非 all"选项加副标题"(部分指标仅全部区域可见)"
> 我建议方案 a。零 UI 状态变化(三板块直接变成提示卡片),用户不会困惑。
#### 改动点 3:service-record-card 命名一致性(P2-4 调研副产物)
**改动文件**:
- `apps/miniprogram/miniprogram/components/service-record-card/service-record-card.ts` 注释
- `apps/miniprogram/miniprogram/components/service-record-card/service-record-card.wxss` 类名
- 引用方:`apps/miniprogram/miniprogram/pages/customer-service-records/`、`apps/miniprogram/miniprogram/pages/task-detail/`
把 `vip/tip` 重命名为 `room/incentive`,与三个 -records 页面统一。
#### 改动点 4:文档对齐
- `BD_manual_cfg_skill_type.md`:删除 ROOM 枚举说明(或加注"仅代码层保留")
- `04c-conflicts-P2-detail.md`:补充本调研结论
---
## 三、给 Neo 的决策清单
### P2-4 课程体系
1. [ ] **方案选择**:A(启用 ROOM 三分类,改 cfg_skill_type) / **B(代码清理,合并到 BASE)** / 维持现状不动
2. [ ] **service-record-card 命名**:是否同步把 vip/tip 重命名为 room/incentive
3. [ ] **room_course_price 138 元配置去留**:工资计算时,星级以下助教做包厢应按"星级 base 价(138)" 还是"自身等级 base 价"?(决策依据是真实业务)
### P2-7 看板切换限制(全看板共发现 2 个组合限制问题)
1. [ ] **board-coach 双向禁用**:确认按"改动点 1"方案实施(filter-dropdown 加 disabledValues prop + 联动 ts)
2. [ ] **board-finance area≠all 三板块消失问题**:
- 是否同样修复?(方案 a 占位提示 / 方案 b 选项副标题)
- 还是接受现状(area≠all 是高级用法,暂不优化)
3. [ ] **board-customer**:本次未发现组合限制问题,无需修复
4. [ ] **后端 422 校验补强**:目前 board-coach 是 HTTP 400(detail 中文),是否升级为 422 + 标准化错误码?
5. [ ] **可访问性兜底**:如果用户用直链/分享 URL 进入非法组合页面,前端是否需在 onLoad 兜底重置一个合法默认值?
### 调研副产物(本轮发现的其他问题)
1. [ ] cfg_skill_type 数据库实际无 ROOM 行,但 BD 手册写了 ROOM 枚举 → 文档过期
2. [ ] dws_assistant_daily_detail / monthly_summary 的 room_* 三列实际可能恒为 0 → 需 SQL 验证
3. [ ] dws_assistant_order_contribution 表完全没有 course_type 字段,与文档预期不符 → 与 P2-4 关联