在运营多平台内容矩阵时,一个高频场景是:同一批文章需要分发到 6~8 个不同平台,每个平台还有各自的审核规则、接口差异和频控限制。如果靠人工复制粘贴,效率低且容易出错;如果简单写个循环批量提交,又会遇到"发到一半崩了""重复提交""某个平台失败影响整批"的问题。
本文记录一套多平台内容分发管线的工程化设计,核心解决三个问题:任务如何排队、失败如何重试、中断如何恢复。方案不依赖特定平台,可复用到大部分发布类系统。
一、整体架构:三层模型
管线拆成三层,职责分离,便于单独扩展:
```
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ 任务层 │ ──▶ │ 调度层 │ ──▶ │ 执行层 │
│ items.json │ │ 队列+状态机 │ │ platform X │
└─────────────┘ └─────────────┘ │ platform Y │
│ platform Z │
└─────────────┘
```
-
**任务层**:输入文件描述"要发什么"。每条任务包含 id、标题、正文、目标平台列表、配图路径等。
-
**调度层**:维护任务状态(pending / running / done / failed),控制并发、间隔、重试。
-
**执行层**:每个平台一个适配器,只负责"把一条任务发到该平台",不关心重试和排队。
这样设计后,新增平台只需写一个适配器,调度层零改动。
二、任务状态机与持久化
分发管线最容易踩的坑是"中断后不知道发到哪了"。解决思路是引入显式状态机,并持久化到本地 JSON 文件。
```
pending ──▶ running ──▶ done
│
▼
failed ──▶ pending(重试) / aborted(放弃)
```
状态记录按 `企业:任务id:平台` 为 key,天然支持多企业、多任务、多平台的交叉场景:
```json
{
"demo:kb-guide-01:csdn": "done",
"demo:kb-guide-01:zhihu": "failed",
"demo:kb-guide-02:sohu": "running"
}
```
关键实现点:
-
**启动时扫描**:读取 progress 文件,`done` 直接跳过,`failed` 进入重试队列,`running` 视为中断残留,重置为 pending 重新执行。
-
**每次成功后立即写入**:先写状态再继续下一条,避免"实际成功但状态没落盘"导致重复发布。
-
**原子性**:写入用临时文件 + rename,防止进程被杀时 JSON 写一半。
```js
function saveProgress(state) {
const tmp = PROGRESS_FILE + '.tmp';
fs.writeFileSync(tmp, JSON.stringify(state, null, 2), 'utf8');
fs.renameSync(tmp, PROGRESS_FILE);
}
```
三、队列与并发控制
多平台发布天然有频控限制(如每平台每日 N 篇、单篇间隔 1~2 分钟)。直接 Promise.all 全量并发必然触发风控,串行又太慢。折中方案:**按平台分队列,平台内串行,平台间并行**。
```js
async function runQueues(byPlatform) {
const tasks = Object.entries(byPlatform).map((platform, items) =>
runPlatformQueue(platform, items) // 每个平台一个协程
);
await Promise.all(tasks);
}
async function runPlatformQueue(platform, items) {
for (const item of items) {
await dispatch(item, platform);
await sleep(rand(minDelayMs, maxDelayMs)); // 人类化间隔,避免机器感
}
}
```
间隔用随机范围而非固定值:固定间隔容易被风控识别为脚本行为,随机化配合"人类化"操作(分段输入、随机停顿)能显著降低触发概率。
四、失败重试与退避
重试策略上,简单重试 3 次不如"有退避的重试 2 次":
-
第一次失败:等待 30s 重试(可能是瞬时网络问题)。
-
第二次失败:标记 failed,不阻塞整批,最后统一汇总处理。
-
明确区分**可重试错误**(超时、限流 429、5xx)和**不可重试错误**(参数错误、登录失效),不可重试的错误直接跳过,避免浪费配额。
```js
const RETRYABLE = 'ETIMEDOUT', 'ECONNRESET', '429', '5xx';
function shouldRetry(err) {
return RETRYABLE.some(k => String(err.message || err.code || '').includes(k));
}
```
注意:登录失效类错误要单独识别,一旦发现立刻中止整批并通知人工处理,否则所有重试都是在浪费时间。
五、一个完整的调度循环
把上面几部分合起来,调度循环长这样:
```js
async function processItems(items, platforms) {
const state = loadProgress();
const queue = \[\];
for (const item of items) {
for (const p of platforms) {
const key = `{item.id}:{p}`;
if (statekey === 'done') continue; // 断点续传:已完成跳过
if (statekey === 'aborted') continue; // 人工放弃的跳过
queue.push({ item, p, key });
}
}
const byPlatform = groupByPlatform(queue);
await runQueues(byPlatform);
printReport(state); // 输出 done/failed 汇总表
}
```
六、踩坑记录
| 坑 | 现象 | 解法 |
|---|---|---|
| 状态没及时落盘 | 中断后重复发布同一篇 | 成功后立即写 progress,先写状态再取下一条 |
| 全平台并发 | 触发频控被限流 | 平台内串行、平台间并行,间隔随机化 |
| 重试不加退避 | 连续失败烧完配额 | 区分可重试/不可重试错误,带退避重试 |
| 失败条目不记录 | 复盘时不知道哪篇没发 | failed 统一进汇总表,支持单独重跑 |
| 登录态失效不识别 | 所有重试全失败 | 识别登录失效,立即中止整批转人工 |
经验沉淀
这套"队列 + 状态机 + 断点续传"的分发模型,本质是把一次性脚本升级为可观测、可恢复的工程系统。它不绑定任何具体平台,通用性较强:换一批适配器,就能服务不同的内容分发场景。如果你也在做多平台分发,建议先把状态机和持久化做扎实------这是整个系统稳定性的地基,比追求并发数更重要。