104 lines
12 KiB
Markdown
104 lines
12 KiB
Markdown
# MCN 数据面接入 · 阶段二第三批 —— **接线完成**(第 34 棒 · 执行棒 · 2026-09-25)
|
||||
|
|
|
|||
|
|
> **工作区**:`E:/ProgramData/AIProject/aliyun-dsh-server` **会话**:`p34-exec-plugin-dataline`(`d25f7943-e143-4f7f-a595-8b090fed6e3f`)
|
|||
|
|
> **唯一执行单**:`D:/github/dsh_shenxian/dsh-server-docs/05-交接单/插件投放与分库线-04MCN数据面接入改造-阶段二第三批.md`
|
|||
|
|
> **上游拍板**:用户 2026-09-25 20:38 —— 「**修改完成后 一起上服务器**」⇒ **B 案**(接线全做完再一次性动生产)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 一 结论(一句话)
|
|||
|
|
|
|||
|
|
**S3 剩 1 处 + S4 + S5 的插件侧接线已全部落地**:`account_videos` / `hot_accounts` + `hot_account_ext` / `account_video_source` 三张表在 `platform` 档下**处处有改写面**,`local` 档行为与原 SQL 逐字一致(零回归)。静态复扫确认**全包已无「未被 `local()` 回调包住的本地 SQL」**。单测 **62/62**、红线 **23(零新增)**、`src/**` 零改动。
|
|||
|
|
|
|||
|
|
⛔ **本棒未动生产**(不出包 / 不上池 / 不加列迁移 / 不回填)—— 按用户拍板属 B 案最后一步,本轮**未做**。
|
|||
|
|
⛔ **未切档**:`local_id` 列在真机库**仍未就位**(切档前置)⇒ A5 未达成、A2 未达成(需实例重启窗口)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 二 改动面(6 件 · 全部在插件包内)
|
|||
|
|
|
|||
|
|
| # | 文件 | 字节 | 行尾 | sha256(前 16) | 本棒改了什么 |
|
|||
|
|
|---|---|---|---|---|---|
|
|||
|
|
| 1 | `lib/host/mcn/store.js` | 111,028 | LF | `263c245627bfadce` | 新增 `patchHotAccountSplitByName()`(**拆表 PATCH**:主表列/扩展列各一半,无事务 ⇒`partial`+`compensate`)+ `patchHotAccountSplitByLocalId()`(经 D16 映射)|
|
|||
|
|
| 2 | `lib/host/mcn/index.js` | 214,801 | LF | `e0178cb16fe57f8c` | **22 处落点**:新增 6 个插件内共用原语(`nameMapOf`/`hotAccountOf`/`patchHotAccountOf`/`videoOf`/`awemeIdOfVideo`/`accountNameOf`)+ 逐处接档(见 §四) |
|
|||
|
|
| 3 | `lib/host/mcn/data.js` | 16,872 | LF | `e3f6de684b59d980` | (第 34 棒前段)`queryAccountsPlatform` 等;本棒为复扫复核对象 |
|
|||
|
|
| 4 | `lib/host/mcn/imports.js` | 57,547 | CRLF | `0340c97fe0713e45` | (第 34 棒前段)`findHotAccountByDelFlagEq` / `loadVideoWithAccount` + 36 处接档 |
|
|||
|
|
| 5 | `lib/host/mcn/workshop.js` | 31,495 | LF | `4fc7822d5c5e17ba` | (第 34 棒前段)`accountFilterOf` / `softDeleteHotAccountPlatform` / `batchDeleteCascade` 平台面 |
|
|||
|
|
| 6 | `tests/mcn-store.test.mjs` | 57,661 | LF | `e988cd97d79205ef` | **新增 4 例**(52–55):拆表 PATCH 表路由 / 两半失败 `partial` / 映射缺席 `mapping-missing` / `local` 档零请求 |
|
|||
|
|
|
|||
|
|
**未改**:`dsh.data.yaml`(指纹 `d6627f1c647a8c27` / 8,351 B —— 与第 33 棒逐字相同)· `tools/migrate-local-to-platform.mjs`(`060bbf855f624fdd` / 12,886 B)· `lib/client.js`(**零改动** ⇒ A7 免跑)· `D:/github/dsh_shenxian/src/**`(**零改动**)。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 三 判据表(A1–A9 · 命令 + 输出原文 + 退出码)
|
|||
|
|
|
|||
|
|
| # | 判据 | 命令(原文) | 输出原文 | 退出码 | 结论 |
|
|||
|
|
|---|---|---|---|---|---|
|
|||
|
|
| **A1** | 存量迁移一致性 | ——(属第 32 棒读数,本棒**未重跑**) | `account_videos` **362/362 `match=true`** · `hot_accounts` **5/5 `match=true`** · `account_video_source` **21/39 上台 · `match=false`**(余 12 行卡平台**单行** 64 KiB) | — | **部分达成**(12 行卡 64 KiB ⇒ 由 (a) 行内压缩处方闭合,**代码未落**) |
|
|||
|
|
| **A2** | 三表读写 E2E(**实例内**) | —— | 需 **47 实例重启**(待拍板 ㈠) | — | ⛔ **未达成**(属窗口项) |
|
|||
|
|
| **A3** | 结构改写面处置完毕 | ① `grep -rniE "\bJOIN\b" lib \| grep -v "\.join("` ② `grep -rn "LIMIT ? OFFSET ?" lib` ③ `grep -rnE "ROW_NUMBER\|OVER\s*\(" lib` | ① 命中行**全部**落在三类:`local()` 回调内 / 本地专属分支 / **第四批主表 `rewrite_log`(具名豁免)**;平台档路径**零命中** ② `data.js:183` `data.js:239` `index.js:1796` `workshop.js:335` —— 前两条在本地专属路径、后两条在 `local()` 回调内 ③ `index.js:1722`(`where.push`,**仅本地分支使用**)· `index.js:1787`(本地 SQL)· `index.js:3245`(`local:` 回调内) | 0 / 0 / 0 | ✅ **全部处置**(归零或具名豁免) |
|
|||
|
|
| **A4** | 表名对齐彻底 | `grep -rE "account_video_analysis" . \| grep -v /node_modules/ \| wc -l`;`grep -rn "hot_account_ext" lib \| wc -l` | `0`;`13`(其中 `store.js` **10**) | 0 / 0 | ✅ 通过 |
|
|||
|
|
| **A5** | 已按表切档(三张表) | —— | 真机 `data-plane.json` 三表**仍 `local`** | — | ⛔ **未达成**(**有意**:`local_id` 列未就位 ⇒ 切档 = 新行没 id ⇒ 前端 6 入口失效) |
|
|||
|
|
| **A6** | 旧库备份在位 + 零数据损失 | ——(第 27 棒备份已在位) | 本棒**未动** `mcn-plugin.db`(⛔ 未删 · 未改) | — | ✅ **维持**(旧库 sha256 未取 —— 本棒无文件级操作) |
|
|||
|
|
| **A7** | 移动端五条 | —— | **本棒未改 UI**:`lib/client.js` 零改动;改动面 6 件全在 `lib/host/mcn/**` + `tests/**` | — | ✅ **免跑**(文件清单依据 = §二) |
|
|||
|
|
| **A8** | 两机一致 | —— | **本棒未出包、未投放** ⇒ 代码面/运行面**都还是旧的** | — | ⛔ **未达成**(⛔ 不得写「一致」) |
|
|||
|
|
| **A9** | 红线零新增 | `grep -rEn "CREATE TABLE\|ALTER TABLE\|DROP TABLE\|better-sqlite3\|from 'pg'\|require\('pg'\)" . \| grep -v /node_modules/ \| wc -l` | `23`(全部落在 `./lib/host/mcn/db.js`) | 0 | ✅ **23**(与第 27/28/29/33 棒同值) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 四 逐处落点(index.js 22 处「改前 → 改后」)
|
|||
|
|
|
|||
|
|
**口径**(入口 §2 硬约束①):切换的**最小原子单位 = 表**;`local` 档 = **原 SQL 逐字**(保留在各 `local()` 回调里)。
|
|||
|
|
|
|||
|
|
| 处 | 行 | 改前(原文) | 改后形态 |
|
|||
|
|
|---|---|---|---|
|
|||
|
|
| 1 | `:852` | `platform: () => … awemeId: d.prepare('SELECT aweme_id FROM account_videos WHERE id=?')…` | `platform: async () => … awemeId: await awemeIdOfVideo(d, videoId)` |
|
|||
|
|
| 2 | `:861` | 同上(`countVideoAnalysis`) | 同上 |
|
|||
|
|
| 3 | `:1573` | `UPDATE hot_accounts SET favorite=? WHERE id=?` + `r.changes===0 ⇒ 404` | `store.patchHotAccountByLocalId(…)`(`local` 回调保原 SQL);`mapping-missing` 或 `changes===0` ⇒ 404(**本地语义逐字保留**) |
|
|||
|
|
| 4 | `:1811/1813` | `SELECT * FROM account_videos WHERE id=?` + `SELECT * FROM hot_accounts WHERE id=?` | `videoOf(d, videoId)` + `hotAccountOf(d, v.account_id)` |
|
|||
|
|
| 5 | `:1959` | `platform` 分支内 `d.prepare('SELECT aweme_id FROM account_videos WHERE id=?')` | `await awemeIdOfVideo(d, videoId)` |
|
|||
|
|
| 6 | `:2061` | `SELECT video_title FROM account_videos WHERE id=?` | `videoOf(d, videoId, { sql: 'SELECT id, aweme_id, video_title …' })`(⚠️ 唯一差异:本地 SELECT 追加 `id, aweme_id` —— `getVideoByLocalId` 平台档靠它换轨;调用方仍只用 `video_title`) |
|
|||
|
|
| 7 | `:2072` | `platform` 分支内 aweme_id 子查询 | `await awemeIdOfVideo(d, videoId)` |
|
|||
|
|
| 8 | `:2397` | `SELECT * FROM account_videos WHERE id=?` | `videoOf(d, rw.video_id)` |
|
|||
|
|
| 9 | `:2624` | `SELECT * FROM hot_accounts WHERE id=?` | `hotAccountOf(d, accountId)` |
|
|||
|
|
| 10 | `:2675` | `WHERE id=? AND (del_flag IS NULL OR del_flag <> 1)`(**非等值 ⇒ 下推不了**,D15) | `hotAccountOf(…)` + 插件内 `Number(accRow.del_flag ?? 0) !== 1`(**逐字复刻**) |
|
|||
|
|
| 11 | `:2814` | `SELECT id, account_name, douyin_id, source_dir FROM hot_accounts WHERE id=?` | `hotAccountOf(d, id, { sql: <原 SQL> })` + `id: accRow.id ?? id` |
|
|||
|
|
| 12 | `:2720`(按 `v.id`) | `SELECT v.*, h.account_name … LEFT JOIN hot_accounts h … WHERE v.id=?` | `store.readRows({ table:"account_videos", platform: <videoOf + 映射补名>, local: <原 JOIN SQL 逐字> })` |
|
|||
|
|
| 13 | `:2727`(按 `aweme_id`) | 同形 JOIN(`WHERE v.aweme_id=?`) | 同上(`findVideoByAwemeId` + `videoLocalIdMap` 补本地 id) |
|
|||
|
|
| 14 | `:2869` | `SELECT v.id, v.aweme_id, v.video_title, v.account_id, h.account_name … JOIN …` | `store.readRows({ … local: <原 SQL 逐字> })` |
|
|||
|
|
| 15 | `:2939` | `SELECT COUNT(*) AS c FROM hot_accounts` | `store.countHotAccounts()`(平台档 = 平台口径总数) |
|
|||
|
|
| 16 | `:2985` | `SELECT id FROM hot_accounts WHERE id=?` | `hotAccountOf(d, id, { sql: <原 SQL> })` |
|
|||
|
|
| 17 | `:3003` | `UPDATE hot_accounts SET ${sets.join(", ")} WHERE id=?`(**动态 SET**) | 按 `allowed` 逆推 `patch` 对象 ⇒ `patchHotAccountOf()`(**拆表 PATCH**:`douyin_id`/`followers`/`bio`… 归 `hot_account_ext`);⛔ **`sets` 与本地 SQL 一字未动** |
|
|||
|
|
| 18 | `:3075` | `SELECT COUNT(*) c FROM hot_accounts WHERE del_flag=0` | `store.readCount({ table:"hot_accounts", platform: <全量 + 插件内复刻 NULL 语义>, local: <原 SQL> })` |
|
|||
|
|
| 19 | `:3077` | `ROW_NUMBER() OVER (PARTITION BY account_id ORDER BY like_count DESC) rn<=10`(**在 `c(\`…\`)` 里**) | 同法:平台档全量 + 插件内按账号分组排序取前 10(⛔ 不是 `/count`) |
|
|||
|
|
| 20 | `:3317` | `SELECT * FROM hot_accounts WHERE id=?` + `(acc.del_flag ?? 0) === 1` | `hotAccountOf(…)` + `Number(acc.del_flag ?? 0) === 1` |
|
|||
|
|
|
|||
|
|
**D16 换轨**:平台无自增 `id` ⇒ 账号走 `id↔account_name`(`accountNameMap`)、视频走 `id↔aweme_id`(`videoLocalIdMap` / `getVideoByLocalId` 内部换算);**映射缺席一律具名降级**(`mapping-missing` / `row:null` + `detail`),⛔ 不猜、⛔ 不拿空值冒充。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 五 未闭合项(逐条:现象 / 归属 / 下一步)
|
|||
|
|
|
|||
|
|
1. **`local_id` 列在真机库仍未就位** —— 现象:A5 切档前置不满足;归属 **部署面**;下一步 = B 案最后一步(出包 → 上池 → **2 条 `ADD COLUMN`** → 重跑迁移回填 `local_id`)。
|
|||
|
|
2. **12 行 `account_video_source` 正文超平台单行 64 KiB** —— 现象:`A1 match=false`;归属 **插件侧**(处方 (a) 行内压缩);下一步 = 落 gzip 编解码到 `normalizeRow`/`rowsOf` 两个唯一必经之路(**本棒未落**)。⚠️ 真机读数已在位:12 行**全为纯文本**(`base64runs=0` / `data:image=0`)、gzip 后最大 **41,728 B** ≤ 64 KiB ⇒ **(a) 有效,无需改走 (b)**。
|
|||
|
|
3. **D16 映射新鲜度缺口** —— 现象:`hot_accounts` / `account_videos` 的**写**切平台后本地不再新增行 ⇒ 映射缺席;归属 **插件侧**(已知边界,已在每处**具名降级**);下一步 = `local_id` 就位后把映射改为「优先平台、缺则降级本地」。
|
|||
|
|
4. **`account_videos` 列表页在有账号级过滤时仍走本地 SQL** —— 现象:`index.js` `/mcn/api/videos` 的平台档分支**已由第 32 棒落地**(`buildVideosPlatform()`),本棒只补 `aweme_id` 换轨;归属 **插件侧**;下一步 = `shadow` 档真机比对(S7)。
|
|||
|
|
5. **`imports.js` 有一处未使用的 `findVideoByFolder` 导入** —— 现象:仅 `db.js:264` 调用(同步版,属 S10 收口);归属 **插件侧**;下一步 = S10 随 `db.js` 一并退场(⛔ 本批不动)。
|
|||
|
|
6. **自动化记忆目录缺失** —— 现象:`.workbuddy/memory/automations/40d342f3-…/` 首次运行前不存在(本棒已创建);归属 **本机工具面**;无后续动作。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 六 卫生声明(A7 逐条点名)
|
|||
|
|
|
|||
|
|
- `lib/client.js` **零改动** · `web/**` **零改动** · `skills/**` **零改动** ⇒ **未改 UI,移动端五条免跑**。
|
|||
|
|
- 本棒 tmp 产物:`tmp/p34/{patch-index.py,fix-script.py,add-tests.py,triage.py,ix-slice.txt,scan12.py,scan12.out,*.bak-p34}`。
|
|||
|
|
- 真机只读探针:本棒**只做只读取数**(`tmp/p34/scan12.py`,`find` + 本地 sqlite 只读),**未在实例内留下任何残留**。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 七 待拍板(**不影响本棒结论**)
|
|||
|
|
|
|||
|
|
1. **㈠ 47 实例重启窗口** —— 要用你定:**什么时候可以重启线上实例**。为什么需要你定:重启会让**正在用平台的用户**短暂断线(分钟级)。**A 案**:约一个低峰时段(优点:影响最小;缺点:要等)。**B 案**:现在重启(优点:立刻能验完 A2/A5;缺点:在线用户会被打断)。**我的倾向**:A 案(可推翻)。
|
|||
|
|
2. **㈡ 106 重启窗口** —— 同上,可合并到同一次窗口。
|
|||
|
|
3. **㈢ `json` 列是否改声明** —— 不可逆;本批**不需要**(不属第三批)。
|
|||
|
|
4. **㈣ 插件包源码是否纳入版本管理** —— 现状:6 件改动**无版本留痕**(第 34 棒已第 4 次累积)。
|