# 07 工程回顾与落地盘点

> ⚠️ **已被 `08_全链路复盘_2026-10-03.md` 取代现状结论**——本文为 2026-09-28 基线快照，其中「适当性单位不一致」「通知链路全无」「guest_id 半落地」等项已修复，「闸门断链」「回放断链」仍未闭环。现状以 08 为准。
> 时间：2026-09-28 ｜ 盘点对象：`/mnt/data/live-console`（Flask 单体，线上 `live.netf.top`）
> 配套：`DESIGN.md`(v1) ｜ `DESIGN_v2_补充.md`(v2) ｜ `02_产品需求文档(PRD).md` ｜ `06_国泰君安直播间管理页面功能扒取.md`
> **方法：只认证据** —— 路由表（app.py）、表结构+行数（sqlite）、依赖清单（pip list）、运行日志（logs/access.log）、systemd 单元。结论均附证据位。

---

## 一、大逻辑确认

工程要解决的一件事：**把保利威直播能力收进自有后台，让「预告 → 直播间 → 回放」一条链自动流转，并在链上挂一道期货业合规闸门（适当性 + VIP）。**

### 1.1 主链（5 段，时序驱动）

```
运营建预告(preview, 绑直播间 + 配板块/品种/门槛)
   │
   ├─[T-30min 定时] 备份直播间名 → 注入预告标题 → 同步 PolyV /channel/update → status=live
   │
   ├─[每 60s 定时] 命中检测：读 PolyV live_status 缓存，planned ± 20min 内是否真开播
   │        ├── live  → hit_status=hit   ──► 【应通知预约用户】◄── 断链点 ①
   │        └── 超窗  → hit_status=missed（不通知）
   │
   ├─[T+30min 定时] name 回退 name_backup → 同步 PolyV → status=ended
   │
   └─[PolyV ended 回调] 拉 VOD vid → 生成 playback(继承板块/品种) → 回放中心可检索
                                              ◄── 断链点 ②（回调与编排都不存在）
```

**主链落地情况**

| 段 | 设计 | 实际 | 证据 |
|---|---|---|---|
| 预告 CRUD | `/api/previews` | ✅ 已落地 | app.py:331/342/369/388；preview 表 2 行；audit_log `preview.create` |
| T-30 注入 | injection_service.inject | ✅ 已落地（内联在 `preview_service.inject_preview`） | 写 `injected_at` + status=live + 同步 PolyV；audit_log `preview.inject` 有记录 |
| 命中检测 | hit_service.check, 每 1min | ✅ 已落地（`check_preview_hit` + `sync_preview_hits`） | 命中写 `hit_status/hit_at`；audit_log `preview.missed` ×2 |
| T+30 恢复 | injection_service.restore | ✅ 已落地（`restore_room`） | 写 `restored_at` + status=ended；audit_log `preview.restore` |
| **hit → 通知预约用户** | notification_service | ❌ **断链** | `sync_preview_hits()` 命中后只写状态+审计，无任何通知调用；subscription / notification 表 **0 行** |
| **ended → 生成回放** | playback_service + `/webhook/polyv/status` | ❌ **断链** | playback 表 **0 行**；`/webhook/polyv/status` 路由不存在；无 playback_service |

> **一句话**：主链的「注入 / 命中 / 恢复」三段是通的，「命中之后」和「结束之后」两段是空的。链子在 `hit` 和 `ended` 两处断开。

### 1.2 三条横切

| 横切 | 设计口径 | 实际 | 判定 |
|---|---|---|---|
| **闸门** | 适当性 C1<C2<C3<C4<C5、VIP 0<1<2<3，**适当性优先**；直播与回放同源；频道侧走 PolyV 自定义授权回调 | 频道侧 ✅ 182 个频道 `watch_condition=custom` / `auth_type=custom`，`/webhook/polyv/auth` 路由已实现（含验签 + 写 audience）；判定侧 ❌ `auth.check_view_permission` **0 个调用点**，回调用 mock provider 固定放行、不查房间门槛 | ⚠️ **只有壳，闸门没落下** |
| **身份** | 阶段1 游客 `guest_id`（uuid 存 localStorage）→ 阶段3 SSO 回填 `user_code` | ⚠️ user 表仅 1 行 `guest-mock-001`；前端页面**没有生成/携带 gid 的逻辑**（room.html / live.html 内无 guest_id）；`/api/auth/check` 从未被调用 | ⚠️ 半落地 |
| **事件/通知** | Redis Streams MQ + flask-socketio WS 实时推（事件：inject/started/hit/ended/recover） | ❌ 全无。`flask-socketio` / `redis` / `eventlet` **均未安装**；gunicorn 仍 `-w 2` **sync** worker；代码里 `event_bus` / `ws_gateway` 只出现在控制台看板的"待做"文案里 | ❌ 未落地 |

