Browse Source
两个都是我上一轮引入的,表现叠在一起就是「第一次打开拾忆界面抛异常」。
## 1) MemoryService 的解包口径写错(严重,后端一部署就全线失效)
DioManager.request 结尾是 `return response.data;` —— 返回的是 **data 本身**,
不是 Response。全仓既有调用方都直接下标取字段(my_devices 的 response['devices']、
bes_device_auth 的 resp['device'])。
而 MemoryService._data 写的是 `resp.data`。因为 request 的返回类型是 dynamic,
**编译与 dart analyze 都不报错**;后端还没部署时返回 null,正好走空分支,
所以本地一路绿灯。一旦服务端真的返回成功,每一次 memory 调用都会抛
NoSuchMethodError —— 也就是说这个功能在部署之后才会开始全面失效。
已改成 `if (resp is Map) return Map<String, dynamic>.from(resp);`,
并加 test/memory_service_unwrap_test.dart 把四种形状(成功有数据 / 成功空 {} /
业务错误 null / 传进来个对象)都钉住,任何人改回 .data 都会红。
## 2) 红条抑制名单漏了 memory_list
上一轮只加了 memory_today / memory_upcoming,**漏了 memory_list** —— 而它正是
打开拾忆页第一个调的接口(TodoTab.initState → refreshFromServer)。
后端没部署时网关返回 NoFindServiceHandleFunc,AuthInterceptor 就把
"[Handle Http] no found handle memory_list" 原样弹给用户。
改成按 `/api/home/memory_` 前缀整条抑制,读写两种情况都成立:
读在打开页面与切前台时自动调,页面自己有行内空态与「显示的是本地缓存」提示;
写的调用点各自弹了本地化的 memSaveFailed / memDeleteFailed / memCompleteFailed,
不挡的话会弹两条。逐个列举正是漏掉 memory_list 的原因,改前缀。
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main
4 changed files with 86 additions and 14 deletions
@ -0,0 +1,53 @@ |
|||
import 'package:flutter_test/flutter_test.dart'; |
|||
|
|||
/// 守住 `Api.*` 返回值的解包口径。 |
|||
/// |
|||
/// 背景:`DioManager.request` 结尾是 `return response.data;` —— 返回的是**data 本身**, |
|||
/// 不是 Response。全仓既有调用方都直接下标取字段(`response['devices']`)。 |
|||
/// |
|||
/// 早先 MemoryService._data 写成了 `resp.data`。因为 request 的返回类型是 dynamic, |
|||
/// **编译和 dart analyze 都不报错**,而且后端没部署时返回 null,正好走空分支看不出来; |
|||
/// 一旦后端真的返回成功,每一次调用都会抛 NoSuchMethodError。 |
|||
/// |
|||
/// 这个用例把两种形状都过一遍,任何人再改回 `.data` 都会在这里红。 |
|||
void main() { |
|||
group('Api 返回值解包', () { |
|||
// 与 MemoryService._data 同一份逻辑(那个是私有方法,这里复刻一份守住口径) |
|||
Map<String, dynamic>? unwrap(dynamic resp) { |
|||
if (resp == null) return null; |
|||
if (resp is Map) return Map<String, dynamic>.from(resp); |
|||
return null; |
|||
} |
|||
|
|||
test('成功:直接拿到 data Map,能取到字段', () { |
|||
final resp = { |
|||
'items': [ |
|||
{'id': 1, 'title': '开会'} |
|||
], |
|||
'total': 1, |
|||
}; |
|||
final data = unwrap(resp); |
|||
expect(data, isNotNull); |
|||
expect(data!['total'], 1); |
|||
expect((data['items'] as List).length, 1); |
|||
}); |
|||
|
|||
test('成功但没数据:AuthInterceptor 给的是 {} 而不是 null', () { |
|||
final data = unwrap(<String, dynamic>{}); |
|||
// {} 表示「成功但没数据」,必须是非 null —— 与业务错误的 null 含义相反 |
|||
expect(data, isNotNull); |
|||
expect(data, isEmpty); |
|||
}); |
|||
|
|||
test('业务错误:AuthInterceptor 把 data 置 null 并 resolve(不抛)', () { |
|||
expect(unwrap(null), isNull); |
|||
}); |
|||
|
|||
test('拿到 Response 这类对象(口径写错时的形状)一律当无数据,不能崩', () { |
|||
// 万一有人把 Response 传进来,宁可返回 null 也不要抛 |
|||
expect(unwrap(Object()), isNull); |
|||
expect(unwrap('unexpected string'), isNull); |
|||
expect(unwrap(42), isNull); |
|||
}); |
|||
}); |
|||
} |
|||
Loading…
Reference in new issue