feat(client): P4 节点实测延迟/设备/套餐价/兑换/统计接真(#6 6D)
ci-pangolin / Lint — shellcheck (push) Has been cancelled
ci-pangolin / OpenAPI Sync Check (push) Has been cancelled
ci-pangolin / Redline Scan — 脱敏 (UI 文案) (push) Has been cancelled
ci-pangolin / Flutter — analyze + test (push) Has been cancelled

- latency_probe.dart:并行 TCP 握手实测各节点 per-client RTT(服务端无法代知)。
- nodes_provider:解析 /v1/nodes 的 host/port,后台测延迟回填 Node.ping;智能选择
  按实测最小;去掉 8 个伪造演示节点(空列表用中性占位,不再伪造服务器)。
- Node:+host/port/copyWith/pingLabel(未测显示 —);各端 ping 显示改 pingLabel。
- account_screens:PlansScreen 接 /v1/plans(价格 priceLabel + me 标当前档);
  DevicesScreen 接 /v1/me/devices + 移除 DELETE + last_seen;RedeemScreen 接
  /v1/redeem(成功刷新 me,失败显示后端文案)。
- stats_page:周柱接 me.weekly_gb,本月流量/时长接 usage(30),延迟用生效节点实测。
- 顺带补登记早前 log_time.dart 的删除(log.dart 重构遗漏 stage)。

flutter analyze 0 error;115 tests passed;受影响 4 golden 重生成。
本机设备高亮 + 真实 device id 随 P6(device_info_plus)落地。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-19 00:04:25 +08:00
parent bb39b36d84
commit f20eddad55
15 changed files with 321 additions and 162 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 26 KiB

+32 -12
View File
@@ -25,13 +25,23 @@ class _NullTokenStore implements TokenStore {
Future<bool> isOnboarded() async => true;
}
// ── 辅助:带 stub 覆盖的 ProviderContainer ──────────────────────
// 直接喂固定节点列表(不触发真实探针/HTTP)。
class _FakeNodes extends NodesNotifier {
_FakeNodes(this.list);
final List<Node> list;
@override
Future<List<Node>> build() async => list;
}
const _nodes = [
Node(code: 'HK', nameZh: '香港', nameEn: 'Hong Kong', ping: 30, uuid: 'hk'),
Node(code: 'JP', nameZh: '日本', nameEn: 'Tokyo', ping: 12, uuid: 'jp'),
Node(code: 'US', nameZh: '美国', nameEn: 'LA', ping: 200, uuid: 'us'),
];
ProviderContainer makeContainer({List<Override> overrides = const []}) =>
ProviderContainer(overrides: [
tokenStoreProvider.overrideWithValue(const _NullTokenStore()),
// effectiveNodeProvider 在 nodesProvider 加载时退回 kDemoNodes
// 所以测试直接读 effectiveNodeProvider,无需等待 nodesProvider.future。
...overrides,
]);
@@ -43,21 +53,31 @@ void main() {
expect(c.read(isSmartSelectProvider), true);
});
test('智能选择取延迟最小节点(退回 kDemoNodes)', () {
test('无节点时 effectiveNode 返回占位(不再伪造演示节点)', () {
final c = makeContainer();
addTearDown(c.dispose);
// nodesProvider 加载中时退回 kDemoNodeseffectiveNodeProvider 立即可用。
final node = c.read(effectiveNodeProvider);
final minPing = kDemoNodes.map((n) => n.ping).reduce((a, b) => a < b ? a : b);
expect(node.ping, minPing);
expect(c.read(effectiveNodeProvider).code, kPlaceholderNode.code);
});
test('选中具体节点后生效节点随之改变', () {
final c = makeContainer();
test('智能选择取实测延迟最小节点', () async {
final c = makeContainer(overrides: [nodesProvider.overrideWith(() => _FakeNodes(_nodes))]);
addTearDown(c.dispose);
c.read(selectedNodeCodeProvider.notifier).state = 'JP';
await c.read(nodesProvider.future);
expect(c.read(effectiveNodeProvider).code, 'JP'); // ping 12 最小
});
test('选中具体节点后生效节点随之改变', () async {
final c = makeContainer(overrides: [nodesProvider.overrideWith(() => _FakeNodes(_nodes))]);
addTearDown(c.dispose);
await c.read(nodesProvider.future);
c.read(selectedNodeCodeProvider.notifier).state = 'US';
expect(c.read(isSmartSelectProvider), false);
expect(c.read(effectiveNodeProvider).code, 'JP');
expect(c.read(effectiveNodeProvider).code, 'US');
});
test('pingLabel:未测得显示 —', () {
expect(const Node(code: 'X', nameZh: 'x', nameEn: 'x', ping: 0).pingLabel, '');
expect(const Node(code: 'X', nameZh: 'x', nameEn: 'x', ping: 42).pingLabel, '42ms');
});
test('localizedSub 不串语言:无标签用拉丁地名', () {