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
+4 -30
View File
@@ -35,20 +35,12 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
bool _sortAsc = true;
List<String> _series = [];
List<String> _spec = [];
PageResult<Inventory>? _cache;
@override
Future<PageResult<Inventory>> 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<PageResult<Inventory>> _fetch() {
@@ -125,14 +117,9 @@ class InventoryListNotifier extends AsyncNotifier<PageResult<Inventory>> {
state = const AsyncValue<PageResult<Inventory>>.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<PageResult<InventoryLog>> {
int _page = 1;
int _pageSize = AppConstants.defaultPageSize;
PageResult<InventoryLog>? _cache;
@override
Future<PageResult<InventoryLog>> 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<PageResult<InventoryLog>> _fetch() {
@@ -182,14 +161,9 @@ class InventoryLogNotifier extends AsyncNotifier<PageResult<InventoryLog>> {
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);
});
}
}
@@ -17,19 +17,11 @@ class PurchaseListNotifier extends AsyncNotifier<PurchaseListResult> {
int _page = 1;
int _pageSize = 10;
String _status = ''; // '' | pending | paid | failed
PurchaseListResult? _cache;
@override
Future<PurchaseListResult> 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<PurchaseListResult> _fetch() {
@@ -46,17 +38,12 @@ class PurchaseListNotifier extends AsyncNotifier<PurchaseListResult> {
void reload() {
// 保留上一次数据(isReloading)→ 屏幕可继续渲染旧内容 + 叠加半透明进度,不白屏。
state = const AsyncValue<PurchaseListResult>.loading()
.copyWithPrevious(state);
state =
const AsyncValue<PurchaseListResult>.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);
});
}
+2 -16
View File
@@ -14,33 +14,19 @@ final numberRuleListProvider =
);
class NumberRuleListNotifier extends AsyncNotifier<List<NumberRule>> {
List<NumberRule> _cache = [];
@override
Future<List<NumberRule>> 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<void> 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);
}
}
+2 -15
View File
@@ -72,7 +72,6 @@ class PartnerListNotifier extends AsyncNotifier<PageResult<Partner>> {
int _page = 1;
int _pageSize;
String _keyword = '';
PageResult<Partner>? _cache;
PartnerListNotifier({this.type, int pageSize = AppConstants.defaultPageSize})
: _pageSize = pageSize;
@@ -81,14 +80,7 @@ class PartnerListNotifier extends AsyncNotifier<PageResult<Partner>> {
Future<PageResult<Partner>> 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<PageResult<Partner>> _fetch() {
@@ -129,14 +121,9 @@ class PartnerListNotifier extends AsyncNotifier<PageResult<Partner>> {
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);
});
}
+2 -15
View File
@@ -21,20 +21,12 @@ class ProductListNotifier extends AsyncNotifier<PageResult<Product>> {
int _pageSize = AppConstants.defaultPageSize;
String _keyword = '';
int? _categoryId;
PageResult<Product>? _cache;
@override
Future<PageResult<Product>> 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<PageResult<Product>> _fetch() {
@@ -73,14 +65,9 @@ class ProductListNotifier extends AsyncNotifier<PageResult<Product>> {
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);
});
}
+2 -16
View File
@@ -14,33 +14,19 @@ final sessionListProvider =
);
class SessionListNotifier extends AsyncNotifier<List<DeviceSession>> {
List<DeviceSession> _cache = [];
@override
Future<List<DeviceSession>> 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<void> 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);
}
}
+2 -15
View File
@@ -44,7 +44,6 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
String? _endDate;
String _keyword = '';
Map<String, String> _detail = const {};
PageResult<StockInOrder>? _cache;
Timer? _searchDebounce;
@override
@@ -52,14 +51,7 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
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<PageResult<StockInOrder>> _fetch() {
@@ -130,14 +122,9 @@ class StockInListNotifier extends AsyncNotifier<PageResult<StockInOrder>> {
state = const AsyncValue<PageResult<StockInOrder>>.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);
});
}
+2 -15
View File
@@ -45,7 +45,6 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
String _keyword = '';
String _productCode = '';
Map<String, String> _detail = const {};
PageResult<StockOutOrder>? _cache;
Timer? _searchDebounce;
@override
@@ -53,14 +52,7 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
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<PageResult<StockOutOrder>> _fetch() {
@@ -139,14 +131,9 @@ class StockOutListNotifier extends AsyncNotifier<PageResult<StockOutOrder>> {
state = const AsyncValue<PageResult<StockOutOrder>>.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);
});
}
+3 -1
View File
@@ -140,7 +140,9 @@ const _definedVersion = String.fromEnvironment('APP_VERSION');
Future<String> 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}';
+2 -16
View File
@@ -13,33 +13,19 @@ final userListProvider = AsyncNotifierProvider<UserListNotifier, List<AppUser>>(
);
class UserListNotifier extends AsyncNotifier<List<AppUser>> {
List<AppUser> _cache = [];
@override
Future<List<AppUser>> 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<void> 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);
}
}
+2 -16
View File
@@ -14,33 +14,19 @@ final warehouseListProvider =
);
class WarehouseListNotifier extends AsyncNotifier<List<Warehouse>> {
List<Warehouse> _cache = [];
@override
Future<List<Warehouse>> 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<void> 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);
}
}