### 1.3 发布与运维

| 项 | 设计 | 实际 |
|---|---|---|
| 承载 | `live.netf.top` | ✅ 已上线（`/live`、`/live/{cid}`、`/console/*` 均 200） |
| 进程 | gunicorn **:8100** + **eventlet** | ⚠️ gunicorn **:8095** + **sync×2**（与 socketio 不兼容，上 WS 前必须改） |
| 定时 | APScheduler 进程内 | ✅ 已跑，60s 一次，`flock` 防多 worker 重复启动 |
| 凭证 | `.env` 600 外部化 | ✅ `POLYV_USER_ID/APP_ID/APP_SECRET/PLATFORM_PUBLIC_KEY/API_BASE`，无硬编码 |
| 版本控制 | — | ❌ 无 `.git`，回滚靠 `.bak` 文件（当前 6 个 .bak） |

---

## 二、落地盘点矩阵

### ✅ 已落地（可跑、有证据）

| 能力 | 落点 | 证据 |
|---|---|---|
| 14 张表（12 设计 + `room_tag`/`researcher_variety`） | `models.py` 406 行 | sqlite 表清单齐全 |
| 182 频道同步 + 直播状态缓存 | `/api/polyv/sync`、`refresh_live_status_cache` | polyv_channel 182 行，scheduler 60s 刷新 |
| 签名体系（MD5 secret+key升序+secret） | `polyv.polyv_sign` | `/live/v3/user/category/list` 通 |
| 频道 CRUD / 批量删 / basic-get / 设 custom 观看条件 | polyv.py 12 个函数 | 182 频道全 custom |
| 团队 / 研究员 / 品种关联 | `/api/teams`、`/api/researchers`、`/api/varieties`、`/api/futures/*` | team 8、researcher 43、researcher_variety 110 |
| 对客端直播大厅 | `/live` | access.log 143 次 |
| 对客端房间页（自绘播放器 + 聊天 + 课件 + 分级展示） | `/live/{cid}` + 3 个 `/api/play/*` | stats 921 次、docs 64 次、chat-token 55 次调用 |
| 管理后台 4 页 | `/console`、`/console/previews`、`/console/rooms`、`/console/teams` | 路由存在，模板 4 个 |
| 预告逻辑层三段 | `preview_service.py` | audit_log 有 inject/restore/hit/missed 记录 |
| 审计留痕 | `audit_log` | 23 行（room.update 14 / preview.* 6 / polyv.sync 1） |

### ⚠️ 半落地（有壳无芯 / 有芯无口）

| 能力 | 缺什么 |
|---|---|
| 观看闸门 | 判定函数写了但没接线；房间门槛字段只有 1 个房间填了（`3062764` R3/VIP1，其余 7 个可见房间为 NULL） |
| 游客身份 gid | `auth.MockAuthProvider` 会按 gid upsert user，但前端页面从不生成/传 gid |
| 回放能力 | polyv.py 已有 `list_playbacks` + `get_playback_safe_token`（能力就绪），缺编排与页面 |
| 通知 | 表结构就绪（`notification.event_type` 已按设计加了 ws 事件位），缺 service/渠道/触发点 |
| 控制台看板 | **状态卡文案已过期**：把"注入/恢复/命中检测/对客端 H5"标为"阶段2 待做"，但这些实际已完成 |

### ❌ 未落地（设计有、代码无）

| 类 | 明细 |
|---|---|
| 对客端页面 6 页 | V1 `/`（大厅首页）、V2 `/schedule`、V4 `/playback`、V5 `/preview/{id}`（含预约）、V7 `/vod/{vid}`、V8 `/go/{preview_id}` |
| 管理后台 3 页 | A4 `/console/playbacks`、A7 `/console/audiences`、A8 `/console/audit` |
| Service 层 4 个 | `subscription_service`、`notification_service`、`playback_service`（`hit_service`/`injection_service` 已内联进 `preview_service`，属命名偏离不算缺） |
| 回调 | `/webhook/polyv/status`（直播状态变更） |
| 订阅 API | `POST /api/previews/{id}/subscribe`、`/cancel` |
| MQ/WS | Redis Streams `polyv:events:*`、`event_bus`、ws-gateway、flask-socketio、eventlet worker |
| 数据字段 | `live_room.publisher`（主持人）、`live_room.polyv_anchor_id`（主播实体关联） |
| 真实鉴权 | `RealAuthProvider`（阶段3） |

