< Back to blog list

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

提示

YoBFF 遵循 Apache License 2.0 协议开源,仓库链接 WavesMan/YoBFF

为什么“可用”不等于“可运营”

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

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

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

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

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

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

go
1type AuditLogQuery struct {
2    Action    string
3    Operator  string
4    Target    string
5    StartTime string
6    EndTime   string
7    Limit     int
8    Page      int
9    PageSize  int
10}

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

go
1pageRaw := strings.TrimSpace(r.URL.Query().Get("page"))
2if pageRaw != "" {
3    page, err := strconv.Atoi(pageRaw)
4    if err != nil || page <= 0 {
5        writeError(w, http.StatusBadRequest, "invalid_request", "page must be positive", r)
6        return
7    }
8    query.Page = page
9}

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

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

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

go
1writeJSON(w, http.StatusOK, map[string]any{
2    "items":    items,
3    "total":    total,
4    "page":     query.Page,
5    "pageSize": query.PageSize,
6})

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

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

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

tsx
1const [auditPage, setAuditPage] = useState(1)
2const [auditPageSize, setAuditPageSize] = useState(15)
3const [auditTotal, setAuditTotal] = useState(0)

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

tsx
1const response = await fetchAuditLogs(token, {
2    action: auditAction.trim(),
3    operator: auditOperator.trim(),
4    target: auditTarget.trim(),
5    startTime: startText,
6    endTime: endText,
7    limit: auditLimit,
8    page,
9    pageSize,
10})

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

tsx
1pagination={{
2    total: auditTotal,
3    page: auditPage,
4    pageSize: auditPageSize,
5    onPageChange: (page) => setAuditPage(page),
6    onPageSizeChange: (pageSize) => {
7        setAuditPageSize(pageSize)
8        setAuditPage(1)
9    },
10}}

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

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

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

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

tsx
1const handleAuditQuery = async () => {
2    if (auditPage !== 1) {
3        setAuditPage(1)
4        return
5    }
6    await loadAuditLogs(1, auditPageSize)
7}

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

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

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

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

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

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

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

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

< Back to blog list

<BLOG>

</BLOG>