feat(client): 慢请求监控(>500ms 记日志+上报);拆除列表 provider 的 _cache 失败兜底

- SlowRequestInterceptor:单次 API 调用超 500ms 即 debugPrint long-request
  日志并经 ErrorReporter 上报(error_type=slow_api,按 method+归一路径 5 分钟
  节流)。服务端 GIN 日志只见自身处理耗时,网络往返段只有客户端可观测。
- 拆除 10 个列表 provider 的 _cache 失败兜底:实测线上接口客户端视角典型
  50-120ms、最坏约 270ms(列表类全部 <500ms),失败静默端旧数据弊大于利
  (曾放大跨账号残留问题),改为明确进入错误态由用户重试。

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-07-15 00:49:29 +08:00
parent 1723e480c1
commit 3888cf2d65
14 changed files with 88 additions and 172 deletions
+5
View File
@@ -9,6 +9,7 @@ import '../errors/error_reporter.dart';
import '../../providers/connectivity_provider.dart';
import '../../providers/license_provider.dart';
import 'retry_interceptor.dart';
import 'slow_request_interceptor.dart';
/// Public Dio instance for unauthenticated calls (login / refresh)
final _publicDio = _buildPublicDio();
@@ -19,6 +20,7 @@ Dio _buildPublicDio() {
connectTimeout: AppConstants.publicConnectTimeout,
receiveTimeout: AppConstants.publicReceiveTimeout,
));
dio.interceptors.add(SlowRequestInterceptor());
dio.interceptors.add(RetryInterceptor(dio));
return dio;
}
@@ -110,6 +112,9 @@ class ApiClient {
},
));
// 慢请求监控(最外层,量到的是用户感知耗时)
_dio.interceptors.add(SlowRequestInterceptor());
// 网络层错误自动重试(须在错误处理拦截器之前,先重试再走 401/上报逻辑)
_dio.interceptors.add(RetryInterceptor(_dio));
@@ -0,0 +1,55 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../errors/error_reporter.dart';
/// 慢请求监控:单次 API 调用从发起到收到响应超过阈值(500ms)即记 long-request
/// 日志(debugPrint)并上报服务端(error_type=slow_api)。
///
/// 服务端 GIN 日志只能看到自身处理耗时,网络往返这一段(弱网用户 / 网关劣化)
/// 只有客户端能观测,故上报补齐用户感知视角。按 method+归一路径 5 分钟节流,
/// 弱网下不至于刷量;上报本身走 ErrorReporterfire-and-forget 失败静默。
class SlowRequestInterceptor extends Interceptor {
static const _threshold = Duration(milliseconds: 500);
static const _reportInterval = Duration(minutes: 5);
static const _startKey = '_slowReqStart';
/// 归一路径键 → 上次上报时间(节流窗口内只报一次)
final Map<String, DateTime> _lastReported = {};
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
options.extra[_startKey] = DateTime.now();
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
_check(response.requestOptions, response.statusCode);
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
_check(err.requestOptions, err.response?.statusCode);
handler.next(err);
}
void _check(RequestOptions options, int? status) {
final start = options.extra[_startKey];
if (start is! DateTime) return;
final elapsed = DateTime.now().difference(start);
if (elapsed < _threshold) return;
// 含数字的路径段(id / 单号 / 店铺码)归一为 :id,同类慢请求共用节流键
final path =
options.path.replaceAll(RegExp(r'/[^/]*\d[^/]*'), '/:id');
final key = '${options.method} $path';
final msg = '[SlowAPI] $key ${elapsed.inMilliseconds}ms status=$status';
debugPrint(msg);
final last = _lastReported[key];
final now = DateTime.now();
if (last != null && now.difference(last) < _reportInterval) return;
_lastReported[key] = now;
ErrorReporter.instance.report(errorType: ErrorType.slowApi, errorMsg: msg);
}
}