YoBFF 遵循 Apache License 2.0 协议开源,仓库链接 WavesMan/YoBFF
为什么“可用”不等于“可运营”
很多系统都有审计日志接口,但常见形态只是“能查到数据”。
在真实运维场景里,日志查询是否可运营,取决于三个标准:
- 结果可控:单次请求返回规模有上限,不因日志量增长拖垮接口。
- 定位可持续:可以按动作、操作者、目标、时间区间逐步收敛问题范围。
- 交互可闭环:翻页、改页大小、改筛选条件时行为一致,不让用户反复“丢上下文”。
这次 YoBFF 的改动,核心就是把审计日志从“能看”升级为“能长期高频使用”。
后端:从全量读取到分页契约
审计查询参数扩展到标准分页模型,AuditLogQuery 同时承载筛选与分页字段:
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,避免“默默容错”导致排障歧义:
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:
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,前端可直接构建稳定分页交互:
1writeJSON(w, http.StatusOK, map[string]any{
2 "items": items,
3 "total": total,
4 "page": query.Page,
5 "pageSize": query.PageSize,
6})这一步的关键价值不是“多了两个参数”,而是把日志读取从“按感觉拉一把”升级为“可预测的查询协议”。
前端:分页交互不是按钮,而是状态机
前端在观测中心引入了三元分页状态:
1const [auditPage, setAuditPage] = useState(1)
2const [auditPageSize, setAuditPageSize] = useState(15)
3const [auditTotal, setAuditTotal] = useState(0)请求参数与后端契约对齐,筛选条件和分页参数一起进入请求:
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 与翻页回调全部由外层状态驱动:
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 的处理策略是:筛选触发时优先重置到第一页。
1const handleAuditQuery = async () => {
2 if (auditPage !== 1) {
3 setAuditPage(1)
4 return
5 }
6 await loadAuditLogs(1, auditPageSize)
7}配合 useEffect 监听 auditPage/auditPageSize,可保证筛选、翻页、改页大小三类行为不会互相打架,交互语义始终一致。
这次改造真正带来的运维收益
从工程视角看,这轮改造的收益主要体现在四点:
- 接口稳定性:单次查询成本被
pageSize上限约束,避免高峰时日志接口异常膨胀。 - 排障效率:动作/操作者/目标/时间的组合筛选,显著缩短问题定位路径。
- 操作一致性:前后端对分页字段统一命名与语义,减少联调歧义。
- 长期可维护性:
total回传后,前端可持续扩展为跳页、区间缓存、导出等能力,而无需重做协议。
给同类系统的一个落地建议
如果你也在做“日志可运营化”,建议优先守住这条最小闭环:
- 后端先定义好稳定分页契约(
page/pageSize/total),并做边界保护。 - 前端以状态驱动分页,不把分页逻辑散落在多个组件里。
- 筛选动作默认回第一页,避免分页状态污染筛选结果。
- 响应里始终返回当前
page/pageSize,确保调试与排障可观测。
把这四步做好,日志系统就能从“临时查一下”真正进入“日常可运营”的阶段。
< Back to blog list