---

## 三、三个真问题（不是"没做"，是"做错/断了"）

### 问题 1 · 适当性单位不一致 `R3` vs `C3` —— 接上真闸门会全员被拒

- `live_room.min_suit_level` 实际值 = **`R3`**（见 `channel_id=3062764`）
- `config.SUIT_ORDER` = `["C1","C2","C3","C4","C5"]`
- `auth.check_view_permission()` 里 `SUIT_ORDER.index(user_suit) < SUIT_ORDER.index(min_suit)` → `index("R3")` 抛 `ValueError` → 被 `except ValueError` 捕获 → **`return False, "适当性等级校验失败"`**

**后果**：闸门一旦接线，`3062764` 会对**所有人**返回拒绝，且理由显示为"校验失败"而非"等级不足"，排查时会误导。
**修法**：统一到一种口径（建议全链路用 `C1–C5`，前端展示时再映射成 `R1–R5` 文案），并给 `index()` 加防御。

### 问题 2 · 闸门三处断链

| # | 断点 | 证据 |
|---|---|---|
| 1 | `check_view_permission` **定义了但没人调** | `grep -rn` 全仓只有 `auth.py:44` 定义 + `app.py:32` import，**0 调用点** |
| 2 | `/api/auth/check` 是 mock 固定放行，且**不读房间门槛** | `get_provider().check()` 恒返回 `allow=True, C4, VIP1`；access.log **0 次调用**（前端压根没调） |
| 3 | `/webhook/polyv/auth` 只认 mock，**不查房间门槛** | 调 `get_provider().check()` 后直接 `result.allow`，房间的 `min_suit_level/min_vip_tier` 未参与判定；access.log **0 次调用** → `audience` 表 **0 行** |

**后果**：当前只要拿到频道号就能看，与 `04_RAG对查报告.md` 里点出的 P0 是同一个问题，至今**未闭环**。

### 问题 3 · 根路径给了后台

`/` → `redirect("/console")`（app.py:107-109）。设计里 V1 是**观众侧 H5 大厅**，现在观众打开 `live.netf.top` 直接掉进管理后台。

---

## 四、建议推进顺序

| 序 | 事项 | 理由 | 前置 |
|---|---|---|---|
| 1 | 统一适当性单位 + 把 `check_view_permission` 接进 V6/V7 与 auth 回调 + 落 `audience` | 合规硬约束；P0 历史遗留；成本最低（改动集中） | 无 |
| 2 | 回放链路：`/webhook/polyv/status` → `playback_service` → V4/V7 页面 | 主链第二个断点；polyv.py 能力已就绪 | 无 |
| 3 | 通知链路：先站内（`notification.channel=inapp`）跑通 `hit → 通知` 闭环 | 主链第一个断点；**先把模型/落库/触发跑通，再上 MQ/WS** | 需先确认通知渠道口径 |
| 4 | 补对客端 6 页 + 后台 3 页（A7/A8 只是把已有数据表格化，成本低） | 观众无落脚点；审计页只差展示 | 2、3 完成后 A4 才有数据 |
| 5 | 补 `publisher` / `polyv_anchor_id` 字段 + 给可见房间补种分级门槛 | 数据完整性；往期回放目前是"其他 visible 频道"顶替，非真回放 | 2 完成后往期才有真数据 |
| 6 | 上 MQ/WS 时：装 `redis`/`flask-socketio`/`eventlet` + gunicorn 换 worker + `live-mq` 容器 | 否则 WS 不通 | 3 之后 |

---

## 五、复现本盘点的命令

```bash
cd /mnt/data/live-console
# 路由
grep -n "@app.route" app.py
# 表与行数
./venv/bin/python -c "import sqlite3;c=sqlite3.connect('live_console.db');print([(t[0],c.execute('select count(*) from '+t[0]).fetchone()[0]) for t in c.execute(\"select name from sqlite_master where type='table' and name not like 'sqlite_%'\")])"
# 依赖
./venv/bin/pip list | grep -iE "flask|redis|socket|apscheduler|eventlet"
# 闸门调用点（应为 0）
grep -rn "check_view_permission" --include=*.py . | grep -v venv
# 回调真实调用次数（当前 0）
grep -c "webhook" logs/access.log
# 服务单元
systemctl --user cat web-live.service
```

---

_盘点人：拾光 ⟡ ｜ 本文只陈述证据与差异，不含方案变更。修哪几项由隽侬定序。_
