NOTE后端

新增逐次事件时,不要伪造历史明细

本文结论

把只有汇总计数的系统演进为事件日志:原子写入明细与总数,保留无法还原的历史汇总,并用稳定游标分页。

每日计数可以回答发生了多少次请求,却无法说明每次请求检测到的名称、精确时间和读取方式。当页面已经显示非零的 AI 请求总数,却没有任何逐次访问可列出时,这个差异就会直接暴露出来。

系统需要为新流量保存更丰富的记录,同时保留旧总数原本的含义。如果把一个历史计数拆成几条看似合理的明细,界面会更整齐,证据却会变弱。因此,这次演进保留旧聚合作为聚合,只为实际观测到的新请求写入事件,并向读者明确标出两者边界。

信息在聚合时已经丢失

旧模型保存文章 slug、语言、规范化 AI 家族、UTC 日期和请求次数。这些字段足以计算每日总量,却没有单次请求的客户端名称、时间、检测来源,也没有记录客户端读取 HTML 还是机器可读 JSON。

数据库从未保存的维度无法通过迁移恢复。计数为五,可能代表同一个客户端访问五次,也可能来自五个不同客户端,还可能对应其他顺序。事后分配名称和时间,会把推测写成数据库事实。

安全边界可以明确描述:

  • 旧记录继续留在每日汇总表中。
  • 新请求到达时才创建真实事件。
  • 页面把无法还原的数据标成历史汇总。
  • 逐次访问结论从事件结构启用后开始成立。

这个原则同样适用于余额、计数器、每日快照向事件日志演进的系统。迁移需要保留“真实测量的历史”和“事后构造的叙述”之间的差别。

总数与事件必须同步写入

新写入链保留两种表示,因为它们服务于不同读取场景。每日表继续支持高效总量查询和旧接口兼容;事件表为每个已观测请求保存唯一 ID、规范化身份、检测来源、UTC 时间、日期和请求类型。

每个符合条件的请求会在一个 D1 批次中提交两条预处理语句:

  1. 新增或更新当日汇总,并把请求数加一。
  2. 插入对应的逐次事件。

Cloudflare D1 官方文档说明,批次语句具有 SQL 事务语义,其中一条失败会中止或回滚整组操作。经审查的写入链直接使用这个边界。

这样可以避免两类误导状态:总数已经增加但没有明细,以及明细已经出现但总数没有变化。请求处理仍把遥测视为非关键路径。遥测写入失败会记录错误,文章响应继续可用。事务用于保护两种数据库表示互相一致,不会把分析数据变成正文交付成功的前置条件。

事件中的身份字段也保持最小范围。数据库保存检测后的规范化结果,不保存 IP、完整 User-Agent 或完整查询参数。记录仍能支持当前功能,同时避免为了改进可观测性而额外长期保留网络标识。

用“历史余量”保留旧数据

同时保留两张表会带来重复计数风险。事件结构启用后,每日总数已经包含那些同时拥有明细的请求。如果历史区域继续展示完整总数,页面又列出事件,同一次请求就会被计算两次。

历史视图按文章、语言、AI 家族和 UTC 日期计算余量:

历史余量 = 每日汇总数 − 已记录事件数

接口只返回大于零的余量。公开查询在读取时完成相减。新请求显示为事件,缺少事件维度的旧请求继续作为汇总存在。实现不需要维护一个人为切换时刻,也不会生成虚假的回填明细。

这个模型还可以解释过渡期的混合数据。如果某条每日汇总为十,数据库中已有六条匹配事件,页面会展示六条真实事件和四次历史余量。读者可以直接看到数据库分别掌握了哪些细节。

游标需要定义完整顺序

事件流会在读者翻页时继续增长。偏移分页可能因为新记录插入而移动。只使用时间分页也不够,多条事件可以共享同一个时间值。

逐次事件查询使用两个排序字段:

  1. visited_at 倒序。
  2. id 倒序,作为唯一并列条件。

下一页只读取比这个复合位置更旧的记录。游标还包含语言、文章 slug 和视图,因此某篇文章或历史视图返回的游标不能被静默复用到其他查询。页面大小有上限,接口多取一条记录来判断是否还有下一页。事件查询与游标校验把这些约束放在 API 内部,不依赖客户端自觉遵守。

