diff --git a/client/lib/screens/connect_page.dart b/client/lib/screens/connect_page.dart index de7c6d8..f8aa63f 100644 --- a/client/lib/screens/connect_page.dart +++ b/client/lib/screens/connect_page.dart @@ -33,8 +33,8 @@ class ConnectPage extends ConsumerWidget { final isFree = ref.watch(isFreePlanProvider); final quota = ref.watch(quotaProvider); final stats = ref.watch(vpnStatsProvider).valueOrNull; - final down = _mbps(stats?.downloadSpeed); - final up = _mbps(stats?.uploadSpeed); + final down = _speed(stats?.downloadSpeed); + final up = _speed(stats?.uploadSpeed); // 延迟来源:连接后用内核 urltest 实测(代理真实 RTT,TCP 探针被隧道吞掉时仍准); // 未连接时用节点 TCP 探针 ping。0/无 → 显示 —。 final livePing = conn.phase == VpnPhase.on ? _bestUrltest(stats) : node.ping; @@ -201,9 +201,16 @@ class ConnectPage extends ConsumerWidget { } } -/// 字节/秒 → Mb/s 字符串(null = 未取到,显示 —)。 -String _mbps(double? bytesPerSec) => - bytesPerSec == null ? '—' : (bytesPerSec * 8 / 1000000).toStringAsFixed(1); +/// 字节/秒 → 动态单位 (数值, 单位):B/s · KB/s · MB/s · GB/s(小数据也看得清)。 +/// null = 未取到,显示 —。 +(String, String) _speed(double? bytesPerSec) { + if (bytesPerSec == null) return ('—', 'KB/s'); + final b = bytesPerSec; + if (b < 1024) return (b.toStringAsFixed(0), 'B/s'); + if (b < 1024 * 1024) return ((b / 1024).toStringAsFixed(1), 'KB/s'); + if (b < 1024 * 1024 * 1024) return ((b / (1024 * 1024)).toStringAsFixed(1), 'MB/s'); + return ((b / (1024 * 1024 * 1024)).toStringAsFixed(2), 'GB/s'); +} /// 内核 urltest 各出站中的最小正延迟(ms);无则 0。 int _bestUrltest(VpnStatsEvent? s) { @@ -216,16 +223,16 @@ int _bestUrltest(VpnStatsEvent? s) { class _SpeedRow extends StatelessWidget { const _SpeedRow({required this.t, required this.down, required this.up, required this.latency}); final AppText t; - final String down; - final String up; + final (String, String) down; + final (String, String) up; final String latency; @override Widget build(BuildContext context) { final c = context.pangolin; final metrics = [ - (PangolinIcons.arrowDown, t.download, down, 'Mb/s'), - (PangolinIcons.arrowUp, t.upload, up, 'Mb/s'), + (PangolinIcons.arrowDown, t.download, down.$1, down.$2), + (PangolinIcons.arrowUp, t.upload, up.$1, up.$2), (PangolinIcons.zap, t.latency, latency, 'ms'), ]; return Row(children: [ diff --git a/server/internal/usage/handler.go b/server/internal/usage/handler.go index 156e487..96371ec 100644 --- a/server/internal/usage/handler.go +++ b/server/internal/usage/handler.go @@ -6,19 +6,17 @@ import ( "strconv" "github.com/wangjia/pangolin/server/internal/apierr" + "github.com/wangjia/pangolin/server/internal/codes" ) -// ctxKey is the context key type for request-scoped values. Defined here to -// avoid an import cycle; the JWT auth middleware sets the same key. -type ctxKey string - -// CtxKeyUserID is the context key carrying the authenticated int64 user ID. -const CtxKeyUserID ctxKey = "user_id" - // userIDFromContext extracts the authenticated user ID set by the auth // middleware, or 0 if absent. +// +// 关键:必须读 codes.CtxKeyUserID —— auth.RequireAuth 正是把 user id 存在这个键 +// 下。此前本包自定义了同名常量 ctxKey("user_id"),但 Go 的 context key 按 +// 「类型+值」比较,命名类型不同 → 取不到 → /v1/usage 恒 401(统计页全 0)。 func userIDFromContext(r *http.Request) int64 { - id, _ := r.Context().Value(CtxKeyUserID).(int64) + id, _ := r.Context().Value(codes.CtxKeyUserID).(int64) return id }