YoBFF 实战复盘:让审计日志从“可用”走向“可运营

!TIP\] `YoBFF` 遵循 Apache License 2.0 协议开源,仓库链接 [WavesMan/YoBFF](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2FWavesMan%2FYoBFF "https://github.com/WavesMan/YoBFF")

为什么"可用"不等于"可运营"

很多系统都有审计日志接口,但常见形态只是"能查到数据"。

在真实运维场景里,日志查询是否可运营,取决于三个标准:

  1. 结果可控:单次请求返回规模有上限,不因日志量增长拖垮接口。
  2. 定位可持续:可以按动作、操作者、目标、时间区间逐步收敛问题范围。
  3. 交互可闭环:翻页、改页大小、改筛选条件时行为一致,不让用户反复"丢上下文"。

这次 YoBFF 的改动,核心就是把审计日志从"能看"升级为"能长期高频使用"。

后端:从全量读取到分页契约

审计查询参数扩展到标准分页模型,AuditLogQuery 同时承载筛选与分页字段:

go 复制代码
type AuditLogQuery struct {
    Action    string
    Operator  string
    Target    string
    StartTime string
    EndTime   string
    Limit     int
    Page      int
    PageSize  int
}

接口层读取 page/pageSize,并在参数非法时直接返回 400,避免"默默容错"导致排障歧义:

go 复制代码
pageRaw := strings.TrimSpace(r.URL.Query().Get("page"))
if pageRaw != "" {
    page, err := strconv.Atoi(pageRaw)
    if err != nil || page <= 0 {
        writeError(w, http.StatusBadRequest, "invalid_request", "page must be positive", r)
        return
    }
    query.Page = page
}

查询层将分页语义落到 SQL:

go 复制代码
offset := (page - 1) * pageSize
sqlText := `SELECT id, action, target, created_at, operator, detail_json` +
    filterSQL + ` ORDER BY created_at DESC LIMIT ? OFFSET ?`

并返回 items + total + page + pageSize,前端可直接构建稳定分页交互:

go 复制代码
writeJSON(w, http.StatusOK, map[string]any{
    "items":    items,
    "total":    total,
    "page":     query.Page,
    "pageSize": query.PageSize,
})

这一步的关键价值不是"多了两个参数",而是把日志读取从"按感觉拉一把"升级为"可预测的查询协议"。

前端:分页交互不是按钮,而是状态机

前端在观测中心引入了三元分页状态:

tsx 复制代码
const [auditPage, setAuditPage] = useState(1)
const [auditPageSize, setAuditPageSize] = useState(15)
const [auditTotal, setAuditTotal] = useState(0)

请求参数与后端契约对齐,筛选条件和分页参数一起进入请求:

tsx 复制代码
const response = await fetchAuditLogs(token, {
    action: auditAction.trim(),
    operator: auditOperator.trim(),
    target: auditTarget.trim(),
    startTime: startText,
    endTime: endText,
    limit: auditLimit,
    page,
    pageSize,
})

表格组件承载统一分页行为,total/page/pageSize 与翻页回调全部由外层状态驱动:

tsx 复制代码
pagination={{
    total: auditTotal,
    page: auditPage,
    pageSize: auditPageSize,
    onPageChange: (page) => setAuditPage(page),
    onPageSizeChange: (pageSize) => {
        setAuditPageSize(pageSize)
        setAuditPage(1)
    },
}}

这意味着 UI 不再是"点击后碰运气刷新",而是一个可推理的状态流:任意时刻都知道当前页、页大小、总量和筛选条件是什么。

筛选联动:避免"翻页状态污染查询语义"

日志系统里有个很常见的 UX 问题:用户在第 8 页输入新筛选条件,结果还停在第 8 页,直接出现空列表,误以为"筛选失效"。

YoBFF 的处理策略是:筛选触发时优先重置到第一页

tsx 复制代码
const handleAuditQuery = async () => {
    if (auditPage !== 1) {
        setAuditPage(1)
        return
    }
    await loadAuditLogs(1, auditPageSize)
}

配合 useEffect 监听 auditPage/auditPageSize,可保证筛选、翻页、改页大小三类行为不会互相打架,交互语义始终一致。

这次改造真正带来的运维收益

从工程视角看,这轮改造的收益主要体现在四点:

  • 接口稳定性 :单次查询成本被 pageSize 上限约束,避免高峰时日志接口异常膨胀。
  • 排障效率:动作/操作者/目标/时间的组合筛选,显著缩短问题定位路径。
  • 操作一致性:前后端对分页字段统一命名与语义,减少联调歧义。
  • 长期可维护性total 回传后,前端可持续扩展为跳页、区间缓存、导出等能力,而无需重做协议。

给同类系统的一个落地建议

如果你也在做"日志可运营化",建议优先守住这条最小闭环:

  1. 后端先定义好稳定分页契约(page/pageSize/total),并做边界保护。
  2. 前端以状态驱动分页,不把分页逻辑散落在多个组件里。
  3. 筛选动作默认回第一页,避免分页状态污染筛选结果。
  4. 响应里始终返回当前 page/pageSize,确保调试与排障可观测。

把这四步做好,日志系统就能从"临时查一下"真正进入"日常可运营"的阶段。

相关推荐
SimonKing1 小时前
企微、QQ统统接入OpenClaw,蓄水池已满,准备养虾
java·后端·程序员
CodeSheep2 小时前
王自如公开招聘01号员工,这要求有多离谱?
前端·后端·程序员
洛阳泰山2 小时前
我用 Java 21 虚拟线程重写了一个 RAG 平台:从架构设计到踩坑实录
java·人工智能·后端
moxiaoran57532 小时前
使用springboot+flowable实现一个简单的订单审批工作流
java·spring boot·后端
IT_陈寒2 小时前
JavaScript 闭包陷阱:90%开发者踩过的5个坑,你中招了吗?
前端·人工智能·后端
Java面试题总结2 小时前
go从零单排之方法
开发语言·后端·golang
ZHOUPUYU2 小时前
PHP性能分析与调优:从定位瓶颈到实战优化
开发语言·后端·html·php
稻草猫.2 小时前
MyBatis-Plus高效开发全攻略
java·数据库·后端·spring·java-ee·mybatis·mybatis-plus
lars_lhuan2 小时前
Go atomic
开发语言·后端·golang