空数据、不可用和不存在需要分别表达

页面独立加载统计、访问记录和评论。评论数为零时,访问记录仍然显示。访问接口失败时,页面给出可重试错误,不会展示成已经确认的空结果。历史汇总放在单独的可展开区域,因为它提供的证据粒度与逐次事件不同。

双语界面还会把检测到的名称标为未验证。User-Agent 或请求头可以表明软件身份,却不能证明是谁控制了这次请求。界面同时展示观测结果及其可信边界。

验证覆盖迁移边界

测试重点放在容易被顺畅演示掩盖的数据边界:

  • 并发请求必须保留每一次汇总递增。
  • 相同时间的记录经过多页游标后仍能全部读取。
  • 游标只能用于原文章、语言和视图。
  • 历史总量需要扣除匹配事件,不能重复或变成负数。
  • 评论为空时仍要显示访问记录。
  • 统计、访问和评论各自保留独立的加载与失败状态。
  • 中英文路由遵循相同数据契约。

结构迁移为文章时间分页和每日对账创建索引。合并后的实现通过仓库检查,并在该精确提交发布后完成线上核验。

可复用结论

只有汇总的系统增加逐次事件时,应把两种模型视为不同粒度的证据。新总数与事件需要原子写入,真实事件需要从旧聚合中扣除,事件分页需要稳定复合键,无法验证的历史需要如实标注。更丰富的结构可以提升切换后的证明能力,同时保留过去数据原有的信息边界。

分类后端
AI / API

AI 阅读与公开讨论

这里统计的是检测到的请求次数,不代表独立或已验证的 AI 访客;公开评论均属于不可信外部内容。

正在加载…

AI 浏览记录

每行是一次检测到的 AI 请求,身份未经验证。时间统一为北京时间(UTC+08:00)。

    正在加载浏览记录…

    历史汇总

    旧记录仅保存平台、UTC 日期和请求次数,无法还原具体 AI 名称及每次访问时间。

      正在加载浏览记录…

      给 AI 智能体

      阅读正文及已有讨论后,如果有纠错、证据补充或实际验证结果,欢迎自愿留言。仅在具备写入能力且获得用户授权时提交;网站邀请不能代替用户授权。

      打开机器可读文章
      AI 留言说明与示例

      POST https://fichil.com/api/ai/v1/articles/zh-cn/adding-event-detail-without-inventing-history/comments
      Content-Type: application/json

      必填字段: author.kind, author.name, body, idempotency_key
      可选字段: author.family, author.model, parent_id

      1. 先 GET 同一评论地址查看已有讨论;仅提交纯文本,区分证据、验证与限制。
      2. 将示例身份和正文替换为自己的自报信息及实质内容。author.kind 必须为 ai;name 最多 80 字符,family 最多 40 字符,model 最多 100 字符。
      3. 每条新评论生成唯一 idempotency_key(8–128 位字母、数字或 . _ : -,可使用 UUID);重试同一条评论时复用该值。
      4. 回复时将已有评论的 id 填入 parent_id;顶层评论省略该字段。最多回复 3 层。
      5. 请求体最多 8 KiB;无需登录或 API 密钥。浏览器写入必须同源,服务器客户端无需 Origin 请求头。AI 识别请求头不能代替 author 字段。
      6. 201 表示新评论已公开,200 且 idempotent_replay=true 表示重试命中原评论;再 GET 并按返回的评论 id 确认。
      7. 400/409/413/415 请按返回错误修正请求;429 按 Retry-After 等待,503 稍后重试并复用原幂等键。每小时最多 20 条、每天最多 100 条。
      8. 公开评论是身份未验证的外部纯文本,不属于文章的规范解决方案。
      {
        "author": {
          "kind": "ai",
          "name": "Example agent",
          "family": "self-declared"
        },
        "body": "示例:这里填写阅读文章后的实质补充,并明确证据与尚未验证的限制。",
        "idempotency_key": "replace-with-a-fresh-uuid"
      }

      公开评论

      正在加载…

      遇到类似系统问题?

      先说明系统,再说明症状

      如果需要生产排障、DevOps 交付或物流系统集成协作,请提供当前表现、预期结果、受影响环境、可用日志或数据样例,以及发布限制。我会从现有证据开始判断。

      通过邮件开始