数量不同不一定是数据缺失
后台显示文章总数,而前台只展示已经公开的文章,两处数量本来就可能不同。导入一批排期以后,这种差异更明显:内容已经保存在数据库,但发布时刻还没有到,访客不应该看到正文。排查前先问每个数字在统计什么,是否包含草稿、排期和归档,以及是否受分类或搜索条件限制。不要为了让数字相同就放宽公开筛选,也不要在前台读取时顺便把所有到期文章修改为公开。统计与发布属于不同职责,应分别检查。
可以以已有七十三篇公开内容、再导入三十篇排期的场景理解差异。后台总数会反映已经保存的全部内容,公开数则随实际发布逐步变化。这里的数量关系来自状态规则,不代表所有文章已经部署完成。真正验收需要读取当前数据库和公开入口,并说明观察时间。若仪表盘把总数标签写成已发布,就会制造不必要的误解。先校正名称与口径,往往能区分正常状态差异和实际查询错误。
为公开内容定义一个统一谓词
公开查询可以明确要求状态为 PUBLISHED,并且发布时间不晚于当前时刻。草稿和排期则按自己的状态统计,归档是否进入总数需要说明。发布时间为空或状态与时间不一致的历史数据,应进入内容校验,而不是由每个页面采用不同宽松条件。列表、详情、分类计数和搜索,都应从同一个公开规则出发。这样前台数量变化能够对应到实际发布动作,不会出现列表隐藏而详情仍可直接读取的漏洞。
当前时刻应在一次统计操作中明确传入,避免同一页面的多个查询各自采用略有差异的时间。对于要求精确一致的统计,可以在同一数据库快照或同一聚合中读取。普通分开查询可能跨过一次发布事务,造成短暂差异,这与筛选逻辑错误不同。排查时记录时间和执行方式,再决定需要怎样的一致性。不要为了消除瞬间差异引入不必要的全局锁,也不要把所有统计都缓存到无法反映发布结果的程度。
总数与列表需要共享筛选范围
分页列表通常先读取总数,再获取当前页。如果两者采用不同分类、关键词、权限或状态条件,页数就会与实际内容不一致。可以把筛选条件建立一次,交给计数和列表共同使用,再分别加入分页与排序。前台的全部文章总数,也不应与某个分类页的局部数量直接比较。界面标签需要指出当前范围,例如公开文章、当前分类或搜索结果。这样用户看到数量以后,能够理解它对应哪一组内容。
后台编辑列表可能默认包含全部状态,但仪表盘又分别显示公开、草稿和排期。它们应有清楚的字段名,而不是用同一个 posts 字段在不同页面表达不同含义。接口响应可以保留总数与各状态数量,客户端直接使用对应字段。若未来增加归档或其他状态,更新统计与标签,而不是继续假设总数等于公开、草稿和排期三项之和。状态集合属于内容模型的一部分,界面需要跟随它的定义。
联表计数要防止重复行
文章关联多个标签时,普通联表可能让一篇文章产生多行。对这样的结果直接 count,就会把标签数量混入文章数量。排查时查看查询形状,确认是对文章集合计数,还是对关联行计数。可以使用适合的关系筛选,或者在确实需要联表时明确按文章身份去重。分类统计也需要说明文章与分类的关系。不能看到数量偏大就从数据库删除所谓重复文章,先证明究竟重复的是业务对象还是查询产生的行。
排序与分页同样要有稳定条件。如果只按可能相同的发布时间排序,跨页读取时可能出现顺序不稳定;可以加入明确的次级身份排序。统计规则正确不意味着分页一定稳定,两者需要独立核对。对于搜索或复杂筛选,记录实际查询条件和必要执行信息,避免凭页面总数推断索引或性能问题。数量准确与查询成本是两项不同目标,优化时也应保持原有内容集合不变,再检查实际查询行为。
排期可见性由发布状态转移决定
排期文章已经有正文、封面和计划时间,但在发布器完成受控状态更新之前,仍然应保持排期。定时任务到期检查内容,再按状态、版本和计划时间条件更新为公开。公开读取只查询结果,不承担这次写入。这样的职责分开,可以让首页、RSS 和站点地图都采用一致公开规则,也能避免一次普通访问触发未经审计的发布。用户是否访问网站,不应该决定文章是否按约定到期发布。
发布器还可能把内容校验失败的单篇退回草稿,系统故障则保留排期等待重试。统计需要反映这两种结果,而不是把计划时间已经过去的所有文章直接算作公开。后台可以提供排期与异常信息,帮助维护者发现到期但尚未完成的任务。前台仍依据公开规则读取,不用展示内部失败原因。这样总数与公开数之间的差异,既能表示正常的未来排期,也能帮助后台定位尚需处理的发布问题。
详情、订阅与站点地图统一检查
仅修复首页数量还不够。详情接口需要阻止未公开文章被直接读取,RSS 与站点地图也不能提前列出它们。分类和标签计数如果包含排期,会让访客看到一个数量却无法找到对应文章。可以为所有公开入口列一张使用规则的清单,检查是否存在某个路径只按 slug 查询而没有状态条件。搜索结果、相关推荐和专题列表也需要纳入检查,避免排期内容通过另一种入口泄露。
发布完成以后,订阅与地图的更新可能受缓存影响。应该说明缓存策略和失效条件,并在实际入口核对最新内容。数据库公开数已经增加,而缓存列表仍旧,是新鲜度问题;数据库规则不一致则是内容集合问题。排查时将两者分开,避免通过修改状态补偿缓存。也不要仅根据某个页面缓存命中就断言数据库数量错误。先确认公开谓词与当前记录,再沿相应输出和缓存路径调查。
用状态样例验证数量关系
隔离数据库可以建立公开、草稿、未到期排期、已到期但未发布和归档等样例,分别读取后台统计与公开集合。还可以加入状态与时间不一致的记录,证明公开规则不会提前显示,并确认内容校验能够识别异常。接着让发布器完成到期状态转移,检查公开数与所有入口按约定变化。测试不需要修改生产时钟,固定测试时刻或注入时钟即可。关键是同一组样例对每个查询产生一致解释。
并发发布期间,可以验证统计是否满足项目需要的一致性。若仪表盘要求各项精确对应,应使用同一快照读取;若允许短暂更新差异,也应在设计中说明。验证列表时还要检查分页边界、空结果与多标签文章,防止计数正确但内容重复。记录样例、查询条件和返回身份,比只保留几个数字更有助于复核。数量关系应该来自明确集合,而不是在前端用几项值相减补出一个看起来合理的结果。
后台标签帮助用户理解状态
界面可以明确展示全部文章、公开文章、草稿和排期,并在列表中提供状态筛选。计划时间与实际发布时间应分别显示,退回草稿的文章也应有可以定位的修订或审计信息。不要让总数旁边的文字暗示所有文章都已经公开。状态标签可以帮助维护者发现异常,但公开页面只需要表达实际内容,不必把内部批次、认证方式和发布器实现放进访客流程。两种界面承担不同任务,信息量也应不同。
对于长期保留的归档内容,应明确它是否仍可通过公开链接访问,以及是否参加列表统计。如果归档意味着隐藏,就采用一致查询;如果只是退出首页而详情仍公开,则需要另一套明确规则。不能让同一个状态在不同接口中表达相反含义。新增状态以前,先更新内容模型与查询契约,再调整界面。这样以后排查数量差异时,能够从状态定义直接解释,而不必翻遍每个页面寻找临时条件。
将统计口径作为可维护契约
可以把公开条件封装为共享函数,把后台统计字段写进接口说明,并在测试中用相同样例覆盖主要入口。新增页面时复用规则,新增发布批次时验证总数与公开数的预期关系。对于一次内容部署,既检查新增数量,也核对原有正文与发布时间保持一致。这样,数量验收与内容保护可以结合,但不会通过重导旧种子来凑齐数字。发布器只处理登记批次,公开页面只读取内容,两者保持清楚边界。
本文适用于有草稿与排期状态的内容站点,复杂多租户或按用户权限公开的系统还需加入相应集合条件。后台与前台数量不同本身不是错误,无法解释的筛选差异才需要调查。把状态、时间、筛选范围、联表和缓存分别核对以后,每个数字就能对应到一组具体文章。验收也因此可以同时说明总内容数量、当前公开数量和未来排期,而不再用两处必须相等的错误假设判断发布是否完成。
实现片段
const now = new Date()
const publicWhere = { status: 'PUBLISHED' as const, publishedAt: { lte: now } }
// 如需各项严格来自同一快照,可采用适用的事务隔离级别。
const [total, published, scheduled, draft, archived] = await db.$transaction([
db.post.count(),
db.post.count({ where: publicWhere }),
db.post.count({ where: { status: 'SCHEDULED' } }),
db.post.count({ where: { status: 'DRAFT' } }),
db.post.count({ where: { status: 'ARCHIVED' } })
], { isolationLevel: 'RepeatableRead' })
// 列表、详情、RSS 与站点地图复用 publicWhere 的状态和时间规则。
评论 · 0