导读(3行收益):官网内容发布最常见的三个事故:未审内容被直接发出去、定时发布到点没执行、下线的文章还能被旧缓存读到,每个都影响对外形象。根源是发布状态散落在业务字段里。本文用一张内容状态机把草稿、审核、定时、下线理顺,附迁移表与定时任务 SQL。看完能直接改进你的内容发布流程。
一、内容发布为什么老出错
- 未审内容上线:编辑保存了草稿,结果发布接口把草稿直接发出去;
- 定时发布失败:定时任务到点没执行,或者执行了两次,文章重复发布;
- 下线不彻底:状态改了,但 CDN/浏览器缓存还在,用户仍能看到旧内容。
三个事故的根源:发布状态用多个字段拼凑(is_published、publish_time 是不是 null……),谁都能改,改了没有约束。正确做法是把内容做成一条带状态机的记录,发布、下线都走固定迁移。
二、内容状态机设计
CREATE TABLE article (
id BIGINT PRIMARY KEY,
title VARCHAR(128),
status TINYINT NOT NULL DEFAULT 0,
-- 0草稿 1待审核 2已发布 3已下线 4审核驳回
scheduled_at DATETIME, -- 定时发布时间(空=立即)
published_at DATETIME,
version INT NOT NULL DEFAULT 1,
updated_by BIGINT
);
CREATE INDEX idx_status_schedule ON article(status, scheduled_at);
状态迁移规则:
| 当前状态 | 事件 | 目标状态 |
|---|---|---|
| 草稿 | 提交审核 | 待审核 |
| 待审核 | 审核通过 | 已发布 |
| 待审核 | 审核驳回 | 审核驳回 |
| 已发布 | 手动下线 | 已下线 |
| 已下线 | 重新发布 | 已发布 |
| 已发布/已下线 | 定时发布到点 | 已发布(仅当 scheduled_at 设置时) |
核心约束:只有「待审核」才能变「已发布」,只有「已发布」才能下线,其余迁移一律在代码里拒绝。定时发布与手动发布共用同一个迁移出口,避免两条路径的状态不一致。
三、审核流:发布必须过审
发布动作拆成两步:编辑提交审核、管理员审核通过。审核通过时做条件更新:
def approve(article_id: int, operator: int) -> bool:
affected = db.execute(
"UPDATE article SET status = 2, published_at = NOW(), version = version + 1 "
"WHERE id = ? AND status = 1", -- 只有待审核可过审
article_id,
).rowcount
if affected == 1:
invalidate_cache(article_id)
return True
return False # 状态不是待审核(可能是草稿被直接调用了审核接口)
避坑:审核接口必须做状态条件更新——如果先 SELECT 看状态再 UPDATE,两个管理员同时过审同一篇会有竞态;用条件更新直接挡掉非法迁移。
四、定时发布:任务表与幂等执行
定时发布用任务表而不是裸扫内容表:
CREATE TABLE publish_task (
id BIGINT PRIMARY KEY,
article_id BIGINT NOT NULL,
publish_at DATETIME NOT NULL,
status TINYINT NOT NULL DEFAULT 0, -- 0待执行 1已执行 2已取消
UNIQUE KEY uk_article_task (article_id)
);
def run_scheduled_publish(task_id: int) -> bool:
# 抢占式更新:同一任务只能被一个执行器拿走
affected = db.execute(
"UPDATE publish_task SET status = 1 WHERE id = ? AND status = 0",
task_id,
).rowcount
if affected != 1:
return False # 已被其他执行器处理,幂等跳过
db.execute(
"UPDATE article SET status = 2, published_at = NOW() "
"WHERE id = ? AND status IN (0, 1)", # 草稿或待审核可直接定时发布
article_id,
)
invalidate_cache(article_id)
return True
避坑:定时任务抢占式更新 + 幂等执行——同一任务重复触发不会发两次;执行器扩容时也不会重复发布。任务取消要把 status 置 2,别直接删行(保留审计痕迹)。
五、下线:状态与缓存一起失效
下线不止改状态,还要让缓存失效:
def unpublish(article_id: int, operator: int) -> bool:
affected = db.execute(
"UPDATE article SET status = 3, version = version + 1 WHERE id = ? AND status = 2",
article_id,
).rowcount
if affected == 1:
invalidate_cache(article_id) # 通知 CDN/应用缓存失效
return True
return False
避坑:缓存失效要带版本号——只按 URL 失效在并发更新下可能失效后又写入旧版本;在缓存 key 里带 article version,版本变化自动读不到旧缓存。下线后重新发布会生成新版本号,历史版本保留在库中,支持追溯与回滚。
六、踩坑清单
- 状态用多字段拼凑:is_published + publish_time 组合判断,谁都能改出非法状态;
- 审核接口不做事前状态校验:草稿被直接过审上线;
- 定时任务无抢占:重复执行导致重复发布;
- 下线只改状态:CDN/浏览器缓存还挂着旧内容;
- 缓存失效无版本:并发更新下旧版本被写回;
- 取消任务直接删行:审计追溯时看不到曾经的计划。
七、工程落地建议
内容发布建议先画状态迁移图再写接口,审核、定时、下线三处都做条件更新,缓存失效带版本号。若自建成本高,可基于成型平台(如乔拓云企业网站)的内容管理组件快速起步,重点核对状态流转与缓存联动。
八、复盘清单(可直接抄走)
- [ ] 状态机:草稿/待审核/已发布/已下线/审核驳回是否都有页面入口;
- [ ] 审核接口:条件更新是否挡住非法迁移;
- [ ] 定时任务:抢占式更新 + 幂等是否覆盖所有执行器;
- [ ] 下线链路:状态 + 缓存版本失效是否联动;
- [ ] 审计:状态变更是否都有操作人与时间记录。
结语
官网内容发布的乱,多数不是功能缺失,而是发布状态没有模型化。一张状态机、三处条件更新、一个带版本的缓存失效,就能把误发、重复发、下不净这三个事故按下去。本文仅作技术分享,具体功能以各平台官方实时信息为准。