fix: /v1/usage 401(统计页全0真因) + 连接页速度改动态字节单位
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

- /v1/usage 一直 401:usage 包自定义了 ctxKey("user_id") 读 user id,但
  auth.RequireAuth 把 user id 存在 codes.CtxKeyUserID 下;Go context key 按
  「类型+值」比较,命名类型不同 → 取到 nil → 401 → 统计页本月流量/时长恒 0
  (而 /v1/me weekly 走 account.go 直查,有数,故只半边为 0)。改 usage 读
  codes.CtxKeyUserID(与 auth.UserIDFromContext 同键,/v1/me 已验证可用)。
  同时修好 /v1/ads/unlock(同一函数)。
- 连接页速度:Mb/s(兆比特,小流量显示 0.0)→ 动态字节单位 B/s·KB/s·MB/s·GB/s,
  小数据也看得清。

go build/test 通过;flutter analyze 0 error;114 tests passed。server 已部署。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
wangjia
2026-06-19 07:17:58 +08:00
parent e10f214dee
commit 03e268edc5
2 changed files with 22 additions and 17 deletions
+16 -9
View File
@@ -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: [
+6 -8
View File
@@ -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
}