From 8a1db07d1948af18e545fbc844776f4695585928 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Mon, 29 Jun 2026 08:37:11 +0800 Subject: [PATCH] =?UTF-8?q?fix(devices+stats):=20/v1/me/devices=20401=20?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20+=20=E8=AE=BE=E5=A4=87=E4=B8=8B=E6=8B=89?= =?UTF-8?q?=E6=8E=A5=E5=B7=B2=E7=99=BB=E5=BD=95=E8=AE=BE=E5=A4=87=20+=20?= =?UTF-8?q?=E7=89=88=E6=9C=AC=E5=8F=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 「我的设备打不开」根因:devices handler 读 devices.CtxKeyUserID,而鉴权中间件 把 user id 写在 auth 的 key 下(类型不同→取不到)→ /v1/me/devices 一直 401。 改 handler 用 auth.UserIDFromContext(与 accountAPI 等一致)。 ② 统计页设备下拉改读 devicesProvider(已登录设备),不再只列有用量的设备 → windows 设备现在会出现。 ③ pubspec version 1.0.2+3 → 1.0.11+12,client_version 不再显示陈旧的 v1.0.2。 测试:devices_screen + stats_device_filter widget(下拉读 /v1/me/devices)+全量绿。 Co-Authored-By: Claude Opus 4.8 --- client/lib/screens/stats_page.dart | 16 +++++++++------- client/pubspec.yaml | 2 +- client/test/widget/stats_device_filter_test.dart | 7 +++++-- server/internal/devices/handler.go | 7 ++++--- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/client/lib/screens/stats_page.dart b/client/lib/screens/stats_page.dart index 04d3ccc..da4aa7c 100644 --- a/client/lib/screens/stats_page.dart +++ b/client/lib/screens/stats_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../l10n/app_text.dart'; -import '../models/device_usage.dart'; +import '../models/device.dart'; import '../models/usage_point.dart'; import '../pangolin_theme.dart'; import '../state/account_providers.dart'; @@ -138,8 +138,8 @@ class StatsPage extends ConsumerWidget { /// 待 per-device 每日曲线 API(#10);本层先把下拉内容接成真实设备。 final statsDeviceProvider = StateProvider((ref) => null); -/// 设备下拉(右上角):「全部」+ 近 30 天有用量的真实设备名(来自 deviceUsageProvider)。 -/// 无每设备用量时只显示「全部」(稀疏,正常)。 +/// 设备下拉(右上角):「全部」+ 用户**已登录的真实设备**(来自 devicesProvider, +/// 即 GET /v1/me/devices)。选某台 → 卡片/折线只算那台(#10②)。 class _DeviceDropdown extends ConsumerWidget { const _DeviceDropdown({required this.t}); final AppText t; @@ -147,11 +147,13 @@ class _DeviceDropdown extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final c = context.pangolin; - final devices = ref.watch(deviceUsageProvider(30)).valueOrNull ?? const []; + final devices = ref.watch(devicesProvider).valueOrNull ?? const []; final selected = ref.watch(statsDeviceProvider); - String nameOf(String uuid) => devices - .firstWhere((d) => d.uuid == uuid, orElse: () => const DeviceUsage(uuid: '', name: '', platform: '', bytesUp: 0, bytesDown: 0, minutesUsed: 0)) - .name; + String nameOf(String uuid) { + final d = devices.where((e) => e.uuid == uuid); + return d.isEmpty ? '' : (d.first.name.isNotEmpty ? d.first.name : d.first.platform); + } + final label = selected == null ? t.allDevices : (nameOf(selected).isNotEmpty ? nameOf(selected) : t.allDevices); diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 80d0fb8..e4908b5 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -1,7 +1,7 @@ name: pangolin_vpn description: 穿山甲 · Pangolin — 极简、稳定、跨平台网络加速客户端。 publish_to: "none" -version: 1.0.2+3 +version: 1.0.11+12 environment: sdk: ^3.5.0 diff --git a/client/test/widget/stats_device_filter_test.dart b/client/test/widget/stats_device_filter_test.dart index 1536bfd..9d3e2d6 100644 --- a/client/test/widget/stats_device_filter_test.dart +++ b/client/test/widget/stats_device_filter_test.dart @@ -54,11 +54,14 @@ void main() { usageDeviceParams.add(req.url.queryParameters['device']); return http.Response(jsonEncode({'points': []}), 200); } - if (p == '/v1/usage/devices') { + if (p == '/v1/me/devices') { return http.Response(jsonEncode({'devices': [ - {'uuid': 'dev-1', 'name': 'iPhone', 'platform': 'ios', 'bytes_up': 10, 'bytes_down': 20, 'minutes_used': 3}, + {'uuid': 'dev-1', 'name': 'iPhone', 'platform': 'ios', 'online': false, 'client_version': 'v1', 'last_login': null}, ]}), 200); } + if (p == '/v1/usage/devices') { + return http.Response(jsonEncode({'devices': []}), 200); + } return http.Response('{}', 200); }); diff --git a/server/internal/devices/handler.go b/server/internal/devices/handler.go index e36f357..3a47a5f 100644 --- a/server/internal/devices/handler.go +++ b/server/internal/devices/handler.go @@ -6,6 +6,7 @@ import ( "github.com/go-chi/chi/v5" "github.com/wangjia/pangolin/server/internal/apierr" + "github.com/wangjia/pangolin/server/internal/auth" ) // Handler serves the account device endpoints. It assumes the JWT auth @@ -35,7 +36,7 @@ type listDevicesResponse struct { // ListDevices handles GET /v1/me/devices. func (h *Handler) ListDevices(w http.ResponseWriter, r *http.Request) { - userID, ok := UserIDFromContext(r.Context()) + userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return @@ -54,7 +55,7 @@ func (h *Handler) ListDevices(w http.ResponseWriter, r *http.Request) { // DeleteDevice handles DELETE /v1/me/devices/{id}. func (h *Handler) DeleteDevice(w http.ResponseWriter, r *http.Request) { - userID, ok := UserIDFromContext(r.Context()) + userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return @@ -72,7 +73,7 @@ func (h *Handler) DeleteDevice(w http.ResponseWriter, r *http.Request) { // ForceLogout handles POST /v1/me/devices/{id}/logout — revoke the device's // sessions (kick it offline); the device stays in the list. func (h *Handler) ForceLogout(w http.ResponseWriter, r *http.Request) { - userID, ok := UserIDFromContext(r.Context()) + userID, ok := auth.UserIDFromContext(r.Context()) if !ok { apierr.WriteJSON(w, http.StatusUnauthorized, apierr.ErrUnauthorized) return