!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")
为什么"可用"不等于"可运营"
很多系统都有审计日志接口,但常见形态只是"能查到数据"。
在真实运维场景里,日志查询是否可运营,取决于三个标准:
- 结果可控:单次请求返回规模有上限,不因日志量增长拖垮接口。
- 定位可持续:可以按动作、操作者、目标、时间区间逐步收敛问题范围。
- 交互可闭环:翻页、改页大小、改筛选条件时行为一致,不让用户反复"丢上下文"。
这次 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回传后,前端可持续扩展为跳页、区间缓存、导出等能力,而无需重做协议。
给同类系统的一个落地建议
如果你也在做"日志可运营化",建议优先守住这条最小闭环:
- 后端先定义好稳定分页契约(
page/pageSize/total),并做边界保护。 - 前端以状态驱动分页,不把分页逻辑散落在多个组件里。
- 筛选动作默认回第一页,避免分页状态污染筛选结果。
- 响应里始终返回当前
page/pageSize,确保调试与排障可观测。
把这四步做好,日志系统就能从"临时查一下"真正进入"日常可运营"的阶段。