From 3888cf2d65aabb799340cbbcc646bb633e41db28 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Wed, 15 Jul 2026 00:49:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(client):=20=E6=85=A2=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E7=9B=91=E6=8E=A7=EF=BC=88>500ms=20=E8=AE=B0=E6=97=A5=E5=BF=97?= =?UTF-8?q?+=E4=B8=8A=E6=8A=A5=EF=BC=89=EF=BC=9B=E6=8B=86=E9=99=A4?= =?UTF-8?q?=E5=88=97=E8=A1=A8=20provider=20=E7=9A=84=20=5Fcache=20?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=85=9C=E5=BA=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- client/lib/core/api/api_client.dart | 5 ++ .../core/api/slow_request_interceptor.dart | 55 +++++++++++++++++++ client/lib/core/errors/error_reporter.dart | 1 + client/lib/providers/inventory_provider.dart | 34 ++---------- .../providers/license_purchase_provider.dart | 21 ++----- .../lib/providers/number_rule_provider.dart | 18 +----- client/lib/providers/partner_provider.dart | 17 +----- client/lib/providers/product_provider.dart | 17 +----- client/lib/providers/session_provider.dart | 18 +----- client/lib/providers/stock_in_provider.dart | 17 +----- client/lib/providers/stock_out_provider.dart | 17 +----- client/lib/providers/update_provider.dart | 4 +- client/lib/providers/user_provider.dart | 18 +----- client/lib/providers/warehouse_provider.dart | 18 +----- 14 files changed, 88 insertions(+), 172 deletions(-) create mode 100644 client/lib/core/api/slow_request_interceptor.dart diff --git a/client/lib/core/api/api_client.dart b/client/lib/core/api/api_client.dart index 9c8f35f..e0c6a65 100644 --- a/client/lib/core/api/api_client.dart +++ b/client/lib/core/api/api_client.dart @@ -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)); diff --git a/client/lib/core/api/slow_request_interceptor.dart b/client/lib/core/api/slow_request_interceptor.dart new file mode 100644 index 0000000..e072cae --- /dev/null +++ b/client/lib/core/api/slow_request_interceptor.dart @@ -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 分钟节流, +/// 弱网下不至于刷量;上报本身走 ErrorReporter,fire-and-forget 失败静默。 +class SlowRequestInterceptor extends Interceptor { + static const _threshold = Duration(milliseconds: 500); + static const _reportInterval = Duration(minutes: 5); + static const _startKey = '_slowReqStart'; + + /// 归一路径键 → 上次上报时间(节流窗口内只报一次) + final Map _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); + } +} diff --git a/client/lib/core/errors/error_reporter.dart b/client/lib/core/errors/error_reporter.dart index b4450fa..ced9dbf 100644 --- a/client/lib/core/errors/error_reporter.dart +++ b/client/lib/core/errors/error_reporter.dart @@ -14,6 +14,7 @@ class ErrorType { static const zoneError = 'zone_error'; static const caughtException = 'caught_exception'; static const apiError = 'api_error'; + static const slowApi = 'slow_api'; // 客户端感知的慢请求(>500ms),见 SlowRequestInterceptor } class ErrorReporter { diff --git a/client/lib/providers/inventory_provider.dart b/client/lib/providers/inventory_provider.dart index 090db55..3865a6d 100644 --- a/client/lib/providers/inventory_provider.dart +++ b/client/lib/providers/inventory_provider.dart @@ -35,20 +35,12 @@ class InventoryListNotifier extends AsyncNotifier> { bool _sortAsc = true; List _series = []; List _spec = []; - PageResult? _cache; @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(networkRecoveryCountProvider); // 网络恢复时自动刷新 - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future> _fetch() { @@ -125,14 +117,9 @@ class InventoryListNotifier extends AsyncNotifier> { state = const AsyncValue>.loading() .copyWithPrevious(state); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (e, st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } } @@ -145,20 +132,12 @@ final inventoryLogProvider = class InventoryLogNotifier extends AsyncNotifier> { int _page = 1; int _pageSize = AppConstants.defaultPageSize; - PageResult? _cache; @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(networkRecoveryCountProvider); - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future> _fetch() { @@ -182,14 +161,9 @@ class InventoryLogNotifier extends AsyncNotifier> { void reload() { state = const AsyncValue.loading(); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (e, st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } } diff --git a/client/lib/providers/license_purchase_provider.dart b/client/lib/providers/license_purchase_provider.dart index d53fce8..6a66182 100644 --- a/client/lib/providers/license_purchase_provider.dart +++ b/client/lib/providers/license_purchase_provider.dart @@ -17,19 +17,11 @@ class PurchaseListNotifier extends AsyncNotifier { int _page = 1; int _pageSize = 10; String _status = ''; // '' | pending | paid | failed - PurchaseListResult? _cache; @override Future build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future _fetch() { @@ -46,17 +38,12 @@ class PurchaseListNotifier extends AsyncNotifier { void reload() { // 保留上一次数据(isReloading)→ 屏幕可继续渲染旧内容 + 叠加半透明进度,不白屏。 - state = const AsyncValue.loading() - .copyWithPrevious(state); + state = + const AsyncValue.loading().copyWithPrevious(state); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (Object e, StackTrace st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } diff --git a/client/lib/providers/number_rule_provider.dart b/client/lib/providers/number_rule_provider.dart index 09e6954..e517b6d 100644 --- a/client/lib/providers/number_rule_provider.dart +++ b/client/lib/providers/number_rule_provider.dart @@ -14,33 +14,19 @@ final numberRuleListProvider = ); class NumberRuleListNotifier extends AsyncNotifier> { - List _cache = []; - @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); - try { - final result = await ref.read(numberRuleRepositoryProvider).list(); - _cache = result; - return result; - } catch (_) { - if (_cache.isNotEmpty) return _cache; - rethrow; - } + return ref.read(numberRuleRepositoryProvider).list(); } Future reload() async { state = const AsyncValue.loading(); try { final result = await ref.read(numberRuleRepositoryProvider).list(); - _cache = result; state = AsyncValue.data(result); } catch (e, st) { - if (_cache.isNotEmpty) { - state = AsyncValue.data(_cache); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); } } diff --git a/client/lib/providers/partner_provider.dart b/client/lib/providers/partner_provider.dart index c840d73..4cd13fb 100644 --- a/client/lib/providers/partner_provider.dart +++ b/client/lib/providers/partner_provider.dart @@ -72,7 +72,6 @@ class PartnerListNotifier extends AsyncNotifier> { int _page = 1; int _pageSize; String _keyword = ''; - PageResult? _cache; PartnerListNotifier({this.type, int pageSize = AppConstants.defaultPageSize}) : _pageSize = pageSize; @@ -81,14 +80,7 @@ class PartnerListNotifier extends AsyncNotifier> { Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(networkRecoveryCountProvider); - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future> _fetch() { @@ -129,14 +121,9 @@ class PartnerListNotifier extends AsyncNotifier> { void reload() { state = const AsyncValue.loading(); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (e, st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } diff --git a/client/lib/providers/product_provider.dart b/client/lib/providers/product_provider.dart index 8b1f836..53b81f0 100644 --- a/client/lib/providers/product_provider.dart +++ b/client/lib/providers/product_provider.dart @@ -21,20 +21,12 @@ class ProductListNotifier extends AsyncNotifier> { int _pageSize = AppConstants.defaultPageSize; String _keyword = ''; int? _categoryId; - PageResult? _cache; @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(networkRecoveryCountProvider); - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future> _fetch() { @@ -73,14 +65,9 @@ class ProductListNotifier extends AsyncNotifier> { void reload() { state = const AsyncValue.loading(); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (e, st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } diff --git a/client/lib/providers/session_provider.dart b/client/lib/providers/session_provider.dart index b46ed50..2d79726 100644 --- a/client/lib/providers/session_provider.dart +++ b/client/lib/providers/session_provider.dart @@ -14,33 +14,19 @@ final sessionListProvider = ); class SessionListNotifier extends AsyncNotifier> { - List _cache = []; - @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); - try { - final result = await ref.read(sessionRepositoryProvider).list(); - _cache = result; - return result; - } catch (_) { - if (_cache.isNotEmpty) return _cache; - rethrow; - } + return ref.read(sessionRepositoryProvider).list(); } Future reload() async { state = const AsyncValue.loading(); try { final result = await ref.read(sessionRepositoryProvider).list(); - _cache = result; state = AsyncValue.data(result); } catch (e, st) { - if (_cache.isNotEmpty) { - state = AsyncValue.data(_cache); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); } } diff --git a/client/lib/providers/stock_in_provider.dart b/client/lib/providers/stock_in_provider.dart index 9d8f24c..749b237 100644 --- a/client/lib/providers/stock_in_provider.dart +++ b/client/lib/providers/stock_in_provider.dart @@ -44,7 +44,6 @@ class StockInListNotifier extends AsyncNotifier> { String? _endDate; String _keyword = ''; Map _detail = const {}; - PageResult? _cache; Timer? _searchDebounce; @override @@ -52,14 +51,7 @@ class StockInListNotifier extends AsyncNotifier> { ref.onDispose(() => _searchDebounce?.cancel()); ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(networkRecoveryCountProvider); - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future> _fetch() { @@ -130,14 +122,9 @@ class StockInListNotifier extends AsyncNotifier> { state = const AsyncValue>.loading() .copyWithPrevious(state); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (e, st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } diff --git a/client/lib/providers/stock_out_provider.dart b/client/lib/providers/stock_out_provider.dart index 65a18e2..ec18f83 100644 --- a/client/lib/providers/stock_out_provider.dart +++ b/client/lib/providers/stock_out_provider.dart @@ -45,7 +45,6 @@ class StockOutListNotifier extends AsyncNotifier> { String _keyword = ''; String _productCode = ''; Map _detail = const {}; - PageResult? _cache; Timer? _searchDebounce; @override @@ -53,14 +52,7 @@ class StockOutListNotifier extends AsyncNotifier> { ref.onDispose(() => _searchDebounce?.cancel()); ref.watch(authStateProvider.select((s) => s.user?.shopId)); ref.watch(networkRecoveryCountProvider); - try { - final result = await _fetch(); - _cache = result; - return result; - } catch (_) { - if (_cache != null) return _cache!; - rethrow; - } + return _fetch(); } Future> _fetch() { @@ -139,14 +131,9 @@ class StockOutListNotifier extends AsyncNotifier> { state = const AsyncValue>.loading() .copyWithPrevious(state); _fetch().then((result) { - _cache = result; state = AsyncValue.data(result); }, onError: (e, st) { - if (_cache != null) { - state = AsyncValue.data(_cache!); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); }); } diff --git a/client/lib/providers/update_provider.dart b/client/lib/providers/update_provider.dart index bf0f4e8..5478ba7 100644 --- a/client/lib/providers/update_provider.dart +++ b/client/lib/providers/update_provider.dart @@ -140,7 +140,9 @@ const _definedVersion = String.fromEnvironment('APP_VERSION'); Future currentAppVersion() async { if (_definedVersion.isNotEmpty) { - return _definedVersion.startsWith('v') ? _definedVersion : 'v$_definedVersion'; + return _definedVersion.startsWith('v') + ? _definedVersion + : 'v$_definedVersion'; } final info = await PackageInfo.fromPlatform(); return 'v${info.version}'; diff --git a/client/lib/providers/user_provider.dart b/client/lib/providers/user_provider.dart index b116d3a..99a2fb6 100644 --- a/client/lib/providers/user_provider.dart +++ b/client/lib/providers/user_provider.dart @@ -13,33 +13,19 @@ final userListProvider = AsyncNotifierProvider>( ); class UserListNotifier extends AsyncNotifier> { - List _cache = []; - @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); - try { - final result = await ref.read(userRepositoryProvider).list(); - _cache = result; - return result; - } catch (_) { - if (_cache.isNotEmpty) return _cache; - rethrow; - } + return ref.read(userRepositoryProvider).list(); } Future reload() async { state = const AsyncValue.loading(); try { final result = await ref.read(userRepositoryProvider).list(); - _cache = result; state = AsyncValue.data(result); } catch (e, st) { - if (_cache.isNotEmpty) { - state = AsyncValue.data(_cache); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); } } diff --git a/client/lib/providers/warehouse_provider.dart b/client/lib/providers/warehouse_provider.dart index d0ef81a..1b9981d 100644 --- a/client/lib/providers/warehouse_provider.dart +++ b/client/lib/providers/warehouse_provider.dart @@ -14,33 +14,19 @@ final warehouseListProvider = ); class WarehouseListNotifier extends AsyncNotifier> { - List _cache = []; - @override Future> build() async { ref.watch(authStateProvider.select((s) => s.user?.shopId)); - try { - final result = await ref.read(warehouseRepositoryProvider).list(); - _cache = result; - return result; - } catch (_) { - if (_cache.isNotEmpty) return _cache; - rethrow; - } + return ref.read(warehouseRepositoryProvider).list(); } Future reload() async { state = const AsyncValue.loading(); try { final result = await ref.read(warehouseRepositoryProvider).list(); - _cache = result; state = AsyncValue.data(result); } catch (e, st) { - if (_cache.isNotEmpty) { - state = AsyncValue.data(_cache); - } else { - state = AsyncValue.error(e, st); - } + state = AsyncValue.error(e, st); } }